N+1 문제란?
N+1 문제는 일반적으로 *지연 로딩 방식을 사용할 때 발생할 수 있는 성능 이슈입니다. 엔티티를 조회하는 SQL 쿼리와 연관된 엔티티를 조회하는 SQL 쿼리가 총 N+1번 발생하는 문제입니다. 이로 인해 쿼리 횟수가 증가하여 성능 저하를 야기합니다. 일반적으로 다수의 엔티티를 조회하는 경우에 발생하며, 일대다(1:N) 또는 다대다(N:M) 등의 연관 관계에서 자주 발생합니다.
발생 상황
다음은 N+1 문제가 발생하는 예시 코드입니다. 예를 들어, Department와 Employee 엔티티가 일대다 관계로 매핑되어 있을 때, Department 엔티티를 조회하고 각 부서별 Employee 엔티티를 함께 조회하려고 합니다.
하나의 부서는 여러 직원을 가지고 있으므로 1:N 관계로 설정되어 있습니다.
@Entity
public class Department {
@Id
private Long id;
private String name;
@OneToMany(mappedBy = "department")
private List<Employee> employees;
...
}
@Entity
public class Employee {
@Id
private Long id;
private String name;
@ManyToOne(fetch = FetchType.LAZY)
private Department department;
...
}
다음은 JPQL을 사용하여 Department 엔티티를 조회하고 각 부서별 Employee 엔티티를 함께 조회하는 코드입니다.
// Department 조회 entity는 한번만 수행
List<Department> departments = entityManager.createQuery(
"SELECT d FROM Department d",
Department.class
).getResultList();
// N+1 발생 부분
for (Department department : departments) {
List<Employee> employees = department.getEmployees();
// Do something with employees
}
위 코드에서 Department 엔티티를 조회하는 쿼리는 한 번만 실행됩니다. 그러나 department.getEmployees() 메서드를 호출할 때마다 Employee 엔티티를 조회하는 쿼리가 추가로 실행되어 성능 문제가 발생합니다.
해결법
N+1 문제를 해결하기 위한 방법은 대표적으로 3가지가 있습니다.
1. Fetch Join
SQL Join문처럼, JPQL을 사용해 DB에서 데이터를 가져올 때 처음부터 연관된 데이터까지 같이 가져오게 하는 방법입니다.
2. Batch Size
연관된 엔티티를 한번에 N개씩 로딩 하는 방식으로 N+1 문제를 해결하는 방법입니다. N+1 문제가 발생하더라도, select * from user where team_id = ?이 아닌, select * from user where team_id in(?,?,?) 방식으로 처리하는 것입니다.
3. Entity Graph
@Entity Graph 어노테이션을 사용해서 fetch 조인을 하는 방법입니다.
위 경우, 다음과 같이 JOIN FETCH를 사용하여 Department 엔티티와 Employee 엔티티를 함께 로드할 수 있습니다.
List<Department> departments = entityManager.createQuery(
"SELECT DISTINCT d FROM Department d LEFT JOIN FETCH d.employees",
Department.class
).getResultList();
for (Department department : departments) {
List<Employee> employees = department.getEmployees();
// Do something with employees
}
위의 JPQL 쿼리는 Department 엔티티와 Employee 엔티티를 함께 로드하며, 중복된 결과를 제거하기 위해 DISTINCT 키워드를 사용합니다. 이렇게 함으로써 추가적인 쿼리 호출과 중복이 없는 결과를 조회할 수 있습니다.
실무에서는 어떻게 사용해야 할까?
실무에서 N+1 문제로 DB가 죽어버리는 문제를 방지하기 위해서는 우선 연관관계에 대한 설정이 필요하다면, 지연 로딩을 사용하고 성능 최적화가 필요한 부분에서는 Fetch Join을 사용하는 것이 좋습니다. 또한 기본적으로 Batch Size의 값은 1000 이하로 설정합니다.
연관관계 설정이 필수적이지 않다면 N+1 문제를 방지하기 위해 연관관계를 끊고 사용하는 것도 방법 중 하나입니다.
*지연 로딩은 연관된 엔티티를 실제로 사용할 때까지 데이터베이스에서 로드하지 않고, 필요한 순간에 추가 쿼리를 실행하여 로딩합니다.
'spring(스프링)' 카테고리의 다른 글
[spring(스프링)] JWT(JSON Web Token) _디버깅의 눈물 (0) | 2023.04.19 |
---|---|
[spring(스프링)] Refresh Token이란? _디버깅의 눈물 (0) | 2023.04.17 |
[spring(스프링)] JPA(Java Persistence API)란? _디버깅의 눈물 (0) | 2023.04.15 |
[spring(스프링)] Spring JDBC란? _디버깅의 눈물 (0) | 2023.04.14 |
[spring(스프링)] JPA에서의 즉시 로딩과 지연 로딩 _디버깅의 눈물 (0) | 2023.04.13 |