|

JPA N+1 문제를 SQL 흐름으로 이해하기: fetch join 전에 봐야 할 것

JPA N+1 문제를 SQL 흐름으로 이해하기: fetch join 전에 봐야 할 것
JPA N+1 문제는 첫 조회 1번 뒤에 연관 데이터 조회 N번이 반복될 때 드러난다

JPA N+1 문제는 “fetch join을 쓰면 된다”로 외우면 오래가지 않습니다. 먼저 왜 SQL이 여러 번 나가는지 조회 흐름을 봐야 합니다.

N+1은 보통 첫 목록 조회 1번 뒤에, 각 행의 연관 데이터를 가져오기 위한 추가 조회가 N번 반복되는 상황을 말합니다. 문제의 핵심은 객체 접근 시점과 SQL 실행 시점이 다를 수 있다는 점입니다.

이 글에서는 해결책 이름보다 SQL 흐름을 먼저 보고, fetch join과 다른 선택지를 언제 쓸지 정리합니다. ORM의 관계 로딩 방식은 SQLAlchemy 관계 로딩 문서처럼 다른 ORM 문서에서도 lazy loading과 eager loading으로 나눠 설명합니다. Java 쪽 설계 감각은 Java Optional 글과 함께 보면 좋습니다.


JPA N+1 문제를 볼 때 나눌 것

JPA N+1 문제 조회 흐름 카드
N+1은 해결책 이름보다 어떤 시점에 SQL이 추가로 나가는지 먼저 봐야 한다

JPA N+1은 조회가 두 단계로 벌어질 때 보인다

예를 들어 주문 목록을 조회한 뒤 각 주문의 회원 이름을 화면에 보여준다고 해보겠습니다. 처음에는 주문 목록만 가져오고, 이후 order.getMember().getName()을 호출할 때 회원 조회가 하나씩 나갈 수 있습니다.

1. 주문 목록 조회
   select * from orders;

2. 각 주문의 member 접근
   select * from member where id = 1;
   select * from member where id = 2;
   select * from member where id = 3;
   ...

주문이 100개라면 회원 조회가 추가로 100번 나갈 수 있습니다. 그래서 1 + N이라는 이름이 붙습니다.


lazy loading 자체가 나쁜 것은 아니다

lazy loading은 연관 데이터를 항상 가져오지 않고, 실제로 접근할 때 가져오는 전략입니다. 연관 데이터가 필요 없는 화면에서는 불필요한 조회를 줄일 수 있습니다.

문제는 목록처럼 여러 부모 엔티티를 가져온 뒤, 반복문 안에서 연관 객체에 접근할 때 드러납니다. lazy loading이 나쁜 것이 아니라, 필요한 데이터 패턴과 맞지 않는 방식으로 쓰였을 때 문제가 됩니다.


fetch join은 항상 함께 필요한 데이터에 쓴다

fetch join은 연관 데이터를 한 번의 쿼리로 함께 가져오고 싶을 때 검토합니다. 주문 목록에서 회원 이름이 항상 필요하다면, 처음부터 주문과 회원을 같이 가져오는 편이 자연스럽습니다.

@Query("select o from Order o join fetch o.member where o.status = :status")
List<Order> findOrdersWithMember(OrderStatus status);

다만 fetch join을 남용하면 쿼리가 커지고, 컬렉션 fetch join에서는 중복 row나 페이징 문제가 생길 수 있습니다. 그래서 “항상 필요하다”는 근거가 있을 때 쓰는 편이 좋습니다.

특히 화면마다 필요한 데이터가 다르면 fetch join을 공통 Repository 메서드에 너무 넓게 넣는 것이 부담이 될 수 있습니다. 어떤 화면에서는 회원만 필요하고, 다른 화면에서는 배송지와 쿠폰까지 필요하다면 조회 목적별 메서드를 나누는 편이 더 명확합니다.


페이징과 컬렉션 fetch join은 더 조심한다

N+1을 줄이려고 컬렉션을 fetch join했는데, 페이징과 만나면 다른 문제가 생길 수 있습니다. 부모 행 하나가 자식 행 수만큼 중복되어 SQL 결과가 부풀기 때문입니다.

게시글 10개를 보고 싶다

post 1 - comment 3개
post 2 - comment 5개
post 3 - comment 1개

join 결과는 게시글 기준 3행이 아니라 댓글 수만큼 9행이 된다.
이 상태에서 limit/offset을 적용하면 기대한 게시글 개수와 달라질 수 있다.

그래서 목록 화면에서는 부모 엔티티를 먼저 페이징하고, 필요한 자식 데이터는 batch size나 별도 조회로 보강하는 방식이 더 안정적일 때가 많습니다.


EntityGraph, batch size, DTO 조회도 선택지다

Spring Data JPA에서는 특정 조회에서 연관 데이터를 함께 가져오도록 EntityGraph를 검토할 수 있습니다. Hibernate 환경에서는 batch size로 여러 lazy loading을 묶어 조회하는 방식도 자주 사용됩니다. 화면에 필요한 필드만 확실하다면 DTO 조회가 더 단순할 수도 있습니다.

  • fetch join: 특정 쿼리에서 항상 함께 필요한 연관 데이터를 가져올 때
  • EntityGraph: Repository 메서드 단위로 fetch 계획을 분리하고 싶을 때
  • batch size: lazy loading은 유지하되 추가 조회를 묶고 싶을 때
  • DTO 조회: 화면에 필요한 필드가 명확하고 엔티티 그래프가 복잡할 때

DTO 조회는 특히 목록 API에서 유용합니다. 화면에 주문 번호, 회원 이름, 총액만 필요하다면 엔티티 그래프 전체를 가져올 이유가 없습니다. 반대로 수정 로직처럼 엔티티 상태 변경이 핵심이라면 엔티티 조회가 더 자연스럽습니다.


조회 목적별로 처방이 달라진다

N+1을 발견하면 바로 fetch join을 넣고 싶어집니다. 하지만 조회 목적을 먼저 나누면 더 안전한 선택을 할 수 있습니다.

  • 상세 화면: 한 건의 부모와 연관 데이터를 함께 보여주므로 fetch join이 단순할 수 있다
  • 목록 화면: 페이징이 중요하므로 DTO 조회나 batch size가 더 안정적일 수 있다
  • 관리자 엑셀 다운로드: 많은 데이터를 한 번에 읽으므로 전용 쿼리와 stream 처리까지 검토한다
  • 수정 화면: 엔티티 상태 변경이 필요하므로 필요한 연관만 명확히 가져온다

같은 Order 엔티티라도 목록 API, 상세 API, 수정 API, 통계 API에서 필요한 데이터가 다릅니다. 공통 조회 메서드 하나로 모두 해결하려고 하면 어느 화면에는 과하고, 어느 화면에는 부족한 쿼리가 됩니다.

목록 API
- orderId, memberName, totalPrice만 필요
- DTO projection 검토

상세 API
- order, member, delivery, orderItems가 필요
- fetch join 또는 EntityGraph 검토

통계 API
- 엔티티보다 집계 결과가 필요
- group by 기반 전용 쿼리 검토

이렇게 조회 목적을 분리하면 N+1 해결책도 코드에 덜 억지스럽게 들어갑니다.

가능하면 이 기준을 테스트나 리뷰 체크리스트에도 남겨두는 것이 좋습니다. 목록 API를 추가할 때 SQL 로그를 한 번 확인하고, 상세 API를 만들 때 필요한 연관만 명시하는 식입니다. N+1은 한 번 고치고 끝나는 문제가 아니라 새 조회가 생길 때마다 다시 생길 수 있는 문제입니다.


N+1을 확인하는 가장 좋은 방법은 SQL 로그다

N+1은 코드만 보고 짐작하는 것보다 SQL 로그로 보는 것이 정확합니다. 목록 조회 뒤 반복문에서 추가 SELECT가 계속 찍힌다면 원인을 찾기 쉽습니다.

  1. 목록 API를 실행한다
  2. SQL 로그를 켠다
  3. 첫 SELECT 뒤에 연관 테이블 SELECT가 반복되는지 본다
  4. 해당 화면에서 그 연관 데이터가 항상 필요한지 판단한다
  5. fetch join, EntityGraph, batch size, DTO 조회 중 하나를 고른다

정리

JPA N+1은 fetch join 이름을 외우는 문제가 아닙니다. 첫 조회가 무엇을 가져왔고, 객체 접근 시점에 어떤 SQL이 추가로 나가는지 보는 문제입니다.

SQL 흐름을 먼저 보면 해결책도 덜 헷갈립니다. 항상 함께 필요하면 fetch join, 조건별 fetch 계획이 필요하면 EntityGraph, 반복 lazy loading을 완화하려면 batch size, 화면 전용이면 DTO 조회를 검토하면 됩니다.

함께보면 좋은 글