컬렌션(일대다) SELECT 시, 주의점과 성능 최적화

2021. 2. 4. 15:07JPA/JPA

컬렉션 조회의 문제점

일대다 관계의 컬렉션을 조회하게 될때 데이터는 기준 데이터(루트)보다 훨씬 많은 양의 데이터가 조회될 수 있다. 데이터 양도 문제지만 루트를 기준으로 페이징을 해야할때 데이터가 더 많아지기때문에 페이징이 불가능해진다.

컬렉션 조회 개선점

1. N+1 문제를 해결하기 위해 fetch조인을 고려하자

일대다 관계는 지연로딩을 기본으로 두고 있다. 루트를 조회하고 그 결과 개수에 따라 데이터를 가져오기 위해 프록시를 강제 초기화를 N번 수행하게 된다. 루트의 결과 데이터 개수에 따라 DB와의 커넥션 작업이 많아지게 되고 성능을 악화시키기 때문에 fetch 조인을 사용하여 한번의 쿼리로 데이터를 엔티티에 바인딩해주는 방법을 고려해보자.

일대다 관계이기 때문에 루트 데이터를 기준으로 엔티티를 조회하고 싶다면 distinct를 사용하자.

JPA의 distinct는 DB의 SQL에 모든 컬럼에 대해서 distinct를 수행하고, 어플리케이션에서 엔티티를 식별자 기준으로 다시 중복을 제거해주는 기능이 있다.

2. 페이징이 필요한 경우는 fetch 조인으로 구현이 불가하다

일대다 관계에서 루트를 기준으로 페이징이 필요한 경우, DB에서 루트 기준으로 페이징이 될 수 없기 때문에(루트 데이터 중복이 일어남) JPA입장에서는 모든 데이터를 전부 어플리케이션으로 가져와서 페이징 처리를 한다(굉장히 위험하다.) 모든 데이터를 가져오기 때문에 메모리풀이 날 수도 있고 성능이 떨어진다.

3. 페이징이 필요한 경우, 컬렌션을 지연로딩으로 놔두고 N+1문제를 해결하기 위해 IN 쿼리를 사용하자

1번과 같은 시점에서 N번이 아닌 SQL의 IN절을 이용하여 N을 적당히 나누는 방법이다. JPA에서는 

  • spring.jpa.properties.hibernate.default_batch_fetch_size
  • @BatchSize(일대다는 컬렉션에 다대일은 클래스에 적용)

로 IN절을 지원한다. 해당 size를 100~1000으로 적당히 설정하면 설정한 수만큼 IN절에 데이터가 세팅되어 쿼리가 나가게 된다. DB에 따라 IN절에 데이터를 1000개로 제한하는 경우가 있어 1000개를 최대치로 잡는다.

이 방법은 2번처럼 중복 데이터가 발생하지 않아 루트데이터 기준으로 페이징이 가능하게 된다.

권장순서

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