지금보다 더 코딩 뉴비였던 시절
막무가내로 DTO 에 담아 가져온 데이터를 프론트에 뿌리며
눈물을 흘렸던 경험이 있다.
DTO 는 유용하지만
유용한 DTO 를 더욱 유용하게 활용하는 방법.
먼저 이 쿼리로 가져온 데이터를 출력하는 여러가지 DTO 를 살펴보자.
@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);
Board 엔티티 내부에 user 와 reply 이 들어있기 때문에 엔티티 타입으로는 리턴이 가능하겠지만,
DTO 를 생성하려면 조회된 데이터들을 매핑할 수 있는 클래스가 필요하다.
따라서
board, user, reply
을 포함하는 DTO 를 생성해야 한다.이때 고려해야 할 부분은
게시글에는 댓글이 0개이거나 많을 수 있고

한 게시글에 코멘트가 3개라면 코멘트에 해당하는 부분만
List
로 묶이는 편이 데이터 관리에 용이하다.아래의 코드를 활용하면 게시물/댓글 작성자 여부와 필요한 데이터만 필터링 할 수 있다.
- 게시글에 필요한 항목과
isOwner
를 통해 작성자와 로그인된 유저가 동일인지 확인
public class BoardResponse {
@Data
public static class DetailDTO {
private Integer id;
private String title;
private String content;
private Boolean isOwner;
private String username;
// 댓글들
private List<ReplyDTO> replies = new ArrayList<>(); //엔티티 말고 DTO 를 넣어야함. 엔티티 넣으면 레이지로딩 나옴
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.username = board.getUser().getUsername();
for (Reply reply : board.getReplies()) {
replies.add(new ReplyDTO(reply, sessionUser));
}
}
@Data
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; // 권한체크
}
}
}
}
}
}
{
"id": 5,
"title": "제목5",
"content": "내용5",
"isOwner": false,
"username": "cos",
"replies": [
{
"id": 1,
"comment": "댓글1",
"username": "ssar",
"isOwner": false
},
{
"id": 2,
"comment": "댓글2",
"username": "ssar",
"isOwner": false
}
]
}
누가봐도 너무 좋은 json 데이터.
만약 이런걸 신경쓰지 않고 그냥 넣어놓는다면… 아래와 같은 상황이 발생한다.
- 모든걸 욕심껏 담아놓음.
public class BoardResponse {
@Data
public static class DetailDTO {
private Integer id;
private String title;
private String content;
private User user; // User 엔티티를 직접 포함
private List<Reply> replies; // Reply 엔티티를 직접 포함
public DetailDTO(Board board, User sessionUser) {
this.id = board.getId();
this.title = board.getTitle();
this.content = board.getContent();
this.user = board.getUser(); // 엔티티를 그대로 노출
this.replies = board.getReplies(); // 엔티티를 그대로 노출
}
}
}
Json 출력 결과
{
"id": 5,
"title": "제목5",
"content": "내용5",
"user": {
"id": 2,
"username": "cos",
"password": "1234",
"email": "cos@nate.com",
"createdAt": "2024-09-05T06:10:42.196+00:00"
},
"replies": [
{
"id": 1,
"comment": "댓글1",
"user": {
"id": 1,
"username": "ssar",
"password": "1234",
"email": "ssar@nate.com",
"createdAt": "2024-09-05T06:10:42.195+00:00"
},
"board": {
"id": 5,
"title": "제목5",
"content": "내용5",
"createdAt": "2024-09-05T06:10:42.198+00:00",
"user": {
"id": 2,
"username": "cos",
"password": "1234",
"email": "cos@nate.com",
"createdAt": "2024-09-05T06:10:42.196+00:00"
},
"replies": [
{
"id": 1,
"comment": "댓글1",
"user": {
"id": 1,
"username": "ssar",
"password": "1234",
"email": "ssar@nate.com",
"createdAt": "2024-09-05T06:10:42.195+00:00"
},
"createdAt": "2024-09-05T06:10:42.198+00:00"
},
{
"id": 2,
"comment": "댓글2",
"user": {
"id": 1,
"username": "ssar",
"password": "1234",
"email": "ssar@nate.com",
"createdAt": "2024-09-05T06:10:42.195+00:00"
},
"createdAt": "2024-09-05T06:10:42.199+00:00"
}
]
},
"createdAt": "2024-09-05T06:10:42.198+00:00"
},
{
"id": 2,
"comment": "댓글2",
"user": {
"id": 1,
"username": "ssar",
"password": "1234",
"email": "ssar@nate.com",
"createdAt": "2024-09-05T06:10:42.195+00:00"
},
"board": {
"id": 5,
"title": "제목5",
"content": "내용5",
"createdAt": "2024-09-05T06:10:42.198+00:00",
"user": {
"id": 2,
"username": "cos",
"password": "1234",
"email": "cos@nate.com",
"createdAt": "2024-09-05T06:10:42.196+00:00"
},
"replies": [
{
"id": 1,
"comment": "댓글1",
"user": {
"id": 1,
"username": "ssar",
"password": "1234",
"email": "ssar@nate.com",
"createdAt": "2024-09-05T06:10:42.195+00:00"
},
"createdAt": "2024-09-05T06:10:42.198+00:00"
},
{
"id": 2,
"comment": "댓글2",
"user": {
"id": 1,
"username": "ssar",
"password": "1234",
"email": "ssar@nate.com",
"createdAt": "2024-09-05T06:10:42.195+00:00"
},
"createdAt": "2024-09-05T06:10:42.199+00:00"
}
]
},
"createdAt": "2024-09-05T06:10:42.199+00:00"
}
]
}
무한루프가 호출되려 했으나 어찌 힘내서 갈무리 했다.
그러나 필요없는 데이터가 많고 가독성이 좋지 않다.
그래도 DetailDTO 에 엔티티를 넣었기 때문에 매개변수가 깔끔하게 관리되고 있는데
인텔리제이에서 제공하는 생성자 메서드를 활용하여 만들면 이런 문제가 발생한다.
@Data
public static class DetailDTO {
private Integer id;
private String title;
private String content;
private String username; // 엔티티의 username만 포함
private List<ReplyDTO> replies; // ReplyDTO 리스트
private Boolean isOwner; // 권한 체크 여부
public DetailDTO(Integer id, String title, String content, String username, List<ReplyDTO> replies, Boolean isOwner) {
this.id = id;
this.title = title;
this.content = content;
this.username = username;
this.replies = replies;
this.isOwner = isOwner;
}
@Data
public static class ReplyDTO {
private Integer id;
private String comment;
private String username;
public ReplyDTO(Integer id, String comment, String username) {
this.id = id;
this.comment = comment;
this.username = username;
}
}
}
}
내부에 담긴 컬럼만큼 길어지는 매개변수.
public BoardResponse.DetailDTO 게시글상세보기2(User sessionUser, Integer boardId){
Board boardPS = boardRepository.mFindByIdWithReply(boardId)
.orElseThrow(() -> new Exception404("게시글이 없습니다."));
// 필요한 데이터만 추출하여 DetailDTO를 생성합니다.
List<BoardResponse.DetailDTO.ReplyDTO> replyDTOs = boardPS.getReplies().stream()
.map(reply -> new BoardResponse.DetailDTO.ReplyDTO(reply.getId(), reply.getComment(), reply.getUser().getUsername()))
.collect(Collectors.toList());
return new BoardResponse.DetailDTO(
boardPS.getId(),
boardPS.getTitle(),
boardPS.getContent(),
boardPS.getUser().getUsername(), // Username만 포함
replyDTOs,
sessionUser != null && boardPS.getUser().getId().equals(sessionUser.getId()) // 권한 체크
);
}
필요한 데이터 추출을 위한 추가 로직…
{
"id": 5,
"title": "제목5",
"content": "내용5",
"username": "cos",
"replies": [
{
"id": 1,
"comment": "댓글1",
"username": "ssar"
},
{
"id": 2,
"comment": "댓글2",
"username": "ssar"
}
],
"isOwner": false
}
데이터는 준수하게 출력되나 로직이 골치아파진다.
결론은
- 생성자 매개변수로는 가급적 엔티티를 넣자.
- 데이터 가공은 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";
}
이렇게 전달된 데이터는 템플릿 엔진에서 이처럼 활용 가능하다.


{{#model.replies}}{{/model.replies}}
→ for 문 내부 json 데이터로 치면 빨간 박스 에 해당한다.
이미 컬렉션으로 묶어진
replies
내부이기 때문에 {{usrename}}, {{comment}} 와 같은 매개변수 이름으로 쉽게 접근이 가능해 진다.Share article