1. N+1 문제란?

N+1 문제는 JPA에서 연관된 엔티티를 조회할 때 발생하는 성능 저하 문제 중 하나입니다. 이는 하나의 메인 엔티티를 조회한 후, 연관된 엔티티를 추가로 조회하면서 발생합니다.

예제 코드

@Entity
public class Team {
    @Id @GeneratedValue
    private Long id;
    private String name;
    
    @OneToMany(mappedBy = "team", fetch = FetchType.LAZY)
    private List<Member> members = new ArrayList<>();
}

@Entity
public class Member {
    @Id @GeneratedValue
    private Long id;
    private String username;
    
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "team_id")
    private Team team;
}
List<Team> teams = em.createQuery("SELECT t FROM Team t", Team.class).getResultList();
for (Team team : teams) {
    System.out.println(team.getMembers().size()); // N개의 추가 쿼리 실행
}

위 코드에서 Team을 조회한 후, 각 Team에 속한 Member를 조회하면서 N개의 추가 쿼리가 발생합니다.

2. N+1 문제 해결 방법

(1) LAZY 설정 사용

기본적으로 JPA는 @OneToMany와 같은 컬렉션 연관 관계에 대해 FetchType.LAZY를 적용하는 것이 좋습니다. LAZY 로딩을 사용하면 필요할 때만 연관된 엔티티를 가져와 N+1 문제를 최소화할 수 있습니다.

@OneToMany(mappedBy = "team", fetch = FetchType.LAZY)
private List<Member> members;

FetchType.LAZY를 설정하면 기본적으로 프록시 객체를 반환하며, 실제 엔티티가 필요한 시점에 쿼리가 실행됩니다.

(2) Fetch Join 사용

Fetch Join을 사용하면 한 번의 쿼리로 연관된 엔티티까지 한꺼번에 조회할 수 있습니다.

List<Team> teams = em.createQuery("SELECT t FROM Team t JOIN FETCH t.members", Team.class).getResultList();

(3) Entity Graph 사용

JPA의 @NamedEntityGraph 또는 EntityGraph를 사용하여 동적으로 연관 엔티티를 로딩할 수 있습니다.

@Entity
@NamedEntityGraph(name = "Team.members", attributeNodes = @NamedAttributeNode("members"))
public class Team {
    // ...
}
EntityGraph<?> entityGraph = em.getEntityGraph("Team.members");
Map<String, Object> hints = new HashMap<>();
hints.put("javax.persistence.fetchgraph", entityGraph);
List<Team> teams = em.createQuery("SELECT t FROM Team t", Team.class)
                      .setHint("javax.persistence.fetchgraph", entityGraph)
                      .getResultList();

(4) Batch Size 조정 (@BatchSize 사용)

Hibernate의 @BatchSize 옵션을 사용하면 연관된 엔티티를 한 번의 쿼리로 묶어서 가져올 수 있습니다.

@OneToMany(mappedBy = "team", fetch = FetchType.LAZY)
@BatchSize(size = 10)
private List<Member> members;

또는 글로벌 설정으로 적용할 수도 있습니다.

spring.jpa.properties.hibernate.default_batch_fetch_size=10

3. 결론

N+1 문제는 성능 저하를 유발하는 대표적인 문제이지만, LAZY 설정, Fetch Join, Entity Graph, Batch Size 등의 전략을 활용하면 효과적으로 해결할 수 있습니다. 상황에 맞는 최적화 전략을 선택하여 효율적인 JPA 사용을 고려하는 것이 중요합니다.

+ Recent posts