Spring boot

블로그 - 게시글 목록보기 (Post List View)

ryeonng 2024. 10. 23. 17:12
  1. JPA에서의 게시글 목록 조회 방법 학습
  2. N+1 문제와 해결 방법 학습 - 지연 로딩으로 인한 N+1 문제를 확인하고 해결 방법을 학습한다.
  3. 배치 사이즈(Batch Size) 설정 이해 - default_batch_fetch_size를 설정하여 성능을 최적화하는 방법을 학습한다.
  4. 게시글 목록보기 컨트롤러 및 뷰 구현 - 실제로 게시글 목록을 표시하는 컨트롤러와 화면을 작성한다.

 

게시글 목록보기 쿼리 작성 (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}}

 

N+1 문제란?

  • N+1 문제는 애플리케이션에서 한 번의 쿼리로 N개의 엔티티를 조회한 후, 각 엔티티에 연관된 다른 엔티티를 지연 로딩(Lazy Loading)으로 조회할 때 추가적인 N개의 쿼리가 발생하는 현상을 말한다.
  • 결과적으로 총 1+N개의 쿼리가 실행되며, 이는 데이터베이스 부하와 네트워크 트래픽 증가로인해 성능 저하의 원인이 된다.

왜 문제가 되나?

  • 성능 저하 : 데이터베이스와 애플리케이션 간의 불필요한 통신이 많아져 응답 시간이 길어진다.
  • 리소스 낭비 : 데이터베이스의 연결 수가 증가하고, CPU와 메모리 사용량이 늘어난다.
  • 확장성 문제 : 데이터 양이 많아질수록(예 : 게시글이 수천 개 이상) 성능 저하가 더욱 심각해진다.

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 보다는 약간 느릴 수 있으나, 페이징과의 호환성 등 유연성이 높음
  • 장점 : 페이징과의 호환성, 데이터 중복 문제 없음