본문 바로가기

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

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

1. 게시글 상세보기 구현 (Eager Fetching)

목표

EAGER 페치 전략을 사용하여 게시글 상세보기 기능을 구현하고, 연관된 객체가 즉시 로딩되는 것을 확인한다.

package com.tenco.blog_v1.board;

import com.tenco.blog_v1.user.User;
import jakarta.persistence.*;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.sql.Timestamp;

@NoArgsConstructor
@Entity
@Table(name = "board_tb")
@Data
public class Board {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY) // 기본키 전략 db 위임
    private Integer id;
    private String title;
    private String content;

    @ManyToOne(fetch = FetchType.EAGER)
    @JoinColumn(name = "user_id")
    private User user; // 게시글 작성자 정보

    // created_at 컬럼과 매핑하며, 이 필드는 데이터 저장시 자동으로 설정 됨
    @Column(name = "created_at", insertable = false, updatable = false)
    private Timestamp createdAt;

    @Builder
    public Board(Integer id, String title, String content, User user, Timestamp createdAt) {
        this.id = id;
        this.title = title;
        this.content = content;
        this.user = user;
        this.createdAt = createdAt;
    }

}

fetch = FetchType.EAGER 로 설정하여 Board 엔티티를 조회할 때 연관된 User 엔티티도 즉시 로딩한다.

 

 

 

 

package com.tenco.blog_v1.board;

import jakarta.persistence.EntityManager;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;

@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);
    }

}
{{> layout/header}}

<div class="container p-5">

    <!-- 수정, 삭제버튼 -->
    <div class="d-flex justify-content-end">
        <a href="/board/{{board.id}}/update-form" class="btn btn-warning me-1">수정</a>
        <form action="/board/{{board.id}}/delete" method="post">
            <button class="btn btn-danger">삭제</button>
        </form>
    </div>

    <div class="d-flex justify-content-end">
        <b>작성자</b> : {{board.user.username}}
    </div>

    <!-- 게시글내용 -->
    <div>
        <h2><b>{{board.title}}</b></h2>
        <hr />
        <div class="m-4 p-2">
            {{board.content}}
        </div>
    </div>

    <!-- 댓글 -->
    <div class="card mt-3">
        <!-- 댓글등록 -->
        <div class="card-body">
            <form action="/reply/save" method="post">
                <textarea class="form-control" rows="2" name="comment"></textarea>
                <div class="d-flex justify-content-end">
                    <button type="submit" class="btn btn-outline-primary mt-1">댓글등록</button>
                </div>
            </form>
        </div>

        <!-- 댓글목록 -->
        <div class="card-footer">
            <b>댓글리스트</b>
        </div>
        <div class="list-group">
            <!-- 댓글아이템 -->
            <div class="list-group-item d-flex justify-content-between align-items-center">
                <div class="d-flex">
                    <div class="px-1 me-1 bg-primary text-white rounded">cos</div>
                    <div>댓글 내용입니다</div>
                </div>
                <form action="/reply/1/delete" method="post">
                    <button class="btn">🗑</button>
                </form>
            </div>
            <!-- 댓글아이템 -->
            <div class="list-group-item d-flex justify-content-between align-items-center">
                <div class="d-flex">
                    <div class="px-1 me-1 bg-primary text-white rounded">ssar</div>
                    <div>댓글 내용입니다</div>
                </div>
                <form action="/reply/1/delete" method="post">
                    <button class="btn">🗑</button>
                </form>
            </div>
        </div>
    </div>
</div>

{{> layout/footer}}

 

 

 

 

Fetch 전략 이해하기: EAGER와 LAZY (Fetch) 전략의 차이점과 동작 방식을 이해한다.
 select
        b1_0.id,
        b1_0.content,
        b1_0.created_at,
        b1_0.title,
        u1_0.id,
        u1_0.created_at,
        u1_0.email,
        u1_0.password,
        u1_0.username 
    from
        board_tb b1_0 
    left join
        user_tb u1_0 
            on u1_0.id=b1_0.user_id 
    where
        b1_0.id=?
Hibernate: 
    select
        b1_0.user_id,
        b1_0.id,
        b1_0.content,
        b1_0.created_at,
        b1_0.title 
    from
        board_tb b1_0 
    where
        b1_0.user_id=?

 

Hibernate: 
    select
        b1_0.id,
        b1_0.content,
        b1_0.created_at,
        b1_0.title,
        b1_0.user_id 
    from
        board_tb b1_0 
    where
        b1_0.id=?

 

 

 

 

지연 로딩을 하더라도 결국 사용하는 시점에 객체 쿼리가 발생 된다.
    <div class="d-flex justify-content-end">
        <b>작성자</b> : {{ board.user.username }}
    </div>

 

 


요약

EAGER 전략과 Lazy 전략에 대한 차이점을 이해 한다. 하지만 둘 다 사용하더라도 N + 1 문제가 발생 할 수 있다.

: 지연 로딩이더라고 연관된 데이터를 가져 와야 된다면 —> 쿼리가 두번 생성 호출 된다 (N + 1 문제 발생)

 

 

 

 

 

BoardController 코드 수정
  // 특정 게시글 요청 화면
  // 주소설계 - http://localhost:8080/board/1
  @GetMapping("/board/{id}")
  public String detail(@PathVariable(name = "id") Integer id, HttpServletRequest request) {
      // JPA API 사용
      // Board board = boardRepository.findById(id);

      // JPQL FETCH join 사용
      Board board = boardRepository.findByIdJoinUser(id);
      request.setAttribute("board", board);
      return "board/detail";
  }

 

 

 

 

BoardRepository 코드 추가
package com.tenco.blog_v1.board;

import jakarta.persistence.EntityManager;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;

@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_tb b JOIN FETCH b.user WHERE b.id = :id ";
        return em.createQuery(jpql, Board.class)
                .setParameter("id", id)
                .getSingleResult();
    }

}

User.board 도 LAZY로 변경해야 함 !!

 

 

 

Hibernate: 
    select
        b1_0.id,
        b1_0.content,
        b1_0.created_at,
        b1_0.title,
        u1_0.id,
        u1_0.created_at,
        u1_0.email,
        u1_0.password,
        u1_0.username 
    from
        board_tb b1_0 
    join
        user_tb u1_0 
            on u1_0.id=b1_0.user_id 
    where
        b1_0.id=?

 

 

 


 

JPQL이란?

  • JPQL은 Java Persistence Query Language의 약자로, JPA에서 사용되는 객체 지향 쿼리 언어이다.
  • SQL과 유사하지만, 테이블이 아닌 엔티티 객체를 대상으로 쿼리를 작성한다.
  • JPQL을 사용하면 데이터베이스에 독립적인 쿼리를 작성할 수 있어, 특정 데이터베이스 벤더에 종속되지 않는다.
  • JPQL은 JPA 표준 스펙의 일부로, 대부분의 JPA 구현체(Hibernate 등)에서 지원한다.

 

JPQL - Fetch Join 활용 (게시글 상세 보기)

Fetch Join이란

  • JPQL에서 제공하는 기능으로, 연관된 엔티티를 한 번의 쿼리로 함께 조회하기 위해 사용한다.
  • 지연 로딩 설정과 관계없이 연관된 엔티티를 즉시 로딩한다.

사용 이유

  • N+1 문제를 해결하여 데이터베이스 쿼리 횟수를 줄이고 성능을 최적화하기 위해 사용한다.

사용 방법

  • JPQL 쿼리에서 JOIN FETCH 구문을 사용하여 연관된 엔티티를 함께 조회한다.

 

Fetch Join 사용 시 주의사항

데이터 중복

  • Fetch Join으로 여러 연관 엔티티를 조인하면 결과가 중복될 수 있으므로, 필요한 엔티티만 선택적으로 조인해야 한다.

페이징 제한

  • JPA에서는 Fetch Join을 사용한 상태에서 페이징을 지원하지 않는다.
  • 데이터가 많을 경우 메모리 사용량이 증가할 수 있으므로 주의해야 한다.

적절한 사용

  • 무분별한 사용은 오히려 성능을 저하시킬 수 있으므로, 필요한 경우에만 사용해야 한다.

 

 

JPQL과 SQL의 차이점

  • JPQL은 객체 지향 쿼리 언어로, 엔티티 객체를 대상으로 쿼리한다.
  • SQL은 데이터베이스 테이블과 컬럼을 대상으로 쿼리한다.
  • JPQL은 엔티티와 그 사이의 연관 관계를 사용하므로, 데이터베이스에 독립적인 쿼리를 작성할 수 있다.

 

우리는 어떤 전략을 선택해야 하나?

  • 기본적으로 Lazy Fetching을 사용하여 불필요한 데이터 로딩을 방지한다.
  • 필요한 경우 Fetch Join 등을 사용하여 성능을 최적화한다.

 

 

요약

  • Eager Fetching: 연관된 엔티티를 즉시 로딩하여, 엔티티 조회 시 함께 데이터를 가져온다.
    • 장점: 연관된 데이터를 바로 사용할 수 있다.
    • 단점: 불필요한 데이터까지 로딩되어 성능이 저하될 수 있다.
  • Lazy Fetching: 연관된 엔티티를 실제로 접근할 때까지 로딩을 지연시킨다.
    • 장점: 필요한 시점에만 데이터를 로딩하여 성능을 향상시킨다.
    • 단점: 지연 로딩 시점에 추가적인 SQL 쿼리가 발생한다.
  • Fetch Join: 한 번의 쿼리로 연관된 엔티티를 함께 로딩하여 성능을 최적화한다.
    • 사용 시 주의점: 너무 많이 사용하면 오히려 성능이 저하될 수 있으므로 필요한 경우에만 사용한다.

 

 

Fetch 전략 선택 기준

  • Lazy Fetching을 기본으로 사용하여 불필요한 데이터 로딩을 방지한다.
  • Eager Fetching은 반드시 함께 로딩해야 하는 연관 데이터가 있을 때 신중히 사용한다.

 Fetch Join 사용 시 주의사항

  • Fetch Join은 즉시 로딩과 유사하게 작동하지만, 원하는 시점에 적용할 수 있다.
  • 복잡한 쿼리나 데이터 양이 많은 경우 오히려 성능이 저하될 수 있으므로 주의해야 한다.

N+1 문제

  • 지연 로딩을 사용할 때 연관된 엔티티를 반복적으로 조회하면, 예상치 못한 많은 수의 SQL 쿼리가 발생할 수 있다.
  • 이를 N+1 문제라고 하며, Fetch Join 등을 사용하여 해결할 수 있다.
728x90