DetailDTO 만들때
reply 는 DetailDTO 내부에 컬렉션으로 넣기
DetailDTO {
id
title
content
username
List<Reply> replies
}
이렇게 주어야 프론트가 편하다 !

++
테스트 부분에
@DataJPATest를 붙여주면
→ 레파지토리에서 JPARepository를 상속한 애들이 메모리에 다 뜬다.
→ DataJpaTest 라고 붙여주기!
그러니 UserRepository는 @Import할 필요가 없다.
(@DataJPATest 붙여놨으니 메모리에 떠있음)
UserQueryRepository는 JPARepository 상속받은게 아니라 복잡한 쿼리 따로 작성하려고
만들어둔 클래스이니 @Import 해줘야 한다.


댓글과 게시글을 같이 조회해서 가져오는 mFindByIdWithReply 매서드를 완성 시켰다.
이제 테스트

테스트하면 네이티브 쿼리로 실행시켰던거처럼 board, user, reply, user 순으로 잘 나온다.
- join 은 inner join 이다. (inner는 생략됨)
TIP )
콘솔 한글 에러나면 세팅에서 인텔리제이로 바꾸기

네이티브 쿼리 연습하고 jpql 연습하기! JPQL만 연습하지말구.

Service의 게시글상세보기에 mFindByIdWithReply로 바꿔주기
참고로 m은 my를 의미. 내가 만든거라고 표시해두는 것.
BoardResponse.DetailDTO안에 조회해서 가져온 댓글들을 담을
private List<ReplyDTO> replies;
를 만들어 줘야 하는데,근데 일단
BoardResponse.DetailDTO로 리턴하지 말고
Board 로 리턴하는 걸로 수정해보자 ( → 더 쉬운 버전 )



Board 객체에는 isOwner이 없으니까 삭제.
Board객체에는 username이 없으니까 model.username → model.user.username
댓글 부분도 포함해서 아래와 같이 고친다.
{{>layout/header}}
<div class="container p-5">
<!-- 수정삭제버튼 -->
<div class="d-flex justify-content-end">
<a href="/api/board/{{model.id}}/update-form" class="btn btn-warning me-1">수정</a>
<form action="/api/board/{{model.id}}/delete" method="post">
<button class="btn btn-danger">삭제</button>
</form>
</div>
<div class="d-flex justify-content-end">
<b>작성자</b> : {{model.user.username}}
</div>
<!-- 게시글내용 -->
<div>
<h2><b>{{model.title}}</b></h2>
<hr/>
<div class="m-4 p-2">
{{{model.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">
{{#model.replies}}
<!-- 댓글아이템 -->
<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>
<form action="#" method="post">
<button class="btn">🗑</button>
</form>
</div>
{{/model.replies}}
</div>
</div>
</div>
{{>layout/footer}}
템플릿엔진 JSP 같은 경우에는 Board를 model로 받아서 조건문을 사용해 권한체크 같은 것도 처리를 할 수 있다.
그동안 BoardResponse.DetailDTO로 받은 이유는 Mustache를 쓸 때 뷰에서 조건문 같은걸 못쓰니
안에 isOwner 같은 권한체크를 하고나서 뷰에 넘긴것이다.

위처럼 board를 넘겨도 ( + detail mustache를 수정하고 ) 잘 나오는 것을 확인할 수 있다.
근데 왜 이렇게 안하냐 ?
왜 JPA를 이용해서 ORM을 하고 DTO에 담아서 보내느냐 ?
→ 프론트엔트랑 협업을 할려고 !
// 그림 . 선생님 열강
- 템플릿 엔진 (JSP) 왜 X ?
- DTO 사용 이유
- 프론트엔드 ORM
- 프론트엔드 정확

JPQL 로 작성한건데 이렇게 하면
board 부분의 데이터가 중복되어서 가져와진다.
무슨말이냐면
위의 쿼리를 네이티브 쿼리로 작성한 걸 한번 보자.
SELECT bt.id, bt.title, bt.content, ut.username, rt.comment, rut.username
FROM board_tb bt
INNER JOIN user_tb ut ON bt.user_id = ut.id
LEFT OUTER JOIN reply_tb rt on bt.id = rt.board_id
LEFT OUTER JOIN user_tb rut on rut.id = rt.user_id
where bt.id = 5;

게시글 id 5번의 데이터를 가지고 오면 이렇게 나온다.

그런데 우리가 필요한건 네모친 부분이고
나머지 board 는 inner join을 해서 중복되어 가져와 진 부분이다!
이렇게 프론트엔드한테 전달하면 프론트엔드는 데이터를 모르기 때문에
( 저 3줄의 Board가 join 해서 가져온 같은 데이터인지 모름 )
게시글을 3개 만들어서 각각 게시글에 댓글을 1개씩 뿌릴 위험이 있다.
아니면 항상 일할 때 데이터에 대해서 물어봐야 하는 번거로움이 있을 수 있다.
그래서 이걸 프론트가 뿌리기 쉬운 구조로 담아서 보내야 한다.

이렇게 보내면 “ 아 게시글 5번에 댓글이 3개 있구나 “ 하고 바로 이해할 수 있다.
그러기 위해서는 Board board 를 model 에 담아서 보내지 말고
DTO를 만들어서 위 구조를 만들어 준 뒤에 보내야 한다!
BoardResponse에서 상세페이지에 응답할때 사용하는 detailDTO를 수정해보자.

기존에 아직 댓글기능이 없을때는
이렇게 board와 user 정보만 들고가도록 적어두었다

User를 이렇게 내부 클래스로 만들어서 전달하는 version2 도 만들어 보았다.
이제 DetailDTO 내부에 댓글들(replies)를 담을 컬렉션을 만들어주자.
private List<ReplyDTO> replies = new ArrayList<>();
이때 List안에 엔티티를 넣으면 나중에 레이지 로딩이 발생한다.
(→ 오픈인뷰 때문에)
그러니
엔티티와 DTO의 필드가 동일하더라도 똑같이 생긴 DTO를 만들어서 사용해라!
비영속객체를 만들어서 응답하는게 낫다!
package org.example.springv3.board;
import lombok.Data;
import org.example.springv3.reply.Reply;
import org.example.springv3.user.User;
import java.util.ArrayList;
import java.util.List;
public class BoardResponse {
@Data
public static class DetailDTO {
private Integer id;
private String title;
private String content;
private Boolean isOwner;
private Integer userId;
private String username;
// 댓글들
// 여기 엔티티 넣으면 나중에 레이지 로딩이 발생한다. (-> 오픈인뷰 때문에 )
// 똑같이 생긴 dto를 만들어서 넣어라. (-> 비영속객체를 만들어서 응답하는게 낫다 )
private List<ReplyDTO> replies = new ArrayList<>();
public DetailDTO(Board board, User sessionUser) {
this.id = board.getId();
this.title = board.getTitle();
this.content = board.getContent();
this.isOwner = false;
if (sessionUser != null) {
if (board.getUser().getId() == sessionUser.getId()) {
isOwner = true; // 권한체크
}
}
this.userId = board.getUser().getId();
this.username = board.getUser().getUsername();
for(Reply reply : board.getReplies()){
replies.add(new ReplyDTO(reply, sessionUser));
}
}
@Data
public class ReplyDTO{
private Integer id;
private String comment;
private String username;
private Boolean isOwner;
public ReplyDTO(Reply reply, User sessionUser) {
this.id = reply.getId();
this.comment = reply.getComment();
this.username = reply.getUser().getUsername();
this.isOwner = false;
if (sessionUser != null) {
if (reply.getUser().getId() == sessionUser.getId()) {
isOwner = true; // 권한체크
}
}
}
}
}
}
BoardResponse 전체 코드
살펴보면

→ ReplyDTO를 만들어서 Collection으로 받기 위해(→ 댓글 여러개 넘어오니까 )
DetailDTO 필드에 List<ReplyDTO> replies 를 만들어준다.
DetailDTO 생성자 안에 반복문(forEach)을 돌려서 List안에 ReplyDTO객체를 담아준다.
매개변수로 sessionUser가 필요한 이유는 권한체크 때문이다!
( 댓글을 작성한 유저랑 세션유저랑 동일한지 비교해서
동일하면 삭제 버튼 등을 나타나게 해주려고 ! 권한 체크 해야함 )
++
위에 사용한 for문 대신에 스트림API 쓰면 더 간결하게 쓸 수 있다

ReplyDTO를 보면 이렇게 생겼다.
댓글 ID, 댓글내용, username(작성자), isOwner(버튼을 위한 권한 체크)
이 repliyDTO는 DetailDTO 내부에서만 사용하니까 내부클래스로 만든다 !
++
왜 dto에 담아서 전달 하는가 ?
영속객체(엔티티)를 그대로 넘기지 않는 이유는
내가 화면에 필요한 데이터만 주지 않고, 많은 걸 줬기 때문이다.
→ 엔티티로 안넘기고 영속화 되어있는 객체(엔티티)가 아닌 객체를 써야
레이지로딩이 안일어난다.
Board객체를 그냥 리턴하면
JSON으로 컨버팅 할 때 양방향 매핑된거는 양쪽으로 다 터진다.
(→ @OneToMany 에서 걸려서 무한으로 서로 때림. 무한루프)

물론 @JsonIgnoreProperties({”Json으로 컨버팅할때 무시하고 넘어갈 필드이름”})
을 붙여주면 Json으로 만들때 그건 패스하고 넘어간다.
Board 엔티티에 List<Reply>에 @JsonIgnoreProperties({”board”}) 붙여주고
컨트롤러 추가해서 때려보면
@GetMapping("/v3/board/{id}")
public @ResponseBody Board detailV3(@PathVariable("id") Integer id) {
User sessionUser = (User) session.getAttribute("sessionUser");
Board model = boardService.게시글상세보기V3(sessionUser, id);
return model;
}
데이터를 보기위해 @ResponseBody 를 붙여놨다.
public Board 게시글상세보기V3(User sessionUser, Integer boardId){
Board boardPS = boardRepository.mFindByIdWithReply(boardId)
.orElseThrow(() -> new Exception404("게시글이 없습니다."));
return boardPS;
}
서비스에 추가하고
때려보면

@JsonIgnoreProperties 안붙였을때는 Json 컨버팅할때 에러나는데 이제 잘 나온다!

Board를 뷰에 넘겨주려고 JSON으로 만들어서
{”id” : 5 } 이런식으로 담아서 보낸다고 하나하나 때리는데 replies를 때리면
Reply로 가서 또 다 주워담는다.

근데 Reply에 가면 id랑 comment 담고 또 Board를 담아야 한다.
그럼 또 Board 엔티티로 넘어와서 id부터 주워담는데 이래서 무한 루프 에러가 발생한다.

++

alert('Could not write JSON: Document nesting depth (1001) exceeds the maximum allowed (1000, from `StreamWriteConstraints.getMaxNestingDepth()`)');
history.back();
애초에 entity말고 DTO를 써서 넘겨주면 @JsonIgnoreProperties이든 뭐든 신경 안 써줘도 된다!!
++
@JsonIgnoreProperties 를 사용하면 해당 필드는 JSON만들때 안담고 무시하고 넘어간다.

만약에 댓글안에 있는 이 created_at을 안나오게 하고 싶으면

Board 엔티티의 replies 위에 createdAt 추가
아까때렸는 http://localhost:8080/v3/board/5 url로 때려보면

JSON으로 데이터 넣을 때 replies 객체의 createdAt은 안들어간 걸 확인할 수 있다!

그럼 replies의 user의 password는 안보내고 싶으면 ?

Reply 엔티티의 User 필드에
@JsonIgnoreProperties({"password"})
를 붙여주면 된다 !그럼 Reply JSON 만들다가 User 때려서 들어가서 또 JSON 만들때 password는 무시하고 담는다.

Password 없어짐 : )
이렇게 보낼 데이터와 안보낼 데이터를 구분할 수 있지만
@JsonIgnoreProperties
어노테이션을 붙이는 것보다DTO를 만들어서 넘기는게 깔끔하다!
나중에 수정, 보완에도 DTO를 만드는게 더 용이하다.
엔티티는 저장, 수정 할 때 다같이 쓰니까 이렇게 어노테이션으로 하면 나중에 문제가 있을수도.
어디는 user의 password도 받아야하는데 위처럼 처리해두면 곤란하다.
++
DetailDTO
안에 ReplyDTO
를 만들었는데Reply가 컬렉션이면 내부클래스로 만들지만
그게아니라 1개 데이터만 가져올때는 굳이 내부클래스로 받아올 필요는 없다.

v1에 만들어둔 DetailDto에서도 User의 id와 username을 가져올 거여서 내부 UserDTO 클래스를
만들었지만, 만약 username만 가져오는 거면 그냥 username을 DetailDto의 지역변수로
넣어서 사용하는게 낫다.
추가로

service의 게시글 수정화면을 보면
지금 Board 엔티티(영속객체)를 그대로 return 하고 있는데 이렇게 하면 안되고
boardResponse.boardDTO를 만들어서 이걸로 return 하는 걸로 바꿔야 한다.
++
Board 엔티티를 리턴해서 뿌리다가 에러나서 막 찾아보다가 openinview때문인거 같아가지고
open-in-view
를 true로 바꾸고( 바꾸면 안대…) Board를 return 받으면→ byteBuddy.ByteBuddyInterceptor 에러가 난다.
→ 타이밍의 문제 때문에 터짐.
→ 메세지 컨버터가 NULL인 USER을 때려서 NULL을 바로 넣지
JPA가 어 레이지 로딩이네? username이 없네. 잠시만~ 하고 SELECT 하러간 걸 기다리지 않음!
→ 그래서 발생하는 에러
이걸 해결하려면
- getUser하는 순간 JPA가 select 먼저 하게 하고 메세지 컨버터가 기다렸다가 Select해서 들고온 값을 가지고 뷰에 가게 순서 정해주기.
- 엔티티로 return 하지말고 dto를 만들어서 return 하면 레이지 로딩이고, 메세지컨버터 순서정해주고 나발이고 신경 안써도 된다.
2번을 선택하자!
그러니 Service 레이어에 return 되는거 클래스 타입을 다 dto로 만들어주기!!!
깊은 복사 해서 넘겨라!!!! (→ ORM된 그 객체를 다른 객체(dto)에 복사해서 넘겨주는게 깊은복사?)
? ) 깊은 복사
Share article