https://github.com/HomoEfficio/dev-tips/blob/master/DTO와%20Bean%20Validation.md 에 이어지는 글이라고 봐도 좋을 것 같다.
먼저 이 글에 사용된 DTO 라는 용어는 흔히 통용되고 있긴 하지만 이런 문맥에서는 잘못 사용되고 있다는 점을 밝혀둔다. 왜 적절하지 않은지는 맨 아래에 기술한다.
제목이나 글 자체를 수정하지 않은 이유는 흔히 통용되고 있으니 검색도 DTO 로 할 것이고, 그렇게 들어온 사람들에게 올바른 정보를 제공할 기회를 놓치고 싶지 않아서다(라고 썼지만 이런 걸 낚시라고도.. 하지만 개인적인 욕심은 없으니 이타적인 선한 낚시라고 하자 =3=3)
DTO(Data Transfer Object)가 왜 필요한지 또는 어떤 역할을 하는지는 앞의 링크 글에 잘 나와 있는데 요약하면 다음과 같다.
DTO = Domain Information + View Information
DTO가 View Information을 포함하고 View와 맞닿아 의사소통하는 덕분에, Domain 객체는 View에 구애받지 않고, 순수하게 도메인 로직만을 담당하는 객체로 살아갈 수 있다.
하지만 View Information이 별로 필요하지 않다면, 굳이 DTO을 일부러 따로 만들 필요 없이 Domain 객체만 사용해도 괜찮다. 어차피 View는 DTO인지 Domain 객체인지 알지도 못하고 알 필요도 없다. 그저 View를 그리는데 필요한 정보만 모두 담겨있다면 무엇이든 상관없다.
Domain 객체만으로 모두 커버할 수 있다면 가장 단순하고 우아한 해법일 것이다. 하지만 View는 기대만큼 단순하지 않기 마련이다. 그래서 View Information이 필요하고 결국 DTO가 필요한 경우가 많다.
DTO가 있어야 하는 상황이라면, 프론트엔드의 View에는 결국 DTO가 전달되고, 백엔드의 데이터 저장소에는 결국 Domain 객체가 전달된다. 따라서 중간의 어딘가에서는 DTO와 Domain 객체가 서로 변환되는 지점이 있어야 한다. 그게 어디일까?
처음에는 컨트롤러냐 서비스냐가 고민이었다. 컨트롤러 메서드의 인자로 DTO가 사용되는 점을 감안하면 컨트롤러에 변환 로직을 두는 게 나을 것 같다. 하지만, DTO를 Domain 객체로 만들 때는 Repository를 통한 조회가 필요할 때가 종종 있고, 이 때는 컨트롤러에서 처리할 수가 없다. 그래서 서비스에서 DTO - Domain 객체 변환을 담당하게 했다.
좀 지나서는 별도로 Converter로 빼는 게 좋을 것 같아서 별도의 Converter 계층을 두고 변환 로직을 모두 Converter에 담고, 각 Converter를 서비스에 주입해서 서비스가 DTO - Domain 객체간 변환을 Converter에게 위임해서 처리하게 했다.
더 지나고 보니 방향마다 다르게 보는 것이 낫다는 생각이 들었다.
Domain 객체 -> DTO -> View 방향의 흐름에서는 필요한 모든 Domain 객체를 서비스에서 Repository를 통해 얻어올 수 있고, DTO에 전달할 데이터를 Domain 객체가 모두 가지고 있으므로, Domain 객체를 파라미터로 받는 Converter의 메서드에서 View 관련 데이터(ex SelectBox에 들어갈 특정 데이터 목록 등)를 추가하고 DTO로 변환 하는데 아무 어려움이 없다.
따라서 Converter에서는 서비스로부터 호출될 때 Domain 객체만 인자로 넘겨받는다면 독립적으로 변환 처리가 가능하다.
또는 토비님이 주신 의견인데 별도의 컨버터를 두지 않고 변환 로직을 아예 DTO 안에 두고, 서비스가 Domain 객체를 DTO 안에 주입해주고 DTO를 반환하고, 컨트롤러가 DTO에서 필요한 데이터를 빼낼 때 변환 로직이 실행되는 방식도 좋아 보인다. 직접적인 변환은 컨트롤러가 호출할 때 실행되기는 하지만 DTO를 생성하는 위치는 서비스 계층이고, Domain 객체가 컨트롤러에 반환되는 일은 없으므로, 이 경우에도 굳이 계층을 가리자면 DTO 가 생성되는 지점을 기준으로 서비스 계층으로 분류하는 것이 맞다고 생각한다.
이 방향의 DTO 변환을 컨트롤러 계층에서 한다면 다음과 같은 단점이 있다.
- 클라이언트에 반환할 필요가 없는 데이터까지 Domain 객체에 포함되어 컨트롤러 계층에 까지 넘어온다.
- 여러 Domain 객체로부터 조합되는 DTO의 경우 컨트롤러 계층에서 조합해야 하며 결국 응용 로직이 컨트롤러에 스며든다.
- 여러 Domain 객체를 조회하는 서비스를 각각 호출해야 하므로 의존하는 서비스의 갯수가 늘어날 수 있다.
반면에 이 방향의 DTO 변환을 서비스 계층에서 할 때 어떤 단점이 있는지는 잘 떠오르는 것이 없다.
하지만 View -> DTO -> Domain 객체 방향의 흐름에서는 View에서 전달받은 정보만으로 Domain 객체를 구성할 수가 없다. 쉽게 말해 View에서는 서버에 ID만 전달하기도 하는데, ID만으로는 Domain 객체를 구성할 수 없으니 ID 외의 정보를 Repository를 통해 조회한 후에나 Domain 객체를 구성할 수 있다.
따라서 Converter에서 독립적으로 처리가 불가능하고 Repository 계층에 의존하게 된다. Converter가 Repository에 직접 접근하게 하면서까지 DTO -> Domain 객체 변환을 Converter에서 처리해야하는 걸까? 아니라고 생각한다. 그러느니 DTO -> Domain 객체 변환 책임을 서비스에게 넘기는 것이 낫다고 본다.
이 방향의 DTO 변환을 컨트롤러 계층에서 하는 건 사실 상 불가능하기도 하다. 앞서 설명한 것처럼 서비스 계층에 DTO 가 아닌 도메인 객체를 넘겨줘야 하는데, 컨트롤러는 스스로 Domain 객체를 구성할 능력이 없기 때문이다.
정리하면 다음과 같다.
Domain 객체 <-> DTO 변환은 컨트롤러 계층이 아니라 서비스 계층에서 처리하는 것이 타당하다.
Domain 객체 -> DTO 의 변환은 Converter에서 담당하고, Converter를 서비스에 주입해서 서비스 계층에서 Converter를 호출해서 처리
또는 아예 DTO 내에 변환 로직을 두고 DTO가 Domain 객체를 생성자로 주입 받아서 DTO 내에서 변환, 이 경우에도 DTO가 생성되는 지점을 기준으로 서비스 계층에서 처리한다고 분류DTO -> Domain 객체의 변환은 서비스의 private 메서드에서 처리
원래 이 글의 목적은 이름이 뭐가 됐든 Domain 객체와 화면에 보여질 데이터 사이에 존재하는 중간체를 어디에서 만드는 게 적절하냐는 얘기였다. 하지만 DTO 라는 용어의 적절성도 이 참에 한 번 짚고 넘어가는 게 좋겠다.
이게 이름만의 문제는 아닌 것이, 위 글에서도 그렇게 썼지만 웹 클라이언트 -> 서버, 서버 -> 웹 클라이언트 두 가지 방향에서의 중간체를 모두 DTO 라는 한 가지 용어로 잘못 지칭하다 보니, 객체도 하나만 만들어서 두 가지 용도에 사용하다가 엄청난 고생길로 인도하는 실질적인 병폐도 있기 때문이다.
둘은 책임이 다르다. 그러니 잘못 사용되고 있는 이름과 재사용이라는 유혹 때문에 하나의 객체로 두 가지 용도에 사용하면서 고생하고 있다면 지금 즉시 두 가지로 분리하자. 이름은? 아몰랑 그냥 XXXReq, YYYRes 정도로 =3=3
지금까지 쓴 것처럼 DTO(Data Transfer Object)는 클라이언트(특히 웹 클라이언트)와 서버 간에 데이터를 주고 받을 때 사용되는 셔틀 같은 객체를 지칭하는 것으로 통용되고 있다.
DTO가 셔틀 역할을 하는 것은 맞지만, 마틴 파울러의 글에 따르면 단순한 셔틀이 아니라, 서버가 여러 번의 개별 요청을 받아서 회신해야 할 정보 중에서 함께 사용되는 것들을 DTO에 함께 담아 한 번에 회신함으로써 비싼 원격 호출 횟수를 줄이는 데 주목적이 있다. 아래 그림을 보면 더 쉽게 와닿을 것이다. DTO를 쓰지 않았다면 Album 조회, Artist 조회 이렇게 두 번 조회 요청을 날려야 하는데, 이걸 DTO에 모두 담아서 반환한다면 한 번 요청, 한 번 회신으로 끝낼 수 있다.
그림 출처: https://martinfowler.com/eaaCatalog/dataTransferObject.html
그래서 일단 웹 클라이언트가 서버로 데이터를 보내는 방향(View -> DTO -> Domain 방향)에서 셔틀 역할을 하는 객체에게 DTO 라는 용어를 사용하는 것은 적절하지 않다.
서버가 웹 클라이언트로 데이터를 보내는 방향(Domain -> DTO -> View 방향)에서는 DTO 라는 용어가 적절할 때도 있고 그렇지 않을 때도 있다. 예를 들어 화면에서 필요한 주문자 정보 일부와 주문 정보 일부, 주문 상품 정보 일부, 배송 상태 정보를 서버가 하나의 객체에 담아서 웹 클라이언트에 반환할 때는 DTO 라고 할 수 있다. 하지만 서버가 그저 회원 상세 정보를 웹 클라이언트에게 반환할 때는 DTO 라는 용어는 적절하지 않다.
이규원님이 'View에 사용될 목적의 응답 계약은 ViewModel 이란 패턴이 더 적절할 것 같다'는 의견을 주셨는데 ViewModel을 잘 몰라서 쉽게 설명을 못 하겠다. ViewModel을 알려면 MVC, MVP, PM, MVVM 등을 알아봐야 하는데, 이런 건 노력은 많이 들고 결실은 크지 않을 것 같아서 피하고자 한다. 어디에선가 쉽게 써진 자료를 우연히 발견해서 조금이라도 알게 되면 그때나 업데이트를.. ㅋㅋ
그럼 DTO 대신에 뭐라고 불러야 할까? 솔직히 모르겠다. 개인적으로는 그냥 요청 본문, 응답 본문이 정보 함유량은 좀 떨어져보이지만 가장 무난한 것 같다.