
[ 화면 확인 ]

[ 필요한 데이터부터 만들어보자 ]
boardId title content username (권한체크 위해서) boardUserId (게시글 작성자 아이디) [ (컬렉션) replyId (댓글 내용이니까 댓글의 id는 들고가야 함) comment replyUserId (댓글 작성한 username도 들고와야 하니까 댓글을 쓴 사람의 userId) replyUsername ]
[ 이런 형태로 들고 와야 한다. ]
{ "id":1, "title":"제목1", "content":"내용1", "user" : { "id":1, "username":"ssar" }, "replies" : [ { "id":1, "comment":"댓글1", "user" : { "id":2, "username":"cos" } }, { "id":2, "comment":"댓글2", "user" : { "id":3, "username":"love" } } ] }
이렇게 화면을 보고 json을 만들어 낼 수 있다. 서버가 만들어지지 않았어도, 프런트엔드들이 이 json을 보며 (통신을 했다고 가정하고) 자기들이 객체를 만들어서 화면에 뿌릴 수 있다. 꼭 서버가 나오지 않아도 화면만 보고!! 화면만 제대로 만들어져 있으면!! 이렇게 만들어낼 수 있다!
[ 그러나 이렇게 들고 오는 게 최고!! ]
굴곡이 많으면 내부 클래스가 너무 많아진다. 불편하지 않게만, 사용자가 이해할 수 있을 정도로만 굴곡을 줘서 DTO로 만들면 된다. 굴곡을 줄여보자

“userId” : 1 이라고만 적어도 게시글을 userId 1번인 애가 썼구나? 작성자 아이디구나?
라는 걸 알 수 있다. 이렇게 이해할 수 있을 정도로만 쓰면 된다!
{ "id":1, "title":"제목1", "content":"내용1", "userId":1, "username":"ssar" "replies" : [ { "id":1, "comment":"댓글1", "userId":2, "username":"cos" }, { "id":2, "comment":"댓글2", "userId":3, "username":"love" } ] }
요정도 굴곡을 주면 된다. (이 데이터가 프런트한테 주기 제일 좋음!)
1자로 적는건 절대!!!!!!!!!!!! 안된다!!!!!!!!!!!!!
굴곡을 줘서 적어라
ORM 없이 그냥 JOIN을 하면 이렇게 나온다 (프런트 엔드 혹사 코드)

1자!!!!! 최악!!!!! 이걸 DTO로 옮겨서 예쁘게 다시 적어줘야함

이렇게 쓰고싶니...? 그리고 이거 컬렉션이 아닌데 프런트 엔드가 어떻게 FOR문을 돌리냐?! 이건 프런트 엔드 혹사시키는 코드다
[ 적당한 굴곡을 보고 DTO 만들자 ]
List<String> 타입이 아니라 이렇게 적을 순 없으니 ReplyDTO 타입을 클래스로 하나 만들어줌. DTO랑 쿼리를 잘 짜면 웹에서 60%는 먹고 가는 것(!) 그만큼 진짜 중요하다!!

이렇게 쓰면 안된다!!!!!!!! Reply 객체를 주면 안된다. Reply는 영속화된 것이니까 이 안에 user, board 다 레이지 로딩으로 슉슉 튀어나와서 안됨!
[ BoardResponse ]
public class BoardResponse { @Data public static class DetailDTO { private int id; private String title; private String content; private int userId; private String username; //게시글 작성자 이름 private List<ReplyDTO> replies = new ArrayList<>(); private boolean isOwner; //내부 클래스는 DetailDTO만 쓸 것이기 때문에 static 붙일 필요 없다! public class ReplyDTO { private int id; private String comment; private int userId; //댓글 작성자 아이디 private String username; //댓글 작성자 이름 private boolean isOwner; } }
여기서 더 필요한 데이터 2개 그건 바로…
숨겨진 데이터가 2개 더 필요하다 이건 화면에 안 보여지기 때문에 지금은 안 만들어줘도 됨. 나중에 프런트 엔드가 요청하면 만들어줘도 됨. 그게 바로 isOwner!! 이 게시글의 주인 여부, 이 댓글의 주인 여부 머스태치는 if이런걸 못 쓴다. -> 머스태치는!! 코딩을 못해! 때문에 백엔드가 if를 짜서 프런트한테 주면 좋아할건데... 근데 그걸 처음부터 생각을 못할 수 있다. 눈에 보이지 않기 때문에! 인스타의 하트 기능처럼 isOwner도 로그인 한 사람에 따라서 다르게 보이는 동적인 데이터 정리 * 화면에 안 보이더라도 id는 꼭 줘야함 * 화면에 보이는 정보는 당연히 줘야함 프런트 엔드가 작업을 하다보면 권한 처리 같은걸 할 때가 올 수 있다. 삭제나 이런건 권한을 비교해서 삭제 해야한다. 그런 로직을 프런트 엔드도 짤 수는 있으나... 이 데이터 필요하니 DTO 수정해주세요^^ 할 수가 있음 DTO는 프런트엔드의 요청에 의해서 자주 수정이 됨 엔티티는 바뀌지 않는 고정!! 불변 데이터!! (테이블이 한 번 만들어지면 형태가 불변) 그때 백엔드는 선택하면 된다. 니가 하던지, 내가 하던지............. isOwner은 userId만 주어지면 누구나 다 할 수 있다 !!
이제 굴곡이 들어가 있으니 boardIsOwner 이런식으로 말해주지 않아도 된다.
굴곡에서 누가 쓰는 isOwner인지 알 수 있음
List<ReplyDTO> replies = new ArrayList<>(); → new를 안 해놓으면?
댓글이 하나도 없는 경우에는, 메세지 컨버터가 null을 json으로 만들다가 터져버린다. 그래서 new를 해놓으면 빈배열 []이 들어오기 때문에 new를 해주는 것! 컬렉션은 DB에서 조회해서 못찾으면 0개의 빈 배열을 돌려주지 null을 주진 않음. 근데 1건을 조회했을 때엔, null을 줌. 컬렉션은 못찾으면 빈배열을 주기 때문에 절대 오류가 나지 않는다!
[ DetailDTO의 생성자 만들기 ]
응답할 때에는 생성자를 만들어야함 내부의 생성자 먼저 만들어주자 -> 이때부터는 고정! 머리 안써도 됨. 생각하지 않고 적어라! 하나하나씩 레이지 로딩 걸어주면 됨
public class BoardResponse { @Data public static class DetailDTO { private int id; private String title; private String content; private int userId; private String username; //게시글 작성자 이름 private List<ReplyDTO> replies = new ArrayList<>(); private boolean isOwner; public DetailDTO(Board board, User sessionUser) { this.id = board.getId(); this.title = board.getTitle(); this.content = board.getContent(); this.userId = board.getUser().getId(); this.username = board.getUser().getUsername(); this.isOwner = false; if (sessionUser != null) { if (sessionUser.getId() == userId) { isOwner = true; } } this.replies = replies; }
isOwner은 세션 정보를 받아야 만들어줄 수 있으니 sessionUser 받기
모든 변수는 가까이 있는걸 먼저 찾는다.
[ 바로 밑에 댓글 DTO 만들기 ]
@Data //내부 클래스는 DetailDTO만 쓸 것이기 때문에 static 붙일 필요 없다! public class ReplyDTO { private int id; private String comment; private int userId; //댓글 작성자 아이디 private String username; //댓글 작성자 이름 private boolean isOwner; //생성자 만들기 public ReplyDTO(Reply reply, User sessionUser) { this.id = reply.getId(); this.comment = reply.getComment(); //이때는 id니까 레이지 로딩 안걸림 this.userId = reply.getUser().getId(); //이때부터 레이지 로딩이 걸림. 레이지 로딩 발동! this.username = reply.getUser().getUsername(); this.isOwner = false; if (sessionUser != null) { if (sessionUser.getId() == userId) { isOwner = true; } } } } }
[ 마지막! ]

기존 댓글 리스트를 리플리 댓글 리스트로 옮겨놔야함 이것만 만들어주면 완성!
this.replies = board.getReplies().stream().map(reply -> new ReplyDTO(reply, sessionUser)).toList(); Board 객체가 댓글 리스트를 가지고 있다. 이걸 DTO 타입으로 바꿔서 집어넣기 위해 stream에 던져서 map으로 가공하고, 이 객체를 DTO로 바꿔서 넣어주는 것!
[ 완성 코드 ]
public class BoardResponse { @Data public static class DetailDTO { private int id; private String title; private String content; private int userId; private String username; //게시글 작성자 이름 private List<ReplyDTO> replies = new ArrayList<>(); private boolean isOwner; public DetailDTO(Board board, User sessionUser) { this.id = board.getId(); this.title = board.getTitle(); this.content = board.getContent(); //이때는 id니까 레이지 로딩 안걸림 this.userId = board.getUser().getId(); //이때부터 레이지 로딩이 걸림. 레이지 로딩 발동! //안 들고있는 거를 getter 할 때 레이지 로딩 발동 this.username = board.getUser().getUsername(); this.isOwner = false; if (sessionUser != null) { if (sessionUser.getId() == userId) { isOwner = true; } } this.replies = board.getReplies().stream().map(reply -> new ReplyDTO(reply, sessionUser)).toList(); } @Data //내부 클래스는 DetailDTO만 쓸 것이기 때문에 static 붙일 필요 없다! public class ReplyDTO { private int id; private String comment; private int userId; //댓글 작성자 아이디 private String username; //댓글 작성자 이름 private boolean isOwner; public ReplyDTO(Reply reply, User sessionUser) { //id 조차 없었으니 여기서부터 lazy Loading 발동! this.id = reply.getId(); this.comment = reply.getComment(); this.userId = reply.getUser().getId(); //레이지 로딩 발동 this.username = reply.getUser().getUsername(); this.isOwner = false; if (sessionUser != null) { if (sessionUser.getId() == userId) { isOwner = true; } } } } }
쿼리가 총 3번 날아감 → 쿼리 확인 아래에
쿼리 확인 필수!!!!!!!!
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=? Hibernate: select r1_0.board_id, r1_0.id, r1_0.comment, r1_0.created_at, r1_0.user_id from reply_tb r1_0 where r1_0.board_id=? order by r1_0.id desc Hibernate: select u1_0.id, u1_0.created_at, u1_0.email, u1_0.password, u1_0.username from user_tb u1_0 where u1_0.id in (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
[지금 쿼리 3번 나옴 ]
제일 처음 1.Board랑 User랑 조인하면서 2.댓글 셀렉트가 일어난다. (댓글 3개 다 뽑았다) 그다음에 댓글에서 getuser를 뽑아야하니까 3. 유저도 레이지 로딩해서 뽑아올 것임 원래라면 댓글 쓴 유저가 2명이니 (ssar, ssar, cos) 2번 날아가야한다. 근데 in쿼리가 발동해서 한방으로 날아감! default_batch_fetch_size가 없으면 select 2번 날아가야함!
사실... 이거 계속하다보면 직접 셀렉트 2번해서 조인해서 들고오는걸 선호하게 됨 보드랑 유저 조인 / 댓글이랑 유저 조인 -> 셀렉트 2번 날려서 DTO 만들어서 들고오면 끝!!
[ 화면 확인 ]

이 board에는 현재 Board와 User만 존재 함

http://localhost:8080/boards/4/detail 들어가서 화면 확인해보자! (인터셉터 걸어놔서 api 제외)
[ boards/4/detail 화면 ]
// 20240321123913 // http://localhost:8080/boards/4/detail { "status": 200, "msg": "성공", "body": { "id": 4, "title": "제목4", "content": "내용4", "userId": 3, "username": "love", "replies": [ { "id": 3, "comment": "댓글3", "userId": 2, "username": "cos", "owner": false }, { "id": 2, "comment": "댓글2", "userId": 1, "username": "ssar", "owner": false }, { "id": 1, "comment": "댓글1", "userId": 1, "username": "ssar", "owner": false } ], "owner": false } }
Share article