N+1 문제는 JPA 또는 Hibernate 같은 ORM(Object-Relational Mapping) 프레임워크에서 연관된 엔티티를 조회할 때 자주 발생하는 성능 문제이다. N+1의 개념부터 원인, 구체적인 예시, 그리고 해결 방안까지 설명하려한다.
N+1문제: 하나의 쿼리(1)로 데이터를 조회한 후, 연관된 엔티티를 조회하기 위해 N개의 추가 쿼리가 실행되는 상황.
결과적으로 총 N + 1번의 쿼리가 실행되며, 이는 성능을 크게 저하시킬 수 있음.
<예시 상황>
Member와 Team이 다대일(N:1) 관계라고 가정:
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
private String name;
@ManyToOne(fetch = FetchType.LAZY)
private Team team;
}
@Entity
public class Team {
@Id @GeneratedValue
private Long id;
private String name;
}
위처럼 있을 때, 아래같은 모든 Member를 조회하는 코드를 작성한다면 발생하는 쿼리 수는?
List<Member> members = em.createQuery("SELECT m FROM Member m", Member.class)
.getResultList();
for (Member member : members) {
System.out.println(member.getTeam().getName());
}
SELECT * FROM Member;SELECT * FROM Team WHERE id = ?;JPA에서 연관된 엔티티를 매핑할 때, 기본적으로 다음 전략을 따른다:
-> @ManyToOne, @OneToMany 등의 연관 관계는 기본적으로 LAZY를 사용한다.
즉, 연관된 엔티티를 실제로 접근하는 시점에 쿼리를 날려서 로딩되는 것임.
위의 예시에선 member.getTeam()을 호출하는 시점에 DB 쿼리가 실행된다.
LAZY 설정된 필드는 프록시 객체(가짜 객체)로 대체된다!
프록시가 실제 사용될 때 → JPA가 DB에 쿼리를 보내 데이터를 채운다.
결과적으로 루프 내부에서 매번 getTeam()을 호출하면 그때마다 DB로부터 데이터를 불러오게 되어 N개의 쿼리가 추가 발생하게 되는 것.
연관된 엔티티를 한 번에 조인해서 함께 조회하는 방법이다.
List<Member> members = em.createQuery(
"SELECT m FROM Member m JOIN FETCH m.team", Member.class)
.getResultList();
한 번의 쿼리로 Member + Team을 모두 가져와 N+1 문제 방지할 수 있지만, 컬렉션(@OneToMany) 을 fetch join할 경우, 페이징이 불가능하다. 또한, 한 쿼리로 너무 많은 데이터가 한 번에 로딩될 수 있다. (조인으로 인해 중복) 그러니 필요한 상황에만 사용하는 것이 좋음.
@EntityGraph(attributePaths = {"team"})
@Query("SELECT m FROM Member m")
List<Member> findAllWithTeam();
EntityGraph는 어떤 연관 엔티티를 로딩할지 명시적으로 지정한다. 내부적으로 fetch join과 유사한 방식으로 동작한다. Spring Data JPA에서 많이 사용하는 방식이다.
attributePaths: 로딩할 연관 엔티티의 경로 (필드 이름) 지정type (선택): FETCH (기본값), LOAD 등 지정 가능Hibernate에 설정을 추가하여 연관된 엔티티들을 IN 절로 한 번에 조회할 수 있도록 하는 방법
-properties-
spring.jpa.properties.hibernate.default_batch_fetch_size=100
-코드 작성!-
@ManyToOne(fetch = FetchType.LAZY)
@BatchSize(size = 100)
private Team team;
위처럼 해두면, member.getTeam()을 반복하는 경우, JPA가 100개씩 묶어서 아래와 같이 실행한다.
SELECT * FROM Team WHERE id IN (1, 2, 3, ..., 100);
각 연관 엔티티를 미리 가져온다 (지연 로딩이지만 성능 개선)
fetch join처럼 조인으로 인한 중복 데이터가 없고, 페이징에도 사용이 가능하다.
<참고 자료>
https://www.baeldung.com/spring-hibernate-n1-problem
https://programmer93.tistory.com/83#google_vignette