게시글 목록보기 (Post List View)

 

목차

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

    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)
    // @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id")
    private User user; // 게시글 작성자 정보

     

    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}}

     

    → LAZY든 EAGER이든 username을 가져오려면 여러번 쿼리를 날려야함

     

     

    N+1 문제란 무엇인가?

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

    왜 문제가 되나?

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

    2. 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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)

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

    목차로 돌아가기