상세 컨텐츠

본문 제목

[JPA] JPA N+1 문제와 해결 방안

😎 지식/자바_스프링_테스트☕

by :부셔져버린개발자 2025. 1. 10. 18:52

본문

JPA N+1 문제

LAZY Loading에서의 N+1문제

연관된 엔티티를 가져올 때 하나의 쿼리로 부모 엔티티를 가져온 후,
각 부모 엔티티마다 자식 엔티티를 조회하는 추가 쿼리(N개의 쿼리)가 발생하는 문제

 
N+1문제란 연관된 엔티티를 조회할 때, 연관된 엔티티마다 별도의 쿼리를 실행하는 문제이다.

더보기
로딩 시점연관된 엔티티가 실제로 필요할 때 로드됨연관된 엔티티가 즉시 로드됨
N+1 문제 발생 원인1개의 부모 엔티티를 로드한 후, 각 부모에 대해 자식 엔티티를 개별적으로 로드.부모 엔티티를 로드하면서 연관된 자식 엔티티를 모두 로드하기 위해 추가 쿼리가 발생 (부모 엔티티 수 * 자식 엔티티 수)
쿼리 수1개의 부모 엔티티를 위한 쿼리 + 각 부모에 대해 자식 엔티티를 로드하는 추가 쿼리들.부모 엔티티를 위한 1개의 쿼리 + 자식 엔티티를 위한 1개의 쿼리 (각 부모마다)
예시 쿼리1. SELECT * FROM Parent
2. SELECT * FROM Child WHERE parent_id = ? 반복 실행.
1. SELECT * FROM Parent
2. SELECT * FROM Child WHERE parent_id = ? 한번에 실행 (조인)
발생 가능 상황부모 엔티티 조회 후, 자식 엔티티에 접근할 때 발생.부모 엔티티 조회 시, 연관된 자식 엔티티를 즉시 로딩하려 할 때 발생.
해결 방법- FetchType.LAZY로 기본 설정 후 필요한 시점에만 데이터 로딩.
- Fetch Join, EntityGraph, BatchSize 등 활용.
- Fetch Join, EntityGraph, BatchSize 등을 사용하여 한 번의 쿼리로 연관 데이터 로드.

 

@Entity 
public class AEntity {
    private String name;
    @OneToMany(mappedBy = "aEntity")
    List<BEntity> bEntities;
}

@Entity
public class BEntity {
    @ManyToOne
    @JoinColumn(name = "a_entity_id")
    private AEntity aEntity;
}

 
@OneToMany의 default Fetch Type은 LAZY다
따라서, A엔티티를 조회할 때 처음에는 name과 Proxy로 감싸진 bEntities가 조회된다
 
bEntities.get(x)를 하기 전까지는 쿼리가 실행되지 않다가 
접근 하는 순간 쿼리가 실행된다
 
그런데, 만약에 AEntity를 조회하자마자 bEntities에 모두 접근하게 되면 bEntities의 개수만큼 쿼리가 만들어진다
 
즉, 1 + N 개의 쿼리가 실행되게 된다
 
이를 해결하기 위한 기본 아이디어는
 AEntity를 조회함과 동시에 모든 bEntities에 접근해야한다면 조인해서 다 가져오자이다
 


해결 방안

1. JPQL에서 Fetch Join 사용

Fetch Join은 연관된 데이터를 한 번의 쿼리로 가져오도록 JPA에게 지시한다

SELECT a FROM AEntity a JOIN FETCH a.bEntities WHERE a.id  = :id

 
: ) 한번의 쿼리로 부모와 자식 엔티티를 모두 로드한다
: ( 너무 많은 데이터가 조회되면 성능 저하가 발생할 수 있다
 

2. Hibernate의 @BatchSize

@BatchSize를 사용하면 여러 연관 엔티티를 한 번에 로드하도록 설정할 수 있다 
 

@Entity 
public class AEntity {
    private String name;
    @OneToMany(mappedBy = "aEntity", fetch = FetchType.LAZY)
    @BatchSize(size = 10) // 한번에 10개의 자식 엔티티 로드
    List<BEntity> bEntities;
}

 
: ) 설정된 BatchSize 크기만큼 자식 엔티티를 묶어서 조회한다
: ( 여러 부모 엔티티의 자식 엔티티를 묶어 한 번에 로드하므로 N+1문제를 완화한다
 
 

3. EntityGraph 사용

JPA의 EntityGraph를 사용하면 특정 로딩 전략을 동적으로 설정할 수 있다 
 
엔티티 그래프를 통해 LAZY를 사용하는 필드도 즉시 로딩(EAGER)로 가져온다
명시적으로 어떤 연관 필드를 로드할지 지정할 수 있다 

@Entity
@NamedEntityGraph(
   name="AEntity.withBEntities",
   attributeNodes = @NamedAttributeNode("bEntities")
)
public class AEntity {}


TypedQuery<AEntity> query = entityManger.createNamedQuery(
   "SELECT a FROM AEntity a", AEntity.class
).setHint("javax.persistence.fetchgraph", entityManager.getEntityGraph("AEntity.withBEntities"));
List<AEntity> result = query.getResultList();

 
 

4. Hibernate의 @Subselect 활용

@Entity
@Subselect("SELECT a.*, b.* FROM AEntity a LEFT JOIN bEntities b ON a.id = b.a_entity_id")
public class AEntity {
//

: ) 한 번의 SQL로 부모와 자식을 모두 로드
: ( 복잡한 쿼리에서는 성능 저하 가능


그외 고려해야 할 문제

JOIN Fetch와 Pagination 문제 해결

Fetch Join과 페이징을 함께 사용하면 JPA가 결과를 올바르게 처리하지 못할 수 있다.
이를 해결하기 위해 서브쿼리 방식이다 @BatchSize를 병행하거나 QueryDSL 같은 라이브러리를 사용한다
 
 


비교 표

데이터가 많고 여러 부모-자식 관계가 있음@BatchSize 설정
특정 연관 데이터를 항상 로드해야 함Fetch Join, EntityGraph
동적으로 어떤 연관 데이터를 로드할지 결정EntityGraph
페이징과 함께 사용BatchSize, 서브쿼리 방식

 

728x90

관련글 더보기