DTO 활용하기

dto 를 활용하여 예쁜 데이터를 만들어 보자
HootJem's avatar
Sep 05, 2024
DTO 활용하기
지금보다 더 코딩 뉴비였던 시절 막무가내로 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개이거나 많을 수 있고
notion image
한 게시글에 코멘트가 3개라면 코멘트에 해당하는 부분만 List 로 묶이는 편이 데이터 관리에 용이하다.
아래의 코드를 활용하면 게시물/댓글 작성자 여부와 필요한 데이터만 필터링 할 수 있다.
  1. 게시글에 필요한 항목과 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 데이터.
만약 이런걸 신경쓰지 않고 그냥 넣어놓는다면… 아래와 같은 상황이 발생한다.
  1. 모든걸 욕심껏 담아놓음.
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 }
데이터는 준수하게 출력되나 로직이 골치아파진다.
 

결론은
  1. 생성자 매개변수로는 가급적 엔티티를 넣자.
  1. 데이터 가공은 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"; }
 
이렇게 전달된 데이터는 템플릿 엔진에서 이처럼 활용 가능하다.
notion image
notion image
{{#model.replies}}{{/model.replies}}
→ for 문 내부 json 데이터로 치면 빨간 박스 에 해당한다.
 
이미 컬렉션으로 묶어진 replies 내부이기 때문에 {{usrename}}, {{comment}} 와 같은 매개변수 이름으로 쉽게 접근이 가능해 진다.
Share article

[HootJem] 개발 기록 블로그