오늘은 Spring Boot 프로젝트를 시작할 때마다 고민의 대상이 되는 DTO 에 대해 알아보도록 하겠습니다.

매번 DTO 의 필요성에 대해 깊이 고민하지 않고 사용해서 그 기준을 잡기가 어려웠는데요.

이번 포스팅을 통해 DTO 의 책임과 역할을 이해하고 저만의 사용 가이드라인을 만들어보도록 하겠습니다.

 


 

DTO (Data Transfer Object)

 

우선, DTO 의 정의를 간략하게 알아보겠습니다.

DTO는 프로세스 간에 데이터를 전달하는 용도의 객체입니다. 비즈니스 로직을 포함하지 않는 데이터를 전달하기 위한 단순한 객체 를 뜻합니다.

 

 

MVC 패턴에서는 주로 Client 와 Controller 사이에서 DTO 가 사용됩니다.

저는 보통 Controller 에서는 DTO 로 데이터를 전달받고, 애플리케이션 내부에서는 Domain(또는 Entity 또는 Model) 을 통해 데이터를 전달합니다.

 

예를 들면 Controller, Service, Repository, DataBase 는 서로 User 라는 도메인 객체를 통해 데이터를 전달하고,

Controller 에서 Client 로 리턴할 때는 UserDTO 에 매핑하여 리턴하는 것입니다.

 

소스 코드로 구현하면 아래와 같습니다. 코드는 baeldung 의 예제를 가져왔습니다.

 

 

DTO 를 통해 원하는 정보만 보여주기

 

public class User {

    private String id;
    private String name;
    private String email;
    private String password;

    public User(String name, String email, String password) {
        this.name = Objects.requireNonNull(name);
        this.email = Objects.requireNonNull(email);
        this.password = this.encrypt(password);
    }

    // Getters and Setters

   String encrypt(String password) {
       // encryption logic
   }
}

 

User 객체는 id, name, email, password 의 값을 가지고 있습니다.

password 라는 공개할 수 없는 값을 가지고 있기 때문에 User 객체 그대로 Client 에게 전달할 수 없습니다.

그렇기 때문에 Client 에게 노출해도 되는 필드로만 이루어진 UserDTO 가 필요합니다.

 

public class UserDTO {
    private String name;
    private String email;
    
    // standard getters and setters
}

 

Service, Repository 에서는 User 객체로 데이터를 전달하고

Controller 에서 DTO 객체로 매핑해서 Client 에게 리턴합니다.

 

@RestController
@RequestMapping("/users")
class UserController {

    private UserService userService;
    private Mapper mapper;

    // Constructor

    @GetMapping
    @ResponseBody
    public List<UserDTO> getUsers() {
        return userService.getAll()
          .stream()
          .map(mapper::toDto)
          .collect(toList());
    }
}
@Component
class Mapper {
    public UserDTO toDto(User user) {
        String name = user.getName();
        String email = user.getEmail();

        return new UserDTO(name, email);
    }
}

 

이처럼 DTO 를 사용하면 Client 에게 password 와 같은 값을 노출하지 않고 원하는 값만 리턴할 수 있습니다.

 

 

그런데 막상 DTO 를 사용하려고하면 이런 의문들이 생깁니다.

  1. 그렇다면 클라이언트에게 노출해도 되는 값만 가진 도메인 객체라면 그대로 리턴해도 되는걸까?
  2. 매번 Controller 에서 도메인 객체를 DTO 객체로 변환하는 비용을 치르더라도 DTO 를 사용해야만 할까?

 

이제부터 두 가지 의문에 대한 답을 찾아보도록 하겠습니다.

 

 

 

DTO 와 Domain 을 분리하는 이유

그렇다면 클라이언트에게 노출해도 되는 값만 가진 도메인 객체라면 그대로 리턴해도 되는걸까?

 

 

관심사의 분리(Separation of Concerns, SoC)

우리가 Spring Boot 와 같은 프레임 워크에서 MVC 패턴을 사용하는 이유는 무엇일까요?

모델1 방식을 사용해보신 분들은 아시겠지만, Model-Controller-View 가 각자의 역할을 수행하여 유지보수가 편리하기 때문입니다.

Controller 는 중간 다리의 역할로 client 와 request/response 하는 책임이 있고, Model 은 DataBase 에서 받아온 데이터를 다루는 책임이 있습니다.

 

우리가 사는 세상에서도 복잡하고 큰 기업일수록 팀이 철저하게 구분되고 다른 팀이 무엇을 하는지조차 모르는 경우가 있는 것처럼

OOP 세상에서는 관심사의 분리를 통해 복잡한 시스템을 효율적으로 작동하게 합니다.

 

 

그런데 '관심사' 라는 말이 잘 와닿지 않습니다. 네이버 영어사전에 concern 을 검색해보았더니 "영향을 미치다" 라는 의미가 나왔습니다.

그렇다면 concerns 를 분리한다는 것은 "애플리케이션에 같은 영향을 미치는 코드들끼리 분리한다"는 것이라고 이해할 수 있습니다.

 

 

관심사의 수평적 분리

 

 

관심사의 수평적 분리는 애플리케이션 내에서 동일한 역할을 수행하는 논리적 계층으로 애플리케이션을 나눈 것입니다.

프로세스에 따라 MVC 패턴을 녹이면 다음 그림과 같습니다.

 

 

  • Presentation Layer: 애플리케이션의 기능과 데이터를 사용자에게 제공
  • Business Layer: 애플리케이션의 핵심 비즈니스 로직 및 나머지 두 계층 간에 전달되는 데이터 처리
  • Data Access Layer: 데이터베이스와 상호 작용하는 역할

 

여기까지 멀리 돌아왔는데요.. DTO 와 Domain 을 분리해야하는 이유가 바로 여기에서 나옵니다.

 

DTO 는 오직 데이터를 전달하는 목적으로 Presentation Layer 에 속합니다.

그리고 Domain 은 비즈니스 로직을 담는 Business Layer 에서 역할을 수행합니다.

 

두 객체를 분리하지 않는다면 Presentation, Business 가 혼합된 god class 가 탄생하게 됩니다.

IT 대기업에서 영업과 개발을 함께하는 그저 god team 을 만들어 버리는 거랄까요.

 

이렇게 god class 를 만든다면 변경이 있을 때 두 레이어에 영향을 미치게 됩니다.

좋은 객체지향 설계를 위해서는 하나의 객체에는 하나의 책임만 존재해야 합니다. 변경이 있을 때 파급 효과를 최소한으로 하기 위해서죠.

 

 

 

 

마틴 파울러의 DTO 패턴

매번 Controller 에서 도메인 객체를 DTO 객체로 변환하는 비용을 치르더라도 DTO 를 사용해야만 할까?

 

 

마틴 파울러는 저서 Patterns of Enterprise Application Architecture(P of EAA) 에서 DTO 를 소개하며

DTO 의 주요 목적에 대해 아래와 같이 말했습니다.

The main reason for using a Data Transfer Object is to batch up what would be multiple remote calls into a single call

 

 

마틴 파울러가 말하는 DTO 의 주요 목적은 한 번의 호출로 여러 매개 변수를 일괄 처리해서 서버의 왕복을 줄이는 것입니다.

해당 호출에 관련 된 모든 데이터를 가지고 있는 DTO 객체를 만들어서 네트워크 비용을 줄인다는 의미입니다.

 

OKKY 에 이해하기 좋은 댓글이 있어서 첨부하도록 하겠습니다.

대표적으로 유저 테이블이 있다고 가정하면, 이 유저라는 정보는 광범위하게 사용될 것 입니다.
또 게시판이 있을 수 있고, 상품 판매 화면이 있을 수 있고, 결제가 있을 수 있고, Q&A  게시판이 있을 수 있습니다.

그렇다면 예를 들어, 상품정보와 Q&A는 서로 다른 테이블에 구현이 되겠는데
특정 상품 소개 페이지 안에 해당 상품과 관련은 없어도 연관성은 있는
질문들의 Q&A 만 뽑아서 보여주고 싶다면? 거기에 구매한 몇몇 유저들의 정보까지 포함시키고 싶다면?

상품소개 API  1개
Q&A  API 1개
유저 API 1개

이렇게 3개의 API를 호출하실건가요? 네 이것도 틀린 방법은 아닙니다. 잘못된 방법도 아닙니다.
충분히 유효한 방법입니다.

그런데 그렇다면 모든 불필요한 정보까지 전부 반환이 될 것이고

결국 통신도 3회 이루어졌는데, 이걸 1회로 줄일 수 있다면?

방법은 간단하겠죠. 서비스 3개는 존재할테고, 이를 부르는 컨트롤러 하나에서
DTO를 준비하고, 여기에 필요한 필드를 미리 구성해놓은 다음에
서비스 3개에서 리턴된 오브젝트에서 뽑아서 매핑시켜주면 됩니다.

그러면 컨트롤러 1개가 불필요한 정보 없이 API 1개만으로 원하는 응답을 모두 해줬습니다.

출처: [OKKY] 카카오콘 님, https://okky.kr/articles/1293573

 

추가로 인프런에 김영한 님이 답변하신 글도 참고하면 좋을 것 같아 첨부하겠습니다.

항상 유지보수에서 가장 문제가 되는 것이 바로 애매한 것입니다.
특히 같은 필드인데, 어떤 경우에는 null이고 어떤 경우에는 값이 있고 이렇게 모호하면 정말 유지보수가 어려워집니다.
그래서 API 응답 스펙이 정해지면 그 필드에 값은 항상 같은 원칙으로 반환되도록 명확하게 설계하는 것이 중요합니다.
클래스를 여러개 만들더라도, 코드가 중복되는 것 처럼 보일지라도, 명확한 것이 훨씬 더 나은 선택이라는 것이지요.

다만 API를 제공할 때 또 모든 케이스에 대응해서 만들면 API 자체가 너무 많아집니다.
그래서 null 값 대신에 실제 값을 채워서 반환하는 API를 제공하는 것이 좋습니다.
예를 들어서 username, age 둘다 제공하는 공통 API 하나를 여러곳에서 사용하도록 제공하는 것이지요.
실무에서 API를 설계할 때 진짜 고민은, 생각보다 너무 복잡하다는 것입니다.

어떻게 보면 제공 단위를 크게 만들어서 모든 데이터를 다 반환하는 API 하나를 만들면 될 것 같지만, 이렇게 너무 공통화해도 유지보수가 어렵고, 성능 이슈가 있습니다. 반대로 너무 각각의 케이스를 대응하도록 만들어도 API 자체가 많아져서 유지보수가 어렵습니다.
이 사이에서 적절한 단위로 API를 설계하고 제공하는 것이 묘미이지요^^!

제가 선호하는 방법은 기본 공통 API를 제공하고, 이 기본 공통 API로 해결이 안되는 특수한 경우에 한해서 별도의 API를 제공하는 방법을 선호합니다.

출처: [인프런] 김영한 님, https://www.inflearn.com/questions/72423/dto

 

여러 의견을 종합해 본 저의 결론은 다음과 같습니다.

 

  1. DTO 를 사용하는 주요 목적은 한 번의 호출로 해당 호출에 관련 된 모든 데이터를 담은 객체를 리턴 받아 사용하는 것입니다.
  2. 네트워크 비용 > 컨트롤러에서 매번 변환하는 비용 이라고 판단한 것입니다.
  3. 이 때, 같은 필드인데 어떤 경우에는 null 이고 어떤 경우에는 값이 있다면 유지보수가 어려워집니다.
  4. 그렇기 때문에 null 없이 모든 필드에 값이 있는 공통 DTO 를 사용합니다.
  5. 공통 DTO 가 커져서 성능에 이슈가 생기거나, 특수한 경우에만 별도의 DTO 를 사용합니다.

 

 

 

 

마치며

이번 포스팅에서는 제가 DTO 에 대해 가지고 있던 의문을 다뤄보았습니다.

아직 DTO 를 공통으로 사용하는 것에 대한 의문이 조금 있지만, 사용하다가 개선하게 되면 업데이트 하도록 하겠습니다. :)

 

 

Reference

복사했습니다!