관리 메뉴

너와 나의 스토리

로딩 전략(FetchType)과 영속성 전이(Cascade), 즉시 로딩(FetchType.EAGER)의 문제점 본문

개발

로딩 전략(FetchType)과 영속성 전이(Cascade), 즉시 로딩(FetchType.EAGER)의 문제점

노는게제일좋아! 2022. 12. 30. 14:54
반응형

로딩 전략

  • JPA 매핑을 설정할 때 항상 기억할 점: "애그리거트에 속한 객체가 모두 모여야 완전한 하나가 된다."
  • 다음과 같이 애그리거트 루트를 로딩하면 루트에 속한 모든 객체가 완전한 상태여야 함을 의미한다.
Product product = productRepository.findById(id);
  • 조회 시점에 애그리거트가 완전한 상태가 되도록 하려면, 연관 매핑의 조회 방식을 즉시 로딩(FetchType.EAGER)으로 설정하면 된다.
@Entity
@Table(name= "product")
public class Product {

    ...
    
    // @Entity 컬렉션에 대한 즉시 로딩 설정
    @OneToMany(cascade = {CascadeType.PERSIST, CascadeType.REMOVE}, orphanRemoval =true, fetch = FetchType.EAGER)
    @JoinColumn(name = "product_id")
    @OrderColumn(name= "list_idx")
    private List<Image> images = new ArrayList<>();
    
    // @Embeddable 컬렉션에 대한 즉시 로딩 설정
    @ElementCollection(fetch = FetchType.EAGER)
    @CollectionTable(name = "order_line", joinColumns = @JoinColumn(name="order_number"))
    @OrderColumn(name = "line_idx")
    private List<OrderLine> orderLines;
    
    ...
}

 

즉시 로딩 방식의 문제점

@Entity
@Table(name= "product")
public class Product {
    ...
    @OneToMany(cascade = {CascadeType.PERSIST, CascadeType.REMOVE}, orphanRemoval =true, fetch = FetchType.EAGER)
    @JoinColumn(name = "product_id")
    @OrderColumn(name= "list_idx")
    private List<Image> images = new ArrayList<>();
    
    @ElementCollection(fetch = FetchType.EAGER)
    @CollectionTable(name = "product_option", joinColumns = @JoinColumn(name="product_id"))
    @OrderColumn(name = "line_idx")
    private List<Option> options = new ArrayList<>();
    ...
}
  • 위 코드를 보면 Product 조회 시 Image와 Option 테이블을 조인한 쿼리가 실행될 것이다.
  • 두 테이블 모두, product_id를 기준으로 카타시안 조인(Cartesian Join)을 하는데 이 결과,  쿼리 결과에 많은 중복이 발생한다. 
  • Product의 Image가 N 개이고, Option이 M 개라면 실행 결과 쿼리는 (M*N) 개의 행일 리턴한다.
    • 실제 필요한 행의 개수는 (N+M+1) 개
  • 하이버네이트가 알아서 중복 데이터를 제거해서 리턴해 주지만, 데이터가 커지면 속도 저하 문제가 생길 수 있다.

 

애그리거트가 완전해야 하는 이유

  1. 상태를 변경하기 위해(연관된 데이터의 상태 수정)
  2. 애그러거트의 상태 정보를 보여줄 때

 

애그리거트 완전성을 위해 꼭 즉시 로딩을 사용해야 하는가?

  • 먼저 2번 이유라면, 
    • 즉시 로딩 대신 별도의 조회 전용 기능과 모델을 구현하는 방식을 사용하는 것이 더 유리하다.
  • 1번 이유라면,
    • 상태 변경을 위해 조회 시점에 즉시 로딩을 이용해 애그리거트를 완전한 상태로 로딩할 필요는 없다.
    • JPA는 트랜잭션 범위 내에서 지연 로딩을 허용하기 때문
    • 일반적인 애플리케이션은 상태 변경 기능보다 조회 기능을 실행하는 빈도가 훨씬 높다. 그래서, 상태 변경을 위한 지연 로딩을 사용할 때 발생하는 추가 쿼리는 보통 문제가 되지 않는다.
@Transactional
public void removeOptions(ProductId id, int optIdxToBeDeleted) {
        Product product = productRepository.findById(id); // Option 컬렉션은 지연 로딩으로 설정되어 있으므로, options를 제외하고 로딩
        product.removeOption(optIdxToBeDeleted); // 트랜잭션 범위이므로 지연 로딩으로 설정한 연관 로딩 가능
}
        
@Entity
@Table(name = "product")
public class Product {

    @ElementCollection(fetch = FetchType.LAZY)
    @CollectionTable(name = "product_option", joinColumns = @JoinColumn(name = "product_id"))
    @OrderColumn(name = "line_idx")
    private List<Option> options = new ArrayList<>();

    // 실제 컬렉션에 접근할 때 로딩
    public void removeOption(int optIdx) {
        this.options.remove(optIdx);
    }
}

 

 

영속성 전파

  • 애그리거트가 완전한 상태여야 한다는 것은 애그리거트 루트를 조회할 때뿐만 아니라 저장 및 삭제할 때도 하나로 처리해야 함을 의미한다.
    • 예: 저장 메서드는 애그리거트 루트만 저장하면 안 되고 애그리거트에 속한 모든 객체를 저장해야 한다.
  • @Embeddable 매핑 타입은 함께 저장되고 삭제되므로 cascade 속성을 추가로 설정하지 않아도 된다. 
  • 하지만, @Entity 타입에 대한 매핑은 cascade 설정이 필요하다.
@OneToMany(cascade = {CascadeType.PERSIST, CascadeType.REMOVE}, orphanRemoval = true)
  •  종류(CascadeType)
    • ALL: 모든 경우 상태 전파
    • PERSIST: 엔티티를 저장할 때, 연관된 하위 엔티티도 모두 저장
    • MERGE: 엔티티 상태를 병합할 때, 연관된 하위 엔티티도 모두 병합
    • REMOVE: 엔티티 제거할 때, 연관된 하위 엔티티도 모두 제거
    • REFRESH: 엔티티를 새로고침할 때, 하위 엔티티도 모두 새로고침
    • DETACH: 연관된 하위 엔티티에 변경 사항 반영하지 않는다.

 

 

참고

- [도메인 주도 개발 시작하기]

반응형
Comments