BoardService 코드 수정 (게시글 상세 보기)
/**
* 게시글 상세보기 서비스, 게시글 주인 여부 판별
*/
public Board getBoardDetails(int boardId, User sessionUser) {
Board board = boardJPARepository
.findById(boardId)
.orElseThrow(() -> new Exception404("게시글을 찾을 수 없어요"));
// 현재 사용자가 게시글을 작성했는지 여부 판별
boolean isBoardOwner = false;
if(sessionUser != null ) {
if(sessionUser.getId().equals(board.getUser().getId())) {
isBoardOwner = true;
}
}
// 집중 - 코드 추가
// 내가 작성한 댓글인가를 구현 해야 한다.
board.getReplies().forEach( reply -> {
boolean isReplayOwner = false;
if(sessionUser != null) {
if(sessionUser.getId().equals(reply.getUser().getId())) {
isReplayOwner = true;
}
}
// 객체에서만 존재하는 필드 - 리플 객체 엔티티 상태값 변경 처리
reply.setReplyOwner(isReplayOwner);
});
board.setBoardOwner(isBoardOwner);
return board;
}
지연 로딩
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=?
Hibernate:
select
r1_0.board_id,
r1_0.id,
r1_0.comment,
r1_0.created_at,
r1_0.status,
r1_0.user_id
from
reply_tb r1_0
where
r1_0.board_id=?
Hibernate:
select
u1_0.id,
u1_0.created_at,
u1_0.email,
u1_0.password,
u1_0.role,
u1_0.username
from
user_tb u1_0
where
u1_0.id in (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
즉시 로딩
Hibernate:
select
b1_0.id,
b1_0.content,
b1_0.created_at,
b1_0.title,
b1_0.user_id,
r1_0.board_id,
r1_0.id,
r1_0.comment,
r1_0.created_at,
r1_0.status,
r1_0.user_id
from
board_tb b1_0
left join
reply_tb r1_0
on b1_0.id=r1_0.board_id
where
b1_0.id=?
Hibernate:
select
u1_0.id,
u1_0.created_at,
u1_0.email,
u1_0.password,
u1_0.role,
u1_0.username
from
user_tb u1_0
where
u1_0.id in (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
⭐⭐⭐ 중요 !! ⭐⭐⭐
우리들의 페치 전략(Fetch Strategy)은 어떻게 사용해야 할까??
- 모든 연관 관계(OneToOne, ManyToOne, OneToMany, ManyToMany)에서는 기본적으로 지연 로딩(LAZY)을 사용한다. (특히 컬렉션일 때)
- 한 엔티티가 다른 엔티티와 연결될 때, 필요한 시점까지 로딩을 지연하는 것이 성능 면에서 유리하다.
- 연관된 엔티티를 실제로 필요로 할 때만 로딩하여 자원 낭비를 줄일 수 있다.
- 필요한 경우에만 연관된 엔티티를 함께 로딩한다.
- JPQL의 FETCH JOIN이나 네이티브 쿼리를 사용하여 연관된 엔티티를 한 번에 로딩한다.
- 이를 통해 N+1 문제를 방지하고 성능을 최적화할 수 있다.
- 페이징 처리 등으로 많은 데이터를 가져와야 할 때는 지연 로딩(LAZY) 전략에 배치 사이즈(batch size)를 설정한다.
- 배치 사이즈를 설정하면 지연 로딩 시 한 번에 가져오는 엔티티의 수를 조절할 수 있다.
- N+1 문제를 완화하고, 데이터베이스 쿼리 횟수를 줄여 성능을 향상시킬 수 있다.
- application.yml 설정 확인
spring:
jpa:
properties:
hibernate:
default_batch_fetch_size: 20
결론 - 기본적으로 LAZY 전략 설정을 하고 많은 양에 데이터는 페이칭 처리를 한다.
// @Repository 생략 가능
public interface BoardJPARepository extends JpaRepository<Board, Integer> {
// 커스텀 쿼리 메서드 만들어 보기
// Board 와 User 엔티티를 조인하여 특정 Board 엔티티를 조회
@Query("select b from Board b join fetch b.user u where b.id = :id")
Optional<Board> findByIdJoinUser(@Param("id") int id);
}
**BoardService - 게시글 상세보기 수정
핵심 내용 !** JPQL JOIN FETCH 사용 즉 Board 와 USER 엔티티를 한번에 조인 처리 그리고 Replay 은 LAZY 전략에 배치 사이즈 설정으로 가져오는 것
/**
* 게시글 상세보기 서비스, 게시글 주인 여부 판별
*/
public Board getBoardDetails(int boardId, User sessionUser) {
// 전략 2번
// JPQL - JOIN FETCH 사용, 즉 User 엔티티를 한번에 조인 처리
Board board = boardJPARepository
.findByIdJoinUser(boardId).orElseThrow(() -> new Exception404("게시글을 찾을 수 없습니다."));
// 전략을 1번 JAP 가 객체간에 관계를 통해 직접 쿼리를 만들고 가지고 왔다.
// Board board = boardJPARepository
// .findById(boardId)
// .orElseThrow(() -> new Exception404("게시글을 찾을 수 없어요"));
// 현재 사용자가 게시글을 작성했는지 여부 판별
boolean isBoardOwner = false;
if(sessionUser != null ) {
if(sessionUser.getId().equals(board.getUser().getId())) {
isBoardOwner = true;
}
}
// 집중 - 코드 추가
// 내가 작성한 댓글인가를 구현 해야 한다.
board.getReplies().forEach( reply -> {
boolean isReplayOwner = false;
if(sessionUser != null) {
if(sessionUser.getId().equals(reply.getUser().getId())) {
isReplayOwner = true;
}
}
// 객체에서만 존재하는 필드 - 리플 객체 엔티티 상태값 변경 처리
reply.setReplyOwner(isReplayOwner);
});
board.setBoardOwner(isBoardOwner);
return board;
}
그런데 한번에 Reply 정보도 조인해서 들고 오면 안될까?
String jpql = "SELECT b FROM Board b JOIN FETCH b.user LEFT JOIN FETCH b.replies r LEFT JOIN FETCH r.user WHERE b.id = :id";
데이터 중복 발생
- 여러 개의 JOIN FETCH를 사용하면 결과 집합에 중복 데이터가 포함될 수 있습니다.
- Board가 하나이고 Reply가 여러 개라면, Board와 User의 정보가 Reply의 개수만큼 중복됩니다.
- 이는 애플리케이션 레벨에서 데이터 중복을 처리해야 하는 부담을 줍니다.
JPA의 제약사항
- JPA에서는 한 쿼리에서 둘 이상의 컬렉션을 페치 조인하는 것을 권장하지 않습니다.
- JPA 표준 스펙에서는 컬렉션을 둘 이상 페치 조인하면 결과가 정의되지 않는다고 명시하고 있습니다.
- 일부 JPA 구현체(Hibernate 등)에서는 동작할 수 있지만, 예상치 못한 동작이나 성능 문제가 발생할 수 있습니다.
팁!! 컬렉션은 지연 로딩(LAZY)으로 유지하고 필요한 경우에만 로딩한다.
detail.mustache [ {{#board.replies}}, {{#replyOwner}} ] 코드 추가 및 수정
{{> layout/header}}
<div class="container p-5">
<!-- 수정, 삭제버튼 -->
{{# isOwner}}
<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>
{{/ isOwner}}
<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">
{{#board.replies}}
<!-- 댓글아이템 2-->
<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">{{user.username}}</div>
<div>{{comment}}</div>
</div>
{{#replyOwner}}
<form action="/reply/1/delete" method="post">
<button class="btn">🗑</button>
</form>
{{/replyOwner}}
</div>
{{/board.replies}}
</div>
</div>
</div>
{{> layout/footer}}
'Spring boot > 개념 공부' 카테고리의 다른 글
댓글 쓰기 및 삭제 및 인터셉터 적용 (0) | 2024.11.08 |
---|---|
게시글 삭제 오류 해결 (0) | 2024.11.08 |
댓글 테이블 설계 (엔티티) (0) | 2024.11.08 |
Service 레이어 만들기 (0) | 2024.11.08 |
JPARepository란? (1) | 2024.10.20 |