본문 바로가기
JAVA

[Java] DTO (Data Transfer Object)

by solar_youn 2022. 5. 3.

DTO란?

DTO라는 단어는 마틴 파울러의 책 P of EAA(Patterns of Enterprise Application Architecture)에서 처음 소개했는데, Data Transfer Object의 약자입니다. DTO는 말 그대로 데이터 전송 객체로, 메서드 호출 수를 줄이기 위해 프로세스 간에 데이터를 전송하기 위해 사용됩니다.

 

마틴 파울러에 의하면 DTO를 사용하는 주목적은 단일 호출에서 여러 매개변수를 일괄 처리하여 서버로의 왕복 여행을 줄이는 것이라고 설명했습니다. 이를 통해 네트워크 통신 비용에 대한 오버헤드를 줄일 수 있게 된다고 하는데 다음 예시를 통해 쉽게 알아보도록 하겠습니다.

만약, User에 대한 정보에 Role을 포함해서 반환해야 한다면?
→ REST API를 두 번 호출하지 않고 User 정보에 Role을 포함시켜 DTO로 반환하는 방식을 사용할 수 있다.

 

또 다른 목적은 민감한 Domain 정보가 노출되는 것을 방지하기 위해서입니다. 이는 다음 사진을 통해서 알아보도록 하겠습니다.

 

해당 그림은 MVC 모델을 표현한 것입니다. MVC란 애플리케이션을 Model, View, Controller의 역할로 구분하는 디자인 패턴입니다. 이를 이용하면 비즈니스 처리 로직(Model)과 사용자단(View)가 서로의 존재를 알지 못하게 하며 그 중간에 Controller를 두어 마치 두 사이를 연결하는 중개자로 사용할 수 있습니다.

 

그림의 맨 왼쪽을 살펴보면 클라이언트 측에서 요청이 Controller로 들어왔을 때, Controller는 요청에 맞게 Model(Service Layer)에 넘겨줘 필요한 작업을 수행하도록 합니다. 이후, 특정 반환 값이 필요하다면 반대로 Model → Controller → View 순으로 클라이언트 측으로 데이터가 흘러가게 됩니다. 이렇게 Model과 View를 분리하게 되면, 서로의 결합도를 낮출 수 있고 독립적으로 개발이 가능한 환경을 만들 수 있습니다.

 

그런데 이때 그림을 자세히 살펴보면 각 계층별로 데이터를 주고받을 때 DTO가 사용되는 것을 확인할 수 있습니다. 해당 위치에서 DTO를 사용하는 이유는 간단합니다. Domain 객체를 서로 주고받을 수 있지만, 민감한 정보를 포함할 수 있으며 주요 비즈니스 로직이 노출될 수 있으므로 DTO를 사용해 Model과 View 사이에 의존성을 낮춤으로써 해당 문제를 보완할 수 있습니다. 즉, Domain을 직접 접근하지 않음으로써 데이터를 보호할 수 있게 됩니다.

 

사용 예

User.java

public class User {

    private String id;
    private String name;
    private String password; // 민감한 도메인 정보
    private List<Role> roles;

    public User(final String name, final String password, final List<Role> roles) {
        this.name = name;
        this.password = password;
        this.roles = roles;
    }

    // Getter 생략
}

Role.java

public class Role {

    private String id;
    private String name;

		public Role(final String name) {
			this.name = name;
		}

    // Getter 생략
}

UserDto.java

public class UserDto {

    public final Long id;
    public final String name;
    public final List<String> roles;

    // 생성자 생략

    public static UserDto from(final User user) {
        return new UserDto(user.getId(), user.getName(), user.getRoles());
    }
}

UserController.java

@GetMapping
public ResponseEntity<UserDto> showUsers(@PathVariable final Long id) {
    final User user = userService.findById(id);
    return ResponseEntity.ok().body(UserDto.from(user));
}

이런 식으로 DTO를 생성하면 두 가지 이점을 얻을 수 있습니다.

→ 민감한 정보(password)에 대한 정보를 DTO로 캡슐화할 때 포함하지 않도록 하여 UI 화면에서 보여줄 데이터를 선택적으로 보낼 수 있다.

→ User에 대한 전반적인 정보를 보내줄 때, Role에 대한 정보도 함께 보내줄 수 있다.

이러한 DTO의 장점에 의해 이번 Spring 체스 미션에서 DTO를 사용했었는데, 다음과 같은 피드백을 받게 되었습니다.

dto로의 변환은 service의 역할일까요 controller의 역할일까요?
썬이 생각하는 service와 controller의 역할과 책임은 무엇인가요?

그저 Domain을 숨기기 위해서 사용했었는데, 이 얘기를 듣고 생각해보니 DTO가 어디서 변환되어야 하지?라는 의문이 생기게 되었고 이에 대해 알아보기로 했습니다.

DTO, 언제 변환해야 할까?

DTO를 언제 변환하지?라는 고민에 대한 답을 내리기 전에 우선 Controller와 Service에 대한 역할과 책임에 대해 알아보도록 하겠습니다.

Controller의 역할과 책임

  • 클라이언트의 요청에 따라 어떤 비즈니스 메서드를 호출해야 할지 결정
  • 기본적인 요청에 대한 검증 (null, empty, header check 등)

Service의 역할과 책임

  • DB에 찾고자 하는 데이터가 존재하는지 등에 대한 검증
  • 실질적인 비즈니스 로직 수행 (Transaction에 대한 처리)

즉, Controller는 요청/응답에 대한 처리만 하고 Service는 핵심 로직에 대한 처리를 해야 합니다.

 

지금까지 두 레이어의 역할과 책임에 대해 잠깐 살펴봤는데,
결론부터 얘기하자면 글을 작성한 시점에 저는 DTO를 Controller에서 변환했고, 이유는 다음과 같습니다.

 

Controller에서 DTO로 변환한 이유

우선 저는 각 레이어의 역할과 책임에 의해 Controller에서 DTO로 변환했습니다. Controller에서 DTO로 변환할 경우, Controller가 View와 통신하는 목적에 맞는 책임을 부여받을 수 있습니다. 만약, 여러 종류의 Controller가 존재한다고 가정했을 때, 같은 비즈니스 로직을 불러와도 Service에서 동일한 Domain을 반환해주기 때문에 Controller에서 목적에 부합하는 DTO로 변환해줄 수 있다는 장점이 있습니다.

 

만약, Service에서 DTO로 변환한다면 핵심 로직만을 포함해야 하는 Service Layer가 여러 DTO로의 변환에 의해 자칫 무거워질 수 있습니다. 또한, Service가 또 다른 Service를 내포할 경우가 생긴다면 여러 개의 Controller에서 Service의 메서드를 호출할 경우, Domain을 반환하지 않았기 때문에 공통적으로 사용할 수 없게 됩니다. Domain의 데이터 가진 DTO를 반환할 수도 있지만, 그렇게 된다면 또 다른 DTO로 변환해야 하고 이는 결국 Domain 그 자체를 반환한 것과 별반 차이가 없게 됩니다.

 

이외에도 DTO로의 변환을 edge 즉, 가장 앞단에서 처리한다는 장점이 있습니다. Service 및 Business 로직을 사용자에 관계없이 독립적으로 유지하기 위해서는 API 수준에서 유지하는 것이 더 좋은데, 이는 해당 서비스를 사용하려는 사용자가 두 명 이상일 경우 유용할 것입니다.

Controller에서 DTO로 변환할 때에 대한 단점

물론 Controller에서 변환할 경우의 단점도 존재합니다.

우선 Domain이 Controller 단까지 넘어올 경우 Controller에서 Domain 의존성이 높아질 수 있습니다.

또한, 요청/응답에 대한 처리만 해야 하는 Controller에서 DTO로의 변환하는 로직을 지니게 됩니다.

 

결론

각각의 장단점이 있지만, 현재 제가 생각하는 Controller에서 변환했을 때에 대한 장점이 더 크다고 생각해 해당 방식을 이용하려고 합니다. 설계 또는 관점에 따라 다르겠지만, 변환에 대한 로직을 Controller가 갖게 되는 것은 어쩔 수 없는 Trade-off라고 생각합니다.

 

이에 대한 논쟁은 아직까지도 현재 진행형이며 개발자마다 다른 의견이 있을 수 있습니다.

 

DTO 패턴을 제시한 P of EAA의 저자 마틴 파울러도 그의 블로그(?)에서 이에 대해 언급한 적이 있는데, 그는 DTO가 Service Layer에서 변환되는 것에 반대한다는 글을 작성한 적이 있습니다. 이에 대해 더 궁금하신 분들은 (2004년에 작성되긴 했지만) 여기에서 찾아볼 수 있습니다.


참고

https://tecoble.techcourse.co.kr/post/2021-04-25-dto-layer-scope/

https://stackoverflow.com/questions/21554977/should-services-always-return-dtos-or-can-they-also-return-domain-models

https://www.baeldung.com/java-dto-pattern

댓글