본문 바로가기
프로젝트/텀블벅 클론 코딩

[고민] DTO - Object의 변환은 어느 계층에서 이루어 져야 하는가?

by zangsu_ 2023. 5. 18.

현재 진행 중인 텀블벅 클론 코딩의 백엔드 개발은 MVC 패턴을 이용하여 진행되고 있다. 

(물론, View의 부분은 FE의 담당이므로 구현하지 않는다.)

 

이 때, DB에 접근하는 로직과 FE의 요청을 처리하는 로직 등의 의존성을 제거하고 관심사를 분리하기 위해 레이어드 아키텍쳐 패턴을 사용하고 있다. 즉, 하나의 주제에 대해 DB 접근을 위한 Repository, 핵심 비즈니스 로직인 Service, FE의 요청을 처리하고 HTTP 메시지를 생성하여 전송하는 Controller를 개별적으로 구현하고 있다. 계층간의 관계는 Repository - Service - Controller로 구성이 된다.

 

이렇게 계층을 분리하고 나면, 각 계층에서 필요한 데이터의 성질이 조금 달라진다. 예를 들면, DB에 접근 하는 Repository에서는 실제 데이터의 변경 상황을 제외하고는 데이터가 변경되는 일이 거의 없다. 반면, Controller 입장에서는 데이터의 변경이 빈번하게 이루어 진다. 또한, Controller가 사용하는 객체에는 사용자의 비밀번호와 같은 민감한 정보를 포함하는 것은 위험하지만, 비즈니스 로직의 구현을 위해 사용하는 객체에는 비밀번호의 데이터가 포함되어 있어야 한다. 이 때문에 실제 비즈니스 로직에 사용되고, DB에 저장되는 객체와 데이터 전송에 사용되는 객체를 분리하는 것이 바람직하다. 이 때 데이터 전송에 사용하게 되는 객체를 DTO(Data Transfer Object)라 한다.

 

나의 고민은 여기서 시작되었다.

 

DB에 실질적으로 접근하는 Repository는 Entity 객체를 받아오게 된다. 그리고, Controller가 데이터를 전송하는 시점에서 서버는 DTO 객체를 사용해야 한다. 그렇다면 구현되는 특정 계층은 Entity 객체가 DTO 객체로 변환되는 과정을 포함하고 있어야 한다. 이 과정은 어느 계층에서 이루어 지는 것이 올바른 방법일까?

 

일단, Repository는 실질적으로 DB에 접근하는 계층이므로 Entity 객체를 사용해야 하고, 데이터의 전송은 관심사가 아니기 때문에 Repository는 후보군에서 제외 시켰다.

그리고, 나는 다음과 같은 이유로 Controller 계층에서 DTO 변환 로직을 포함 시켰다.

  1. 실질적으로 데이터 전송이라는 관심사에 해당하는 계층은 Controller 이다.
  2. Service 계층에서 DTO를 변환하게 되면 다양한 Controller에서 해당 Service 클래스를 사용하기 어렵다.
  3. Controller 계층과 Service 계층의 결합도가 높아지게 된다.

좀 더 설명하자면, 다음과 같은 예시를 들 수 있다. 사용자의 정보를 관리하기 위한 UserService와 UserController 클래스가 각각 구현되어 있다고 가정하자. 이 때 DTO와 Entity의 변환 과정은 Service 계층에서 핵심 비즈니스 로직과 함께 구현되어 있으며, Controller에서는 단순히 요청에 대한 HTTP 메시지를 생성하는 역할을 담당하고 있다. UserService에서 DTO 객체가 Entity로 변환되기 때문에 UserController는 요청받은 데이터를 그대로 DTO 객체로 생성 후 UserService에 넘겨주면 된다.

이 때 상품 할인을 위한 DiscountService와 DiscountController를 새로 구현한다고 가정하자. 그리고, 상품 할인율은 회원 등급에 따라 달라지기 때문에 Discount 관련 로직들 역시 User 정보를 사용해야 한다고 가정하자. 이 때, DiscountService는 User 정보를 DTO 객체로 전달 받는다. 때문에 DiscountService 내부에는 이미 UserService에서 구현되어 있는 UserDTO - UserEntity 변환 로직이 중복해서 구현되어야 한다. 또한, 이는 메인 비즈니스 로직과 관련 있는 관심사가 아니기 때문에 Service 계층과 Controller 계층의 결합도 역시 높아지게 된다.

또, 만약 DiscountController에서 UserService의 특정 로직을 사용해야 한다면 DiscountController 계층은 UserService에서 입력을 받고자 하는 UserDTO 객체 형식을 강제해야 하며, 이는 유연한 서비스 구현에 걸림돌이 된다. 그렇지 않다면 Controller 계층은 입력받은 DTO 객체를 Service 계층이 요구하는 DTO로 변환하는 과정을 포함해야 하며, 이는 필요 없는 오버헤드가 발생하는 것이다. 즉, Controller 계층에서 Service 계층에서 요구하는 DTO 형식을 알고 있어야 하기 때문에 모듈화가 잘 이루어 지지 않는 다고 생각했다.

 

반대로, Controller 계층에서 DTO - Entity를 변환한다고 가정하자.

Controller는 다양한 형태의 DTO를 입력을 받고, 일정한 형식의 Entity를 만들어 주기만 하면 되므로 Service 계층과의 결합도가 현저히 떨어지게 된다. 또한, Service 입장에서는 항상 같은 형식의 Entity 입력을 보장받을 수 있으므로 Service 클래스의 재사용성이 높아진다.

 

그러나, 함께 백엔드 개발을 담당하고 있는 팀원은 DTO 변환 로직을 Service 계층에 포함시켰다.

그리고 그 이유로 하나의 DTO 만으로 완전한 Entity를 만들 지 못하는 경우를 설명했다.

 

우리의 서비스는 판매 항목인 Project 객체가 존재하며, Project 객체는 개별 판매 옵션인 Product를 포함한다. 그리고 Product 객체는 구성품인 Component를 포함한다. 즉, 하나의 Project Entity를 만들기 위해 3개의 다른 DTO를 결합해야 하며, 이는 그 자체로 비즈니스 로직으로 볼 수 있다. 또한, HTTP 요청 메시지에서는 하나의 완전한 Project DTO가 입력 되며, 이 DTO를 분리하여 각기 다른 Entity 3개를 생성해야 하는데, 이 과정을 Project Controller에서 모두 수행하는 것 보다 각각의 Service 계층에서 수행하는 것이 관심사 분리에 훨씬 알맞을 것 같다는 내용이었다.

 

결과적으로, 이번 프로젝트에서는 Service 계층에서 DTO의 변환을 수행하기로 하였다.

두 방법 모두 각자의 장단점이 있으나, User Entity는 두 방법 모두 큰 오버헤드가 존재하지 않는 반면, Project Entity에서는 Controller 계층에서 DTO를 변환하는 로직을 포함하는 것 만으로 웹 전송 계층이 필요 이상으로 무거워 지는 것이 그 이유였다. 

예비군을 다녀오는 동안 팀원의 개발이 엄청 진행된 것 역시 무시할 수 없었다...

 

결국, 각 서비스에 따라 더 적합한 구현 방법이 있을 뿐 정답은 없는 것 같다.

그리고, 내가 찾게 된 나만의 기준은 다음과 같다.

  1. 원칙적으로 DTO-Entity 변환은 Controller 에서 수행한다.
    1. 어쨌든, DTO는 말 그대로 웹 전송을 위한 객체이며 DTO의 역할은 Controller에서 끝나는 것이 올바르다.
    2. 설계에 따라 특정 Service를 여러 Controller가 사용할 수 있다.
  2. DTO의 변환 과정이 복잡한 등의 특정 이유가 있다면 Service 계층에서 변환을 수행한다.
    1. 위와 같은 이유를 설계 단계에서 발견하지 못했다면 Controller 계층에서 DTO를 변환한다.
    2. 이후 해당 상황을 발견하게 되면 리팩토링을 진행한다.

이 고민을 비슷하게 한 좋은 블로그 글이 있어 아래에 링크해 두겠다.

 

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

 

DTO의 사용 범위에 대하여

1. DTO란? DTO(Data Transfer Object)란 계층간 데이터 교환을 위해 사용하는 객체(Java Beans)입니다. 간략하게 DTO의 구체적인 용례 및 필요성을 MVC 패턴을 통해 알아볼까요? 🚀 1.1. MVC 패턴 MVC…

tecoble.techcourse.co.kr

댓글