JPA를 사용할 때 N+1 문제는 성능상 가장 주의해야하는 부분 중 하나이다.
부서와 사원 같은 1:N 관계의 엔티티 구조에서
부서만 먼저 조회한 뒤 나중에 부서에 속한 사원 목록에 접근할 때
조회한 부서 수만큼 사원 목록 조회 쿼리가 수행되면서 성능 문제가 발생하는 경우라고 할 수 있다.
N+1 번 쿼리가 수행된다고 해서 N+1 문제라고 부른다.
1은 처음에 부서 목록 조회한 쿼리, N은 조회한 부서 개수만큼 수행되는 사원 목록 조회 쿼리.
부서 엔티티에서 사원 목록 fetch 설정을 즉시 로딩(EAGER), 지연 로딩(LAZY)으로 하던 모두 발생할 수 있다.
즉시 로딩
다음과 같이 EntityManager를 통해 조회하면 즉시 로딩 설정된 사원 목록이 같이 조회되기 때문에 N+1 문제가 발생하지 않는다.
EntityManager#find(Department.class, departmentId);
하지만 JPQL, QueryDSL을 사용해 다음과 부서 정보만 조회하면 사원 정보는 함께 조회되지 않고
이후에 즉시 로딩을 위해 사원 목록 조회가 추가적으로 실행된다.
jpaQueryFactory.selectFrom(department)
.where(department.id.eq(departmentId))
.fetch();
지연 로딩
지연 로딩으로 설정 시 EntityManager를 통해 조회하던, JPQL, QueryDSL을 사용하던 조회 시점에는 N+1 문제가 발생하지 않는다.
하지만 로직에서 부서의 사원 목록 필드를 사용하는 시점에 지연 로딩이 발생해서 쿼리가 수행되게 된다.
# 해결 방법
fetch join 사용
가장 일반적인 방법은 fetch join을 사용하는 것이다.
다음과 같이 JPQL을 실행한다. 참고로 join fetch 문법은 SQL에서 지원하는 문법은 아니고 JPQL에서 지원되는 문법이다.
SELECT d from Department d join fetch d.employees
QueryDSL로는 다음과 같이 작성할 수 있다.
jpaQueryFactory.selectFrom(department)
.leftJoin(department.employees).fetchJoin()
.fetch();
기타 방법
하이버네이트 @BatchSize
org.hibernate.annotations.BatchSize 어노테이션을 사용해서
연관 엔티티 조회 시 지정한 size만큼 SQL IN 절을 사용해 조회하는 방법이다.
@BatchSize(size = 10)
@OneToMany(mappedBy = "deparment", fetch = FetchType.EAGER)
private List<Employee> employees = new ArrayList<>();
즉시 로딩 설정 시 : 조회 시점에 쪼개서 여러 번 조회해둠
지연 로딩 설정 시 : 데이터를 사용하는 시점에 그 때 그 때 조회함
하이버네이트 @Fetch(FetchMode.SUBSELECT)
org.hibernate.annotations.Fetch 어노테이션 사용해서
연관 엔티티 조회 시 서브 쿼리를 사용하도록 한다.
@Fetch(FetchMode.SUBSELECT)
@OneToMany(mappedBy = "deparment", fetch = FetchType.EAGER)
private List<Employee> employees = new ArrayList<>();
아이디가 10을 초과하는 부서를 조회했다면 다음과 같은 서브 쿼리가 수행되게 된다.
SELECT e FROM employee e WHERE e.department_id IN (
SELECT d.department_id FROM department d WHERE d.department_id > 10
)
# 정리
가장 좋은 방법은 지연 로딩을 사용하고 성능 최적화가 필요한 곳에 fetch join을 적용하는 것이다.
참고 자료 : 자바 ORM 표준 JPA 프로그래밍
'개발 > 자바' 카테고리의 다른 글
JVM 트러블 슈팅 및 분석을 위한 명령어들 (0) | 2021.03.08 |
---|---|
Java Metaspace에 대해서 (0) | 2020.10.22 |
sun.reflect.GeneratedMethodAccessor 클래스에 대해서 (0) | 2020.10.13 |
자바 성능을 결정짓는 코딩 습관과 튜닝 이야기 (0) | 2020.06.09 |
Effective Java 3/E - 8장 메서드 요약정리 (0) | 2019.05.26 |