DTO? 내 생각에는!

Kevin·2023년 4월 26일
0

? 내 생각에는!

목록 보기
1/5
post-thumbnail

이 글에서는 DTO에 대한 간략한 정의와 내가 DTO를 사용하면서 생겼던 질문들과 내 나름대로 내린 답 대해서 적어보려고 한다. 이 글은 극히 주관적이고, 내 답이 반드시 정답이 아니며, 읽어주시는 독자분들은 얘가 밥먹고, 이런 생각을 했구나라는 정도만 알아주면 고맙겠다.

 

DTO란?


  • DTO는 프로세스 간에 데이터를 전달하는 용도의 객체이며, 비즈니스 로직을 포함하지 않는 데이터를 전달하기 위한 단순한 객체를 의미한다.
  • DTO가 필요한 이유는 아래와 같다.
    1. DB 설계와 동일한 엔티티 객체를 Response 하는 것은 너무 위험하다.
      1. DB의 스키마 구조가 모두 노출될 수 있기 때문이다.
    2. Response 하는 응답 객체는 엔티티와 동일하지 않고, 이를 응용해서 사용하기 때문에
      1. 내가 원하는 필드들만 Response 하고 싶을 때
    3. 순환참조 문제
    4. 어떤 값을 요청하고, 어떤 값을 응답하는지에 대한 설계서 역할을 하기 때문에

 

어디부터 어디까지 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님)께서는 아래와 같이 답변을 해주셨고, 나만의 생각의 생각을 정리하는데 큰 도움을 받았다.

ServiceRepository간에는 Dto를 주고 받을지 Entity를 주고 받을 것인가?

  • SC님이 주셨던 Comment : Service와 Repository 간은 [Persistence Tier(영속 계층 혹은 데이터 계층)]과 가까이 있기에 Entity를 주고 받는 것이 맞다.

ControllerService 간에는 Dto를 주고 받을지 Entity를 주고 받을 것인가?

  • SC님이 주셨던 Comment : Dto를 주고 받는게 맞다. Dto를 데이터 전송을 위해 사용하고, 엔티티는 데이터 전송간 사용안하는게 좋다. 그 이유는 엔티티의 변경 위험이나 여러가지 제약이 있기 때문이다.

 
내가 내린 나만의 결론

  • ControllerService 간 DTO를 주고 받았을 때보다 Entity를 주고 받았을 때 생기는 문제점들이 많고, DTO의 본래 책임인 Data Transfer 역할에 맞게끔 DTO를 주고 받는게 맞다. 라는 생각을 하게 되었다.

  • Controller Service 간에는 DTO를 주고 받자.

    • Controller Service 시에는 Controller가 받은 requestDto를 Service가 원하는 형태로 변경해서 인자로 넘겨준다.
    • Service Controller 시에는 Service에서 Repository로부터 받은 Entity를 Response Api에 맞게 변형해서 인자로 넘겨준다.

 

Dto에게 변환의 책임을 주는게 맞을까?


그 다음 질문인 위 질문은 객체지향을 공부하면서 생각이 들었던 질문이다.

객체지향에서는 하나의 객체에 하나의 책임만을 부여하는 것을 권장하고 있는데, 기존의 나는 DTO에게 기존 역할인 데이터 전송의 역할뿐만 아니라 DtoToEntity나 EntityToDto로의 변환의 책임 또한 부여하고 있다는 것을 알게 되었다. 이러한 부분에 궁금증이 생겨서, 이번에도 우리 동아리 출신의 SY님에게 질문을 드려보았다.

 
내 질문

"현재 저는 Dto를 단순히 데이터를 전달하기 위한 객체로 사용하는 것을 넘어서 DtoToEntity나 EntityToDto로의 변환에도 사용하고 있습니다. 그러나 저는 Dto의 역할은 데이터를 전달하는 것이 Dto 본연의 역할이라고 생각하여서, 변환의 책임은 Mapper등에 따로 부여하는게 맞지 않나라는 생각이 듭니다.질문은 다음과 같습니다.

  1. SY님은 Dto에서 변환을 사용하고 계시나요?? 사용하고 계시다면 이유가 궁금합니다!
  2. Dto에서 변환을 사용하시지 않는다면 어떤 방법으로 변환하시는지 궁금합니다!"

 
SY님의 답변

"먼저 DTO 변환을 어떤 부분에서 해 주어야 하는지 고민은 정말 좋은 질문인것 같아요! 일단 말씀드리고 싶은것은 이것에 대해서 어떤 것이 best practice라고 정의하기는 어려울 것 같아요. 사람마다 선호하는 방법이 다르고 기준이 다르기 때문에 Kevin님만의 기준이 있다면 그것도 정답이 될 수 있을 것 같습니다.

  1. 저는 Kevin님이 말씀해주신것처럼 DTO가 변환의 책임을 갖는 것이 두 개의 책임을 갖는것이라고 생각합니다.
  2. 그래서, 말씀해주신 Mapper의 역할과 비슷한 역할을 하는 Converter 클래스로 책임을 분리해서 Converter 내의 from, to 메서드를 이용해 각각 entity 를 dto로, dto를 entitiy로 변환하는 책임을 수행하도록 하는것을 선호합니다!

하지만, DTO의 갯수가 많지 않은 소규모 프로젝트의 경우에는 DTO 내에서 변환을 처리하는것이 전체적인 코드 복잡도를 줄이는 방법이 될 수도 있어서

본인만의 기준을 만드시고 그 기준대로 지켜나가신다면 어떤 상황에서도 올바른 방법으로 사용하실 수 있을 거라고 생각합니다~!"

 

SY님이 말씀해주셨던 “본인만의 기준을 만드시고 그 기준대로 지켜나가신다면 어떤 상황에서도 올바른 방법으로 사용하실 수 있을 거라고 생각합니다” 이라는 문장이 나에게는 굉장히 큰 울림으로 다가왔다. 이 한 문장의 말이 이 글을 쓰게 된 동기이자 내가 기존까지 해오던 개발, 즉 남들이 하는것을 아무런 생각도 없이 수용하던 개발을 되돌아 보게 된 계기가 되었다.

이러한 조언으로부터 나는 나만의 결론을 내려보기전에 그럼 과연 나만의 개발 기준은 무엇일까라는 생각을 가져보고, 나만의 기준을 만들어보았다.

개발에 대한 내 기준

  1. 책임을 최대한 분리하자. → Dto에게는 데이터를 전달하기 위한 책임 하나만 부여해야한다.
  2. 늘 변경과 확장을 생각하자.
  3. 타인이 보기 쉬운 코드를 만들자.
  4. 내 스스로가 납득이 가고, 이해가 가는 코드를 만들자.

내가 내린 나만의 결론

  • 개발에 대한 내 기준 1번에 의하여 아래와 같이 결정을 하였다.
  • DTO가 기존에 가지고 있던 본인의 책임인 데이터 전달이 아닌 변환의 책임을 별도의 객체에게 분리하자.
    • Dto가 아닌 별도의 ConverterMapper Class를 도입하여서, 변환 책임을 부여하자. → Mapsturctur 사용하기로 결정

 

DTO를 어떻게 만들 것인가? → 모든 요청, 응답마다 DTO 클래스를 생성할 것인가??


해당 질문은 프로젝트를 진행할 때 매우 많은 DTO 클래스들로 인해서 보기가 안좋아지고, DTO 클래스가 늘어나면 늘어 날수록 관리가 힘들어졌기에 생겼다.

프로젝트가 큰 규모가 아니었음에도 보기에 굉장히 너저분한 모습이다. 물론 사람에 따라서 지저분해 보이지 않을 수도 있다.

지저분해진 가장 큰 이유는 모든 요청 응답마다 클래스를 생성, 즉 RequestDto와 ResponseDto 2개의 클래스로 분리를 하여 생성을 하다보니, 자연스럽게 클래스 파일들이 늘어났기 때문이라 생각한다.

이러한 문제에 대해서 내가 참고한 레퍼런스에서는 RequestDto와 ResponseDto를 하나의 Inner 클래스로 만들었다.

나 또한 이렇게 Inner 클래스로 Request, Response 응답들을 하나로 묶는 것에 동의를 한다. 그 이유로는 한 클래스의 길이가 길어진다는 단점이 있지만, 클래스들을 관리하기 편하고, 클래스 구조를 보기에 더 편하다는 장점이 있기 때문이고 이러한 장점이 단점보다 크다고 판단하였기 때문이다.

내가 내린 나만의 결론

  • 개발에 대한 내 기준 3번에 의하여 아래와 같이 결정을 하였다.
  • 기존 RequestDto와 ResponseDto 2개의 Dto 클래스를 만들었던 방법에서 하나의 Inner 클래스로 만드는 것으로 결정을 하였다.
  • 아래 코드는 기존 RequestDto와 ResponseDto를 하나의 Inner 클래스로 만든 나의 코드이다.
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());
        }

    }

}
profile
Hello, World! \n

0개의 댓글