15 스프링부트(블로그만들기) 댓글 V3 3.4

윤주헌's avatar
Sep 23, 2024
15 스프링부트(블로그만들기) 댓글 V3 3.4
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
notion image
이러면 첫 줄은 필요한데 두번째 줄부터 di title, username은 필요가 없다
 
아릅답게 만드려면
이거 담고
notion image
내부클래스로
notion image
담자
 
class DetailDTO{ private int id; private String title; private String content; private String username; List<Reply> replies; 이러면 중복되는게 없다 }
만약 플랫하게 평평하게 주면 프론트 앤드가 필요없는게 많다! 플랫하게 준다??? 아직 이해 못함
 
 
보드 테이블만 조회해라
select b from Board b
 
 
select b from Board b join fetch b.user
 
이제 양방향 메핑 해야한다!
여기까지하면 게시글 작성자 댓글들까지 뽑을 수 있ㄷ
select b from Board b join fetch b.user left join fetch b.replies
 
이제 댓글의 유저를 뽑아야 한다
r.board할 필요 없다 이미 뽑았으니까
select b from Board b join fetch b.user left join fetch b.replies r left join fetch r.user where b.id=:id

보드 레파지토리에서

@Query("select b from Board b join fetch b.user left join fetch b.replies r left join fetch r.user where b.id=:id") Optional<Board> mFindByIdWithReply(@Param("id")int id)

테스트

@Test public void mFindByIdwithReply_test(){ Board board = boardRepository.mFindByIdWithReply(5).get(); System.out.println(board.getReplies().get(0).getComment()); }
보드 객체에 다 담겨서 된다!

화면에 뿌린다(보드 서비스로 게시글 상세보기)

public BoardResponse.DetailDTO 게시글상세보기(User sessionUser, Integer boardId){ Board boardPS = boardRepository.mFindByIdWithReply(boardId) .orElseThrow(() -> new Exception404("게시글이 없습니다.")); return new BoardResponse.DetailDTO(boardPS,sessionUser); }

보드 리스폰스 (댓글들 바꿔야 함) 디테일 DTO로

리플라이를 객체로 받는다!
조금 있다가 하고

다시 서비스 가서

public Board 게시글상세보기(User sessionUser, Integer boardId){ Board boardPS = boardRepository.mFindByIdWithReply(boardId) .orElseThrow(() -> new Exception404("게시글이 없습니다.")); return boardPS;
DTO안만들면

컨트롤러

@GetMapping("/board/{id}") public String detail(@PathVariable("id") Integer id, HttpServletRequest request) { User sessionUser = (User) session.getAttribute("sessionUser"); BoardResponse.DetailDTO model = boardService.게시글상세보기(sessionUser, id); request.setAttribute("model", model); return "board/detail"; }
@GetMapping("/board/{id}") public String detail(@PathVariable("id") Integer id, HttpServletRequest request) { User sessionUser = (User) session.getAttribute("sessionUser"); Board model = boardService.게시글상세보기(sessionUser, id); request.setAttribute("model", model); return "board/detail"; }

디테일머스테치

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

왜 DTO가 필요한지

조금 있다가 설명해줌

리펙토링하기

BoardResponse가서변경

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 static 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; // 권한체크 } } } } }

테스트 해보자 보드컨트롤러로 이동

//필요한 정보만 가지고 오는지 json으로 확인해보자 @GetMapping("/v2/board/{id}") public @ResponseBody BoardResponse.DetailDTO detailV2(@PathVariable("id") Integer id) { User sessionUser = (User) session.getAttribute("sessionUser"); BoardResponse.DetailDTO model = boardService.게시글상세보기(sessionUser, id); return model; }

위에서 본다고 BoardResponse.DetailDTO를 Board로 바꿔둔것 바꾸기

DTO안 만들고 전부를 가지고 온 상태 Board로 가지고 왔을 때

notion image

DTO를 만든 후 페이지 상태

notion image

디테일머스테치 변경

추가
위쪽 삭제버튼 {{#model.isOwner}} 댓글 삭제버튼 쪽 {{#isOwner}} {{username}}
{{>layout/header}} <div class="container p-5"> <!-- 수정삭제버튼 --> {{#model.isOwner}} <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> {{/model.isOwner}} <div class="d-flex justify-content-end"> <b>작성자</b> : {{model.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">{{username}}</div> <div>{{comment}}</div> </div> {{#isOwner}} <form action="#" method="post"> <button class="btn">🗑</button> </form> {{/isOwner}} </div> {{/model.replies}} </div> </div> </div> {{>layout/footer}}

핵심

엔티티를 왜 리턴 안했나? 쓸대 없는 데이터가 많아서!
깊은 복사(클래스에 있는 것을 다른 클래스에 옮긴다)
 
DTO는 가방이다
 
어떤 사람에게 데이터를 달라 했는데 내가 필요 없는 데이터 통으로 다 준거야! 이거를 집에 들고가면 무거우니 다른 가방에 담은것 뿐이다!
장점
  1. 화면에 필요한 데이터만 필요함
  1. 옮긴데이터는 영속데이터가 아니여서
  1. 양방향으로 연결돼있으니까 getter하다보면 양방향으로 터질 수 있다
  1. reply객체 replyDTO 차이를 봐라 id comment username 딱 필요한 것만 있다
 
이거를 연습해야한다!!
나는 양방향이 아직 모자란거 같다
 
아니면 예전처럼 requestsetAttribute 때려 박아야 한다!
 
사실
this.replies = board.getReplies().steam().map(reply -> new ReplyDTO(repl, sessionUser)).toList();
스트림 이거 더 편하기는 한데 나중에 알려주신다 했음
 
@responsebody 안 붙이면 return 을 뭘로 보내야한다했찌 기억력 실환가
 
V3
@GetMapping("/v3/board/{id}") public @ResponseBody Board(@PathVariable("id") Integer id) { User sessionUser = (User) session.getAttribute("sessionUser"); Board model = boardService.게시글상세보기(sessionUser, id); return model; }
public Board 게시글상세보기V3(User sessionUser, Integer boardId){ Board boardPS = boardRepository.mFindByIdWithReply(boardId) .orElseThrow(() -> new Exception404("게시글이 없습니다.")); return boardPS; }
보드 객체 그대로 받으면 왜 터지냐?
무한로딩됨
 
왜?
→ 보드를 리턴하면 json
user 다 만들어 그것까지는 괜찮음
근데 reply할 때 private User user, private Board board이 있어서 계속 무한으로 때린다!!!
그래서 문제 생김
 
reply안에 board를 json으로 뽑을 필요 없으니까
board로 가서
스프링 니 json으로 컨버팅할 때 건드리지마!!
@JsonIgnoreProperties({"board"}) @OneToMany(mappedBy = "board") //게시글 조회할 때 댓글여러개 필요해서 적어준 것이다, 포린키의 주인이 누군지 알려줘야 한다! 나는 아니야(안적으면 컬럼 생성한다) private List<Reply> replies;
리플리 안의 board JSON을 무시해!
하지만 @JsonIgnoreProperties는 엔티티도 더러워지고 복잡해져서
DTO를 만들어라!
 
OpenInView한번 더 찾아보자!!
 
no session은 왜 생겼지? 모르는 사람은 openInView를 true로 바꾼다 (이러면 안됨)
레이지 로딩이 돼서 board객체만 찾고 때릴라하니 user안에 뭐가 없어서 문제 생김
 
타이밍 문제다!!
get유저값을 json으로 넣으려는데 없으니 널을 집어 넣으려고 하고있고 하이버네이트는 데이터 넣으려 하고있고
select 안 기다려서 그냥 넣으려 하니까 타이밍 문제 때문에 오류가 생김!!!!!!!!!!!!!!
 
@GetMapping("/v2/api/board/{id}/update-form") public @ResponseBody BoardResponse.DTO updateForm(@PathVariable("id") int id) { User sessionUser = (User) session.getAttribute("sessionUser"); BoardResponse.DTO model = boardService.게시글수정화면V2(id, sessionUser); return model; } public BoardResponse.DTO 게시글수정화면V2(int id, User sessionUser) { Board board = boardRepository.findById(id) .orElseThrow(()-> new Exception404("게시글을 찾을 수 없습니다")); if (board.getUser().getId() != sessionUser.getId()) { throw new Exception403("게시글 수정 권한이 없습니다."); } return new BoardResponse.DTO(board); } Board에 필요한 것만 들어가게 만들기 그냥 borad하면 나중에 꺼낼때 힘들기 때문 @Data public static class DTO { private Integer id; private String title; private String content; public DTO(Board board) { this.id = board.getId(); this.title = board.getTitle(); this.content = board.getContent(); } }
Share article

code-sudal