[JPA] N+1 문제 원인 및 해결
오늘은 N+1 문제가 왜 발생되는지 원인과 함께 해결방법에 대해 간략하게 정리해보려고 한다.
N+1 문제는 데이터베이스와 관련된 성능 문제로, 일반적으로 JPA와 같은 ORM 프레임워크에서 자주 발생한다.
N+1 문제
1번의 쿼리를 날렸을 때, N번의 추가 쿼리가 발생하는 것을 의미한다.
N+1 문제 발생 상황
언제 발생하는가?
JPA Repository를 활용해 인터페이스 메서드를 호출할 때.
누가 발생시키는가?
1:N 또는 N:1 관계를 가진 엔티티를 조회할 때.
어떤 상황에 발생되는가?
즉시 로딩 : JPA Fetch 전략이 EAGER 전략으로 데이터를 조회하는 경우
지연 로딩 : JPA Fetch 전략이 LAZY 전략으로 데이터를 가져온 이후 연관 관계인 하위 엔티티를 다시 조회하는 경우
왜 발생하는가?
JPA Repository로 find 시, 첫 쿼리에서 하위 엔티티까지 한 번에 가져오지 않고, 하위 엔티티를 사용할 때 추가로 조회하기 때문에 발생
JPQL은 기본적으로 글로벌 Fetch 전략을 무시하고 JPQL만 가지고 SQL을 생성하기 때문
즉시 로딩(Eager Loading) 인 경우
연관된 엔티티를 기본 쿼리와 함께 가져오는 방식
- JPQL에서 생성된 SQL을 통해 기본 엔티티 조회
- 이후 JPA가 Fetch 전략에 따라 연관 관계에 있는 하위 엔티티들을 추가적으로 조회
- 이로 인해 N+1 문제 발생. 즉, 첫 번째 쿼리로 1번의 조회가 발생하고, 이후 연관된 엔티티를 개별적으로 N번 추가 조회하는 상황
지연 로딩(Lazy Loading) 인 경우
연관된 엔티티를 실제로 사용할 때 가져오는 방식
- JPQL에서 생성된 SQL을 통해 기본 엔티티 조회
- 이후 JPA가 Fetch 전략에 따라 연관된 엔티티를 가져오지 않고, 필요할 때만 데이터를 조회
- 그러나 연관된 엔티티에 접근하는 시점에 추가 조회가 발생하며, 결과적으로 N+1 문제가 발생
문제 해결 방법
1. Fetch Join
연관된 다른 테이블을 따로 조회하기 때문에 N+1 문제가 발생한다. 따라서, 미리 두 테이블을 JOIN하여 한 번에 모든 데이터를 가져올 수 있다면 문제가 발생하지 않을 것이다.
하지만 jpaRepository에서 제공해주는 것은 아니고 JPQL 쿼리에서 JOIN FETCH를 사용한다. 실제로 실행해보면 Inner Join해서 가져오는 것을 볼 수 있다.
하지만, Fetch Join을 사용하면 데이터 호출 시점에 모든 연관 관계의 데이터를 가져오기 때문에 FetchType.LAZY 설정이 무시된다. 또한, 하나의 쿼리로 모든 데이터를 가져오게 되므로, 페이징 쿼리를 사용할 수 없다.
2. @Entity Graph
JPA의 @EntityGraph를 사용하여 특정 엔티티를 조회할 때 연관된 속성을 지정하여 Eager 로딩하도록 설정하는 방법이다. Fetch Join과 동일하게 JPQL을 사용하며 query 문을 작성하고 필요한 연관관계를 EntityGraph에 설정하면 된다.
Fetch Join과는 다르게 Outer Join을 사용하여 데이터를 조회한다. 이로 인해 Inner Join보다 성능이 떨어질 수 있다.
3. BatchSize
@BatchSize 어노테이션을 이용하면 연관된 엔티티를 배치 단위로 조회하는 방법이다. SQL 쿼리에서 'IN' 절을 사용하여 N개의 엔티티를 일괄 조회할 수 있다. 연관된 엔티티를 배치 단위로 조회하므로, 여러 개의 추가 쿼리가 발생하지 않고, 성능을 개선할 수 있다.
🔻위의 내용들을 참고해서 실제 프로젝트에 적용했던 내용
1:N 관계인 좋아요 테이블은 @BatchSize 적용 / N:1 관계인 유저 테이블은 Fetch Join 적용
[부하테스트/JPA] Artillery로 성능 향상을 위한 N+1 문제 해결 과정
마지막 요구사항인 부하테스트를 artillery를 이용해 진행해 보니 게시물 조회 API에 대한 성능 문제를 발견할 수 있었다. 쿼리문을 확인해 보니 여러 개의 select 문이 찍혀있는 것을 확인할 수 있었
auny.tistory.com
참고 자료