[JPA] FetchType: LAZY vs EAGER, N+1, fetch join
정의
FetchType 은 JPA 가 연관 entity 를 언제 로드할지 결정하는 hint.
EAGER: entity 로딩 시 즉시 함께 SELECTLAZY: 실제 사용 시점 (proxy 메서드 호출) 까지 미룸
기본값:
@OneToOne,@ManyToOne: EAGER (위험)@OneToMany,@ManyToMany: LAZY
규칙: 모든 연관관계는 명시적으로 LAZY. EAGER 는 N+1 폭탄의 시작. spring-data-jpa 의 ## 관계와 fetch 가 개요라면, 이 페이지는 FetchType 매핑 자체와 해결 도구 개요를 다룬다. 개별 도구 상세는 fetch join 과 N+1 쿼리 문제 참고.
시각화
EAGER 가 만드는 N+1 → JOIN FETCH 로 1쿼리 해결.
EAGER 의 함정
@Entity
class Post {
@Id Long id;
String title;
@ManyToOne // ← 기본 EAGER
@JoinColumn(name = "author_id")
User author;
}
List<Post> posts = em.createQuery("SELECT p FROM Post p", Post.class).getResultList();
발생 쿼리:
-- 위 코드의 의도: posts 1번 조회
SELECT p.* FROM post p;SELECT p.* FROM post p; -- 1번
SELECT u.* FROM user u WHERE u.id = ?; -- N번 (각 post 마다)
SELECT u.* FROM user u WHERE u.id = ?;
SELECT u.* FROM user u WHERE u.id = ?;
... (post 개수만큼)posts 100 개면 SQL 101 번. N+1 (1번 + N번) 문제. 매번 author 가 필요한지 알 수 없어도 무조건 가져옴.
LAZY 전환
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "author_id")
User author;
이제 posts.get(0).getAuthor().getName() 호출 시점에야 SQL 실행. 사용 안 하면 author SELECT 도 안 일어남.
함정: 트랜잭션 범위를 벗어나면 LazyInitializationException. spring-data-jpa 의 ## 함정 / 2. 트랜잭션 없이 lazy 참고. 해결 도구가 아래 fetch join / EntityGraph.
fetch join
JPQL 에서 명시적으로 함께 가져옴. 상세는 spring-jpa-fetch-join 참고. 이 절은 개요만 정리.
List<Post> posts = em.createQuery("""
SELECT p
FROM Post p
JOIN FETCH p.author
""", Post.class).getResultList();
for (Post p : posts) {
System.out.println(p.getAuthor().getName()); // 추가 쿼리 없음
}SELECT p.*, u.*
FROM post p
INNER JOIN user u ON p.author_id = u.id;
-- 단 1번
-- author 접근은 이미 로딩된 데이터에서장점: 1번에 다 가져옴. 단점:
- 컬렉션 fetch join 시 중복 row (Post 1 + comments 5 → Post 가 5번 반환).
DISTINCT필요 - Pageable 과 조합 시 컬렉션 fetch join 불가 (in-memory paging 으로 fallback)
MultipleBagFetchException: 컬렉션 2개 이상 동시 fetch join 시 카르테시안 폭발
@EntityGraph
쿼리마다 fetch join 작성하지 않고 선언적:
@Entity
@NamedEntityGraph(
name = "Post.author",
attributeNodes = @NamedAttributeNode("author")
)
class Post { ... }
// Repository
@EntityGraph(value = "Post.author", type = EntityGraphType.FETCH)
List<Post> findByCreatedAtAfter(LocalDateTime date);
Spring Data JPA 에서 가장 깔끔. ad-hoc EntityGraph 도 가능:
@EntityGraph(attributePaths = {"author", "comments"})
List<Post> findAll();
EAGER 와 차이: 그 쿼리에만 적용. 다른 곳에서는 여전히 LAZY.
@BatchSize (Hibernate 확장)
여러 LAZY proxy 를 한 번에 IN 쿼리로:
@OneToMany(mappedBy = "post", fetch = FetchType.LAZY)
@BatchSize(size = 100)
List<Comment> comments;
N+1 시나리오:
posts 50개 조회
for (post : posts) post.getComments().size();
@BatchSize 없으면:
- 1 (posts) + 50 (각 comments) = 51 쿼리
@BatchSize(size = 100) 있으면:
- 1 (posts) + 1 (
WHERE post_id IN (1, 2, ..., 50)) = 2 쿼리
전역 설정도 가능: spring.jpa.properties.hibernate.default_batch_fetch_size=100.
EntityGraph 보다 덜 침투적. 컬렉션 paging 과도 잘 어울림. 첫 번째 N+1 방어선으로 추천.
fetch join vs EntityGraph vs @BatchSize 비교
| 방법 | 쿼리 수 | 코드 위치 | Paging | 컬렉션 중복 |
|---|---|---|---|---|
| fetch join | 1 | JPQL | 컬렉션 안 됨 | DISTINCT 필요 |
| @EntityGraph | 1 | Repository method | 컬렉션 안 됨 | DISTINCT 필요 |
| @BatchSize | 2 (IN 절) | Entity / 전역 | OK | 없음 |
| LAZY 만 | N+1 | 어디서든 | OK | 없음 |
실무 전략:
- 모든 연관 LAZY 로 선언
- 전역
default_batch_fetch_size=100설정 (안전망) - List 조회 + 즉시 사용 시
@EntityGraph명시 - 단건 + paging 시 LAZY +
@BatchSize조합 - 복잡한 join + projection 시 QueryDSL / native SQL
EntityGraph 의 두 모드
@EntityGraph(value = "Post.author", type = EntityGraphType.FETCH) // 명시한 것만 EAGER, 나머지 LAZY
@EntityGraph(value = "Post.author", type = EntityGraphType.LOAD) // 명시한 것 EAGER, 나머지는 entity 기본값
FETCH 가 더 예측 가능. LOAD 는 entity 의 EAGER 설정과 합쳐져 의도치 않은 추가 fetch 발생 가능.
실전 N+1 디버깅
일반적 N+1 원인 / 감지 / 해결 개요는 n-plus-one-problem 참고. 여기서는 Spring / Hibernate 환경 설정 위주.
spring:
jpa:
properties:
hibernate:
# 모든 SQL 로깅
show_sql: true
format_sql: true
# 핵심: N+1 정적 분석 (warn 로그)
session.events.log.LOG_QUERIES_SLOWER_THAN_MS: 100
logging:
level:
org.hibernate.SQL: DEBUG
org.hibernate.orm.jdbc.bind: TRACE
p6spy, datasource-proxy 같은 도구가 더 깔끔. 통합 테스트에서 N+1 검출은 Hypersistence Utils 또는 직접 Statement count 측정.
함정과 베스트 프랙티스
- EAGER 절대 사용 금지: 한번 EAGER 하면 그 entity 가 쓰이는 모든 쿼리에 영향
@OneToOne도 LAZY 가능하지만 까다로움: optional=false + bytecode enhancement 필요- 컬렉션 fetch join + Pageable = in-memory paging: 로그에
firstResult/maxResults specified with collection fetch; applying in memory!warning. 대용량 데이터에서 OOM 위험 DISTINCT는hibernate.query.passDistinctThrough=false와 조합 시 SQL DISTINCT 안 보냄 (메모리 dedup 만)- OSIV (Open Session In View) 가 운영에서 켜져 있으면 LAZY 함정이 가려짐. 끄는 게 안전:
spring.jpa.open-in-view=false @BatchSize가 만능 아님: 인-메모리 join 처럼 보이게 함, 대용량 IN 절은 DB 별 제한 (Postgres OK, Oracle 1000개 제한)
관련 위키
- spring-jpa-fetch-join - fetch join (JOIN FETCH) 상세
- n-plus-one-problem - N+1 문제 개념 정리
- spring-data-jpa
- spring-jpa-persistence-context
- spring-jpa-cascade-orphan
- spring-jpa-locking
- spring-transaction
- jpa-rails-eager-comparison - JPA EAGER vs Rails eager_load
이 글의 용어 (8개)
- [비교] JPA EAGER vs Rails eager_loadframeworks
- 정의 JPA 와 Rails 는 둘 다 "eager" 라는 용어를 ORM 의 연관 로딩 전략에 사용한다. 이름은 비슷하지만 의미는 정반대. - JPA : 매핑 선언 시점에 영구 설…
- [JPA] CascadeType과 orphanRemovalspring
- 정의 JPA 의 entity 연산 ( , , , , ) 을 연관 entity 에 자동 전파 하는 메커니즘. 같이 선언. 별개 개념인 orphanRemoval 은 부모 컬렉션에서 …
- [JPA] fetch join: JOIN FETCH 로 연관 entity 즉시 로드spring
- 정의 fetch join 은 JPQL 에서 연관 entity 를 한 번의 SQL 로 함께 로드하는 문법. 로 선언한다. 일반 은 SQL JOIN 만 수행하고 결과 entity 만…
- [JPA] Optimistic Lock과 Pessimistic Lockspring
- 정의 동시 수정으로 데이터가 깨지는 상황을 막는 두 가지 전략: - Optimistic (낙관적): 충돌이 드물다고 가정, 커밋 직전에 version 비교로 검증. 충돌 시 예외…
- [JPA] PersistenceContext: 1차 캐시, dirty checking, flushspring
- 정의 PersistenceContext 는 가 관리하는 entity 의 메모리 저장소 + 변경 추적기. 트랜잭션 안에서만 살아 있다 ( 진입 시 생성, commit/rollbac…
- [Spring] Spring Data JPA: Repository, Query Methodsspring
- 정의 Spring Data JPA는 JPA(Hibernate) 위에 Repository 추상화를 얹어 boilerplate를 제거. 인터페이스만 정의하면 Spring이 구현체 자…
- [Spring] Transaction: @Transactional, propagation, isolationspring
- 정의 Spring 트랜잭션은 AOP 기반 선언형 트랜잭션 관리. 한 줄로 메서드 전체를 트랜잭션 범위로. JDBC, JPA, MongoDB 등 backend에 동일 API. 기본…
- N+1 쿼리 문제database-internals
- 정의 N+1 쿼리 문제 = ORM 이 부모 entity N 개를 조회한 뒤, 각 부모의 연관 entity 를 하나씩 로드하면서 추가 SQL 이 N 번 발생하는 성능 안티패턴. 이…
이 개념을 다룬 위키 페이지 (14)
- wiki[DB Internals] B-Tree와 B+Tree 인덱싱
- wikiN+1 쿼리 문제
- wiki[비교] JPA EAGER vs Rails eager_load
- wiki[Rails] 쿼리 최적화: preload, eager_load, includes, references
- wiki[Spring Data] MongoDB: MongoTemplate, MongoRepository
- wiki[JPA] CascadeType과 orphanRemoval
- wiki[JPA] PersistenceContext vs PersistenceUnit 비교
- wiki[JPA] fetch join: JOIN FETCH 로 연관 entity 즉시 로드
- wiki[JPA] 상속 전략: SINGLE_TABLE, JOINED, TABLE_PER_CLASS
- wiki[JPA] Optimistic Lock과 Pessimistic Lock
- wiki[JPA] PersistenceContext: 1차 캐시, dirty checking, flush
- wiki[JPA] JPQL: Jakarta Persistence Query Language
- wiki[JPA] 동적 쿼리: Criteria API, QueryDSL, Specification
- wiki[JPA] JPQL DTO Projection: constructor expression, interface, record
💬 댓글