JPA를 사용할 때, 연관된 엔티티를 지연로딩(LAZY) 으로 설정하면 발생하는 대표적인 성능 문제이다.
예를 들어)
Todo엔티티가 User엔티티와 @ManyToOne(fetch = FetchType.LAZY)로 연관되어 있을 때
List<Todo> todos = todoRepository.findAll();
for (Todo todo : todos) {
System.out.println(todo.getUser().getName());
}
이 코드를 실행하면 다음과 같은 쿼리가 발생한다.
SELECT * FROM todos → 1번
SELECT * FROM users WHERE id = ? → todos.size()만큼 반복 (N번)
➡️ 총 1 + N번의 쿼리가 발생하므로, 이를 N+1 문제라고 한다.
fetch join (JPQL)JPA의 JPQL에서 JOIN FETCH 구문을 사용하면 한 번에 데이터를 가져올 수 있다.
@Query("SELECT t FROM Todo t JOIN FETCH t.user")
List<Todo> findAllWithUser();
Todo와 연관된 User를 한 번의 쿼리로 조회fetch join이 포함 된 JPQL에서 count 쿼리를 자동 생성하지 못함@EntityGraph는 연관 된 엔티티를 함께 조회할 수 있도록 JPA에 알려주는 어노테이션
@EntityGraph(attributePaths = {"user"})
Page<Todo> findAllByOrderByModifiedAtDesc(Pageable pageable);
fetch join과 같은 효과@Query("SELECT t FROM Todo t LEFT JOIN FETCH t.user u ORDER BY t.modifiedAt DESC")
Page<Todo> findAllByOrderByModifiedAtDesc(Pageable pageable);
@EntityGraph)@EntityGraph(attributePaths = {"user"})
Page<Todo> findAllByOrderByModifiedAtDesc(Pageable pageable);
@EntityGraph(attributePaths = {"user", "comments", "managers"})
Page<Todo> findAllByOrderByModifiedAtDesc(Pageable pageable);
단, List처럼 1:N 관계가 많을 경우 페이징 성능에 영향을 줄 수 있으므로 DTO Projection을 병행하는 것이 좋다.
구분 | 설명
--------------------------------------------------------------------
LAZY 연관 로딩 | 필요한 시점에 쿼리 발생 → N+1 문제 발생 가능
fetch join | JPQL로 조인해서 한 번에 조회 가능 (단점: 페이징과 호환 어려움)
@EntityGraph | 선언형 방식으로 연관 로딩 + JPQL 없이 사용 가능 + 페이징 완벽 지원
@EntityGraph는 JPA에서 성능 최적화와 유지보수성을 동시에 잡을 수 있는 강력한 도구이다.
특히 페이징이 필요한 서비스에서 연관 엔티티를 함께 로딩하고 싶다면, @EntityGraph를 적극 사용하는 것이 좋다.