JPA

4. N + 1 문제

ggomjiu 2025. 6. 17. 18:42

N+1 문제

: ORM을 사용하는 애플리케이션에서 관계형 데이터베이스와의 데이터 조회 작업에서 발생할 수 있는 성능 문제

- 한 번의 초기 쿼리 실행으로 가져온 데이터를 사용하는 도중 추가로 N번의 쿼리를 실행해야 하는 상황

- 이로 인해, 데이터베이스와의 불필요한 네트워크 통신이 발생하며, 성능 저하와 불필요한 데이터베이스 부하를 초래할 수 있음

 

FetchType.EAGER vs FetchType.LAZY

  • FetchType.EAGER
    • JPQL에서 만든 SQL을 통해 데이터를 조회
    • 이후 JPA에서 Fetch 전략을 가지고 해당 데이터와의 연관 관계인 하위 엔티티들을 추가 조회
    • 2번 과정으로 N+1 문제 발생
  • FetchType.LAZY
    • JPQL에서 만든 SQL을 통해 데이터 조회
    • JPA에서 Fetch 전략을 가지지만, 지연 로딩이기 때문에 추가 조회는 하지 않음
    • But, 하위 엔티티를 가지고 작업하게 되면 추가 조회가 발생하기 때문에 결국 N+1 문제 발생

N+1 문제 발생 원인

  1. JpaRepository에 정의한 인터페이스 메서드를 실행하면 JPA는 메서드 이름을 분석해서 JPQL을 생성하여 실행
  2. JPQL은 SQL을 추상화한 객체 지향 뭐리 언어로서 특정 SQL에 종속되지 않고 엔티티 객체와 필드 이름을 가지고 쿼리를 함
  3. 그렇기에 JPQL은 findAll()이란 메소드를 수행하였을 때, 해당 엔티티를 조회하는 쿼리만 실행하게 됨
  4. jpql 입장에서는 연관관계 데이터를 무시하고 해당 엔티티 기준으로 쿼리를 조회하기 때문
  5. 그렇기에 연관된 엔티티 데이터가 필요한 경우, FetchType으로 지정한 시점에 조회를 별도로 호출함

해결 방법

1. Fetch Join

: 연관된 엔티티나 컬렉션을 SQL 한번에 함께 조회하는 기능

- SQL 조인의 종류가 아님

- JPQL에서 성능 최적화를 위해 제공하는 기술

- join fetch 명령어를 사용함

// Fetch join을 사용한 코드
@Entity
public class Author {
    // ...

    @OneToMany(fetch = FetchType.LAZY)
    @JoinColumn(name = "author_id")
    private List<Book> books;

    // ...
}

// Fetch join을 적용한 조회
@EntityGraph(attributePaths = "books")
Optional<Author> retrievedAuthor = authorRepository.findByIdWithBooks(author.getId());

// 결과 출력
System.out.println(retrievedAuthor.get().getName()); // John Smith
System.out.println(retrievedAuthor.get().getBooks().size()); // 2

- Author 엔티티에서 books 필드에 Fetch Join을 적용하여 작가와 연관된 책을 한 번에 가져옴

- AuthorRepository.findByIdWithBooks() 메서드는 Fetch Join이 적용된 쿼리를 실행하여 작가와 연관된 책의 정보를 함께 가져옴

- 단점 )

  • Fetch Join은 필요한 모든 연관 엔티티를 한 번에 가져오기 때문에 데이터의 양이 많을 경우 성능 저하가 발생할 수 있음
  • Fetch Join은 JPA 구현체에 의존하기 때문에 이식성이 떨어질 수 있음

cf) 지연 로딩

  1. 연관관계가 있는 엔티티를 조회할 경우, 지연 로딩으로 설정되어 있으면 연관관계에서 종속된 엔티티는 실행 시 select되지 않고 proxy 객체를 만들어 엔티티가 적용시킴
  2. 해당 proxy 객체를 호출할 때마다 그때 그때 select 쿼리가 실행됨
  3. 위 같은 연관 관계가 지연 로딩으로 되어있을 경우, fetch join을 사용하여 여러 번의 쿼리를 한 번에 해결할 수 있음

2. EntityGraph

: Data JPA에서 fetch join을 어노테이션으로 사용할 수 있도록 만들어준 기능

// EntityGraph를 사용한 코드
@Entity
public class Author {
    // ...
    
    @OneToMany(fetch = FetchType.LAZY)
    @JoinColumn(name = "author_id")
    private List<Book> books;

    // ...
}

// EntityGraph를 적용한 조회
EntityGraph<Author> entityGraph = entityManager.createEntityGraph(Author.class);
entityGraph.addAttributeNodes("books");

Map<String, Object> properties = new HashMap<>();
properties.put("javax.persistence.fetchgraph", entityGraph);

Author retrievedAuthor = entityManager.find(Author.class, author.getId(), properties);

// 결과 출력
System.out.println(retrievedAuthor.getName()); // John Smith
System.out.println(retrievedAuthor.getBooks().size()); // 2

- Author 엔티티에서 books 필드에 EntityGraph를 적용하여 작가와 연관된 책을 한 번에 가져오는 방식

- EntityGraph를 사용하여 작가 엔티티 조회 시 즉시 필요한 연관 엔티티를 로드할 수 있음

- 단점 )

  • EntityGraph는 NamedEntityGraph를 정의하고 사용해야 하므로 추가 작업이 필요함
  • EntityGraph는 동적인 쿼리 생성이 제한적일 수 있음

3. BatchSize

: 다수의 프록시 객체를 조회할 때, where절이 같은 여러 개의 select 쿼리들을 하나의 in 쿼리로 만들어줌

- JPA의 성능 개선을 위한 옵션

- 데이터의 전체 row를 예상할 수 있는 범위에서는 좋은 방안이긴 하지만, 정확한 size를 알 수 없을 때는 적정값을 설정해 주긴 어려움

// BatchSize를 사용한 코드
@Entity
public class Author {
    // ...

    @OneToMany(mappedBy = "author", fetch = FetchType.LAZY)
    @BatchSize(size = 10)
    private List<Book> books;

    // ...
}

// BatchSize를 적용한 조회
Author retrievedAuthor = authorRepository.findById(author.getId()).orElse(null);
Hibernate.initialize(retrievedAuthor.getBooks());

// 결과 출력
System.out.println(retrievedAuthor.getName()); // John Smith
System.out.println(retrievedAuthor.getBooks().size()); // 2

- Author 엔티티에서 books 필드에 BatchSize를 적용하여 일괄 로딩을 수행하는 방식

- @BatchSize 어노테이션을 사용하여 한 번의 쿼리로 지정된 개수의 연관 엔티티를 로드함

- 단점 )

  • BatchSize는 일괄 로딩을 수행하지만, 연관 엔티티가 많은 경우 여전히 N+1 문제가 발생할 수 있음
  • 성능을 향상시키기 위해 적절한 BatchSize 값을 설정해야 함

4. QueryBuilder

: 동적 쿼리를 프로그래밍 방식으로 작성할 수 있게 도와주는 기능

// QueryBuilder를 사용한 코드
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<Author> query = cb.createQuery(Author.class);
Root<Author> root = query.from(Author.class);
root.fetch("books", JoinType.LEFT);
query.select(root).where(cb.equal(root.get("id"), author.getId()));

Author retrievedAuthor = entityManager.createQuery(query).getSingleResult();

// 결과 출력
System.out.println(retrievedAuthor.getName()); // John Smith
System.out.println(retrievedAuthor.getBooks().size()); // 2

- Criteria API를 사용하여 작가와 연관된 책을 함께 조회하는 방식

- QueryBuilder를 통해 작가와 연관된 책의 정보를 로드하고, 한 번의 결과를 가져옴

 

cf) Criteria API

: 자바의 JPA사양의 일부로서 기존 텍스트로 구성된 JPQL을 빌더 클래스를 사용하여 타입-세이프한 쿼리를 생성하는 API

- 기존의 JPQL의 방식은 텍스트로 SQL을 작성하는 방식이기에 컴파일 타임에 오류를 검출할 수 없었으나, Criteria API 방식은 컴파일 안정성을 제공하면서 컴파일 타임에 오류를 검출할 수 있음

 

- 단점 )

  • QueryBuilder를 사용하면 복잡한 쿼리 작성이 필요하고, JPA의 객체 지향적인 특성을 잃어버릴 수 있음
  • 유지보수가 어려울 수 있음

 

 

 

 

 

 

 

 

 

 

Reference

https://wonin.tistory.com/496

 

@EntityGraph에 대해 알아보자

@EntityGraph란? 연관관계가 있는 엔티티를 조회할 경우 지연 로딩으로 설정되어 있으면 연관관계에서 종속된 엔티티는 쿼리 실행 시 select 되지 않고 proxy 객체를 만들어 엔티티가 적용시킵니다. 그

wonin.tistory.com

https://adjh54.tistory.com/483

 

[Java/JPA] Spring Boot Data JPA + Criteria API 이해하기 -1 : 정의 및 기본동작

해당 글에서는 Spring Boot Data JPA 내에서 기능 중 하나인  Criteria API에 대해 이해하고 기본 동작들에 대해 이해를 돕기 위해 작성한 글입니다. 💡 [참고] JPA 관련해서 구성 내용에 대해 궁금하시

adjh54.tistory.com

 

'JPA' 카테고리의 다른 글

6. JPA의 캐시  (0) 2025.06.17
5. M : N 해결 전략  (0) 2025.06.17
3. Eager, Lazy Loading  (0) 2025.06.06
2. 영속성 컨텍스트(캐시, 동일성 보장, 변경 감지, 트랜잭션 지연)  (0) 2025.06.06
1. JPA & Hibernate  (2) 2025.06.06