JPA N + 1 문제 해결하기

0_0_yoon·2023년 10월 9일
0

문제상황

JPA 를 사용할 때 특정한 상황에서 추가적인 쿼리가 실행된다.
이때 DB ConnectionPool(Spring 의 경우 HikariCP 사용)은 한정 자원이므로 DB Connection 사용은 줄이는 게 좋다.

원인

JPA 의 @JoinColumn 을 사용하여 연관관계 객체를 삽입할 때 추가적인 쿼리가 실행된다.(Fetch Type 과 상관없이 발생)
예를 들어 메뉴가 메뉴 상품과 함께 조회되어야 한다면 아래와 같이 연관 관계를 맺어준다.

@Entity
class Menu {
	// 생략
	@OneToMany
	@JoinColumn(name = "menu_id")
    private List<MenuProduct> menuProducts;
}

@Entity
class MenuProduct {
	// 생략
}

이때 메뉴 전체를 조회한 뒤(1) 각 메뉴의 메뉴 제품에 접근할 때 추가 쿼리가 실행된다(N).(메뉴 전체 조회 쿼리 한 번, 조회된 메뉴의 메뉴 제품을 삽입하기 위한 쿼리 N 번)

@DataJpaTest 주의사항


하나의 테스트 메서드 안에서 n+1 문제를 확인하려 할 때 테스트를 위한 세팅(given 절)을 마친 뒤에 영속성 컨텍스트를 비워줘야 한다. 그렇지 않으면 지연 로딩이 걸리지 않고 1차 캐시에 있는 객체들이 재사용되어 n+1 문제를 확인할 수 없다.

당연히 통합테스트, 인수테스트에서는 해당 사항 없음, 서로 다른 트랜잭션에서 실행되므로.

해결

join fetch 사용

@Query("SELECT DISTINCT m from Menu m join fetch m.memuProducts")
List<Menu> findAll();

위와 같이 @Query 에 직접 JPQL 을 작성한다. 이때 join fetch 을 사용한다. JPQL 의 join fetch 를 사용하게 되면 join 의 대상이 되는 엔티티도 영속화된다.(일반 join 의 경우 당연히 영속화되지 않는다)

중복제거

1 : N 관계에서 1 에 해당하는 엔티티를 조회할 때 주의할 점이 있다.
우리가 1 을 조회할 때 1 에 N 이 당연히 포함되는 관계지만 DB 에서는 inner join 해서 조회하기 때문에 N 의 개수만큼(1*N,곱집합) 튜플이 조회된다. 즉 메뉴를 1 개 조회하더라도 메뉴에 포함된 메뉴 제품이 3 개라면 메뉴가 3 개 조회된다. 이 때 DISTINCT 키워드를 사용해서 중복을 제거하면 해결할 수 있다.(DISTINCT 는 애플리케이션 레벨에서 적용된다)

@EntityGraph 사용

@EntityGraph(attributePaths = "menuProducts")
List<Menu> findAll();

@EntityGraph 를 사용하면 join fetch 와 마찬가지로 필요 엔티티를 join 해서 조회한다.(join fetch 의 경우 inner join, @EntityGraph 의 경우 left outer join 이 실행된다)

정리

연산에 필요한 쿼리와 연관관계를 맺어주기 위한 쿼리를 한 번에 실행함으로써 DBConnection 의 사용을 줄인다. 그러면 우리 서버는 더 많은 연산(DB 접근이 필요한 연산)을 처리할 수 있다.

profile
꾸준하게 쌓아가자

0개의 댓글