
JPA 공부하다 보면 어느 순간 마주치는 이슈가 있다.
겉으론 잘 작동하는데, 로그를 보면 쿼리가 엄청 많이 날아가고 있다면?
그게 바로 N+1 문제다.이 글은 내가 공부하면서 정리한 내용을 바탕으로,
N+1 문제의 개념부터 실무에서 자주 쓰는 해결 방법까지 간단히 정리한 기록이다.
1 + N 쿼리 발생List<Department> departments = departmentRepository.findAll();
for (Department dept : departments) {
for (Employee emp : dept.getEmployees()) {
System.out.println(emp.getName());
}
}
부서가 3개라면 총 4번의 쿼리, 100개면 101번의 쿼리가 실행된다.
JPA에서는 @OneToMany, @ManyToOne 같은 연관 관계가 기본적으로 지연 로딩(LAZY) 설정이다.
@OneToMany(mappedBy = "department", fetch = FetchType.LAZY)
private List<Employee> employees;
.getEmployees()가 실행될 때마다 쿼리가 날아간다N+1 문제가 발생했는지 확인하려면 SQL 로그를 직접 확인하는 게 가장 확실하다.
# application.yml 예시
spring:
jpa:
show-sql: true
properties:
hibernate.format_sql: true
그리고 아래처럼 연관 객체에 접근하는 시점에 쿼리가 나가는지 확인해본다.
dept.getEmployees().size(); // 이때 쿼리가 발생하는지 로그로 체크
@Query("SELECT DISTINCT d FROM Department d JOIN FETCH d.employees")
List<Department> findAllWithEmployees();
DISTINCT 사용 안 하면 중복 데이터 생길 수 있음@EntityGraph(attributePaths = "employees")
List<Department> findAll();
@OneToMany(mappedBy = "department", fetch = FetchType.LAZY)
@BatchSize(size = 10)
private List<Employee> employees;
혹은 application.yml에서 전역 설정 가능:
spring:
jpa:
properties:
hibernate.default_batch_fetch_size: 10
QDepartment d = QDepartment.department;
QEmployee e = QEmployee.employee;
queryFactory
.selectFrom(d)
.leftJoin(d.employees, e).fetchJoin()
.fetch();
@OneToMany(fetch = FetchType.EAGER)
처음엔 EAGER로 바꾸면 다 해결될 것 같지만,
실제로는 언제 어디서든 쿼리가 튀어나와서 제어하기 어려워진다.
그래서 대부분의 경우엔 LAZY를 유지하면서 명시적으로 fetch를 제어하는 게 정석이다.
| 상황 | 추천 해결책 |
|---|---|
| 단순 조회만 필요할 때 | Fetch Join / EntityGraph |
| 페이징도 함께 써야 할 때 | Batch Size |
| 조건이 복잡한 검색 쿼리일 때 | QueryDSL |
| 유지보수와 확장성까지 고려 시 | QueryDSL + Batch Size 조합 추천 |
JPA에서 N+1 문제는 흔하게 발생하는 성능 이슈다.
처음엔 막연했지만, 공부하다 보니 여러 해결 방법이 있다는 걸 알게 됐다.