서비스 계층에서 어떤 타입의 객체를 반환해야 할지 따지려면, 서비스 계층 내에서의 의존을 허용할 것인가를 먼저 정의해야 될 것 같다.
서비스 계층 내에서의 의존을 허용하는지 여부에 따라서 다음 두 가지로 구분할 수 있을 것이다.
OrderService
가 존재하고, 외부 서비스에서 주문 관련 기능이 수행되어야 한다고 가정하자.
OrderRepository
을 참조한다.OrderService
를 참조한다.서비스간의 참조는 없고, 한 서비스 클래스에서 여러 Repository를 참조하는 구조가 될 것이다.
만약 외부 서비스에서 주문이 이뤄진다면, 주문 생성과 관련된 비즈니스 규칙을 모두 검증하는 과정이 외부 서비스에 필요할 것이다. OrderService
에 해당 로직이 이미 존재하지만, 서비스는 다른 서비스를 참조하지 않기 때문에 중복코드를 작성하게 되는 것이다.
이는 곧 응집도가 낮아짐을 의미하기도 한다.
위의 예시처럼 외부 서비스에서 주문 관련 로직을 구현하고 있다면, 주문 관련 로직은 한 곳에 모여 있지 않고 여기저기 흩어져 있는 상태가 될 것이다. 만약 주문 도메인 규칙이 변경된다면, 이를 구현하고 있는 모든 서비스 클래스를 수정해야 될 것이다.
💡 복잡한 비즈니스 규칙이 없는, DB 내용을 단순 조회하는 로직이라면?
주문 생성 같은 경우, 비즈니스 규칙이 많이 개입되는 복잡한 로직이다. 그렇다면 단순 조회 작업은 어떨까?
이때는 Repository를 참조해도 괜찮다고 생각한다. 단순 조회이기 때문에 어차피 OrderService
에서도 Repository를 단순 호출하는 구조일 것이다. 이는 오히려 서비스간의 순환 참조를 방지할 수 있는 좋은 방법일 것이다.
위의 예시로 돌아와서, 외부 서비스에서 주문을 생성한다면 OrderService
의 주문 생성 메서드를 호출할 것이다. 관련 비즈니스 로직은 이미 구현되어 있기 때문에, 호출만 하면 된다. 즉, 서비스 클래스를 재사용할 수 있다.
Repository를 참조할 때 생긴 2가지 문제를 해결할 수 있다.
OrderService
를 통해서 이뤄진다 → 응집도 높아짐이렇게 서비스간의 의존 관계를 가지면서 주의해야 할 점은, 더이상 서비스 계층의 클라이언트는 표현 계층만 존재하지 않는다는 것이다.
(순환 참조 관련 문제도 있지만, 이에 대해 중점적으로 다루지는 않는다.)
이와 관련하여, 서비스가 어떤 타입의 객체를 반환하면 좋을지 생각해보자.
서비스 계층 내에서 서비스 클래스를 의존하지 않고, Repository만 의존하는 상황을 생각해보자.
Layered Architecture의 단방향 의존이 잘 지켜진다는 가정 하에, 서비스 계층의 클라이언트는 오직 표현 계층으로 한정된다.
그렇기 때문에 표현 계층이 원하는 형태의 반환값, 즉, DTO를 서비스 계층에서 만들어서 반환해도 괜찮았을 것이다.
(여기서의 DTO는 컨트롤러가 뷰에 응답할 ViewModel일 수도 있고, 도메인 객체를 감싼 DTO일 수도 있다.)
만약 서비스간 의존을 하는 상황이라면? B 서비스
는 DTO를 반환하고, A 서비스
가 B 서비스
를 의존한다고 가정하자.
A 서비스
는 B 서비스
의 메서드를 호출하고, DTO를 반환받는다.A 서비스
는 로직을 수행하기 위해 DTO를 도메인 객체로 변환하는 과정이 필요하다.B 서비스
를 의존하는 모든 서비스 클래스가 변환 로직이 필요하다 → 중복 코드 발생DTO를 반환하기 위해 필요한 과정을 각 클래스 입장에서 보면, 다음과 같이 정리할 수 있다.
B 서비스
: 도메인 객체 → DTO 변환A 서비스
: B 서비스
로부터 반환받은 DTO → 도메인 객체로의 변환DTO를 관리하는 과정에서 상당한 비용이 발생한다고 볼 수 있다.
그렇다면, 서비스가 DTO를 반환하지 않고 도메인 객체를 반환하도록 해보자.
DTO ↔ 엔티티 변환과정이 사라지지만, 도메인 객체가 서비스를 의존하는 표현 계층까지 자연스럽게 전달될 것이다. 이로 인해 생기는 문제는 다음과 같다.
비즈니스 로직을 담고 있는 도메인 객체가 전달됨으로써, 표현 계층에서 비즈니스 로직이 호출될 수 있다. 이는 예상치 못한 결과를 초래한다.
즉, 도메인이 보호받지 못하고, 이를 오용할 여지가 생기는 것이다. 근데 .. 지금 시점에서 나의 생각은 다음과 같다.
1. 컨트롤러가 도메인 객체를 알고 있는 것은 의존 방향에 어긋나지 않는다. 도메인 객체를 View 요구사항에 맞게 변환하는 것은 컨트롤러의 책임으로 봐도 합리적이다.
2. 과연 이러한 일이 쉽게 일어날 수 있을지 의문이 생긴다.
각 계층의 역할에 대해 이해하고 있는 구성원과 함께 작업한다면, 도메인 객체를 오용하는 상황이 쉽게 찾아오지 않을 것 같다. 최악의 경우를 고려하면 충분히 일어날 수 있는 일이지만, 개발의 편의성을 내려놓으면서까지 경계해야 할지는 모르겠다.
(현실에서 보기 힘든 최악의 동료 개발자를 상상하며 쉐도우 복싱을 한다는 느낌을 받았다.)
하지만 DTO를 사용하는 데에도 비용이 발생한다는 것은 누구나 알고 있다. 그럼에도 사용하는 데에는 이유가 있을 것이다.
결국 DTO 관리 비용을 감수하면서도 DTO를 사용할 가치가 있는지를 따져봐야 한다.
DTO의 필요성에 대해 작성한 아티클을 참고해보니, 프로젝트의 주기가 길거나, 팀의 규모가 크다면 DTO를 쓰는 것이 바람직하다고 말한다.
그렇지만 나는 아직 큰 규모의 프로젝트를 경험해보지 않았고, 막 와닿지 않는다. 내가 납득할 수 있는 DTO의 필요성은 도메인 객체를 흘려보내지 않을 수 있다는 것이다. 근데 이 마저도 개발 비용과 비교했을 때에는 의문이 생긴다.
‘도메인 객체를 흘려 보내지 않기 위해 DTO로 전달해야 돼!!’ vs ‘오용될 가능성이 적은데, 관리 비용을 들이면서까지 DTO를 써야 돼?’
너무나도 뻔한 말이지만 두 생각 사이에서 딱 답을 내리기 보다는, 프로젝트 규모와 팀 컨벤션 그리고 도메인에 따라 선택할 수 있는 기준을 만드는 것이 중요하다고 생각한다.
http://guntherpopp.blogspot.com/2010/09/to-dto-or-not-to-dto.html