@ManyToOne@OneToOne은 기본 Fetch 전략이 EAGER다. 반면 @OneToMany@ManyToMany는 LAZY다. 같은 JPA인데 왜 어노테이션마다 기본값이 다를까. 이 설계 결정에는 이유가 있다.

관련 문서

- JPA Fetch 타입 완전 정복 — Fetch 전략 전체 개념 정리

설계 당시의 판단 기준

JPA 스펙이 만들어진 2006년 당시, 설계자들은 연관관계를 두 가지로 구분했다.

  • 단일 객체 연관 (@ManyToOne, @OneToOne) — 연관 대상이 딱 하나
  • 컬렉션 연관 (@OneToMany, @ManyToMany) — 연관 대상이 여러 개

단일 객체는 JOIN 한 번이면 함께 가져올 수 있다. 데이터 크기도 예측 가능하다. 어차피 같이 쓸 가능성이 높다고 봤고, EAGER가 더 자연스럽다고 판단했다.

컬렉션은 다르다. 몇 개가 들어올지 모른다. 10개일 수도 있고 10만 개일 수도 있다. 항상 즉시 가져오면 언제 어디서 성능 문제가 터질지 예측이 안 된다. 그래서 LAZY를 기본으로 했다.

요약하면 다음과 같다.

  • 단일 객체 → 크기 예측 가능 → EAGER
  • 컬렉션 → 크기 예측 불가 → LAZY

왜 지금은 문제가 되는가

설계 당시의 전제는 "대부분 단건 조회를 한다"였다. 단건 조회에서는 EAGER가 문제없다.

Feed feed = feedRepository.findById(1L);
// → Feed + User를 JOIN 한 번으로 가져옴 → 문제없음

하지만 실제 서비스에서는 목록 조회가 훨씬 많다.

List<Feed> feeds = feedRepository.findAll();
// Feed가 100개면 EAGER로 인해 User 쿼리가 100번 추가로 실행됨

EAGER가 단건 조회 전제에서 설계됐는데, 현실은 목록 조회가 주력이었다. 설계 당시의 가정이 틀린 것이다.

왜 지금도 바꾸지 않는가

하위 호환성 때문이다. 기본값을 LAZY로 바꾸면 기존에 EAGER를 가정하고 작성된 수많은 코드가 동작 방식이 달라진다. 트랜잭션 밖에서 연관 필드에 접근하던 코드는 LazyInitializationException으로 터진다.

JPA 스펙을 관리하는 Jakarta EE 측도 이 문제를 인지하고 있다. 공식 문서에서 직접 권고한다.

JPA 스펙 권고

"It is recommended to use FetchType.LAZY for all associations."

— Jakarta Persistence specification

기본값은 EAGER지만, 쓰지 말라고 권장하는 상황이다. 기본값이 권장 사항과 반대인 셈이다.

실제 적용

결론은 단순하다. 기본값을 믿지 말고 항상 명시적으로 LAZY를 설정한다.

// 기본값에 의존하면 EAGER — 위험
@ManyToOne
private User author;

// 명시적으로 LAZY 설정 — 안전
@ManyToOne(fetch = FetchType.LAZY)
private User author;

// OneToOne도 마찬가지
@OneToOne(fetch = FetchType.LAZY)
private Profile profile;

필요할 때는 fetch join으로 명시적으로 가져오면 된다.