지금보다 더 코딩 뉴비였던 시절
막무가내로 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";
}이렇게 전달된 데이터는 템플릿 엔진에서 이처럼 활용 가능하다.

Share article
