1. 연관관계 매핑 시 고려사항 3가지
다중성
JPA가 다중성에 맞는 4가지 애노테이션을 제공한다. DB 관점에서의 다중성을 기준으로 고민하면 된다. 헷갈린다면 반대의 상황을 생각해 보자. 예를 들어 회원과 팀의 관계가 헷갈린다면, 팀과 회원의 관계로 다시 생각해 보면 된다. 대칭성이 있기 때문에 반대로 생각해 보면 더 이해하기 쉬워지기도 하기 때문이다.
다중성(차수) | 애노테이션 |
다대일 [N:1] | @ManyToOne |
일대다 [1:N] | @OneToMany |
일대일 [1:1] | @OneToOne |
다대다 [N:M] | @ManyToMany |
참고
'다대다 [N:M]'는 실무에서 쓰면 안 된다.
단방향 & 양방향
저번 섹션에서 객체와 테이블의 연관관계를 맺는 방법의 차이에 대해 학습했다.
- 객체
- 참조용 필드가 있는 쪽으로만 참조 가능
- 한쪽만 참조하면 단방향 / 양쪽이 서로 참조하면 양방향
- 테이블
- 외래 키 하나로 양쪽 조인 가능 (사실 방향이라는 개념이 없음)
양방향 - 연관관계의 주인
양방향 연관관계에서 가장 중요한 포인트는 연관관계의 주인을 결정해야 한다는 것이다.
- 테이블은 외래 키 하나로 두 테이블이 연관관계를 맺지만, 객체의 양방향 관계는 A → B, B → A처럼 서로 참조를 해야 함
- 참조가 두 곳에 존재하기 때문에 둘 중 테이블의 외래 키를 관리할 곳을 지정해야 함
- 연관관계의 주인 = 외래 키를 관리하는 참조
- 주인의 반대 편 = 외래 키에 영향을 주지 않으며, 단순 조회만 가능
2. 다대일 [N:1]
다대일 단방향 매핑
가장 많이 사용하는 연관관계로, 다대일의 반대는 일대다가 된다.
참고
다대일 관계에서 외래 키는 '다' 쪽인 테이블에 들어가야 한다.
다대일 양방향 매핑
외래 키가 있는 테이블에 매핑되는 쪽이 연관관계의 주인이다. 양방향 연관관계면 서로 참조하도록 개발해야 한다.
- 이때, 양방향 연관관계를 만들기 위해 연관관계의 주인의 반대편에 있는 엔티티에 필드를 추가한다고 해도 테이블엔 전혀 영향을 주지 않는다.
@ManyToOne
다대일 관계 매핑에서 사용된다. 속성은 아래와 같다.
속성 | 설명 | 기본값 |
optional | false로 설정하면 연관된 엔티티가 항상 있어야 한다. | TRUE |
fetch | 글로벌 페치 전략을 설정한다. | @ManyToOne = FetchType.EAGER |
cascade | 영속성 전이 기능을 사용한다. | |
targetEntity | 연관된 엔티티의 타입 정보를 설정한다. 이 기능은 거의 사용하지 않는다. 컬렉션을 사용해도 제네릭으로 타입 정보를 알 수 있다. |
3. 일대다 [1:N]
일대다 단방향 매핑
다대일 관계와는 다르게, 일대다에서 '일' 쪽에서 외래 키를 관리한다. (무조건 '다' 쪽에서만 외래 키를 관리하는 건 아니다.)
- 객체 입장에선 이렇게 설계해도 괜찮지만, DB 입장에선 외래 키가 항상 '다' 쪽에 들어갈 수밖에 없다. Team의 memberes 값을 바꿨을 때 MEMBER 테이블의 TEAM_ID 외래 키가 업데이트된다.
- 객체와 테이블의 연관관계를 맺는 방법의 차이 때문에 반대편 테이블의 외래 키를 관리하는 특이한 구조를 갖게 된다.
- '일' 쪽에 @JoinColumn을 꼭 사용해야 한다. 그렇지 않으면 조인 테이블 방식을 사용해 중간에 테이블(TEAM_MEMBER 테이블)을 하나 추가한다.
엔티티가 관리하는 외래 키가 다른 테이블에 있기 때문에 연관관계를 위해 추가로 UPDATE SQL을 실행해야 한다는 단점이 있다. 따라서 참조를 더 넣는 한이 있더라도, 일대다 단방향 매핑보다는 다대일 양방향 매핑을 사용하자.
- 실무에서는 테이블이 한두 개로 끝이 아니라 수십 개가 엮여서 돌아간다. 그래서 예시처럼 TEAM 테이블에만 SQL이 실행되는 줄 알았는데 MEMBER 테이블에도 UPDATE SQL이 실행되는 걸 바로 이해하기가 어렵다.
참고
표준 스펙에 정의돼 있긴 하지만, 실무에서는 권장하지 않는다.
일대다 양방향 매핑
이런 매핑이 공식적으로 존재하진 않지만, 가능은 하다. Member 엔티티의 Team team에 @JoinColumn(insertable = false, updatable = false)을 추가하면 된다. 생성과 수정이 안 되는 읽기 전용 필드로 사용해서 양방향처럼 사용하는 방법이다.
@Entity
public class Member {
...
@ManyToOne
@JoinColumn(insertable = false, updatable = false)
private Team team;
...
}
참고
그냥 다대일 양방향 매핑을 사용하자.
@OneToMany
일대다 관계 매핑에서 사용된다. 속성은 아래와 같다.
속성 | 설명 | 기본값 |
mappedBy | 연관관계의 주인 필드를 선택한다. | |
fetch | 글로벌 페치 전략을 설정한다. | @OneToMany = FetchType.LAZY |
cascade | 영속성 전이 기능을 사용한다. | |
targetEntity | 연관된 엔티티의 타입 정보를 설정한다. 이 기능은 거의 사용하지 않는다. 컬렉션을 사용해도 제네릭으로 타입 정보를 알 수 있다. |
4. 일대일 [1:1]
일대일 관계는 그 반대도 일대일이다. 따라서 주 테이블이나 대상 테이블 중 외래 키를 둘 곳을 선택할 수 있다.
- 일대일 관계는 외래 키에 DB 유니크(UNIQUE) 제약조건을 추가해야 한다. 물론 필수는 아니지만, 넣지 않으면 애플리케이션 상에서 관리를 잘해야 한다.
주 테이블에 외래 키 단방향
다대일(@ManyToOne) 단방향 매핑과 유사하다. 사용하는 애노테이션(@OneToOne)만 다르다.
주 테이블에 외래 키 양방향
다대일 양방향 매핑처럼 외래 키가 있는 곳이 연관관계의 주인이 된다. 주인의 반대편은 mappedBy를 적용하는 것도 동일하다.
대상 테이블에 외래 키 단방향
대상 테이블에 외래 키를 두는 단방향 관계는 JPA에서 지원하지 않는다.
대상 테이블에 외래 키 양방향
일대일 주 테이블에 외래 키 양방향 매핑을 하는 것과 동일하게 하면 된다.
일대일 정리
주 테이블에 외래 키를 두는 경우,
- 주 객체가 대상 객체의 참조를 가지는 것처럼, 주 테이블에 외래 키를 두고 대상 테이블을 찾는다.
- JPA 매핑하기에 편리하고 성능상으로도 좋기 때문에 객체지향 개발자들이 선호한다.
- 장점
- 주 테이블만 조회해도 대상 테이블에 데이터가 있는지 확인할 수 있다.
- 단점
- 값이 없으면 외래 키에 null이 허용된다.
대상 테이블에 외래 키를 두는 경우,
- 대상 테이블에 외래 키가 존재하게 된다.
- 전통적인 데이터베이스 개발자들이 선호한다. (+DBA)
- 장점
- 주 테이블과 대상 테이블을 일대일에서 일대다 관계로 변경할 때 테이블 관계를 유지할 수 있다.
- 단점
- JPA에서 단방향을 지원하지 않아서 무조건 양방향으로 설계해야 한다.
- 프록시 기능의 한계로, 지연 로딩을 설정해도 항상 즉시 로딩된다. (테이블에 데이터가 있는지 확인하려면 어차피 한 번에 테이블을 로딩해야 하기 때문이다.)
5. 다대다 [N:M]
특징
RDB는 정규화된 테이블 2개로 다대다 관계를 표현할 수 없다. 따라서 연결(조인) 테이블을 중간에 추가해서 일대다, 다대일 관계로 풀어내야 한다.
객체는 컬렉션을 사용하면 객체 2개로 다대다 관계를 표현할 수 있다. 테이블과 객체가 너무 다르지만 ORM 입장에선 어떻게든 매핑해야 한다. @ManyToMany 애노테이션을 사용하고, @JoinTable로 연결 테이블을 지정하면 된다. 단방향과 양방향 모두 가능하다.
한계와 극복하는 방법
a. 한계
편리해 보이지만 실무에선 사용하지 않는다. 연결 테이블이 단순히 연결만 하고 끝나지 않기 때문이다.
- 예를 들어서, 연결 테이블에 주문시간, 수량 같은 데이터를 넣어야 되는 상황이 생기더라도 다대다 관계로 설정해 두면 데이터를 추가할 수가 없다.
- 엔티티상에는 연결 테이블이 숨겨져 있기 때문에 쿼리가 이상하게 나가는 경우도 생길 수 있다.
b. 극복 방법
연결 테이블용 엔티티를 추가하면 된다. 위에서 @JoinTable로 만든 연결 테이블을 엔티티로 승격시키면 된다.
- @ManyToMany를 @OneToMany와 @ManyToOne으로 쪼갠다.
참고
FK 2개를 하나의 PK로 쓰는 것보단 인조 식별자(@GeneratedValue)를 사용하는 게 낫다. 더 유연한 설계가 가능해진다.