도입
JPA를 처음 배울 때 EAGER는 흔히 “즉시 조인해서 다 가져오는 방식” 정도로 이해되곤 합니다. 하지만 실제 의미는 훨씬 더 모델링에 가깝습니다.
EAGER는 로딩 시점의 보장에 관한 정책입니다. 즉 owner 엔티티가 준비될 때 해당 연관이나 속성도 사용 가능한 상태여야 한다는 의미이지, 반드시 하나의 SQL join 문으로 해결된다는 뜻은 아닙니다.
그래서 EAGER를 이해한다는 것은 단순히 fetch 옵션 하나를 아는 것이 아니라, 기본 fetch graph와 조회별 fetch plan을 어떻게 구분해서 다룰지 이해하는 일과 연결됩니다.
필요성
특히 JPA에서는 to-one 연관의 기본 fetch가 eager 쪽으로 기울어져 있기 때문에, fetch를 명시하지 않은 채 엔티티 관계를 늘려 가면 의도하지 않은 과조회가 쉽게 발생합니다.
이 문제는 처음에는 잘 드러나지 않습니다. 도메인 모델이 작을 때는 조회 비용이 감당 가능해 보이기 때문입니다. 하지만 연관이 많아질수록 단순한 find() 호출도 기본 fetch graph 때문에 점점 무거워질 수 있습니다.
그래서 EAGER는 “편해서 쓰는 옵션”이 아니라, 한 번 켜면 기본 조회 경계를 넓히는 설계 선택으로 이해하는 편이 더 정확합니다.
- 기본 fetch 정책만으로도 조회 비용이 크게 달라질 수 있음
find()와 JPQL query가 같은 방식으로 fetch되지 않는 이유를 이해할 수 있음- EntityGraph, fetch graph, load graph 같은 조회별 fetch plan 도구를 더 정확히 쓸 수 있음
- LAZY 예외를 피하려고 무작정 EAGER로 바꾸는 실수를 줄일 수 있음
정의
Jakarta Persistence 기준으로 FetchType.EAGER는 provider에 대한 requirement입니다. 즉 해당 속성은 eager하게 가져와야 합니다.
반대로 FetchType.LAZY는 힌트입니다. provider는 LAZY로 지정된 것보다 더 많이 fetch해도 되지만, EAGER는 반드시 fetch해야 한다는 의미를 가집니다.
이 차이 때문에 EAGER는 단순히 “조금 더 빨리 불러온다”가 아니라, 매핑 수준에서 provider에게 더 강한 의무를 부여하는 선택으로 봐야 합니다.
"EAGER는 빠름의 약속이 아니라
기본 fetch graph를 넓히는 강한 매핑 선택에 가깝습니다."
핵심 원리
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)은 별도로 조회 범위를 조정
기본 규칙
| 대상 | 기본 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 쪽을 따릅니다.
기본 구현
@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가 다른 서비스와 테스트, 배치 작업의 기본 조회 비용까지 함께 키울 수 있습니다.
Hibernate 공식 문서도 대부분의 association은 lazy로 매핑하고, eager fetching은 필요한 곳에서만 명시적으로 요청하라고 설명합니다. 즉 mapping-level EAGER는 기본 전략이라기보다 예외적인 선택에 더 가깝습니다.
매핑 레벨 EAGER
- 엔티티 정의 자체에 로딩 요구사항을 박아 넣음
- find()의 기본 fetch graph에 영향
- 특정 조회가 아니라 여러 호출 경로에 파급될 수 있음
패턴 2. 조회 시점 EAGER가 더 실용적이다
Jakarta Persistence와 Hibernate 문서 모두 EntityGraph나 left 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);
EntityGraph와 left join fetch를 함께 설명하는 이유는 분명합니다. 대부분의 경우 필요한 것은 “항상 eager”가 아니라 “이번 조회에서만 eager”이기 때문입니다.패턴 3. EntityGraph 는 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을 선언적으로 다루기 좋습니다.
한계와 주의점
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 = 항상 JOIN FETCH라고 생각함
- LazyInitializationException을 피하려고 연관을 전부 EAGER로 바꿈
@ManyToOne,@OneToOne기본 fetch 규칙을 모르고 명시를 생략함- 컬렉션에 EAGER를 가볍게 주고 result set 폭발을 겪음
- 기본 매핑은 그대로 둔 채, 상세 조회용 fetch plan을 따로 설계하지 않음
find()와 JPQL query가 같은 방식으로 fetch될 것이라고 가정함
실무 루틴
- 먼저
@ManyToOne,@OneToOne기본 fetch를 묵시적으로 믿지 말고 명시적으로 결정한다. - 전역 EAGER 매핑은 정말 기본값으로 남아도 되는 속성만 대상으로 삼는다.
- 상세 화면이나 API 응답용 조회는 EntityGraph 또는 join fetch로 설계한다.
- fetch graph와 load graph 차이를 구분해 조회 의도를 분명히 한다.
- SQL 개수뿐 아니라 result set 크기와 중복 root row까지 함께 본다.
- 컬렉션 eager fetch는 특히 조심하고, 병렬 join fetch가 필요한지 먼저 검토한다.
디버깅
@Basic, to-one, to-many 중 무엇인지와 기본 fetch가 무엇인지 확인한다.find() 경로인지, JPQL query 경로인지, repository 메서드 경로인지 구분한다.EntityGraph나 join fetch가 붙어 있는지, 그리고 graph type이 LOAD인지 FETCH인지 확인한다.점검 체크리스트
- 이 속성은 기본 EAGER 대상인가
- fetch를 명시했는가, 아니면 기본값에 의존하고 있는가
- find()인가, JPQL query인가
- EntityGraph / join fetch / repository graph가 붙어 있는가
- LOAD graph인가 FETCH graph인가
- SQL 수보다 result set 크기와 중복 row가 더 큰 문제는 아닌가
요약
- ✅
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이 더 실용적이다.