백엔드 면접 용어 사전
Spring·JPA

fetch join

연관 엔티티를 한 번의 조인 쿼리로 함께 가져오는 JPQL 문법. N+1의 대표 해법이다.

연관 엔티티를 한 번의 조인 쿼리로 함께 가져와 즉시 초기화하는 JPQL 문법. N+1의 대표 해법이다.

일반 조인과 결과 메모리가 다르다

-- 일반 조인 — SELECT 대상만 채워진다. member 는 여전히 프록시
select o from Order o join o.member m

-- fetch join — member 까지 실제 인스턴스로 채워진다
select o from Order o join fetch o.member

메모리로 보면 이렇게 갈린다.

일반 조인 :Order{ member → 프록시(비어 있음) }→ 나중에 SQL 또 나감
fetch join:Order{ member → Member 인스턴스 }→ 추가 SQL 없음

컬렉션 fetch join의 함정 ① — 페이징과 같이 쓰면 안 된다

select t from Team t join fetch t.members

팀 1개에 멤버 3명이면 조인 결과는 3행이 된다. 여기에 limit 2를 걸면 팀이 아니라 이 잘려 데이터가 깨진다.

그래서 하이버네이트는 DB에서 페이징하지 못하고 전체를 메모리로 읽어 와서 자른다. 경고 로그(firstResult/maxResults specified with collection fetch; applying in memory)를 남기며, 데이터가 커지면 그대로 OOM이다.

루트만 페이징하고 컬렉션은 @BatchSize로 푼다.

  • 1) select t from Team t (페이징) — → 팀 10개
  1. select m from Member m where team_id in (…) → IN 절 한 방
  • 총 2번. N+1이 1+1이 된다

spring.jpa.properties.hibernate.default_batch_fetch_size: 100 으로 전역 설정하는 것이 실무에서 가장 흔한 처방이다.

함정 ② — 컬렉션을 둘 이상 fetch join하면 곱집합

멤버 3명 × 프로젝트 4개 = 12행으로 부풀고 중복 제거도 어려워진다. 하나만 fetch join하고 나머지는 batch로 푼다.

중복 제거

컬렉션 조인은 루트가 행 수만큼 중복된다. JPQL distinct는 SQL DISTINCT를 붙이는 동시에 애플리케이션에서 같은 엔티티 참조를 합쳐 준다. 하이버네이트 6부터는 기본 동작이라 distinct를 쓰지 않아도 된다.

함께 보면 좋은 용어

노트에서 맥락과 함께 보기 — Spring·JPA — IoC/DI·AOP·영속성 컨텍스트·N+1