Hibernate N+1 문제 해결 방법 (JPA 성능 최적화)
JPA나 Hibernate를 사용하다 보면 N+1 문제라는 말을 자주 듣게 됩니다.
데이터 조회 성능에서 가장 대표적인 함정이라고도 할 수 있는데요.
이번 글에서는 N+1 문제의 개념을 이해하고, 실제 코드 예제와 함께 다양한 해결 방법을 살펴보겠습니다.
N+1 문제란 무엇일까?
N+1 문제는 보통 연관 관계가 걸린 엔티티를 조회할 때 발생합니다.
예를 들어, Member와 Team이 @ManyToOne 관계라고 가정해봅시다.
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
private String name;
@ManyToOne(fetch = FetchType.LAZY)
private Team team;
}
만약 모든 Member를 조회하는 코드를 작성하면 Hibernate는 먼저 Member 리스트를 조회하는 1번 쿼리를 날립니다.
그리고 각 Member의 Team을 조회하기 위해 추가적으로 N번 쿼리를 실행하게 되죠.
List<Member> members = em.createQuery("select m from Member m", Member.class)
.getResultList();
for (Member m : members) {
System.out.println(m.getTeam().getName()); // 여기서 N번 추가 쿼리 발생
}
결과적으로 1 + N번의 쿼리가 실행되면서 불필요하게 많은 SQL 호출이 발생하는 것입니다.
데이터 양이 많아질수록 성능에 심각한 영향을 미치게 됩니다.
N+1 문제 해결 방법
1. Fetch Join 사용
가장 대표적인 방법은 JPQL에서 Fetch Join을 사용하는 것입니다. 연관된 엔티티를 한 번의 쿼리로 함께 가져올 수 있습니다.
List<Member> members = em.createQuery(
"select m from Member m join fetch m.team", Member.class)
.getResultList();
이렇게 하면 Member와 Team이 조인된 SQL이 실행되고, 추가적인 쿼리가 발생하지 않습니다.
2. EntityGraph 활용
또 다른 방법은 @EntityGraph를 활용하는 것입니다.
JPA 표준 기능으로, Fetch Join과 유사한 효과를 주면서도 코드 가독성이 좋아집니다.
@EntityGraph(attributePaths = {"team"})
@Query("select m from Member m")
List<Member> findAllWithTeam();
Repository 레벨에서 손쉽게 사용할 수 있어, 실무에서 자주 쓰이는 방법 중 하나입니다.
3. @BatchSize 활용
만약 다대일 관계가 아니라 일대다 관계에서 N+1 문제가 발생한다면 @BatchSize 옵션을 활용할 수도 있습니다.
@Entity
public class Team {
@Id @GeneratedValue
private Long id;
private String name;
@OneToMany(mappedBy = "team")
@BatchSize(size = 100)
private List<Member> members = new ArrayList<>();
}
이렇게 설정하면 Hibernate가 IN 절을 사용해서 한번에 데이터를 조회합니다. 대규모 데이터를 처리할 때 유용합니다.
마무리
- 단순 조회라면 Fetch Join으로 간단하게 해결
- 복잡한 쿼리에서는 EntityGraph로 가독성과 유지보수성 확보
- 대량 데이터 조회 시 BatchSize로 최적화
- 캐싱이나 QueryDSL과 같은 도구와 병행 활용