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