JPA N+1 문제와 Fetch Join

code++·2026년 7월 14일

FetchType

JPA에서 연관 엔티티를 언제 조회할지 결정하는 옵션이다.

1. LAZY (지연 로딩)

연관 엔티티를 실제로 사용할 때 조회한다.

@ManyToOne(fetch = FetchType.LAZY)
private Seller seller;

예를 들어 상품을 조회하면 상품만 가져오고,

Product product = productRepository.findById(1L).get();

이 시점에는 Seller를 조회하지 않는다.

하지만

product.getSeller().getName();

을 호출하는 순간 판매자를 조회하는 SQL이 실행된다.

장점

  • 필요한 데이터만 조회하여 성능이 좋다.
  • 실무에서 기본적으로 많이 사용된다.

단점

  • 잘못 사용하면 N+1 문제가 발생할 수 있다.

2. EAGER (즉시 로딩)

부모 엔티티를 조회할 때 연관 엔티티도 함께 조회한다.

@ManyToOne(fetch = FetchType.EAGER)
private Seller seller;

상품을 조회하면 판매자를 사용하지 않아도 함께 조회한다.

장점

  • 연관 엔티티를 바로 사용할 수 있다.

단점

  • 필요 없는 데이터까지 조회할 수 있어 성능이 저하될 수 있다.
  • 연관관계가 많아질수록 예상하지 못한 SQL이 실행될 수 있다.

@ManyToOne@OneToOne의 기본 FetchType은 EAGER이다.
실무에서는 대부분 LAZY로 변경하여 사용한다.


N+1 문제란?

LAZY 상태에서 부모 엔티티를 조회한 후 연관 엔티티를 반복해서 접근하면 추가 SQL이 계속 실행되는 문제이다.

예시

List<Product> products = productRepository.findAll();

for (Product product : products) {
    System.out.println(product.getSeller().getName());
}

실행되는 SQL

-- 1번
SELECT * FROM product;

-- 상품 개수만큼 반복
SELECT * FROM seller WHERE id = 1;
SELECT * FROM seller WHERE id = 2;
...

상품이 100개라면

  • 상품 조회 : 1번
  • 판매자 조회 : 100번

➡️ 총 101번의 SQL이 실행된다.

이를 N+1 문제라고 한다.


Fetch Join

Fetch Join은 연관 엔티티를 부모 엔티티와 함께 한 번에 조회하는 방법이다.

@Query("""
select p
from Product p
join fetch p.seller
""")
List<Product> findAllWithSeller();

실행되는 SQL

SELECT *
FROM product p
JOIN seller s
ON p.seller_id = s.id;

이후

product.getSeller().getName();

을 호출해도 추가 SQL이 발생하지 않는다.

즉,

  • 상품 조회 + 판매자 조회를 한 번의 SQL로 처리한다.
  • N+1 문제를 해결할 수 있다.

정리

구분특징
LAZY연관 엔티티를 실제 사용할 때 조회한다.
EAGER부모를 조회할 때 연관 엔티티도 함께 조회한다.
N+1부모 조회 1번 + 연관 엔티티 조회 N번이 발생하는 성능 문제이다.
Fetch Join부모와 연관 엔티티를 한 번의 SQL로 조회하여 N+1 문제를 해결한다.

사용 방식

  • 기본은 LAZY를 사용한다.
  • 연관 엔티티가 필요한 조회에서는 Fetch Join 또는 @EntityGraph를 사용한다.
  • EAGER는 예기치 않은 성능 문제를 유발할 수 있어 일반적으로 권장되지 않는다.
profile
일상

0개의 댓글