프로젝트를 진행하면서 DTO를 어떻게 관리하면 좋을지 고민되었다. 보통 역할에 맞게 구분해서 사용하지만 프로젝트 크기가 커질수록 관리해야할 DTO가 많아지는 것이 부담되어, 줄일 수 있는 방법을 알아보았다. 이번 글을 통해 DTO 관리 방법을 선택하기까지 어떤 부분을 고민했는지 공유해보려고 한다.
DTO는 계층 간 데이터 전송을 위해 사용되는 객체로 특별한 비즈니스 로직은 포함하지 않는다.
계층간 전송에 도메인 모델이 아닌 DTO를 활용하는 이유는 클라이언트에게 필요한 정보만을 제공하기 위해서다. 도메인 모델은 해당 도메인의 모든 정보를 가지기 때문에 클라이언트가 불필요한 정보에 의존하게 될 수 있다. 따라서 역할에 맞는 DTO를 사용해 계층간 결합도를 낮추고, 특정 계층의 변화가 다른 계층에 영향을 주지 않도록 해야한다.
간단한 CRUD 기능에 대해서, 다음과 같이 DTO를 만들 수 있다.
기능과 요청, 응답 여부에 따라 클래스를 구분했다. 이렇게 구분된 DTO는 명확한 역할을 가지지만, 한 가지 아쉬운 부분이 있다. 하나의 도메인 모델에 벌써 8개의 DTO가 생성되었다. 또, 기능이 추가될 때마다 2개 이상의 DTO가 만들어진다. 역할을 유지하면서 DTO를 줄일 수 있는 방법은 없을까?
요청과 응답을 분리하지 않고, 중첩 클래스를 활용해서 기능 단위로 묶어보자.
요청과 응답이 구조적으로 결합되었지만, 논리적으로는 명확하게 분리되어 있다. 이 방법을 이용하면 DTO 수를 반으로 줄일 수 있다. 하지만 구조적으로 결합되는 것이 조금 찝찝하다.
논리적으로 요청과 응답이 분리되어 있다고 해도 외부 클래스 입장에서는 두 가지 책임을 가지게 되는 것이 아닐까? 그렇다면 단일 책임의 원칙(SRP)을 위배하기 때문에 좋은 설계라고 할 수 없다. 웹 계층의 스펙 변화로 DTO에 수정 사항이 생긴다면 외부 클래스는 요청과 응답, 두 가지 이유로 변경될 수 있다. 결국 변경 주기가 다른 두 가지 책임을 가지기 때문에 중첩 클래스를 활용하는 방식을 지양하는 것이 좋다.
👉 inner class가 아닌 static nested class를 사용하는 이유
inner class는 외부 클래스의 인스턴스도 함께 생성되지만, static nested class는 외부 클래스의 인스턴스가 생성되지 않는다!
"지양하는 것이 좋다"는 어디까지나 두 가지 이상의 책임을 가진다는 전제하에 유효하다. 그 전에 정말 두 가지 책임을 가지는 것이 맞는지 확인부터 해야한다. 응답과 요청을 각각 하나의 책임으로 볼 수도 있지만, 기능을 하나의 책임으로 볼 수 있지 않을까? 결국 책임이라는 것은 기준에 따라 달라질 수 있다. 만약 기능을 하나의 책임으로 본다면 중첩 클래스를 사용해서 DTO를 관리하는 것도 괜찮은 선택이 될 수 있다.
그렇다면 어떤 기준으로 책임을 구분할 수 있을까? 여러 가지 기준들이 있겠지만, 변경 주기를 고려하는 것이 가장 명확하다고 생각한다. 요청과 응답의 변경 주기가 다르다면 다른 책임, 반대로 변경 주기가 같거나 혹은 거의 변경될 가능성이 없다면 하나의 책임으로 봐도 괜찮다. 이렇게 애플리케이션 상황에 맞게 책임을 구분하면 적절한 방식을 선택할 수 있다.
객체 지향적인 관점으로만 접근했지만, 이보다 먼저 고려해야 하는 것은 팀원들의 의사와 일관성이다. 이런 저런 이유들로 설명했지만, 어떤 방식을 선택해도 DTO가 시스템에 크게 영향을 미치진 않는다. 따라서 어떤 방식을 적용할 것인지 충분히 논의하고, 결정된 방식을 일관성있게 적용하는 것이 중요하다.
정답이 있는 문제는 아니다. 개선하고 싶은 부분을 찾고, 방법을 적용하기 까지의 고민하는 과정이 중요하다고 생각한다. 대규모 트래픽을 수용할 수 있는 API를 설계하는 것이 현재 프로젝트의 주 목표이기 때문에 운영 또는 프론트와 상호작용하는 과정이 없다. 따라서 웹 계층의 스펙이 변경될 가능성이 거의 없다고 판단했고, 중첩 클래스를 활용해서 DTO를 관리하는 방식을 선택했다. 마지막으로 DTO에 대해 간단하게 정리해보자.