이 글에서는 DTO에 대한 간략한 정의와 내가 DTO를 사용하면서 생겼던 질문들과 내 나름대로 내린 답 대해서 적어보려고 한다. 이 글은 극히 주관적이고, 내 답이 반드시 정답이 아니며, 읽어주시는 독자분들은 얘가 밥먹고, 이런 생각을 했구나라는 정도만 알아주면 고맙겠다.
데이터를 전달하기 위한 단순한 객체
를 의미한다.
개발을 하던 중 DTO에 대한 첫번째 고민은 바로 위 제목과 같다.
Controller ↔ Service ↔ Repository간 Entity
로 전송을 해줘야 할까? 아니면 DTO
로 전송을 해줘야 할까?라는 고민이 생겼다.
Spring을 처음 접할 때는 단순히 아래와 같이 남들이 하는 방식을 따라하였다.
Controller ↔ Service간에는 DTO
를 반환하였다.
@Transactional(readOnly = true)
public List<PostResponseDto> findAll(Pageable pageable) {
return repository.findAll(pageable).stream().map(PostResponseDto::fromManyPost).collect(Collectors.toList());
}
Service ↔ Controller간에는 Entity
를 반환하였다.
Page<Post> findAll(Pageable pageable);
물론 남들이 하는 방식을 따라하는게 스프링을 처음 접할 당시에는 당연했고, 또 잘못된 것이라 볼 수는 없지만 왜 이렇게 하는지에 대한 이유도 제대로 이해하지도 못하고, 사용하는 것은 내 학습에 아무런 도움이 안된다고 생각을 하였다. 그래서 내가 생각한 이유에 맞는 답을 정하고 싶었다.
이 문제에 대해서 같은 동아리원들께 물어봤었다. 한 동아리원 분(SC님)께서는 아래와 같이 답변을 해주셨고, 나만의 생각의 생각을 정리하는데 큰 도움을 받았다.
Service
↔ Repository
간에는 Dto를 주고 받을지 Entity를 주고 받을 것인가?
Controller
↔ Service
간에는 Dto를 주고 받을지 Entity를 주고 받을 것인가?
내가 내린 나만의 결론
Controller
↔ Service
간 DTO를 주고 받았을 때보다 Entity를 주고 받았을 때 생기는 문제점들이 많고, DTO의 본래 책임인 Data Transfer 역할에 맞게끔 DTO를 주고 받는게 맞다. 라는 생각을 하게 되었다.
Controller
↔ Service
간에는 DTO를 주고 받자.
Controller
→ Service
시에는 Controller가 받은 requestDto를 Service가 원하는 형태로 변경해서 인자로 넘겨준다. Service
→ Controller
시에는 Service에서 Repository로부터 받은 Entity를 Response Api에 맞게 변형해서 인자로 넘겨준다.
그 다음 질문인 위 질문은 객체지향을 공부하면서 생각이 들었던 질문이다.
객체지향에서는 하나의 객체에 하나의 책임만을 부여하는 것을 권장하고 있는데, 기존의 나는 DTO에게 기존 역할인 데이터 전송의 역할뿐만 아니라 DtoToEntity나 EntityToDto로의 변환의 책임 또한 부여하고 있다는 것을 알게 되었다. 이러한 부분에 궁금증이 생겨서, 이번에도 우리 동아리 출신의 SY님에게 질문을 드려보았다.
내 질문
"현재 저는 Dto를 단순히 데이터를 전달하기 위한 객체로 사용하는 것을 넘어서 DtoToEntity나 EntityToDto로의 변환에도 사용하고 있습니다. 그러나 저는 Dto의 역할은 데이터를 전달하는 것이 Dto 본연의 역할이라고 생각하여서, 변환의 책임은 Mapper등에 따로 부여하는게 맞지 않나라는 생각이 듭니다.질문은 다음과 같습니다.
SY님의 답변
"먼저 DTO 변환을 어떤 부분에서 해 주어야 하는지 고민은 정말 좋은 질문인것 같아요! 일단 말씀드리고 싶은것은 이것에 대해서 어떤 것이 best practice라고 정의하기는 어려울 것 같아요. 사람마다 선호하는 방법이 다르고 기준이 다르기 때문에 Kevin님만의 기준이 있다면 그것도 정답이 될 수 있을 것 같습니다.
하지만, DTO의 갯수가 많지 않은 소규모 프로젝트의 경우에는 DTO 내에서 변환을 처리하는것이 전체적인 코드 복잡도를 줄이는 방법이 될 수도 있어서
본인만의 기준을 만드시고 그 기준대로 지켜나가신다면 어떤 상황에서도 올바른 방법으로 사용하실 수 있을 거라고 생각합니다~!"
SY님이 말씀해주셨던 “본인만의 기준을 만드시고 그 기준대로 지켜나가신다면 어떤 상황에서도 올바른 방법으로 사용하실 수 있을 거라고 생각합니다”
이라는 문장이 나에게는 굉장히 큰 울림으로 다가왔다. 이 한 문장의 말이 이 글을 쓰게 된 동기이자 내가 기존까지 해오던 개발, 즉 남들이 하는것을 아무런 생각도 없이 수용하던 개발을 되돌아 보게 된 계기가 되었다.
이러한 조언으로부터 나는 나만의 결론을 내려보기전에 그럼 과연 나만의 개발 기준은 무엇일까라는 생각을 가져보고, 나만의 기준을 만들어보았다.
내가 내린 나만의 결론
Converter
나 Mapper Class
를 도입하여서, 변환 책임을 부여하자. → Mapsturctur
사용하기로 결정
해당 질문은 프로젝트를 진행할 때 매우 많은 DTO 클래스들로 인해서 보기가 안좋아지고, DTO 클래스가 늘어나면 늘어 날수록 관리가 힘들어졌기에 생겼다.
프로젝트가 큰 규모가 아니었음에도 보기에 굉장히 너저분한 모습이다. 물론 사람에 따라서 지저분해 보이지 않을 수도 있다.
지저분해진 가장 큰 이유는 모든 요청 응답마다 클래스를 생성, 즉 RequestDto와 ResponseDto 2개의 클래스로 분리를 하여 생성을 하다보니, 자연스럽게 클래스 파일들이 늘어났기 때문이라 생각한다.
이러한 문제에 대해서 내가 참고한 레퍼런스에서는 RequestDto와 ResponseDto를 하나의 Inner 클래스로 만들었다.
나 또한 이렇게 Inner 클래스로 Request, Response 응답들을 하나로 묶는 것에 동의를 한다. 그 이유로는 한 클래스의 길이가 길어진다는 단점이 있지만, 클래스들을 관리하기 편하고, 클래스 구조를 보기에 더 편하다는 장점이 있기 때문이고 이러한 장점이 단점보다 크다고 판단하였기 때문이다.
내가 내린 나만의 결론
public class PostDto {
@Getter
@Setter
public static class Request {
@NotNull(message = "제목은 필수 입력값입니다.")
private String title; // 제목
@NotNull(message = "내용은 필수 입력값입니다.")
private String content; // 내용
private User writer; // 작성자
private String hashTag; // 해시태그
private Category category; // 카테고리
private List<Map> mapList; // map 리스트
private List<PostMap> postMapList; // postMap 리스트
private String imageUrl; // 이미지 주소
}
@Getter
@Setter
public static class Update {
private Long id; // pk
private String title; // 제목
private String content; // 내용
}
@Getter
@Setter
public static class Response {
private Long id; // pk
private String title; // 제목
private String content; // 내용
private String hashTag; // 해시태그
private String writer; // 작성자
private String profileImg; // 프로필 이미지
private String imageUrl; // 이미지 경로
private Category category; // 카테고리
private List<CommentResponseDto> comments; // 댓글들
private List<PostMapResponseDto> postMapList; // PostMaps
private List<MapResponseDto> mapList; // 위도, 경도
private int commentCnt; // 댓글 수
private int viewCnt; // 조회 수
private int recommendCnt; // 추천 수
private LocalDateTime createdAt; // 생성 날짜
private LocalDateTime modifiedAt; // 수정 날짜
public static Response of(Post post) {
final Response dto = modelMapper.map(post, Response.class);
return dto;
}
public static List<Response> of(List<Post> postList) {
return postList.stream()
.map(Response::of)
.collect(Collectors.toList());
}
}
}