[Spring] JPA N + 1 문제 발생 원인 및 해결 방안

2022. 9. 8. 18:24·🍀 Spring Boot

📌 JPA N + 1 문제란?

조회된 데이터 개수만큼, 연관 관계의 조회 쿼리가 추가로 발생하는 문제를 의미한다.
EX) 카테고리와 게시글

@Entity
@NoArgsConstructor
@Setter
@Getter
public class Board {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  private String title;
  private String content;

  @ManyToOne
  private Category category;

  public Board(String title, String content, Category category) {
    this.title = title;
    this.content = content;
    this.category = category;
  }
}
@Entity
@NoArgsConstructor
@Setter
public class Category {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  private String content;

  @OneToMany(mappedBy = "category", fetch = FetchType.EAGER)
  private List<Board> boardList = new ArrayList<>();

  public Category(String content) {
    this.content = content;
  }
}

 
카테고리 10개와 각 카테고리마다 게시글 10개씩 데이터 추가

@Test
@DisplayName("initData")
@Rollback(value = false)
void test() {
	List<Category> categoryList = new ArrayList<>();
	for (int i = 0; i < 10; i++) {
		Category category = new Category("Category" + i );
		Category savedCategory = categoryRepository.save(category);
		List<Board> boardList = new ArrayList<>();
		for (int j = 0; j < 10; j++) {
			boardList.add(new Board("Board" + j, "Board", savedCategory));
		}
		boardRepository.saveAll(boardList);
		savedCategory.setBoardList(boardList);
		categoryList.add(savedCategory);
	}
	categoryRepository.saveAll(categoryList);
}

 
카테고리 전체 조회 시, JpaRepository를 활용하여 findAll로 가져온다.

List<Category> categoryList = categoryRepository.findAll();

 
실행된 SQL문들을 확인하면, 이러한 형태로 총 11개의 쿼리문이 실행된다.

카테고리 10개만큼, 매핑된 게시글을 조회하기 때문에 10번의 쿼리가 더 발생하게 된다.
이를 JPA N + 1 문제라고 한다.


📌 문제 원인

JPA는 메서드 이름을 분석해서 JPQL이 생성되어 SQL이 실행된다.
따라서, 메서드 이름만을 분석하기 때문에 연관 관계는 파악하지 못한 채 SQL문이 실행된다.
1️⃣ EAGER 에서의 원인
: JPQL을 통한 SQL문으로 조회를 한 후, Fetch 전략을 가지고 하위 엔티티를 즉시 조회한다.
2️⃣ LAZY 에서의 원인
: JPQL을 통한 SQL문으로 조회를 한 후, Fetch 전략이 LAZY이므로 즉시 조회를 하지 않는다.
그렇지만, 추후 Getter와 같은 작업이 필요하게 되면, 하위 엔티티를 조회한다.
따라서, N + 1 문제가 발생한다.


📌 해결 방안

❓ Join Fetch

JpaRepository에서 제공해주지 않아서 직접 JPQL로 작성해주고 실행하게 되면,

@Query("select c from Category c join fetch c.boardList")
List<Category> findAllJoinFetch();

이렇게 하나의 쿼리만 실행되어 N + 1문제를 해결할 수 있다.
자세히 살펴보면,

SELECT DISTINCT category0_.id AS CATEGORY_ID, 
boardlist1_.id AS BOARD_ID, 
category0_.content AS CATEGORY_CONTENT, 
boardlist1_.category_id AS BOARD_CATEGORY_ID, 
boardlist1_.content AS BOARD_CONTENT, 
boardlist1_.title AS BOARD_TITLE, 
boardlist1_.category_id AS BOARD_CATEGORY_ID, 
boardlist1_.id AS BOARD_ID
FROM category category0_ 
INNER JOIN board boardlist1_ 
ON category0_.id=boardlist1_.category_id

게시글과 카테고리를 INNER JOIN을 통해 가지고 오는 모습을 확인할 수 있다.
 

❔ 일반 Join과의 차이

SELECT DISTINCT category0_.id AS CATEGORY_ID, 
category0_.content AS CATEGORY_CONTENT 
FROM category category0_ 
INNER JOIN board boardlist1_ 
ON category0_.id=boardlist1_.category_id

일반적인 JOIN의 경우, 필요한 엔티티 컬럼만 SELECT 하므로 N + 1 문제가 해결되지 않는다.
 

☑️ Fetch Join의 단점

모든 연관 관계 데이터를 가져오므로, FetchType.LAZY가 무의미해진다.
 

❓ Entity Graph

@EntityGraph(attributePaths = {"boardList"})
@Query("select DISTINCT c from Category c")
List<Category> findAllEntityGraph();

@EntityGraph를 통해 attributePath에 설정해준 부분을 EAGER 전략으로 조회해올 수 있다.
따라서 boardList를 attributePaths에 넣어주어 설정해주면, 하나의 쿼리만 실행된다.

SELECT DISTINCT category0_.id AS CATEGORY_ID,
boardlist1_.id AS BOARD_ID, 
category0_.content AS CATEGORY_CONTENT, 
boardlist1_.category_id AS BOARD_CATEGORY_ID, 
boardlist1_.content AS BOARD_CONTENT, 
boardlist1_.title AS BOARD_TITLE, 
boardlist1_.category_id AS BOARD_CATEGORY_ID, 
boardlist1_.id AS BOARD_ID 
FROM category category0_ 
LEFT OUTER JOIN board boardlist1_ 
ON category0_.id=boardlist1_.category_id

Join Fetch 방식과 다르게, LEFT OUTER JOIN을 통해 조회해온다.
 

☑️ Entity Graph의 단점

INNER JOIN보다 성능이 안좋은 OUTER JOIN을 사용하게 된다.
 

❔ DISTINCT의 필요성

: Fetch Join과 Entity Graph의 경우, 카테시안 곱이 발생하므로 중복 제거가 필요하다.
(+, 또는 List 대신 Set 이용하기)
 

❓FetchMode.SUBSELECT

지금까지는 하나의 쿼리로 해결했다면, FetchMode.SUBSELECT의 경우 두 번의 쿼리로 해결한다.

import org.hibernate.annotations.Fetch;
import org.hibernate.annotations.FetchMode;

...

@Fetch(FetchMode.SUBSELECT)
@OneToMany(mappedBy = "category", fetch = FetchType.EAGER)
private List<Board> boardList = new ArrayList<>();

기존 N + 1 문제가 발생했던 Test를 다시 보면, 쿼리가 두 번만 발생하여 문제가 해결되었다.

select category0_.id as id1_1_, 
category0_.content as content2_1_ 
from category category0_

select boardlist0_.category_id as category4_0_1_, 
boardlist0_.id as id1_0_1_, 
boardlist0_.id as id1_0_0_, 
boardlist0_.category_id as category4_0_0_, 
boardlist0_.content as content2_0_0_, 
boardlist0_.title as title3_0_0_ 
from board boardlist0_ 
where boardlist0_.category_id 
in (select category0_.id from category category0_)

카테고리 리스트를 조회하는 쿼리문과 IN을 통해 카테고리 ID에 맞는 게시글들을 조회해오는 쿼리문 총 두 번의 쿼리가 발생한다.
FetchType이 EAGER인 경우, 조회해올 때 위의 쿼리가 실행되며, LAZY인 경우엔 하위 엔티티가 필요할 때 실행된다.
 

📌 Batch Size

@BatchSize(size = 10)
@OneToMany(mappedBy = "category", fetch = FetchType.EAGER)
private List<Board> boardList = new ArrayList<>();
SELECT category0_.id AS id1_1_, c
ategory0_.content AS content2_1_ 
FROM category category0_

SELECT boardlist0_.category_id AS category4_0_1_, 
boardlist0_.id AS id1_0_1_, 
boardlist0_.id AS id1_0_0_, 
boardlist0_.category_id AS category4_0_0_, 
boardlist0_.content AS content2_0_0_, 
boardlist0_.title AS title3_0_0_ 
FROM board boardlist0_ 
WHERE boardlist0_.category_id 
IN (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)

@BatchSize로 지정된 만큼 IN절을 사용하여 조회한다.
쿼리 두 번으로 N + 1 문제를 해결할 수 있다.
만약 Batch Size를 5로 설정해두었다면, 총 쿼리가 3번 실행되게 된다.
 
application.yml 에서의 전체적으로 설정도 가능하다.

jpa:
    properties:
      hibernate:
        default_batch_fetch_size: 1000

데이터량에 따라서 어느 정도의 양이 Batch Size로 적당한지 파악이 필요하다.
 

GitHub - jyajoo/Spring

Contribute to jyajoo/Spring development by creating an account on GitHub.

github.com

 
참고)
N+1 문제

JPA N+1 문제 해결 방법 및 실무 적용 팁 - 삽질중인 개발자

반응형
저작자표시 (새창열림)
'🍀 Spring Boot' 카테고리의 다른 글
  • 'authorizeRequests()' is deprecated 해결 || Spring Security Configuration
  • [Spring] 의존성 주입(DI, Dependency Injection) (생성자 주입을 사용해야 하는 이유)
  • [Error] 해결 org.springframework.validation.BindException: org.springframework.val
  • [Spring] 스프링 입문 - 7. AOP
dmaolon
dmaolon
프로그래밍을 공부한 내용을 기록하는 공간입니다.
  • dmaolon
    기록 남기기
    dmaolon
  • 전체
    오늘
    어제
    • ALL (260)
      • ➰ Series (5)
      • 🎯PS (168)
        • Algorithm (15)
      • ☕ Java (11)
      • 🍀 Spring Boot (29)
      • 💬 Database (9)
      • 🐣 Computer Science (14)
      • 👍 Daily (4)
      • 🎁ReactJS (4)
  • 인기 글

  • 최근 댓글

  • 최근 글

  • 태그

    알고리즘
    BFS
    코딩
    백준
    파이썬
    Spring
    dfs
    자바
    프로그래밍
    프로그래머스
  • hELLO· Designed By정상우.v4.10.1
dmaolon
[Spring] JPA N + 1 문제 발생 원인 및 해결 방안
상단으로

티스토리툴바