연관관계 매핑
키워드
- 방향 (Direction) : 테이블 관계는 항상 외래키를 사용한 양방향이고 객체 관계는 참조를 통한 단방향
- 연관관계의 주인 (Owner) : 객체를 양방향 연관관계로 만들면 연관관계의 주인을 정해야 한다.
객체 연관관계와 테이블 연관관계의 차이

- 객체는 참조(주소 a.getB())로 연관관계를 맺는다. 참조를 사용하는 객체의 연관관계는 단방향이다.
- 객체를 양방향으로 참조하려면 단방향 연관관계를 2개 만들어야 한다.

- 테이블은 외래 키로 양방향이다 연관관계를 맺는다. 외래 키 하나로 양방향 조인할 수 있다.
- A JOIN B, B JOIN A
양방향 연관관계
- 단방향 매핑만으로 테이블과 객체의 연관관계 매핑은 완료
- 단방향을 양방향으로 만들면 반대방향으로 객체 그래프 탐색 기능이 추가된다.
- 양방향 연관관계를 매핑하려면 객체에서 양쪽 방향을 모두 관리해야 한다.
- 연관관계 편의메서드에서 로직을 견고하게 작성할 것
- 먼저 단방향 매핑을 사용하다가 반대 방향의 객체 그래프 탐색이 필요할 때 양방향을 사용하도록 코드를 추가해도 된다.
- lombok toString() 등 호출시 무한루프에 빠지지 않도록 주의, lombok toString()→ exclude옵션

연관관계의 주인
- 양방향 연관관계 매핑 시, 두 연관관계 중 하나를 연관관계의 주인으로 정해야 한다.
- 연관관계의 주인만 데이터베이스 연관관계와 매핑되고 외래 키를 관리(등록, 수정, 삭제)할 수 있다. 반면에 주인이 아닌 쪽은 읽기만 할 수 있다.
- 연관관계의 주인은 외래 키가 있는 곳으로 설정한다. 연관관계의 주인은 외래키의 위치와 관련해서 정해야지 비즈니스 중요도로 접근하면 안된다.
- 데이터베이스 테이블의 다대일, 일대다 관계에서는 항상 다 쪽이 외래키를 갖는다. 다 쪽인 @ManyToOne은 항상 연관관계의 주인이 되므로 mappedBy를 설정할 수 없다. 따라서 @ManyToOne에는 mappedBy 속성이 없다.
다중성
@ManyToOne
속성 | 기능 | 기본값 |
optional | false로 설정하면 연관된 엔티티가 항상 있어야 한다. | true |
fetch | 글로벌 패치 전략 설정 | FetchType.EAGER |
cascade | 영속성 전이 기능 | ㅤ |
@OneToOne
- 주 테이블, 대상 테이블 둘 다 외래 키를 가질 수 있다.
- 프록시를 사용할 때 외래키를 직접 관리하지 않는 일대일 관계는 지연 로딩으로 설정해도 즉시 로딩된다.
- One-To-One 과 Lazy Loading 항목 참고
@OneToMany
@ManyToMany
고급 매핑
상속 관계 매핑
- @Inheritance(strategy = InheritanceType.JOINED)
- 각각의 테이블로 변환 - 모두 테이블로 만들고 조회할 때 조인을 사용하는 조인 전략
- @DiscriminatorColumn(name=”DTYPE”), @DiscriminatorValue(”M”)
- 장점
- 테이블이 정규화된다
- 외래 키 참조 무결성 제약조건 활용할 수 있다.
- 저장공간을 효율적으로 사용
- 단점
- 조회할 때 조인이 많이 사용되므로 성능이 저하될 수 있다.
- 조회 쿼리가 복잡하다
- 데이터를 등록할 때 INSERT SQL을 두 번 실행한다.
- @Inheritance(strategy = InheritanceType.SINGLE_TABLE)
- 통합 테이블로 변환 - 테이블을 하나만 사용해서 통합, 단일 테이블 전략
- 장점
- 조인이 필요 없으므로 일반적으로 조회 성능이 빠르다
- 조회 쿼리가 단순
- 단점
- 자식 엔티티가 매핑한 컬럼은 모두 null을 허용해야 한다.
- 단일 테이블에 모든 것을 저장하므로 테이블이 커질 수 있다. 상황에 따라서 조회 성능이 오히려 느려질 수 있다.
- 특징
- 구분 컬럼을 꼭 사용해야 한다. @DiscriminatorColumn을 꼭 설정해야 한다.
- @DiscriminatorValue를 지정하지 않으면 기본으로 엔티티 이름을 사용
- @Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
- 서브타입 테이블로 변환 - 서브 타입마다 하나의 테이블 생성, 테이블 전략
- 장점
- 서브 타입을 구분해서 처리할 때 효과적
- not null 제약 조건 사용 가능
- 단점
- 여러 자식 테이블을 함께 조회할 때 성능이 느리다
- 자식 테이블을 통합해서 쿼리하기 어렵다
- 특징
- 구분 컬럼을 사용하지 않는다. (@Discriminator...)
- 비추천 ( 조인이나 단일 테이블 전략을 사용할 것)
- MappedSuperclass
- 자식 클래스에 엔티티의 매핑 정보를 상속하기 위해 사용
- 이 클래스를 직접 생성해서 사용할 일은 거의 없으므로 추상 클래스로 만드는 것을 권장
- 테이블과는 관계가 없고 단순히 엔티티가 공통으로 사용하는 매핑 정보를 모아주는 역할
- 복합 키와 식별 관계 매핑
프록시와 연관관계 관리
프록시
- JPA 표준 명세는 지연 로딩의 구현 방법을 JPA 구현체에 위임했다. 구현체마다 동작이 다를 수 있다. 여기서는 하이버네이트 기준. 하이버네이트는 지연 로딩을 지원하기 위해 프록시를 사용하는 방법, 바이트 코드를 수정하는 방법 두 가지 방법을 제공.
- 특징
- 프록시 객체는 처음 사용할 때 한 번만 초기화
- 프록시 객체를 초기화한다고 프록시 객체가 실제 엔티티로 바뀌는 것은 아님
- 프록시 객체가 초기화되면 프록시 객체를 동해서 실제 엔티티에 접근가능
- 초기화는 영속성 컨텍스트의 도움을 받아야 가능하다. 따라서 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태의 프록시를 초기화하면 문제가 발생한다. → 하이버네이트의 LazyInitializationException
- 엔티티를 프록시로 조회할 때 식별자 PK 값을 파라미터로 전달하는데 프록시 객체는 이 식별자 값을 보관한다.
- 프록시 객체의 초기화 여부 확인
PersistenceUnitUtil.isLoaded(Object entity)
사용 가능
즉시 로딩
- 엔티티를 조회할 때 연관된 엔티티도 함께 조회한다.
- 대부분의 JPA 구현체는 즉시 로딩을 최적화하기 위해 가능하면 조인 쿼리를 사용
지연 로딩
- 연관된 엔티티를 프록시로 조회, 프록시를 실제 사용할 때 초기화하면서 데이터베이스를 조회한다.
JPA 기본 페치 전략
- 연관된 엔티티가 하나면 즉시 로딩, 컬렉션이면 지연로딩 사용
fetch 속성의 기본 설정값
- ManyToOne, OneToOne : 즉시 로딩
- OneToMany, ManyToMany : 지연 로딩
⇒ 일단 모든 연관관계에 지연 로딩하는 것을 추천, 개발 완료 단계에서 실제 사용하는 상황을 보고 꼭 필요한 곳에만 즉시 로딩을 사용하도록 최적화
CASCADE
- 엔티티를 영속화할 때 연관된 엔티티도 같이 영속화하는 편리함 제공
고아 객체
- 부모 엔티티의 컬렉션에서 자식 엔티티의 참조만 제거하면 자식 엔티티가 자동으로 삭제
- 참조가 제거된 엔티티는 다른 곳에서 참조하지 않는 고아 객체로 보고 삭제하는 기능 → 참고하는 곳이 하나일 때만 사용해야 함. 만약 삭제한 엔티티를 다른 곳에서도 참조한다면 문제가 발생할 수 있다.
- 부모를 제거할 때 자식은 고아가 되서 삭제됨. CascadeType.REMOVE