카테고리 없음

N+1 문제 해결하기 - Fetch Join

heesoohi 2025. 2. 27. 12:08

JPA와 같은 ORM(Object-Relational Mapping) 프레임워크를 사용할 때 종종 발생하는 성능 문제 중 하나가 바로 N+1 문제이다. 

예를 들어, 할 일 목록을 저장하는 Todo 엔티티가 있고, 이와 연결된 User 엔티티가 있다고 가정해보자. 

사용자가 Todo 목록을 조회할 때, 단 한 번의 쿼리로 모든 데이터를 가져오는 것이 이상적이지만, 기본 설정으로 조회하는 경우에 Todo 목록을 가져오는 쿼리가 1번 실행되는 걸로 끝나지 않고, 각각의 Todo에 연결된 User 정보를 가져오기 위해 추가적으로 N개의 쿼리가 실행되는 문제가 발생하는 것! 

이처럼 불필요한 데이터 베이스 호출이 많아지면, 애플리케이션의 성능이 저하될 수밖에 없다.

 

  #  N+1 문제 발생 상황  

 

아래와 같이 Todo 목록을 조회하는 코드를 실행하는 경우를 살펴보자.

 

 

먼저 Todo 목록을 가져오기 위한 쿼리가 실행되고,

 

 

이후에 각 Todo 엔티티에서 User 정보를 참조할 때마다 추가적인 쿼리가 발생한다. 

 

 

즉, findAll()을 실행하면 Todo 목록을 가져오는 1개의 쿼리가 실행된 후, Todo의 개수(N)에 따라 User 정보를 조회하는 추가적인 N개의 쿼리가 실행된다.

데이터 개수가 많아질수록 불필요한 쿼리가 증가하여 성능 저하 및 데이터 중복 문제를 초래한다.

 

 

  # N+1 문제 해결 방법: JOIN FETCH 사용  

 

Fetch Join은 JOIN FETCH 키워드를 사용하여 한 번의 쿼리로 연관된 엔티티를 함께 가져오도록 한다.

 

1. @QueryJOIN FETCH 활용

TodoRepository에서 Fetch Join을 사용하여 User 엔티티를 한 번에 조회하도록 수정할 수 있다.

위와 같이 JOIN FETCH를 사용하면, Todo 엔티티를 조회할 때 연관된 User 엔티티를 한 번의 쿼리로 가져올 수 있다.

 

이때 실제 실행되는 SQL 쿼리는 다음과 같고, 이제 Todo 리스트를 조회할 때 한 번의 쿼리로 모든 User 정보를 가져오기 때문에 N+1 문제가 해결된다.

SELECT t.*, u.* FROM todos t JOIN users u ON t.user_id = u.id ORDER BY t.modified_at DESC;

 

2. 서비스 레이어에서 Fetch Join 메서드 사용

TodoService에서 findAllWithUser() 메서드를 사용하도록 변경하면, getTodos() 메서드를 호출하면 한 번의 쿼리로 TodoUser 정보를 함께 가져오므로 성능이 크게 향상된다.

 

 

 

 💡 정리   

N+1 문제는 JPA를 사용할 때 연관된 엔티티를 조회하는 과정에서 자주 발생하는 성능 저하 문제다. 이를 방치하면 데이터가 많아질수록 애플리케이션의 응답 속도가 급격히 느려질 수 있다. 하지만 Fetch Join을 활용하면 불필요한 쿼리 실행을 방지하고 한 번의 SQL 실행으로 필요한 데이터를 모두 가져올 수 있어 성능을 최적화할 수 있다.

 

만약 프로젝트에서 N+1 문제가 발생한다면, 단순한 데이터 조회 패턴을 점검하고 JOIN FETCH를 적극적으로 활용하여 최적화하는 습관을 들이자. 작은 변화가 애플리케이션의 성능에 큰 영향을 줄 수 있다!