Spring

[JPA 활용 2편] API 개발 고급 - 컬렉션 조회 최적화

gogi masidda 2024. 7. 5. 12:19

컬렉션인 일대다 관계 (OneToMany)를 조회하고 최적화하기. 

일인 쪽이 하나고, 다 쪽이 3개면 전체 row가 3줄로 뻥튀기가 된다. 이 경우에는 최적화하기 어렵다. 

 

  • 주문 조회 V1: 엔티티 직접 노출
    • 엔티티를 직접 노출하기 때문에 사용하면 안된다. 
  • 주문 조회 V2: 엔티티를 DTO로 변환
    • DTO안에 엔티티가 있으면 안된다. 이 경우에도 다 노출이 되어버린다. 
      • 엔티티에 대한 의존을 완전히 끊어야 한다. 
      • 속에 있는 엔티티도 DTO를 만들어서 바꿔주어야 한다.
    • 하지만 N+1 문제 발생
  • 주문 조회 V3: 엔티티를 DTO로 변환 - 페치 조인 최적화
    • 데이터가 뻥튀기 된다고 하셨는데 Hibernate6부터는 distinct가 되어서 뻥튀기 되지 않음.
    • hibernate5를 사용하면 em.createQuery()를 할 때, distinct를 붙여줘야함.
    • 하지만 페이징 ( setFirstResult(), setMaxResults() ) 이 불가능하다. 
      • 메모리에서 페이징을 해버린다. -> 잘못하면 out of memory 
      • 게다가 DB에서 distinct는 완전히 내용이 같아야 중복처리되는데 내용이 완전히 같지 않아서 뻥튀기가 된 채로 페이징이 되어도 원하는 결과가 나오지 않는다.
    • 컬렉션 두개 이상에 페치 조인을 사용하면 안된다. -> 1대다의 다가 되어서 n*m이 되어 뻥튀기가 심해진다. 
  • 주문 조회 V3.1: 엔티티를 DTO로 변환 - 페이징과 한계 돌파
    • 페이징도 하면서 컬렉션 엔티티를 함께 조회하기
      • xToOne 관계는 모두 페치 조인으로. 뻥튀기가 되는 부분이 아니므로
      • 컬렉션은 지연 로딩으로 조회
        • 지연 로딩 성능 최적화를 위해
          • hibernate.default_batch_fetch_size: 글로벌 설정
            • 100 ~ 1000 사이를 선택하는 것을 권장. 1000으로 설정하면 순간 부하가 증가할 수 있다. 전체 데이터를 불러와야 해서 결국 총 메모리 사용량은 같아진다. 순간 부하를 얼마나 견딜 수 있는지로 결정하면된다.
          • @BatchSize: 개별 최적화
          • 이 옵션을 사용하면 컬렉션이나, 프록시 객체를 설정한 size만큼, in 쿼리로 한꺼번에 조회한다. 
    • N + 1문제가 사라지고, 조인보다 DB 데이터 전송량이 최적화된다.  페치 조인 방식과 비교해서 쿼리 호출 수가 약간 증가하지만, DB 데이터 전송량이 감소한다.
    • 위 방식으로 하면 컬렉션도 페이징이 가능해진다. 
  • 주문 조회 V4: JPA에서 DTO 직접 조회
    • 쿼리는 루트 한번 컬렉션 N+1번
    • ToOne 관계를 먼저 조회하고, 컬렉션(ToMany)은 각각 별도로 처리
    • row 수가 증가하지 않는 ToOne 관계는 조인으로 최적화하고, ToMany 관계는 최적화하기 어려우므로 각각 별도로 처리 
    • 하지만 컬렉션 조회에서 N+1문제가 터짐. 
  • 주문 조회 V5: JPA에서 DTO 직접 조회 - 컬렉션 조회 최적화
    • 쿼리는 루트 한번 컬렉션 한번. in절을 사용해서 한번에 가져옴.
    • ToOne 관계를 먼저 조회하고, 여기서 얻은 식별자 orderId로 ToMany 관계인 OrderItem을 한꺼번에 조회
    • Map을 사용해서 매칭 성능 향상
  • 주문 조회 V6: JPA에서 DTO 직접 조회, 플랫 데이터 최적화
    • 쿼리 한번
    • 쿼리를 한방에 보내고, 뻥튀기가 된 결과를 애플리케이션에서 중복 제거하면서 발라내는 방식
    • 페이징 불가능

정리

권장 순서

  • 엔티티 조회 방식으로 우선 접근
    • 페치 조인으로 쿼리 수를 최적화
    • 컬렉션 최적화
      • 페이징 필요 O ->  hibernate.default_batch_fetch_size, @BatchSize로 최적화 (V3.1 방식)
      • 페이징 필요 X -> 페치 조인 사용 
  • 엔티티 조회 방식으로 해결이 안되면 DTO 조회 방식 사용
  • DTO 조회 방식으로 해결이 안되면 NativeSQL or 스프링 JdbcTemplate

엔티티 조회 방식은 옵션( hibernate.default_batch_fetch_size, @BatchSize 등등 )만 약간 변경해서 다양한 최적화를 시도할 수 있기 때문에, 엔티티 조회 방식을 우선 사용하는 것을 권장. 

반면에 DTO를 직접 조회하는 방식은 성능을 최적화하거나 최적화 방식을 변경할 때 많은 코드를 변경해야함.

 

DTO 조회 방식의 선택지

  • DTO로 조회하는 방식도 각각 장단이 있다. V4, V5, V6에서 단순하게 쿼리가 한번 실행된다고 V6가 좋은게 아니다.
  • V4: 코드가 단순함. 특정 주문 한건만 조회하면 성능이 잘나옴. 
  • V5: 코드가 복잡함. 여러 주문을 조회하는 것이라면 V4보다는 V5 방식을 사용해야 함. 
  • V6: 완전히 다른 접근 방식. 쿼리 한번이라 좋아보이지만, Order를 기준으로 페이징이 불가능하다. 선택하기 어려운 방식이고, 데이터가 많은 V5와 비교해도 성능 차이도 미비하거나 V5가 더 좋을 수도 있다.
728x90