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에 접근해야한다면 조인해서 다 가져오자이다
Fetch Join은 연관된 데이터를 한 번의 쿼리로 가져오도록 JPA에게 지시한다
SELECT a FROM AEntity a JOIN FETCH a.bEntities WHERE a.id = :id
: ) 한번의 쿼리로 부모와 자식 엔티티를 모두 로드한다
: ( 너무 많은 데이터가 조회되면 성능 저하가 발생할 수 있다
@BatchSize를 사용하면 여러 연관 엔티티를 한 번에 로드하도록 설정할 수 있다
@Entity
public class AEntity {
private String name;
@OneToMany(mappedBy = "aEntity", fetch = FetchType.LAZY)
@BatchSize(size = 10) // 한번에 10개의 자식 엔티티 로드
List<BEntity> bEntities;
}
: ) 설정된 BatchSize 크기만큼 자식 엔티티를 묶어서 조회한다
: ( 여러 부모 엔티티의 자식 엔티티를 묶어 한 번에 로드하므로 N+1문제를 완화한다
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();
@Entity
@Subselect("SELECT a.*, b.* FROM AEntity a LEFT JOIN bEntities b ON a.id = b.a_entity_id")
public class AEntity {
//
: ) 한 번의 SQL로 부모와 자식을 모두 로드
: ( 복잡한 쿼리에서는 성능 저하 가능
Fetch Join과 페이징을 함께 사용하면 JPA가 결과를 올바르게 처리하지 못할 수 있다.
이를 해결하기 위해 서브쿼리 방식이다 @BatchSize를 병행하거나 QueryDSL 같은 라이브러리를 사용한다
데이터가 많고 여러 부모-자식 관계가 있음 | @BatchSize 설정 |
특정 연관 데이터를 항상 로드해야 함 | Fetch Join, EntityGraph |
동적으로 어떤 연관 데이터를 로드할지 결정 | EntityGraph |
페이징과 함께 사용 | BatchSize, 서브쿼리 방식 |
[JPA] @JoinColumn : 외래키 지정에 사용되는 어노테이션, @ManyToOne, @OneToOne, @OneToMany : 관계의 종류 (0) | 2025.01.10 |
---|---|
[테스트] 성능테스트(부하테스트) : JMeter, Artillery, Gatling (0) | 2025.01.10 |
[인프런강의] 백엔드 애플리케이션 성능 테스트하기 (0) | 2025.01.08 |
[스프링] Spring AOP 와 동작 원리 (0) | 2025.01.03 |
[JAVA] Random vs SecureRandom vs ThreadLocalRandom (0) | 2024.12.26 |