본문 바로가기

Spring boot/Blog 프로젝트 만들기(JPA)

완성 코드 STEP 02 - 게시글 목록보기 (Post List View) ( JPA API , JPQL 쿼리 사용, 인증(세션 로직 추가 )

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