본문으로 건너뛰기
김신건의 로그

[JPA] FetchType: LAZY vs EAGER, N+1, fetch join

· 수정 · 📖 약 3분 · 1,057자/단어 #jpa #hibernate #fetch #performance #n-plus-one
JPA FetchType, FetchType.LAZY, FetchType.EAGER, EntityGraph, @BatchSize, JPA 페치

정의

FetchType 은 JPA 가 연관 entity 를 언제 로드할지 결정하는 hint.

  • EAGER: entity 로딩 시 즉시 함께 SELECT
  • LAZY: 실제 사용 시점 (proxy 메서드 호출) 까지 미룸

기본값:

  • @OneToOne, @ManyToOne: EAGER (위험)
  • @OneToMany, @ManyToMany: LAZY

규칙: 모든 연관관계는 명시적으로 LAZY. EAGER 는 N+1 폭탄의 시작. spring-data-jpa 의 ## 관계와 fetch 가 개요라면, 이 페이지는 FetchType 매핑 자체와 해결 도구 개요를 다룬다. 개별 도구 상세는 fetch joinN+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();

발생 쿼리:

sql
-- 위 코드의 의도: posts 1번 조회
SELECT p.* FROM post p;
실행된 SQL
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 참고. 이 절은 개요만 정리.

java
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());   // 추가 쿼리 없음
}
실행된 SQL
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 join1JPQL컬렉션 안 됨DISTINCT 필요
@EntityGraph1Repository method컬렉션 안 됨DISTINCT 필요
@BatchSize2 (IN 절)Entity / 전역OK없음
LAZY 만N+1어디서든OK없음

실무 전략:

  1. 모든 연관 LAZY 로 선언
  2. 전역 default_batch_fetch_size=100 설정 (안전망)
  3. List 조회 + 즉시 사용 시 @EntityGraph 명시
  4. 단건 + paging 시 LAZY + @BatchSize 조합
  5. 복잡한 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 위험
  • DISTINCThibernate.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개 제한)

관련 위키

이 글의 용어 (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 번 발생하는 성능 안티패턴. 이…

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기