JPA 프로그래밍 입문 - chapter 12 연관 잘 쓰기

1. 연관의 복잡성

1.1 로딩 설정의 어려움

  • 일관적으로 모든 연관에 즉시 로딩을 적용하진 않는다. 엔티티를 어떤 식으로 사용할지 미리 고민해서 지연로딩과 즉시로딩을 적절하게 적용해야 한다.
  • 상황에 따라 필요한 연관 객체가 다르기 때문에 특정 연관을 지연 로딩이나 즉시 로딩으로 한정할 수 없다.
    • 즉 지연 로딩과 즉시 로딩을 적절하게 섞어 쓰는 것이 쉽지만은 않다. 같은 엔티티라도 기능에 따라서 함께 사용하는 연관 대상이 달라지기 때문이다.

1.2 편리한 객체 탐색과 높은 결합도

  • 모든 엔티티를 연관으로 연결하면 객체 탐색을 통해서 쉽게 원하는 객체에 접근할 수 있다.
  • 한 엔티티에서 다른 엔티티와 연관을 맺고 다른 엔티티의 메소드 등을 실행하면 엔티티가 서로 강하게 엮이게 되면서 서로 수정을 어렵게 만드는 원인이 될 수 있다.

2. 연관 범위 한정과 식별자를 통한 간접 참조

  • 엔티티 간의 참조가 많아질수록 한 엔티티의 기능을 변경할 때 여러 엔티티를 함께 수정해야 할 가능성이 커진다. 이는 코드 변경을 어렵게 만드는 원인이 될 수 있다.
  • 다음 두가지 규칙을 생각하면서 연관을 적용하는게 좋다.
    • 연관의 범위를 도메인 기준으로 한정
    • 도메인을 넘어서는 엔티티간에는 식별자를 이용한 간접 참조 사용
public class Order {
    
    private List<OrderLine> orderLines; // 영역 내 모델은 직접참조
    private String userId;              // 영역 밖의 엔티티는 식별자로 참조
    
    ...
}
  • 식별자를 통한 간접 참조 방식을 사용하면 식별자로 연관된 엔티티를 검색하는 과정이 추가된다. 하지만 식별자를 통한 간접 참조를 사용하면 “로딩 설정의 어려움”, “엔티티 간의 결합도 증가” 문제를 완화할 수 있다.

3. 상태 변경 관련 기능과 조회 관련 기능

  • 연관을 한정해서 사용하면 설정이나 코드 복잡도가 줄어드는 장점이 있다. 하지만 데이터를 조회할 때 여러 엔티티를 직접 조회해야 하는 불편함도 있다.
  • 위와 같은 문제를 해결하는 방법 중 하나는 상태를 변경하는 기능과 조회하는 기능을 분리해서 생각하는 것이다.
  • 상태 변경 관련 기능은 한 도메인 범위에 속한 엔티티의 데이터만 수정하는 경향이 있다. 상태 변경 기능은 한 두개의 엔티티만 로딩하기 때문에 식별자를 사용해서 간접 참조했을 때 장점이 더 크다.
  • 조회 관련 기능은 한 개 이상의 엔티티를 함께 조회하는 경우가 많다. 여러 엔티티의 데이터를 조합해야 하는 조회 기능은 조회 기능에 맞는 모델을 따로 구현하는 것을 고려해보면 좋다.
  • 도메인이 커질수록 한 개의 모델로 상태 변경 기능과 조회 기능을 구현하기 어려워진다. 로딩 방식 문제뿐만 아니라 상태 변경 시점과 조회 시점에 필요한 데이터가 다르기 때문이다.
    • 조회 시점에 필요한 데이터와 변경 시점에 다루는 데이터의 차이가 클수록 조회 전용 모델을 별도로 만들 것을 고려해봐야 한다.

4. 식별자를 공유하는 1:1 연관이 엔티티와 밸류 관계인지 확인

  • 모든 테이블을 엔티티로 매핑하는 것은 모델의 의미를 약화시킬 수 있다.
  • 한 도메인 영역에 속하면서 식별자 공유 방식으로 1:1 연관을 맺는 두 엔티티가 동일한 라이프사이클을 갖는다면 이 관계는 두 엔티티의 1:1 연관이 아닌 엔티티와 밸류 관계일 가능성이 크다.
@Entity
@SecondaryTable(
	name = "appeal_status",
    pkJoinColumns = @PrimaryKeyJoinColumn(
    	name = "id",
        referencedColumnName = "id"
    )
)
public class Appeal {
    @Id
    private String id;
    @Embedded
    private AppealStatus status;
    ...
}

@Embeddable
public class AppealStatus {
    @Id
    private String id;   
    ...
}
  • 식별자를 공유하는 두 엔티티의 1:1 연관은 두 테이블을 사용하는 엔티티-밸류 관계인지 확인할 필요가 있다.

5. 엔티티 콜렉션 연관과 주의 사항

  • 1:N 단방향 연관, 양방향 연관은 코드를 더 복잡하게 만든다.

5.1 1:N 연관보다 N:1 연관 우선

  • 1:N 연관을 사용할 때 주의할 점은 N에 해당하는 부분을 실제 기능에서 어떤 식으로 사용하는지 알아야 한다는 점이다. 만약 1:N 연관에서 콜렉션에 보관된 엔티티를 일부만 사용한다면 N:1 연관을 대신 사용하는게 좋다. 왜냐하면 일부만 사용하더라도 1:N 연관에서는 전체 데이터를 로딩하기 때문에 성능 문제가 발생할 수 있기 때문이다.
  • N:1 연관이 더 단순하기 때문에 연관으로 인해 발생하는 코드의 복잡함도 줄여준다.

5.2 엔티티 간 1:N 연관과 밸류 콜렉션

  • 1:N 연관에서도 엔티티에 대한 콜렉션이 아니라 밸류에 대한 콜렉션이 적합할 때가 있다. 단순히 테이블이 따로 존재한다고 해서 엔티티 간의 1:N 연관으로 매핑하는 것은 옳지 않다. 1:N 연관이 필요하다면 해당 연관이 엔티티 간의 연관인지 밸류 콜렉션인지 검토해야한다.
  • 한 도메인 영역에 속하면서 1:N 연관을 맺는 엔티티가 동일한 라이프사이클을 갖는다면 엔티티 콜렉션이 아닌 밸류 콜렉션이 더 적합하지 않은지 확인해라

5.3 M:N 연관 대체하기 : 연관 엔티티 사용

  • M:N 연관은 구현을 복잡하게 만들기 때문에 가능하면 피하는게 좋다.
  • M:N 연관을 회피하는 방법으로는 다음을 사용한다.
    • 연관 엔티티 사용
    • 한쪽 엔티티에 밸류 콜렉션으로 연관 정보 저장
  • 연관 엔티티 사용 : 조인 테이블을 엔티티로 매핑하는 것이다. M:N 연관으로 코드가 복잡해지는것보다 더 나은 선택이다. 직접적인 M:N 연관을 가졌을때와 달리 연관된 엔티티를 조회하기 위해선 JPQL을 사용해야 한다는 단점도 있다.