당니의 개발자 스토리
일대일 [1:1] 본문
일대일 [1:1]

이번에는 일대일 관계에 대해서 설명을 드리겠습니다.

일대일 관계는 그 반대도 일대일 이겠죠. 그리고 일대일 관계는 재미있는 게 뒤에서 보시면 알겠지만 외래키를 뭘 기준으로 나누냐면, 주 테이블이나 대상 테이블 중에 외래키를 아무데나 다 넣을 수 있어요. 예를 들어서 Member랑 Team이 있으면 Member에도 외래키를 넣어도 되고 Team에도 외래키를 넣어도 돼요. 왜냐? 이게 일대일 관계이기 때문에 가능한 거거든요. 대칭적인 거죠. 그러니까 여기 넣든, 저기 넣든 상관없는 거예요. 둘 중에 한군데만 넣으면 돼요.
그래서 저는 기준을 어떻게 나누냐면 주 테이블에 외래키를 넣거나, 대상 테이블에 외래키를 넣거나 이거를 가지고 저는.. 이제 뭔가 두 테이블이 있어요. 예를 들어 MEMBER가 주 테이블이라고 가정을 했어요. 그러면 MEMBER에 외래키를 넣거나, 반대로 '주 테이블이 MEMBER인데 난 저기 반대쪽 있는 TEAM에다가 외래키를 넣을 거야' 라고 할 수도 있어요. 일대일 관계는 그게 가능하거든요. 전 그거를 가지고 기준을 나눴고요.

그 다음에 외래키에 데이터베이스 유니크(UNI) 제약 조건이 추가가 되어야 일대일이 됩니다. 사실 일대일 관계는 어쩌면 일대다, 다대일이랑 되게 비슷한데 여기에 데이터베이스 외래키의 유니크 제약 조건, 사실 굳이 제약 조건을 넣지 않더라도 할 수 있는데 애플리케이션에서 관리를 엄청 잘해야 되겠죠. DB 입장에서는 외래키에 데이터베이스 유니크 제약 조건이 추가가 된게 일대일 관계가 됩니다.

그럼 먼저 일대일, 주 테이블의 외래키 단방향이라는 관계에 대해서 설명을 드리겠습니다. 쉽게 말해서 저는 지금 Member를 주 테이블이라고 생각하는 거예요. 그니까 주 엔티티인 거예요. 그러면 이거는 예시가 뭐냐면 어떤 Locker, 그러니까 내 사물함이라고 보시면 되고 이 Locker 하나를 회원이 가지고 있는 거예요. 딱 하나밖에 못 가지는 거죠. 사물함 입장에서도 룰이 딱 하나의 Member 밖에 못 가져요. 이런 비즈니스 룰이 있어야 돼요.
일단 룰이 있다고 가정을 할 때 MEMBER 테이블과 LOCKER 입장에서는 외래키를 MEMBER의 LOCKER_ID, 여기에다가 유니크 제약조건을 넣어도 되고요. 이걸 반대로 해도 돼요. LOCKER의 MEMBER_ID를 넣고 그걸 외래키로 한 다음에 유니크 제약조건을 넣어도 돼요. 어떻게 하든 둘 다 일대일 연관관계가 돼요.

자 그래서 이제 첫 번째 예시는 정말 간단하게 MEMBER 테이블에 LOCKER_ID 라는 외래키가 있고 이걸 유니크 제약조건으로 잡아줬을 때는 단순하게 Member의 locker를 딱 잡고 연관관계 맵핑 하면 됩니다. 이게 가장 쉬운 일대일 맵핑입니다.

주 테이블에 외래키가 있는 단방향 맵핑은 마치 다대일 단방향 맵핑과 거의 똑같습니다. 어노테이션만 달라지고요.

그래서 이거를 양방향으로 만들고 싶다면 Locker에다가 Member를 추가하면 되죠.

일단 먼저 이 주 테이블에 외래키 단방향부터 코드로 해볼게요.

이번에는 Locker 클래스를 하나 추가하겠습니다.

이렇게 id랑 name까지 만들어줬구요.

그러면 이제 이 그림을 딱 만들려면 이렇게 하면 됩니다.
자 Member에 가서,

'Member가 Locker를 가질 거야' 그래서 필드에 Locker를 만드시고 @OneToOne 하시면 됩니다.

그 다음에 @JoinColumn을 넣으셔야 합니다. 이거를 안 넣으면 default 값이 있는데 default 값이 굉장히 지저분합니다. 딱 직관적이지 않아요. 그래서 @JoinColumn를 넣어주시는 거를 권장 드리고요.

이렇게 LOCKER_ID로 잡으시면 되죠. 이러면 일대일 관계가 끝납니다. 마치 @ManyToOne이랑 비슷하죠?

XXXToOne 시리즈가 비슷해요.
이렇게 해서 실행을 해보겠습니다.

보시면 테이블 스크립트가 Locker가 생성이 되고,

Member에 지금 여기 LOCKER_ID가 들어갔죠? 오른편의 그림이랑 똑같이 맞춘겁니다.
그 다음에 DB 제약 조건은 나중에 DB create 문 직접 하실때 만들면 되구요. 이제 이번에는 양방향을 만들고 싶다. 그러면 간단하죠.

그 다음에 'Locker도 Member를 알고 싶어' 라고 하면, Locker로 가셔서

이렇게 만들어주시고 @OneToOne 하시고 mappedBy 해줘야겠죠. 이 locker는

Member 클래스에 있는 이 Locker 입니다. 일대일, 다대일이랑 똑같이 mappedBy의 원리가 똑같이 적용이 됩니다.

그래서 반대쪽 사이드에서 걸어 주시면 되고,

이 member는 읽기 전용이 되는 거예요.

그래서 결국 다대일 단방향 맵핑과 유사하다.

이제 양방향을 만들고 싶으면 똑같이 만들고 일대일의 반대도 일대일이니까, 만든 다음에 둘 중 한 곳은 mappedBy로 잡아 주시면 됩니다. 이게 가장 심플하게 떨어지는 일대일 연관관계죠.
근데 좀 더 설명을 드리자면,

다대일 양방향 맵핑처럼 외래키가 있는 곳이 연관관계의 주인입니다. 반대편은 mappedBy를 적용해 주시면 됩니다.

자 그러면 이번에는 일대일 관계인데 대상 테이블에 외래키가 있는 단방향이에요. 무슨 말이냐면 지금 여기 Member인 locker가 연관관계 주인을 하고 싶은데, 외래키가 LOCKER에 있는 거예요. 마치 우리가 이전에 했던 일대다 단방향 같은 거죠. 그거랑 테이블 그림이 비슷한데, 지금 Member에 있는 locker로 반대쪽 사이드에 MEMBER_ID 라는 게 있으면 이걸 관리할 수 있냐? 이거는 안됩니다. 그냥 이거는 아예 지원도 안되고 방법이 없습니다.

자 그래서 이런 대상 테이블에 외래키가 있는 단방향 관계는 JPA에서 지원하지 않고요. 대신에 양방향 관계에서는 이런 게 지원이 됩니다.

지원이 된다는 게 조금 약간 어폐가 있긴 한데, 일대일 관계에서는 이렇게 됩니다. 이거를 양방향으로 만들었단 말이에요. 그러면 대상 테이블에 외래키가 있는 거예요. 내가 Member를 주 테이블이라고 생각했는데 여기 대상 테이블 LOCKER에 MEMBER_ID가 있어요. 그런데 제가 양방향으로 잡는단 말이에요. 근데 그렇게 해도 사실 아무 문제가 없는 게 이 Locker에 있는 member를 연관관계 주인으로 잡아서 그냥 맵핑을 해버리시면 됩니다. member를 연관관계 주인으로 딱 잡고 맵핑을 하면 돼요. 사실 이거는 약간 말의 어폐가 있는 거죠. 왜냐하면,

그냥 이 그림을 딱 뒤집은 거예요.

그러니까 일대일 관계는 딱 정리를 해드리자면 그냥 내가 내 것만 관리할 수 있는 거예요. 그러니까 내 엔티티에 있는 외래키를 내가 직접 관리해야 돼요. 그래서 양방향인 경우에는 이렇게 Locker의 member가 있으면 member를 연관관계 주인으로 잡으시고 Member의 locker는 읽기 전용으로 만들면 된다고 보시면 될 것 같아요. 이렇게 보면 약간 직관적이죠.

사실 일대일 주 테이블의 외래키 양방향과 맵핑 방법은 똑같습니다.

일대일 관계에 대한 정리는 이런데요. 이게 사실은 굉장히 논란거리가 되는 부분이에요.

지금 보시면, 이 Member랑 Locker가 있단 말이에요. 그러면 테이블만 딱 생각합시다. DB 설계상 MEMBER에 있는 이 LOCKER_ID를 지금처럼 MEMBER 테이블이 외래키를 들고 있는 설계가 좋을까요?

아니면 지금 이것처럼 일대일 관계에서 LOCKER에 MEMBER_ID 외래키가 있는게 좋을까요? 사실 외래키를 MEMBER 쪽에 놓든 LOCKER 쪽에 놓든 db만 딱 생각하시는 거에요. 지금 둘 중에 어떤 방법을 써도 사실 일대일 관계가 유효하게 성립합니다.
여기서 어떤 트레이드 오프가 있냐면 내가 DBA란 말이에요. 그러면 여기서 미래를 걱정 안 할 수가 없는데 여러분 만약에 시간이 흘러서 미래에 둘 중에 뭐가 맞냐 라고 했을 때 일단 딱 정답은 없어요. 근데 이제 뭔가 테이블이라는 게 한번 만들어지면 변경하기 어렵잖아요. 그런 관점에서 보면 만약에 나중에 룰이 바뀌는 거에요. 시간이 흘러서 '하나의 회원은 여러개의 Locker를 가질 수 있어' 라고 비즈니스 룰이 바껴요.

그 경우에는 이 그림은 뭐가 좋냐면 이 유니크 제약 조건만 Alter로 해서 빼면 돼요. 그러면 하나의 회원에 여러 개를 막 Insert 할 수가 있는 거죠. 그럼 테이블 입장에서 보면 Annotation을 자연스럽게 일대일에서 일대다로 바꾸기가 되게 쉽거든요.

그런데 만약에 이렇게 되어 있으면, MEMBER에 이 외래키가 있으면 이제 하나의 회원이 여러 개의 Locker를 사용할 수 있다는 시나리오가 되면 여기 LOCKER에 컬럼을 추가하고 기능을 좀 변경을 해야 돼요. 그러니까 코드도 좀 많이 변경을 해야 될 수도 있고 변경 포인트가 좀 많아요. 그리고 이제 LOCKER_ID는 지워야 되겠죠. 별로 의미가 없어지니까.
그래서 이렇게 생각해 보면 맞는데 이제 또 반대로, 비즈니스가 뒤집어져서 반대로 생각해보면 이번에는 '하나의 Locker가 여러 개의 Member를 가질 수 있다' 라고 하면

또 이 그림이 맞는 거예요. 장기적으로 볼 때 뭔가 그렇게 비즈니스가 변경이 됐다고 하면 이때의 선택이 옳았던 거죠.
근데 이제 DB 입장에서는 그렇고 이거를 ORM 맵핑을 해서 실제 개발해야 되는 개발자 입장에서는 또 어떤 딜레마가 있냐면 Member에 이 locker가 있는 게 성능도 그렇고 여러 가지로 유리해요. 어떤 장점이 있냐면,

예를 들어서 이 Member가 Locker를 가지고 있어, 없어? null은 들어갈 수 있으니까 없으면 null이고 가지고 있으면은 뭔가 값이 있겠죠. 그래서 이것만 생각해보면 성능상 뭐가 좋냐면 MEMBER에 Locker가 있는 게 훨씬 좋아요. 내가 MEMBER 테이블을 거의 Select를 많이 한다고 가정을 했을 때, MEMBER 테이블에서 이미 Select 할 때 값을 가져와서 볼 거잖아요. 그러면은 만약 비즈니스 로직에서 LOCKER_ID, 즉 locker에 값이 있으면 어떤 로직이 돌고 locker에 값이 없으면 어떤 로직이 안 돌아. 라는 조건문이 있을 때 Member는 이미 조회를 해온거예요. 대부분의 비즈니스에서 Member는 웬만하면 조회를 해와야 돼요.

그럼 MEMBER를 딱 조회를 해왔는데 여기 보니까 이미 Locker(LOCKER_ID)에 값이 있죠? 그러니까 DB 쿼리 하나로 별 다른 Join 없이 그냥 쿼리 한방으로 MEMBER를 가져왔을 때 이 Locker에 대해서 값이 있냐, 없냐를 판단하는게 되게 쉽게 나오는 거에요. 성능상 이점도 있고 여러가지 장점이 있는 거예요. 그래서 이제 JPA를 하시는 분들 입장에서는 실무를 해보면 이 그림이 조금 더 편리한 게 사실이에요. 그래서 각각 장단이 있어요. 근데 이렇게 하면 또 MEMBER 테이블에 막 Locker가 들어오고, 또 null 값이 들어가야 될 수도 있고 하니까 이제 좀 싫을 수도 있죠. 아무튼 이런 것들을 종합적으로 판단해서 결정해야 되긴 하는데 저는 대체적으로 어떻게 설계를 가지고 가냐면 너무 먼 미래를 생각하지는 않아요.

그래서 저는 이 주 테이블에 외래키 단방향 그림을 웬만하면 가지고 가요. '얘가 명확하게 일대일 관계야' 그러면 그냥 딱 이 그림을 만들어요. 그냥 Member에서 이 Locker를 가지고 있도록. 이렇게 딱 가지고 가는게 제가 선호하는 방식이고 근데 또 여러분들이랑 여러분의 DBA들은 선호하는 게 다를 수 있기 때문에 그때는 그 선호에 맞춰서 이제 반대쪽을 잡고,

이런 식으로 그림을 만들어 가시면 됩니다. 근데 대신 이렇게 하면 이제 양방향으로 만들어야 되죠. 이제 Locker에 외래키가 들어가야 되면 나는 주로 Member를 통해서 Locker를 액세스를 하는데 양방향을 걸기 싫어도 이렇게 그림처럼 되어 있고, 외래키가 저쪽, Locker에 있으면 어쩔 수 없이 양방향을 걸고 Locker의 member가 MEMBER_ID를 바라보게 만들어야 해요. 이게 JPA의 한계라고 볼 수가 있죠. 그렇지만 그냥 양방향 걸면 되죠. 뭐 정말 막 난 순수하게 객체 지향적으로 단방향 관계가 좋으니까 하는 이런 고민도 필요하긴 한데 때로는 트레이드 오프에 대해서 그냥 자연스럽게 받아들이고 넘어가는게 필요해요. 왜냐하면 할 게 충분히 너무 많으니까.

아무튼 그래서 주 테이블에 외래키를 두는 것의 장점은 주 테이블은 주로 많이 액세스하는 테이블이라고 정의했다고 보시면 되고요. 주 객체가 대상 객체의 참조를 가지는 것처럼 주 테이블에 외래키를 두고 대상 테이블을 찾는단 말이에요. 이거는 객체지향 개발자들이 보통 선호를 해요. 그리고 JPA 맵핑이 편리하고요. 장점은 주 테이블만 조회해도 대상 테이블에 데이터가 있는지 확인 가능해요. 단점은 참조 값이 없으면 외래키에 NULL을 허용해야 되는데 이게 DBA 입장에서는 좀 치명적이죠. 어떻게 보면 좀 순수하게 가시는 분들은 약간 치명적일 수 있어요.

자 다음, 대상 테이블에 외래키를 쓰게 되면 대상 테이블에 외래키가 존재하는 거고요. 당연히 뭐 여기 경우에는 LOCKER에 외래키가 있는 거고 전통적인 데이터베이스 개발자 분들은 보통 이 방식을 선호합니다. 이 방식을 사용하면 null을 허용하는 문제도 해결이 되고, 또 주 테이블과 대상 테이블을 일대일에서 일대다 관계로 변경할 때 테이블 구조가 유지가 되고, 또 null 이런 것들을 더 안 넣어도 되니까 이러한 장점이 있습니다.
그럼 단점은 뭐냐면 이거는 JPA 입장에서 보면 아까 말씀드린 대로,

이렇게 하면 양방향으로 만들어야 돼요. 주로 Member에서 locker를 액세스 하는데 JPA의 한계 때문에 어쩔 수 없이 양방향으로 만들어야 되죠. 왜냐면 예를 들어 Member에 있는 locker를 업데이트해서 반대쪽 테이블에 있는 MEMBER_ID를 못한다고 그랬죠. JPA에서 지원 안 한다고 그랬죠. 그래서 양방향으로 만들어야 되고,

그 다음에 이제 약간 치명적인데 나중에 하시다 보면 아실텐데 JPA가 제공하는 기본 프록시 기능에 한계가 있어요. 지연 로딩을 설정해도 이 관계는 항상 즉시 로딩이 됩니다. 지금은 간단하게 설명을 드릴게요.
왜 그러냐면 JPA 입장에서는

프록시 객체라는 거를 만들려면 Member에 locker가 지금 값이 있는지 없는지 JPA가 알아야 돼요.

처음으로 올라가서 JPA 입장에서는 Member를 로딩할 때 이 Locker의 값이 있는지, 없는지 이 Foreign Key(외래키)에 값이 들어있으면 locker에 값이 있다고 가정할 거고 값이 없으면 locker에 null을 딱 집어 넣어주면 돼요. JPA 입장에서 그게 되게 심플하단 말이에요. MEMBER 테이블만 쿼리하면 돼요.
근데 문제는

지금 그림이 만약에 이렇게 되어 있어요. 그럼 제가 Member의 locker를 조회 했단 말이에요. 그런데 문제가 뭐냐면 이 locker의 값이 있는지, 없는지 알려면 이 MEMBER 테이블만 조회해서 될까요? 안된단 말이에요. 어차피 이 LOCKER를 뒤져야 돼요. 이 LOCKER를 뒤져서 MEMBER_ID에 내 거가 있는지, 없는지를 where 문에 넣어 가지고, 값이 있어야 이 Member의 locker가 있다고 인정이 되는 거에요. 그러면 locker에다가 값을 프록시를 넣든, 뭘 넣든 넣고, 없으면 null을 넣어줘야 하고, 있는지 없는지를 확인을 해야 돼요. 어차피 query가 나간단 말이에요. 그러니까 어차피 query가 나간 거 locker를 프록시를 만들 이유가 없는 거에요.

그래서 하이버네트 구현체 같은 경우에는 이걸 그냥 지연 로딩으로 세팅을 해도 일대일 관계고,

이런식으로 대상 테이블에 외래키가 있으면 얘는 즉시 로딩이 무조건 됩니다. 어차피 이 LOCKER 테이블에 쿼리 해봐야 되기 때문에 프록시 객체라는 것은 값이 있거나, 만약 없으면 Member의 locker에 또 null이 들어가야 되잖아요. 프록시를 넣거나 null을 넣거나 판단을 해야 되는데 이미 이 LOCKER 테이블에 쿼리를 해봐야 null로 넣어야 될지 말아야 될지가 나오기 때문에 이미 쿼리를 한 거예요. 그래서 지연 로딩으로 세팅하는 게 아무 의미가 없어요. 쿼리만 한 번 더 나가는 거예요.

암튼 방금 내용이 좀 어려울 수 있는데 이거는 몇 번 써보신 분들은 그래서 그렇구나. 라고 이해하실 거고 아니신 분들은 제가 뒤에서 또 한번 설명드릴게요. 일단 그런 단점이 있어서 저는 좀 치명적으로 보거든요. 그래서 요것도 나중에 뒤에서 배우겠지만 패치 조인 등등 해서 해결하는 방법들이 있어요.

저는 그래서 사실 실무에서는 거의 이 방법을 씁니다. 이렇게 주 테이블에 외래키가 있는 방향을 선호합니다. 근데 이거는 dba 분들이 싫어할 수 있기 때문에 충분히 협의가 돼야 됩니다.

일대일은 정리가 됐구요. 다음 시간에는 다대다에 대해서 알아보겠습니다.
'스프링 > 자바 ORM 표준 JPA 프로그래밍 - 기본편' 카테고리의 다른 글
| 실전 예제 3 - 다양한 연관관계 매핑 (0) | 2024.07.17 |
|---|---|
| 다대다 [N:M] (0) | 2024.07.14 |
| 일대다 [1:N] (0) | 2024.06.23 |
| 다대일 [N:1] (0) | 2024.06.21 |
| 실전 예제 2 - 연관관계 매핑 시작 (0) | 2024.06.19 |