
[ 화면 확인 ]

[ 필요한 데이터부터 만들어보자 ]
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을 하면 이렇게 나온다 (프런트 엔드 혹사 코드)
[ 적당한 굴곡을 보고 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

