다대일
: 단방향, 양방향**
일대다
: 단방향, 양방향**
일대일
: 주 테이블 단방향, 양방향
: 대상 테이블 단방향, 양방향
다대다
: 단방향, 양방향
여태 앞에서 예시로 다뤘던 연관관계이며 기본적으로 다대일 단방향 연관관계를 사용한다.
ERD
![]()
1) 외래키를
MEMBER테이블이 가지고 있으므로 회원 엔티티가 팀을 참조하는 참조 필드를 가지게 된다.2) 회원은 팀을 참조하지만, 팀은 회원을 참조 하지 않는다.
=>팀 -> 회원 으로는 탐색 불가능
이제 팀도 회원에 대한 참조 필드를 추가 하였다. 객체 탐색 방향이 추가가 되었다.
ERD
![]()
1) 팀도 회원들에 대한 필드를 가지고 있으며, 다대일 이므로
List타입으로 저장한다. (양방향)2) 외래키를
MEMBER테이블이 가지고 있으므로 회원 엔티티가 연관관계의 주인이다.
양방향 관계일 경우, 주인 엔티티에만 값을 저장할게 아니라 양쪽에 모두 값을 넣어주는게 좋다. 그러려면 두 엔티티에 세터를 모두 호출해서 집어넣어줘야하는데
그것이 복잡하고 귀찮으므로 "연관관계 편의메서드" 를 하나 작성 하여 메서드 하나만 호출해도 알아서 양쪽 객체에 값을 집어넣게끔 해준다.
연관관계 편의 메서드는 양쪽에 한곳에만 정의해줘도 되는데 지금은 둘 다 작성해주었다. 하지만 이 경우 만일 둘다 호출해버리면 무한 루프가 발생할 수 있으므로 무한루프 방지 로직을 추가해줬다.
(그냥 편의메서드를 한쪽에만 작성해주자)
ERD
![]()
1) 테이블 상에선
MEMBER가 FK를 가지고 있지만, 객체 상에서는 참조필드를 팀 엔티티가 가지고 있다. (역방향)2) 팀 엔티티 만이 회원 엔티티에 대한 참조필드를 가지고 있다. (단방향)
단방향 이므로 회원 엔티티에는 어떤 연관관계 매핑도 되어있지 않다.
팀 엔티티가 회원에 대한 참조 필드를 가지고 있다. 즉, 외래키를 팀 엔티티가 관리한다.
ERD
![]()
1) 테이블 상에선
MEMBER가 FK를 가지고 있지만, 객체 상에서는 참조필드를 팀 엔티티가 가지고 있다. (역방향)2) 팀 엔티티 뿐만 아니라 회원 엔티티도 팀 엔티티에 대한 참조필드를 가지고 있게 되었다. (양방향)
양방향이 되었으므로 회원엔티티에도 연관관계 매핑을 해주었다. 하지만 특이한 점이 있다면, @JoinColumn 의 속성에 insertable 과 updatable 이 모두 false 로 되어있는것이다.
연관관계 주인이 아닌 경우에는 데이터 수정, 저장, 삭제가 불가능해야하는데 하이버네이트는 @ManyToOne 에 mappedBy 속성이 존재하지 않는다.
그래서 "다" 에 해당하는 엔티티가 비주인일경우 위와 같이 편법을 사용해 데이터 삽입과 수정을 제한해놓은것이다.
※ 일대다 관계의 단점
일대다의 경우 엔티티 자신이 매핑하고 있는 테이블이 아닌 상대편 엔티티가 매핑하고 있는 테이블의 외래키를 소유하고 있는셈이다.그래서 성능 문제 와 관리의 부담 이 발생하게된다.
...
// 회원들 영속성 저장
em.persist(member1);
em.persist(member2);
em.persist(member3);
// 팀 - 회원 연관관계 맺기 + 팀 영속성 저장
team.setMembers(List.of(member1, member2, member3));
em.persist(team);
...
1) 우선 member1, member2, member3 에 대한 INSERT 쿼리가 전송이 된다.
INSERT INTO MEMBER (member_id, team_id) VALUES(1, null);
INSERT INTO MEMBER (member_id, team_id) VALUES(2, null);
INSERT INTO MEMBER (member_id, team_id) VALUES(3, null);
모든 멤버들은 현재 본인이 어떤 팀과 맺어질지 모르기 때문에 team_id = null 인 상태로 저장이된다.
2) 마찬가지로 team 에 대한 INSERT 쿼리가 전송이된다.
INSERT INTO TEAM (team_id) VALUES(4);
팀이 저장될 때 비로소 회원들과 팀의 연관관계가 맺어지게 된다. 하지만, TEAM 테이블에는 FK가 없다. 팀 엔티티는 멤버들에 대해 참조하는 필드를 가지고 있긴 하지만 이는 가짜 매핑이지 실제 테이블에는 member_id 라는 컬럼을 가지고 있지 않는다.
3) member1,2,3 의 team_id 를 UPDATE 해준다
UPDATE MEMBER SET team_id = 4 WHERE member_id = 1;
UPDATE MEMBER SET team_id = 4 WHERE member_id = 2;
UPDATE MEMBER SET team_id = 4 WHERE member_id = 3;
그렇기 때문에, team 에 대한 INSERT 쿼리가 날아가고 난뒤, MEMBER 테이블을 UPDATE 하는 쿼리가 따로 또 생성되어야하는 것이다.
만일 다대일 관계였더라면, 회원들이 저장되기전 연관관계를 맺을 때 이미 team_id 를 알고 있기 때문에 굳이 UPDATE 쿼리 없이 INSERT 쿼리 한번만 전송을 하면된다.
...
// 팀 엔티티 저장
em.persist(team);
// 회원 엔티티 저장
em.persist(member);
member.setTeam(team);
...
//INSERT 쿼리
INSERT INTO TEAM (team_id) VALUES(1);
INSERT INTO MEMBER (member_id, team_id) VALUES(1,1);
일대다를 쓸바에는 그냥 다대일 양방향을 사용하도록 하자.
다음과 같은 경우가 일대일 관계이다.
락커룸이 1인 1개씩만 배정이 될 경우 사용자와 락커룸은 일대일 관계이다.
첨부파일을 딱 1개만 게시글에 첨부할 수 있는 경우, 게시글과 첨부파일은 일대일 관계이다.
일대일 관계의 경우 반대도 일대일 관계이기 때문에 다대일 혹은 일대다와 달리두 테이블중 어느곳이든 외래 키를 가질 수 있다. ( 다대일의 경우 반드시 외래키는 '다' 가 가지고 있음)
주 테이블 VS 대상 테이블
주 테이블: 비즈니스 관점에서 말 그대로 메인이 되는 테이블. (게시글과 첨부파일의 경우 게시글이 주 테이블이 될 확률이 높음)
대상 테이블: 무언가의 대상이 되는 테이블. (게시글과 첨부파일의 경우 첨부파일이 대상테이블이 될 확률이 높음)
개발자들은 주 테이블이 FK를 갖게하는걸 선호한다. 그러면 주테이블만 확인해도 대상 테이블과 연관관계를 확인 할 수 있기 때문이다.
DBA 들은 전통적으로 대상 테이블이 FK를 갖게하는것을 선호한다. 예를들어 정책이 바뀌어 대상 테이블이 '다' 가 되더라도 테이블 구조를 그대로 유지 할 수 있다.
ERD
![]()
회원 엔티티가 락커룸에 대한 참조를 가지고 있고 회원 테이블의 FK와 락커룸필드가 매핑된다. (단방향)
@OneToOne 어노테이션만 다를 뿐이지 사실상 다대일 단방향과 거의 다를게 없다.
ERD
![]()
1) 이제 락커룸 엔티티도 회원 엔티티를 참조하게 된다. (점선 연결)
2) 하지만 연관관계 주인은 FK를 가지고 있는 회원엔티티이다.
양방향 연결이 되었으므로 락커룸에도 회원에 대한 참조 필드를 추가해주었다. 하지만 연관관계 주인을 설정해줘야 하므로 락커룸에 mappedBy 속성을 추가 해주었다.
다음과 같은 경우가 다대다 관계이다.
영화배우 테이블 <-> 영화 테이블
영화배우 한명이 여러 영화를 찍을 수도 있지만
반대로 영화 하나에 여러 영화 배우들이 출연을한다.주문 <-> 상품
하나의 상품이 여러 주문에 포함될 수 있지만,
반대로 하나의 주문에 여러 종류의 상품들이 포함될 수도 있다.
이처럼 다대다 관계는 굉장히 다양한 상황에서 사용된다. 문제는 테이블에서는 다대다 관계라는것을 표현할 수가 없다. 다대일, 일대일은 표현할 수 있지만 다대다의 경우 표현할 방법이 없다.
하지만 객체 세상에서는 다대다관계를 손쉽게 나타낼 수 있다. 그냥 두 엔티티가 상대방에 대한 참조필드를 컬렉션 타입으로 생성하여 담아두면 되기 때문이다.
List<Order> orders; // 주문 엔티티의 참조필드
List<Item> items; // 상품 엔티티의 참조필드
JPA는 이와 같은 패러다임의 불일치를 2가지 방법으로 해결해주는데
첫번째는 @ManyToMany 사용이다. 하지만 이방법은 실무에서 사실상 사용되지 않는다. (슬그머니 생략,,뒤에 조금 설명이 나온다,,ㅎㅎ,,)
두번째는 연결엔티티 사용 이다. 이 절에서는 해당 방식에 대해서만 설명을 하겠다.
사실 다대다관계라고 해서 따로 거창하거나 바뀌는것은 없다. 데이터 모델링을 해보신분들이라면 사실 지금 뭘 할지 진작에 눈치를 채셧을 것이다.
앞서 설명했듯이 테이블 세상에는 다대다라는 관계가 존재하지 않는다. 그러면 어떻게 해야 할 까?
답은 쪼개기 이다.
다대다를 두개의 다대일 관계로 쪼개는 것이다. 이때 그냥 쪼개는게 아니라 쪼개는 대신 한개의 추가적인 테이블이 필요하게 되는데 이를 연결 테이블 이라 말한다.
ERD
![]()
회원과 상품이 존재하며 이 두 테이블을 연결해주는
ORDER테이블이 바로 연결 테이블이다.엔티티 또한 테이블과 마찬가지로 연결 엔티티
Order를 생성 해준다.※ Order = MemberProduct 이다.
회원상품 엔티티는 각각 회원과 상품이랑 다대일 관계이다. 물론 '회원상품 엔티티' 가 '다' 에 해당하며 그렇기에 연관관계 주인도 '회원상품엔티티' 가 된다.
또한 주문 개수와 시간을 체크하기 위한 필드도 존재한다. (orderAmount , orderdate)
자유로운 객체 그래프 탐색을 위해 양방향관계로 설정을 하였다. 앞에서 다 언급했던 내용이니 자세한 설명은 생략하겠다.
회원 엔티티와 설명 동일
사실 @ManyToMany 를 사용하게되면 JPA 가 자동으로 연결 테이블을 생성해준다. 오잉? 그러면 오히려 좋은 상황아닌가? 왜 사용하지 않는 것일까?
이유는 엔티티의 필드에 답이있다.
실무에서는 연결테이블을 사용하여 운영을 하다보면 연결테이블의 역할이 서로 연결만 하는 선에서 끝나지 않는다.

사진에서도 볼수 있다시피 현재 MemberProduct 엔티티 에는 FK 필드 말고도 수량과 시간을 저장하는 필드도 추가가 되어있음을 확인할 수 있다.
문제는 @ManyToMany 를 사용하여 자동 생성된 연결 테이블에는 다음과 같은 컬럼이 없고 오직 FK 컬럼들만이 존재한다. 그래서 실무에서는 사용할 수 없는 것이다.
연결 테이블을 생성할 때, 전통적으로는 FK 2개를 합친 복합키가 연결테이블의 PK가 된다.
무슨 말이냐면, 연결 테이블이

사진과 같이 FK 2개로 이루어진 복합키를 식별자로 사용한다는 뜻이다. (식별)
이 경우, 회원과 주문(Member_Product) 그리고 상품과 주문(Member_Product) 은 모두 식별관계가 된다.
하지만 이렇게 테이블을 설계 할 경우 복합키를 위한 또다른 식별자 클래스를 하나 만들어줘야하는둥 JPA로 매핑하기가 굉장히 복잡해진다.
그래서 그냥 다른 테이블과 아무 관계없는 새로운 PK를 하나 따로 생성해서 사용하는 것이 훨씬 편하다. (비식별)

본 포스트는
김영한의 자바 ORM 표준 JPA프로그래밍 기본 강의 및 도서를 참고하여 정리했습니다.