1. 게시글 목록보기 쿼리 작성 (Eager Fetching)
목표: EAGER 페치 전략을 사용하여 게시글 목록을 조회하고, 연관된 User 엔티티가 어떻게 로딩되는지 확인한다.
package com.tenco.blog_v1.board;
import jakarta.persistence.EntityManager;
import jakarta.persistence.TypedQuery;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;
import java.util.List;
@RequiredArgsConstructor
@Repository // IoC
public class BoardRepository {
private final EntityManager em;
/**
* 게시글 조회 메서드
* @param id 조회할 게시글 ID
* @return 조회된 Board 엔티티, 존재하지 않으면 null 반환
*/
public Board findById(int id) {
return em.find(Board.class, id);
}
/**
* JPQL의 FETCH 조인 사용 - 성능 최적화
* 한방에 쿼리를 사용해서 즉, 직접 조인해서 데이터를 가져 옵니다.
* @param id
* @return
*/
public Board findByIdJoinUser(int id) {
// JPQL -> Fetch join 을 사용해 보자.
String jpql = " SELECT b FROM Board b JOIN FETCH b.user WHERE b.id = :id ";
return em.createQuery(jpql, Board.class)
.setParameter("id", id)
.getSingleResult();
}
/**
* 모든 게시글 조회
* @return 게시글 리스트
*/
public List<Board> findAll() {
TypedQuery<Board> jpql = em.createQuery(" SELECT b FROM Board b ORDER BY b.id DESC ", Board.class);
return jpql.getResultList();
}
}
JPQL 쿼리: " SELECT b FROM Board b ORDER BY b.id DESC "
해석
- SELECT b: Board 엔티티를 조회하여 b라는 별칭(alias)으로 선택한다.
- FROM Board b: 데이터 소스로 Board 엔티티를 사용하고, 별칭 b를 부여한다.
- ORDER BY b.id DESC: b.id를 기준으로 내림차순 정렬한다.
요약
- Board 엔티티의 모든 데이터를 id 내림차순으로 조회한다.
Board 엔티티 코드 수정
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "user_id")
private User user; // 게시글 작성자 정보
BoardController 수정
@GetMapping("/")
public String index(Model model) {
//List<Board> boardList = boardNativeRepository.findAll();
// 코드 수정
List<Board> boardList = boardRepository.findAll();
model.addAttribute("boardList", boardList);
return "index";
}
index.mustache 수정
{{> layout/header}}
<div class="container p-5">
<!-- 게시글 목록을 반복 출력 (boardList가 null이 아니고 비어 있지 않다면 출력) -->
{{#boardList}}
<div class="card mb-3">
<div class="card-body">
<h4 class="card-title mb-3">{{title}} | 작성자 : {{user.username}}</h4>
<a href="/board/{{id}}" class="btn btn-primary">상세보기</a>
</div>
</div>
{{/boardList}} <!-- 반드시 섹션을 닫는 태그가 필요 -->
<!-- 게시글이 없을 경우 출력할 내용 -->
{{^boardList}}
<p>게시글이 없습니다.</p>
{{/boardList}}
<ul class="pagination d-flex justify-content-center">
<li class="page-item disabled"><a class="page-link" href="#">Previous</a></li>
<li class="page-item"><a class="page-link" href="#">Next</a></li>
</ul>
</div>
{{> layout/footer}}
1. N+1 문제란 무엇인가요?
- N+1 문제는 애플리케이션에서 한 번의 쿼리로 N개의 엔티티를 조회한 후, 각 엔티티에 연관된 다른 엔티티를 지연 로딩(Lazy Loading)으로 조회할 때 추가적인 N개의 쿼리가 발생하는 현상을 말한다.
- 결과적으로 총 1 + N개의 쿼리가 실행되며, 이는 데이터베이스 부하와 네트워크 트래픽 증가로 인해 성능 저하의 원인이 된다.
2. 왜 문제가 되나요?
- 성능 저하: 데이터베이스와 애플리케이션 간에 불필요한 통신이 많아져 응답 시간이 길어진다.
- 리소스 낭비: 데이터베이스의 연결 수가 증가하고, CPU와 메모리 사용량이 늘어난다.
- 확장성 문제: 데이터 양이 많아질수록(예: 게시글이 수천 개 이상) 성능 저하가 더욱 심각해진다.
3. N+1 문제 해결 (Batch Size 설정)
스프링 JPA에서 default_batch_fetch_size 설정은 복잡한 조회쿼리 작성시,
지연로딩으로 발생해야 하는 쿼리를 IN절로 한번에 모아보내는 기능이다.
목표: 지연 로딩 시 발생하는 N+1 문제를 해결하기 위해 default_batch_fetch_size 설정을 사용한다.
default_batch_fetch_size
- 지연 로딩으로 발생하는 쿼리를 IN 절을 사용하여 한 번에 모아 보낼 수 있도록 하는 설정이다.
- 한 번에 가져올 엔티티의 수를 지정한다.
실행 쿼리 확인
Hibernate:
select
b1_0.id,
b1_0.content,
b1_0.created_at,
b1_0.title,
b1_0.user_id
from
board_tb b1_0
order by
b1_0.id desc
Hibernate:
select
u1_0.id,
u1_0.created_at,
u1_0.email,
u1_0.password,
u1_0.username
from
user_tb u1_0
where
u1_0.id in (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
Batch Size 설정과 Fetch Join(게시글 상세보기에서 사용)의 차이점
Batch Size 설정
- 설명: default_batch_fetch_size 설정을 통해 Hibernate가 지연 로딩 시 여러 엔티티를 한 번에 로딩하도록 한다. 이를 통해 N+1 문제를 완화할 수 있다.
- 동작 방식: 예를 들어, default_batch_fetch_size: 10으로 설정하면, Hibernate는 한 번에 최대 10개의 User 엔티티를 IN 절을 사용하여 한 번의 쿼리로 로딩한다.
- 쿼리 수: 메인 쿼리 1개 + 관련 엔티티를 배치로 로딩하는 쿼리 1개 → 총 2개의 쿼리 실행.
Fetch Join ( 지연 로딩과 관계 없이 즉시 로딩 됨)
- 설명: JPQL에서 JOIN FETCH를 사용하여 연관된 엔티티를 한 번의 쿼리로 함께 조회한다.
- 동작 방식: Board와 User를 조인하여 한 번의 쿼리로 모두 가져온다.
- 쿼리 수: 메인 쿼리 1개로 모든 데이터를 한 번에 로딩 → 총 1개의 쿼리 실행.
Fetch Join을 사용할 때
- 즉시 로딩이 필요한 경우: 연관된 엔티티를 즉시 로딩하여 한 번에 모두 사용해야 할 때.
- N+1 문제 완전 해결이 필요할 때: 연관 엔티티를 모두 한 번에 로딩하여 쿼리 수를 최소화하고자 할 때.
- 페이징이 필요 없는 경우: Fetch Join을 사용할 경우 페이징과의 호환성 문제가 발생할 수 있으므로, 페이징이 필요 없다면 Fetch Join을 사용하는 것이 유리하다.
Batch Size 설정을 사용할 때
- 페이징과 함께 사용할 때: 페이징이 필요한 상황에서는 Fetch Join 대신 Batch Size 설정을 사용하는 것이 좋다.
- 부분적으로 로딩이 필요한 경우: 모든 연관 엔티티를 한 번에 로딩하지 않고, 필요한 만큼만 배치로 로딩하고자 할 때.
- 복잡한 조인 없이도 성능 최적화가 필요한 경우: 복잡한 조인을 사용하지 않고도 쿼리 수를 줄이고자 할 때 Batch Size 설정을 활용할 수 있다.
성능 비교
Fetch Join을 사용한 경우
- 쿼리 수: 1개 (게시글과 작성자 정보를 함께 조회)
- 성능: 일반적으로 더 빠름, 네트워크 및 데이터베이스 부하가 줄어듬
- 단점: 데이터 중복 가능성, 페이징과의 호환성 문제
Batch Size 설정을 사용한 경우
- 쿼리 수: 2개 (게시글 조회 + 배치로 작성자 조회)
- 성능: Fetch Join보다는 약간 느릴 수 있으나, 페이징과의 호환성 등 유연성이 높음
- 장점: 페이징과의 호환성, 데이터 중복 문제 없음
728x90