ABOUT

성능과 운영 안정성을 함께 끌어올리는 개발자입니다.

92% Positional Error Reduction
79% p95 Latency Improvement
90%+ Long Tasks Reduction

2022.02 · 한국장학재단

우수 멘티

한국장학재단 사회 리더 대학생 멘토링 IT

2022.10 · 동작구청

우수 인재상

동작구청 우수 SW 인재

2025.05 · (주) 그랩

프로그래밍 우수상

(주) 그랩 우수 프로그램 개발

2025.05 · AWSKRUG

AWS한국사용자모임 발표

AI agent 스크립트 튜닝 관련 발표

ComputerScience

Development

Engineering

Trouble Shooting

GUESTBOOK

첫 마음부터
함께 나누는 온기

방명록 작성하러 가기

SUBSCRIBE

최신소식을
편하게 만나보세요.

EAGER

도입

EAGER는 데이터를 많이 가져오는 옵션이 아니라, 엔티티를 로드하는 시점에 해당 속성도 함께 준비돼 있어야 한다는 로딩 정책이다.

JPA를 처음 배울 때 EAGER는 흔히 “즉시 조인해서 다 가져오는 방식” 정도로 이해되곤 합니다. 하지만 실제 의미는 훨씬 더 모델링에 가깝습니다.

EAGER는 로딩 시점의 보장에 관한 정책입니다. 즉 owner 엔티티가 준비될 때 해당 연관이나 속성도 사용 가능한 상태여야 한다는 의미이지, 반드시 하나의 SQL join 문으로 해결된다는 뜻은 아닙니다.

그래서 EAGER를 이해한다는 것은 단순히 fetch 옵션 하나를 아는 것이 아니라, 기본 fetch graph와 조회별 fetch plan을 어떻게 구분해서 다룰지 이해하는 일과 연결됩니다.

필요성

EAGER를 제대로 이해하지 못하면 작은 조회 하나가 기본 fetch graph를 타고 예상보다 훨씬 넓은 객체 그래프로 커질 수 있기 때문에, 성능보다 먼저 로딩 경계의 문제로 봐야 한다

특히 JPA에서는 to-one 연관의 기본 fetch가 eager 쪽으로 기울어져 있기 때문에, fetch를 명시하지 않은 채 엔티티 관계를 늘려 가면 의도하지 않은 과조회가 쉽게 발생합니다.

이 문제는 처음에는 잘 드러나지 않습니다. 도메인 모델이 작을 때는 조회 비용이 감당 가능해 보이기 때문입니다. 하지만 연관이 많아질수록 단순한 find() 호출도 기본 fetch graph 때문에 점점 무거워질 수 있습니다.

그래서 EAGER는 “편해서 쓰는 옵션”이 아니라, 한 번 켜면 기본 조회 경계를 넓히는 설계 선택으로 이해하는 편이 더 정확합니다.

EAGER를 알아야 하는 이유
  • 기본 fetch 정책만으로도 조회 비용이 크게 달라질 수 있음
  • find()와 JPQL query가 같은 방식으로 fetch되지 않는 이유를 이해할 수 있음
  • EntityGraph, fetch graph, load graph 같은 조회별 fetch plan 도구를 더 정확히 쓸 수 있음
  • LAZY 예외를 피하려고 무작정 EAGER로 바꾸는 실수를 줄일 수 있음

정의

JPA에서 FetchType.EAGER는 persistence provider가 반드시 eager fetch해야 하는 요구사항이며, LAZY처럼 무시 가능한 힌트가 아니다

Jakarta Persistence 기준으로 FetchType.EAGER는 provider에 대한 requirement입니다. 즉 해당 속성은 eager하게 가져와야 합니다.

반대로 FetchType.LAZY는 힌트입니다. provider는 LAZY로 지정된 것보다 더 많이 fetch해도 되지만, EAGER는 반드시 fetch해야 한다는 의미를 가집니다.

이 차이 때문에 EAGER는 단순히 “조금 더 빨리 불러온다”가 아니라, 매핑 수준에서 provider에게 더 강한 의무를 부여하는 선택으로 봐야 합니다.

핵심 메시지

"EAGER는 빠름의 약속이 아니라

기본 fetch graph를 넓히는 강한 매핑 선택에 가깝습니다."

핵심 원리

EAGER의 핵심은 로딩 시점 보장에 있고, 실제 조회 범위는 기본 fetch graph와 query-time fetch plan이 함께 결정한다

JPA spec 기준으로 find()에 명시적인 entity graph가 없으면, 기본 fetch graph는 EAGER로 선언되었거나 그렇게 default된 속성들의 전이적 폐쇄로 정의됩니다. 즉 eager 속성은 기본적으로 fetch graph 안에 포함됩니다.

하지만 JPQL query는 조금 다릅니다. 명시적인 entity graph가 없으면 query가 가져올 데이터는 query 자체가 결정합니다. 그래서 같은 엔티티라도 find()와 JPQL query가 체감상 다른 fetch 형태를 보일 수 있습니다.

또 entity graph는 fetch graph와 load graph 두 해석을 가집니다. fetch graph는 명시되지 않은 속성을 LAZY처럼 취급하고, load graph는 명시되지 않은 속성을 기존 매핑의 default fetch 전략에 따르게 둡니다.

결국 EAGER는 매핑 레벨 정책이고, 실제 어떤 조회에서 얼마나 넓게 데이터를 가져올지는 query와 entity graph가 별도로 조정하는 구조입니다.

EAGER Loading Model
1) owner 엔티티 로드 요청
2) provider는 EAGER 속성이 사용 가능한 상태가 되도록 보장
3) find()는 기본 fetch graph의 영향을 받음
4) query-time fetch plan(EntityGraph, join fetch)은 별도로 조회 범위를 조정

기본 규칙

EAGER를 이해하려면 연관 종류마다 기본 fetch가 다르다는 점과, 특히 to-one 연관이 명시하지 않으면 eager 쪽으로 흐르기 쉽다는 점을 같이 봐야 한다
대상 기본 fetch 실무 해석
@Basic EAGER 기본 속성은 기본적으로 즉시 fetch 대상으로 취급됨
@ManyToOne DEFAULT (persistence unit 기본 to-one fetch, 기본값은 EAGER) 명시하지 않으면 쉽게 eager 쪽으로 흐르므로 실무에서는 LAZY를 자주 명시함
@OneToOne DEFAULT (persistence unit 기본 to-one fetch, 기본값은 EAGER) to-one 기본 정책을 모르면 기본 fetch graph가 쉽게 커짐
@OneToMany LAZY 컬렉션은 기본적으로 지연 로딩
@ManyToMany LAZY 컬렉션 기반 연관이라 기본 지연 로딩
@ElementCollection LAZY 값 타입 컬렉션도 기본은 지연 로딩

특히 많은 개발자가 @ManyToOne@OneToOne의 기본 fetch를 그냥 LAZY라고 착각합니다. 하지만 최신 Jakarta Persistence 문서에서도 이 둘은 DEFAULT이고, persistence unit 기본 to-one fetch 설정이 없으면 하위 호환성 때문에 eager 쪽을 따릅니다.

기본 구현

문법은 단순하지만, 매핑에 적힌 EAGER는 특정 조회 하나가 아니라 기본 로딩 경계 전체에 영향을 준다는 점이 더 중요하다
@Entity
public class Payment {

    @Id
    private Long id;

    @ManyToOne(fetch = FetchType.EAGER)
    @JoinColumn(name = "currency_id")
    private Currency currency;
}
@Entity
public class Currency {

    @Id
    private String code;

    private String name;
}
실전 포인트
문법만 보면 fetch = FetchType.EAGER는 단순합니다. 하지만 이 선택은 특정 서비스 메서드 하나가 아니라, 해당 연관이 참여하는 기본 로딩 경계 전체에 영향을 줍니다. 그래서 실무에서는 편의 때문에 넣기보다, 정말 기본값으로 남아도 되는 관계인지 먼저 판단하는 편이 안전합니다.

패턴 1. 매핑 레벨 EAGER는 전역 비용이 된다

매핑에 적은 EAGER는 특정 쿼리 최적화가 아니라, 그 속성이 기본 fetch graph에 참여하게 만드는 전역 설정에 가깝다

이 차이를 이해하지 못하면, 화면 하나 때문에 넣은 EAGER가 다른 서비스와 테스트, 배치 작업의 기본 조회 비용까지 함께 키울 수 있습니다.

Hibernate 공식 문서도 대부분의 association은 lazy로 매핑하고, eager fetching은 필요한 곳에서만 명시적으로 요청하라고 설명합니다. 즉 mapping-level EAGER는 기본 전략이라기보다 예외적인 선택에 더 가깝습니다.

매핑 레벨 EAGER
- 엔티티 정의 자체에 로딩 요구사항을 박아 넣음
- find()의 기본 fetch graph에 영향
- 특정 조회가 아니라 여러 호출 경로에 파급될 수 있음

패턴 2. 조회 시점 EAGER가 더 실용적이다

실무에서 더 자주 필요한 것은 전역 EAGER 매핑보다, 특정 조회에서만 eager fetching을 요청하는 query-time fetch plan이다

Jakarta Persistence와 Hibernate 문서 모두 EntityGraphleft join fetch 같은 방식으로 필요한 조회에서만 eager fetching을 요청하는 경로를 제공합니다.

이 접근의 장점은 기본 매핑을 가볍게 유지하면서도, 상세 화면이나 API 응답처럼 정말 필요한 읽기 경로에서만 연관 데이터를 정확히 늘릴 수 있다는 점입니다.

select o
from Order o
left join fetch o.member
where o.id = :id
EntityGraph<Order> graph = entityManager.createEntityGraph(Order.class);
graph.addAttributeNodes("member", "orderLines");

Map<String, Object> hints = Map.of("jakarta.persistence.fetchgraph", graph);

Order order = entityManager.find(Order.class, orderId, hints);
핵심 포인트
공식 문서가 EAGER 자체보다 EntityGraphleft join fetch를 함께 설명하는 이유는 분명합니다. 대부분의 경우 필요한 것은 “항상 eager”가 아니라 “이번 조회에서만 eager”이기 때문입니다.

패턴 3. EntityGraph 는 EAGER를 조회 단위로 제어한다

fetch graph와 load graph를 구분하면, 기본 매핑을 유지한 채 이번 조회에서 어떤 속성을 eager처럼 다룰지 더 세밀하게 제어할 수 있다

JPA spec 기준으로 fetch graph는 그래프에 없는 속성을 LAZY처럼 취급하고, load graph는 그래프에 없는 속성을 기존 매핑의 fetch 전략에 따르게 둡니다.

그래서 “이번 조회에서는 이것만 강하게 eager로 가져오고 나머지는 최대한 눌러 두고 싶다”면 fetch graph, “기본 매핑은 유지하되 몇 개만 더 당겨오고 싶다”면 load graph가 더 잘 맞는 경우가 많습니다.

public interface OrderRepository extends JpaRepository<Order, Long> {

    @EntityGraph(attributePaths = {"member", "orderLines"}, type = EntityGraph.EntityGraphType.LOAD)
    Optional<Order> findDetailById(Long id);
}

Spring Data JPA도 repository 메서드에서 @EntityGraph를 지원하므로, 전역 EAGER 매핑 대신 메서드 단위 fetch plan을 선언적으로 다루기 좋습니다.

한계와 주의점

EAGER는 지연 초기화 예외를 줄여 줄 수는 있지만, 그 대가로 원치 않는 데이터와 과도한 result set, 더 무거운 기본 조회를 만들기 쉽다

Jakarta Persistence API 문서 자체가 fetch = EAGER는 원치 않는 데이터까지 fetch하기 쉽기 때문에 권장되지 않는다고 설명합니다. 즉 eager는 편리함보다 과조회의 위험을 먼저 안고 있는 선택입니다.

Hibernate 문서도 같은 방향입니다. 연관을 eager로 기본 매핑하는 것은 단순한 session 연산이 거의 전체 데이터베이스를 끌고 오는 결과로 이어질 수 있다고 경고합니다.

또 eager join fetching은 일반적으로 효율적이지만, 여러 many-valued association을 동시에 join fetch하면 카테시안 곱과 큰 result set으로 오히려 비효율이 커질 수 있습니다.

주의해야 할 지점
  • EAGER는 과조회 위험을 가진다
  • to-one 기본 fetch를 모르고 두면 기본 fetch graph가 빠르게 커진다
  • 여러 컬렉션을 eager join fetch하면 result set 폭발과 카테시안 곱 문제가 생길 수 있다
  • EAGER는 특정 화면 최적화가 아니라 여러 조회 경로에 퍼지는 전역 영향력을 가진다

자주 하는 실수

EAGER를 어렵게 만드는 원인은 문법 자체보다, 매핑 레벨 정책과 조회별 fetch plan을 같은 것으로 생각하는 데 있다
  • EAGER = 항상 JOIN FETCH라고 생각함
  • LazyInitializationException을 피하려고 연관을 전부 EAGER로 바꿈
  • @ManyToOne, @OneToOne 기본 fetch 규칙을 모르고 명시를 생략함
  • 컬렉션에 EAGER를 가볍게 주고 result set 폭발을 겪음
  • 기본 매핑은 그대로 둔 채, 상세 조회용 fetch plan을 따로 설계하지 않음
  • find()와 JPQL query가 같은 방식으로 fetch될 것이라고 가정함

실무 루틴

실무에서는 EAGER를 기본값으로 늘리는 것보다, 기본 매핑은 보수적으로 두고 필요한 조회에서만 eager fetching을 선언하는 루틴이 훨씬 안정적이다
  1. 먼저 @ManyToOne, @OneToOne 기본 fetch를 묵시적으로 믿지 말고 명시적으로 결정한다.
  2. 전역 EAGER 매핑은 정말 기본값으로 남아도 되는 속성만 대상으로 삼는다.
  3. 상세 화면이나 API 응답용 조회는 EntityGraph 또는 join fetch로 설계한다.
  4. fetch graph와 load graph 차이를 구분해 조회 의도를 분명히 한다.
  5. SQL 개수뿐 아니라 result set 크기와 중복 root row까지 함께 본다.
  6. 컬렉션 eager fetch는 특히 조심하고, 병렬 join fetch가 필요한지 먼저 검토한다.

디버깅

EAGER 관련 문제를 볼 때는 단순히 “쿼리가 많이 나간다”가 아니라, 이 조회가 기본 fetch graph의 영향인지 query-time fetch plan의 영향인지부터 나눠 봐야 한다
1
먼저 해당 속성이 @Basic, to-one, to-many 중 무엇인지와 기본 fetch가 무엇인지 확인한다.
2
문제가 find() 경로인지, JPQL query 경로인지, repository 메서드 경로인지 구분한다.
3
명시적인 EntityGraphjoin fetch가 붙어 있는지, 그리고 graph type이 LOAD인지 FETCH인지 확인한다.
4
SQL 개수만 보지 말고 한 쿼리의 result set이 비정상적으로 커지지 않았는지도 함께 본다.
5
여러 컬렉션을 동시에 eager join fetch하고 있다면 카테시안 곱 가능성을 먼저 의심한다.
점검 체크리스트
- 이 속성은 기본 EAGER 대상인가
- fetch를 명시했는가, 아니면 기본값에 의존하고 있는가
- find()인가, JPQL query인가
- EntityGraph / join fetch / repository graph가 붙어 있는가
- LOAD graph인가 FETCH graph인가
- SQL 수보다 result set 크기와 중복 row가 더 큰 문제는 아닌가

요약

EAGER의 핵심은 즉시 조인 기술이 아니라, 로딩 시점에 해당 상태가 반드시 준비돼 있어야 한다는 요구사항이며, 실무에서는 기본 매핑보다 조회별 fetch plan을 설계하는 관점이 더 중요하다
  • FetchType.EAGER는 provider에 대한 requirement이다.
  • @Basic 기본 fetch는 EAGER다.
  • @ManyToOne, @OneToOne은 fetch 미지정 시 persistence unit 기본 to-one fetch를 따르며, 그 기본값은 EAGER다.
  • @OneToMany, @ManyToMany, @ElementCollection은 기본 LAZY다.
  • find()는 기본 fetch graph의 영향을 받지만, JPQL query는 query 자체가 가져올 데이터를 결정한다.
  • EntityGraph의 FETCH와 LOAD는 동작 의미가 다르다.
  • ✅ 전역 EAGER 매핑은 unwanted data와 더 무거운 기본 조회를 만들기 쉽다.
  • ✅ 대부분의 경우에는 매핑-level EAGER보다 query-time eager fetching이 더 실용적이다.

 

 

728x90