DTO에 관한 생각

Aiden Shin·2020년 2월 24일
30

과거 회사에서 DTO를 사용하지 않고 Entity로 통신을 주고 받는 경우가 있었습니다.. 결국 대참사가 벌어졌고 DTO를 도입하게 되었던적이 있습니다.

대부분의 Entity 클래스들은 대부분 DB 테이블 스키마와 1:1로 매칭된 형태의 구조를 가지고 있기 마련입니다. 특히 JPA를 사용하는 경우는 Entity 클래스 = DB 테이블 이라고 봐도 될 정도로 거의 동일한 구조를 가지게 됩니다.

DTO를 사용해야만 하는 이유는 많지만 몇 가지만 추려보자면

  • DB 설계와 동일한 객체를 화면에 공개?
  • 응답 객체가 항상 엔티티와 동일할까?
  • 순환참조 문제
  • 어떤 값을 요청하고 어떤 값을 응답하는지에 대한 설계서 역할

등등이 떠오릅니다..

DTO를 도입함에 있어 몇 가지 고민되는 부분들이 있었습니다.

  1. 어디서부터 어디까지 DTO를 사용할 것인가
  2. Entity -> DTO, DTO -> Entity 변환은 어떻게 할 것인가
  3. 어떻게 만들 것인가 (모든 요청 응답마다 클래스 생성?)

이에 대한 가장 효율적인 방법은 알지 못하며.. 개인적으로 DTO 도입 시 효율적(?) 이라고 생각했던 방법으로 위 고민들을 해결해보았습니다. (더 좋은 방법이나 의견 댓글 부탁드립니다)


어디서부터 어디까지 DTO를 사용할 것인가

ServiceRepository간에는 Entity를 주고 받는 것이 명확 했는데 ControllerService 간에는 DTO를 주고받을 지 Entity를 주고 받을 지 고민지 많이 되었습니다

그러던 도중 Entity to DTO, DTO to Entity 그리고 ModelMapper 블로그 글을 보고 ControllerService간에도 DTO를 주고 받는 것으로 결정하였습니다.

위 방식으로 진행하였지만 아직도 Service -> ControllerEntity를 넘기는 것과 DTO를 넘기는 것 중 어떤 것이 더 좋은 방법일지 고민중입니다..

시스템의 규모와 아키텍처 마다 어디까지 DTO와 Entity를 사용하는 것이 효율적인지 다른 것 같습니다. 현재 저도 시스템마다 경계를 다르게 하며 사용하고 있습니다.


Entity, DTO 간 변환은 어떻게 할 것인가

Entity와 DTO 간 객체 변환은 어떻게 할 것인지 고민하던 중
ModelMapper 라는 라이브러리를 활용하여 변환하기로 하였습니다.
ModelMapper 을 사용하여 변환하고 Entity에 없는 필드들은 직접 값을 넣어주는 방식으로 변환을 하였습니다.

/* ModelMapper를 통한 변환 */

ModelMapper modelMapper = new ModelMapper();

// 매핑 전략 설정
modelMapper.getConfiguration().setMatchingStrategy(MatchingStrategies.STRICT);

// 변환
final UserBasicInfoDto userBasicInfoDto = modelMapper.map(userEntity, UserBasicInfoDto.class);

변환 작업을 어디서 진행할 지에 대한 고민 끝에 DTO내에 static 매서드를 만들어 변환하기로 하였습니다.

  • Entity -> DTO = of()
  • DTO -> Entitty = toEntity()
/*DTO 변환 Example*/
public class SampleDto {
  
    private Long          id;
    private String        title;
    private String        interviewee;
    private String        company;
   ...

    // Entity -> DTO   
    public static SampleDto of(Sample sample) {
        return modelMapper.map(sample, SampleDto.class);
    }

    // Entity -> DTO (Page의 경우)
    public static Page<SampleDto> of(Page<Sample> sourcePage) {
        return sourcePage.map(SampleDto::of);
    }
}

DTO로 변환하는 내용은 DTO 내부의 of() 에 모두 넣어두고 사용할 때는 아래처럼 사용하도록 하였습니다.

Sample sample = sampleRepository.findById(id)
                                .orElseThrow(() -> new NotExistException("Sample", id));
// DTO로 변환하여 리턴
return SampleDto.of(sample);

Entity로의 변환은 static 매서드가 아닌 일반 매서드로 작성하여 객체 자신의 값을 가지고 Entity를 생성하도록 하였습니다.
주로 요청 DTO에서는 toEntity() 매서드를, 응답 매서드에는 of() 매서드를 갖게 됩니다

// DTO -> Entity
public Sample toEntity() {
    Sample.builder()
          .company(this.company)
          .description(this.description)
             ...
          .build();
    }
public void saveSample(SampleCreateRequest createRequest, Long managerId) {
    // DTO -> Entity
    Sample sample = createRequest.toEntity();

    sampleRepository.save(sample);
}

어떻게 만들 것인가 (모든 요청 응답마다 클래스 생성?)

모든 요청마다 혹은 모든 응답마다 DTO를 만들어야할까??
같은 필드를 가지는 요청 혹은 응답의 경우 같이 사용해도 되지 않을까??
지금은 내용이 같더라도 수정에 의해 영향을 받지는 않을까??

많은 고민들이 있었고 결국, 웬만하면 요청 응답마다 DTO를 생성하는 방향으로 결론을 내렸습니다.

이렇게 하면 수정이 일어나도 다른 API의 요청이나 응답에 영향이 가지 않는다는 장점이 있지만 클래스가 너무 많아져서 관리가 안될 수도 있다는 단점이 있습니다.

이를 조금이나마 해결하기 위해 DTO 내부에 InnerClass를 만들어 관리하도록 하였습니다.

public class SampleDto {

    @Getter
    @Setter
    public static class Create {

        private String content;

        @Builder
        public Create(String content) {
         ...
        }
    }

    @Getter
    @Setter
    public static class Update {

        private Long   id;
        private String content;
        ...
        
    }

    @Getter
    @Setter
    public static class Response {

        private Long   id;
        private String content;
        private String file;
        private Long   userId;
        ...

        public static Response of(Sample sample) {

            final Response dto = modelMapper.map(sample, Response.class);
            ...

            return dto;
        }

        public static List<Response> of(List<Sample> sampleList) {
            return sampleAskList.stream()
                                .map(Response::of)
                                .collect(Collectors.toList());
        }

    }

}

아래와 같이 하나의 DTO 안에 Create, Update, Response 등 DTO를 InnerClass로 둠으로써 하나의 파일로 관리되도록하였습니다.

아직도 DTO에 대해 고민했던 부분들에 대한 명확한 정답은 찾지 못한 것 같습니다.
지금 사용하고 있는 좋은 방식이나 위의 방식들의 단점, 개선 사항들을 댓글로 알려주세요

profile
예전 블로그: https://shinilhyun.github.io/

3개의 댓글

comment-user-thumbnail
2020년 7월 29일

제가 요즘 하는 고민에 대한 한 가지 솔루션으로 보여서 댓글 남깁니다. 우선 제 생각과 비슷한 어프로치여서 뭔가 확신이 들었는데 그 점에 감사드리고요, 한 가지 마음에 걸리는 것이 있어 의견을 구하고 싶습니다.
"외부 client와 Data를 주고 받는 DTO객체는 반드시 dummy(pure)해야 한다"는 출처모를 인터넷의 중론 때문에 static class, 즉 behavior/logic을 DTO 내부에 두는 것에 굉장히 찜찜한 마음이 드는데요, 이에 대해서 어떻게 생각하시는지요?

+) 2020 11 11 오타수정

1개의 답글
comment-user-thumbnail
2021년 1월 7일

안녕하세요, 글 맨 상단에 언급하신 '대참사' 는 어떤걸 말하시는지 대략 알 수 있을까요?
Entity로 직접 통신하면 어떠한 문제가 생기는지 알고 싶습니다.

답글 달기