RDB에는 앞서 언급했던 것 보다 더 많은 관계가 존재한다.
- 1:N(
@OneToMany
,@ManyToOne
) - 1:1(
@OneToOne
) - N:M(
@ManyToMany
)
이를 다중성이라고 한다.
각각의 다중성에서 형성될 수 있는 연관관계들과 그 특징을 나열해보겠다.
모든 다중성은 왼쪽이 연관관계의 주인이라고 가정하겠다(다대일 -> 다
가 연관관계의 주인)
다대일
N:1의 관계이고, N이 연관관계의 주인인(외래키를 관리하는) 형태이다.
RDB에서 외래키는 항상 N쪽에 존재한다는 특성에 가장 잘 들어맞는다.
그러므로 대부분 이 형태를 사용한다.
다대일 단방향(N:1)
1 |
|
Member.team
필드로 TEAM
테이블의 TEAM_ID
외래키를 관리한다.
Member
는 Team
을 참조할 수 있지만 Team
은 Member
를 참조할 수 없다.
이 관계는 선택이 아닌 필수이다.
다대일 양방향(N:1, 1:N)
1 |
|
참조가 양쪽 모두 있으므로 연관관계의 주인을 정해야 한다.
RDB에서 1:N, N:1 관계에서 외래키는 항상 N쪽에 있으므로 Member
가 연관관계의 주인이 된다.
보다시피 mappedBy
속성으로 연관관계의 주인이 아님을 명시해주고 있다.
아주 일반적인 구조이다.
양방향 연관관계는 항상 서로 참조해야 하므로, 각각 연관관계 편의 메서드를 작성해주는 것이 좋다.
RDB에서는 외래키 하나만 넣어줘도 양방향 관계가 성립하지만, 객체에서는 그렇지 않기 때문이다.
이 관계는 편의를 위한 선택이다.
일대다
1:N의 관계이고, 1이 연관관계의 주인인 형태이다.
외래키는 당연히 N쪽 테이블에 있지만, 관리를 1쪽에서 하므로 관리에 불편함이 있다.
일반적으로 잘 쓰이지는 않는 방법이다.
일대다 단방향(1:N)
일대다 단방향 관계는 JPA 2.0부터 지원한다.
1 |
|
외래키는 N쪽 테이블에 있으나, N쪽 엔티티에 외래키를 매핑할 수 있는 참조 필드가 없고, 1쪽에만 참조필드가 있다.
단방향 관계에서는 참조를 가진쪽이 외래키를 관리한다.
즉, 이 상태에서는 1쪽에서 외래키를 관리하는, 조금 특이한(다소 불편한) 형태가 나오게 된다.
외래키를 관리해야하므로 보다시피
@JoinColumn
을 꼭 명시해줘야 한다.
이를 명시해주지 않으면 JPA는@JoinTable
전략을 기본으로 사용해버린다.
이렇게 하면 어떤점이 불편할까?
- 기본적인 RDB의 1:N 구조에 역행하는 방식이므로 관리가 불편하다.
N 엔티티가 직접 외래키를 컨트롤 할 수 없으므로, 외래키를 변경하려면 무조건
Team
을 통해야 한다.
- 성능상 문제도 발생한다.
N 저장 시 연관관계를 설정하고 저장하는 것이 불가능하다.
insert 후 update로 연관관계를 설정해줘야 한다. 쿼리가 2번 필요하다.
아래는 1:N 단방향 형태에서 연관관계를 설정하는 예시이다.
1 | public void save(){ |
실행되는 쿼리는 아래와 같다.
1 | insert into Member (MEMBER_ID, username) values (null, ?) |
보다시피 불필요한 UPDATE
쿼리가 발생한다.
Member
는 Team
의 존재를 모르기 때문에 바로 외래키를 설정할 수 없기 때문이다.
보면 알겠지만 사용될 일이 많이 없는 형태이다.
항상 부모를 통해 접근하고, 자식이 직접 부모를 참조할 일이 없는 구조의 경우 가끔씩 사용하기도 한다.
(이럴 경우라도 위처럼 1쪽에서 의존관계를 관리할 경우는 거의 없다. 대부분 cascade 전략으로 처리한다)
하지만 위와 같지 않고 일반적인 상황이라면 이 구조보다는 다대일 양방향 매핑을 권장한다.
일대다 양방향
일대다 양방향 매핑은 존재하지 않는다.
양방향 연관관계를 형성하게 되면 N쪽 테이블에 @ManyToOne
을 명시하게 되는데,
테이블 상에서 외래키를 가진 애가 객체상에서도 참조를 컨트롤 할수 있게 된 상황에서
굳이 1쪽에서 연관관계를 컨트롤 하도록 할 이유가 없다.
(기능이란건 결국 필요에 의해 만들어지는데, 이 기능은 굳이 지원할 이유가 전혀 없다)
그래서 @ManyToOne
는 mappedBy
속성 자체가 없다.
근데 뭐… 완전히 불가능한 것은 아니고, 설정할 수는 있다.
기본적으로 일대다 단방향으로 설정하고, N쪽의 단방향 매핑을 읽기전용으로 설정하면 된다.
1 |
|
이렇게 까지 사용할 일이 있을라나 모르겠다.
일대일(1:1)
양쪽이 서로 하나의 관계만을 가지는 형태이다. 사람과 사물함의 관계와 같다고 보면 된다.
1:1 구조의 특징은 주 테이블, 대상 테이블 중 어느 곳이던 외래키를 가질 수 있다는 것이다.
즉 일대일 관계에서는 누가 외래키를 가질지 선택해야 한다.
주 테이블에 외래키
객체지향 개발자들이 선호하는 방법이다.
외래키를 객체 참조 비슷하게 사용 할 수 있고, 주 테이블만 확인해도 대상 테이블과의 연관관계를 확인 가능하다.
대상 테이블에 외래키
데이터베이스 개발자들이 선호하는 방법이다.
관계를 일대일에서 일대다로 변경할 떄 테이블 구조를 그대로 유지할 수 있는 장점이 있다.
이제 해당 방식에 대해 객체를 매핑할건데, 결론부터 얘기하자면 JPA는 대상 테이블에 외래키 방식을 지원하지 않는다.
그러므로 연관관계의 주인을 바꿔서 사용하는 방법밖에 없다.
왜 지원하지 않을까 생각해봤는데… 모호한 부분이 많은 듯 하다.
일단 문법적으로, 이런 모양으로 매핑할 수 있는 방법이 없다.
일대다 관계처럼 객체-컬렉션 형태로 구분지어지지도 않기 때문이다.
결국 문법적으로 구분할 수 있는 방법은 mappedBy
로 명시해주는 방법밖에 없는데,
이럴려면 무조건 양방향 매핑을 사용해야 하고,
이렇게 해서 대상테이블에 외래키를 구현한다고 해도, 반대편 엔티티에서 보면 어쩌피 또 주 테이블에 외래키 전략이 된다.
나는 … 이러한 이유로 주 테이블에 외래키 전략을 더 선호한다.
너무 RDB의 형태에 갇혀있을 필요는 없는 것 같다.
일대일 단방향(주테이블에 외래키)
1 |
|
양방향(주테이블에 외래키)
1 |
|
나는 객체지향 관점에서 봤을떄, 주테이블에 외래키를 가지는것이 좀 더 객체지향스럽다고 생각하고, 이 방식을 좀 더 선호한다(!!)
사실상 대상 테이블에 외래키를 가지는 것은 RDB 스러운 방법이라고 생각한다.
주 테이블에 외래키를 가지는 식으로 설계하면 주 테이블에서 관계의 개수만큼 외래키를 관리해줘야 하기 때문이다.
근데… 어플리케이션을 만드는 개발자가, 특히 객체지향을 사용하는 개발자가 이런 관점에 굳이 얽매여있을 필요가 있을까?
객체지향의 장점을 끌어올리고자 ORM을 사용하는 입장에서, 그러한 설계에 얽매이고, 굳이 연관관계를 뒤집어 가며 개발해야 할 이유가 있나 생각이 든다…
게다가, 나중에 나오곘지만 ORM에서는 프록시의 한계 때문에 연관관계의 주인이 아닌쪽에서의 lazy 로딩을 허용하지 않는다.
이 말인 즉, 대상 테이블에 외래키를 사용하는 형태로 양방향 관계를 형성하면, 주 테이블 쪽에서는 조회될 때 마다 자신과 연관된 모든 관계를 다 가져와야 한다는 의미가 된다.
(물론 해결 방법은 있다. byte instrument…)
다대다(N:M)
RDB는 다대다 관계를 표현할 수 없다.
그러므로 연결 테이블
이라는 것을 사용해야 한다.
회원
과상품
은 바로 N:M 관계를 맺을 수 없으니 중간에주문
같은 테이블을 넣어줘야 N:M 관계를 형성할 수 있다.
반면에 객체는 다대다 관계를 표현할 수 있다.
컬렉션을 사용해서 서로 참조하고 있기만 하면된다.
다대다 관계는 연결 테이블 여부
라는 패러다임 차이가 있기 때문에, 이를 풀어줘야 한다.
그러기에 기존의 방식인 다중성 표현 + 외래키 지정
으로는 위의 패러다임 차이를 풀 수 없다.
그래서 JPA는 @JoinTable
이라는 전략을 사용해 이를 지원한다.
단방향
1 | // 다대다 단방향 회원 |
다대다 관계이므로 @ManyToMany
를 사용하였고, 위에서 언급한 @JoinTable
이 등장하였다.
@ManyToMany
는 별다른거 없고(진짜 그런건 아니지만), @JoinTable
의 속성 대해 알아보자.
- name : 연결 테이블을 지정한다
- joinColumns : 현재 방향에서 매핑할 조인 컬럼 정보
- inverseJoinColumns : 반대 방향에서 매핑할 조인 컬럼 정보
이 정보들을 기반으로 연결 테이블을 생성한다.
이로 인해 우리는 연결 테이블을 전혀 신경쓰지 않아도 된다!
아래는 저장하는 코드다.
1 | public void save() { |
1 | INSERT INTO PRODUCT ... |
연결 테이블에 데이터가 저장된다.
아래는 탐색하는 코드이다.
1 | public void find() { |
1 | SELECT * |
연결 테이블과 조인해서 데이터를 들고온다.
양방향
다대다의 반대 또한 다대다이므로, @ManyToMany
로 연결해주면 된다.
1 |
|
mappedBy
로 연관관계의 주인만 지정해주면 된다.
사실상 연결 테이블로 관리되는 다대다 관계에서는 연관관계의 주인이 별로 의미가 없다…
물론 위의 상황에서는 Member
만이 연관관계를 컨트롤할 수 있지만,
연관관계 편의 메서드만 추가해줘도 양쪽에서 컨트롤 할 수 있게 된다.
1 |
|
연관관계의 주인이 아닌쪽에서 편의메서드를 사용하면 결국 연관관계의 주인쪽에도 추가되므로, 연결 테이블이 영향을 받게 된다.
즉, 편의메서드를 통하면 양쪽에서 다 컨트롤 가능하다.
다대다에서는 연관관계 편의메서드를 작성하지 않는것이 좋아보인다. side effect가 많다.
다대다의 한계
@ManyToMany
를 사용하면 연결 테이블을 알아서 관리해주므로 여러모로 편리하지만, 실제 실무에서는 이 정도로만 사용하기에는 한계가 있다.
MEMBER_ID
와 PRODUCT_ID
만 담지 않고, 추가적인 정보를 담는 경우가 많기 떄문이다.
(날짜, 수량등을 추가해서 ORDER
테이블로 사용한다거나…)
하지만 이렇게 컬럼을 추가하면 더이상 @ManyToMany
를 사용할 수 없게된다.
추가 컬럼을 정의한 연결 테이블에 매핑되는 엔티티를 만들어야하고, 테이블간의 관계도 다대다에서 일대다, 다대일의 관계로 풀어야한다.
1 |
|
(현재 @IdClass
라는 것을 사용해서 복합키를 매핑하였는데, 이는 뒷부분에서 다룬다.)
추가적인 컬럼을 가진 MemberProduct
를 정의하였다.
- 외래키를 직접 관리하므로 이 엔티티가 연관관계의 주인이 된다.
@JoinColumn
을 선언했음을 볼 수 있다. Member
엔티티는 외래키를 관리하지 않으므로mappedBy
속성을 줘서 연관관계의 주인이 아님을 명시했다.Product
엔티티에서 직접MemberProduct
를 참조할 일이 없다고 판단해서 연관관계를 추가하지 않았다.
실무(아니 그냥 일반적으로)에서는 위와 같은 방식으로 더 많이 사용된다.
사용하는 방식은 일반적인 다대일, 일대다 관계와 같다.