Backend Developer

목표를 위해 시스템을, 시스템을 위해 회고를

DTO에 대한 고찰


지금까지 저는 개발을 할때 컨트롤러에 요청이 오면 DTO로 매핑한 뒤 파라미터 검증 후, 해당 DTO를 다시 서비스에 넘기는 방법으로 진행해왔습니다.

@PostMapping("/signup")
public ResponseEntity signUp(@Validated @RequestBody SignUpReq signUpUser) {

  ...

  SignUpRes res = userService.signUp(signUpUser);

  ...
}

왜 컨트롤러는 클라이언트와 데이터를 주고받을때 Entity를 안쓰고 DTO 를 통해 데이터를 받을까?

해당 질문에 앞서 우선 DTO가 무엇일까?

DTO는 Data Transfer Object의 약자로, 계층 간 데이터 교환 역할을 한다.
계층간 데이터 교환이 이루어 질 수 있도록 하는 객체이기 때문에 특별한 로직을 가지지 않는 순수한 데이터 객체이다.

다시 원래 질문으로 돌아와서, 간단한 프로젝트의 경우 Entity를 이용해서 바로 파라미터를 받을 수 있지만, 시간이 지나며 규모가 커지면 여러가지 문제들이 발생할 수 있습니다.

여러 API에서 동일한 Entity를 사용하는 경우가 있는데 이럴 경우 DB스키마와 별도의 데이터들이 많이 넘어올 수 있습니다. 예를들어 비밀번호를 변경할 때 newPassword라는 필드로 데이터가 넘어오는 경우도 있는데 DB 스키마에서는 newPassword가 필요없어 Entity에 불필요한 필드를 추가하는 문제가 발생하죠.

또한 각각의 API 마다 다른 유효성 검사가 필요한 경우가 많습니다. 이럴 경우 각각의 유효성 검사를 위해 부가적인 유효성 검사 로직이 추가된다면 Entity가 무거워지고 복잡해지며 가독성이 떨어질 것입니다. 또한 Entity의 필드명이 변경되는 경우 API 스펙이 변경하게 되는 경우가 있으며 이럴경우 기존 클라이언트에게 문제가 발생되어 수정이 필요하게 됩니다.

그래서 컨트롤러가 클라이언트와 데이터를 주고받을 때 DTO를 사용하는 이유를 정리하면 아래와 같다고 볼 수 있습니다.

  • 불필요한 필드 및 로직을 Entity로부터 분리할 수 있다.
  • Entity가 변경되어도 API 스펙이 변하지 않는다.
  • 요청으로 넘어오는 파라미터가 직관적으로 확인이 가능하며, API 유연성을 확보할 수 있다.

그렇다면 컨트롤러와 서비스 간의 데이터 는 어떻게 해야할까?

컨트롤러와 서비스간의 데이터를 주고받을때도 DTO를 사용하는 것이 좋다 고 생각합니다. DTO가 좋다고 생각하는 이유는 아래와 같습니다.

  • 복잡한 어플리케이션의 경우 Entity를 구성하기 위해 여러 Service에 의존하게 되거나 비즈니스 로직이 포함될 수 있다. 컨트롤러가 전달받은 DTO만으로 Entity를 구성하기 어려울 수 있어 부수적인 정보를 조회하여 Entity를 구성해야 될 수 있다. 이럴 경우 여러 서비스를 의존하는 것보다 DTO를 서비스에 넘겨주어 서비스에서 Entity를 구성하는 것이 더 좋을 것으로 생각된다.
  • 컨트롤러에서 도메인 객체를 의존하게 되어 도메인의 변경이 컨트롤러의 변경을 촉발하여 유지보수의 문제로 이어질 수 있다.
  • Entity가 불완전한 경우가 존재한다. 생성 요청의 경우 id 값이 없을 수 있어 불완전한 Entity 객체일 수 있다.
  • DTO는 계층 간 통신을 목적으로 나온 것이다.
  • 클라이언트에게 반환할 필요가 없는 데이터까지 도메인 객체에 포함되어 컨트롤러에 넘어오는 문제가 있다.

하지만 제 생각일 뿐이여서 제가 놓친 부분이 있지 않을까하여 구글링을 한 결과 컨트롤러에서 DTO를 Entity로 변환해서 서비스에 넘겨주는 이야기를 많이 보았습니다. 제 생각에는 Entity를 컨트롤러에서 사용하는 것에 대한 불안한 점들이 보여서 고민하다 혹시 제가 놓친부분이 있지 않을까 해서 주변 개발자분들에게 조언을 구했습니다.

주변 동료 개발자분들과 얘기를 해보았는데 한가지 놓친 부분이 있었습니다. 바로 동일한 DTO를 서비스까지 사용하면 컨트롤러와 서비스 간 의존이 강해져 위험하다 는 피드백을 받았습니다… (충격) 이렇게 중요한 것을 놓치다니…

현재 동일한 DTO를 서비스까지 사용하다보니 클라이언트와 컨트롤러 간의 데이터 통신 파라미터가 변경 될 경우 그 영향이 서비스까지 미치게 되는 문제가 발생합니다. 또한 다른 컨트롤러 혹은 다른 API에서 해당 서비스를 사용할 수 없는 문제가 발생됩니다.

그럼 컨트롤러와 서비스 간의 데이터는 Entity를 사용하는 것이 정답일까?

정답은 없다고 생각됩니다. Entity를 사용하게 되면 서비스와 컨트롤러의 의존성이 요청때 받은 DTO를 사용하는 방법보다 약해져 동일한 DTO를 사용하는 것보다는 Entity를 사용하는 것이 좋다고 생각합니다.

하지만, 저는 컨트롤러에서 Entity를 사용시 발생될 리스트들이 있어 별도의 서비스 DTO를 만들어서 컨트롤러와 서비스 간의 통신을 하는 방향 으로 결정했습니다.

물론 이를 위해서는 DTO를 만들어주거나 DTO간의 변환하는 추가적인 비용이 들지만, 추후 컨트롤러와 서비스에서 원하는 값이 달라졌을 때 유지보수하는 비용을 생각하면 저렴한 비용이라고 생각합니다. 그리고 컨트롤러에서 받은 requestDTO 를 serviceDTO로 변환은 MapStruct 혹은 ModelMapper 와 같은 Object Mapping 라이브러리를 활용하면 쉽게 변환이 되며 동시에 DTO 와 Entity 간의 변환 과정도 간소화할 수 있는 장점이 생겨 여러 이점이 많이 생긴다고 생각되어집니다.

기존에 DTO <-> Entity 변환 과정때 DTO 내부에 toEntity() 와 같은 별도의 메소드를 통해 변환해주었는데 최근 MapStruct 로 바꾸었는데 많은 이점들이 보았고 생산성 향상에 효과를 많이 보아서 Object Mapping에 대한 포스트도 추후 올리겠습니다.

결론

저의 개인적인 의견으로 결론을 내는 것이라서 정답은 아닙니다… 부족한 부분들도 있기때문에 감안해서 읽어주세요..

DTO의 필요성은 대부분 공감하실 것이라 생각됩니다. DTO를 어디까지 사용해야될까에 대한 부분에 대한 저의 생각은 클라이언트 <-> 컨트롤러컨트롤러 <-> 서비스 에 사용하면 좋다고 생각합니다.

클라이언트와 컨트롤러 간의 통신은 DTO를 사용하는 것에 대해서는 대부분의 분들이 이견 없이 공감해 주실 것 같습니다. 문제는 컨트롤러와 서비스 간의 통신인데 저는 의존성 때문에 되도록이면 컨트롤러에 도메인 객체를 의존하거나 많은 서비스를 의존하는 것을 지양하려고 합니다.

서비스를 운영하다보면 비즈니스 로직은 항상 변하고, 도메인 객체도 변하게 됩니다. 이때 컨트롤러에서 도메인 객체를 의존하거나 많은 서비스를 의존하게 되면 서비스 혹은 도메인 객체가 수정되면서 컨트롤러도 같이 수정하게 되는 문제가 발생될 여지가 있어 유지보수 비용이 많이 상승될 것으로 보여집니다.

그래서 DTO를 RequestDTO(클라이언트 <-> 컨트롤러)ServiceDTO(컨트롤러 <-> 서비스)를 통해 컨트롤러와 서비스의 결합과 컨트롤러와 도메인 객체의 결합을 느슨하게 하는 것이 더 좋다고 생각되어집니다.