Domain모델은 3가지 종류가 있습니다. Entity, Value Object, Service가 바로 그 주인공이죠. 이번에는 service를 사용하는 방법에 대하여 이야기를 해볼까 합니다.
그 전에 레이어에 대한 간단한 이야기
미리 들어가지 전에 여기에서는 Domain Layer와 Application Layer를 나누어서 이야기합니다. 우리의 서비스는 Domain Layer / Application Layer(aka Service Layer) / Adaptor / Infra + Others 정도의 레이어로 나누어져 있습니다.
Domain Layer는 엔터프라이즈 업무 규칙이 담겨있습니다. Application Layer는 어플리케이션 업무 규칙이 담겨 있습니다. 그 외에는 전부 외부 세계로, grpc인지 웹인지 db를 쓰는지 등등을 담고 있습니다. Application Layer에서 Application은 진짜의 application을 의미하는게 아닙니다. grpc서버를 띄우는 로직은 Application Layer가 아닙니다. DDD에서 이야기하는 Applcation Service, CA(Clean Architecture)의 UseCase가 여기에 해당됩니다.
위에서 설명했듯 Application Layer는 일종의 도메인 로직을 담고 있습니다. 그러나 여기에선 어플리케이션 로직이라고 이야기하겠습니다.
What is Service?
서비스란 본질적으로 어떠한 사물이 아니고 행동이나 행위입니다. eric evans가 작성한 DDD 108페이지에는 이런 내용이 있습니다.
“이따금 서비스는 특정 연산을 수행하는 것 이상의 의미는 없는 모델 객체로 가장해서 나타나기도 한다. 이같은 “행위자”는 이름끄네 “Manager”와 같은 것이 붙는다”
이러한 방법이 도메인 모델에 억지로 연산을 추가하는 것보다는 좋은 방법입니다. 그러나 108페이지 아래쪽에 추가적인 내용이 나와있습니다.
“(생략)~어떤 연산이 실제로 중요한 도메인 개념이라면 Service는 Model Driven Design의 본연적인 부분을 형성하게 된다. 실제로는 아무것도 나타내지 않는 가짜 객체를 선언하기보다는 모델내에 Service로 선언하면 그와 같은 독립적인 연산이 누구도 잘못된 곳으로 이끌지 않을 것이다.”
예를들어서 어떤 광고가 어떤 유저에게서 클릭될 확률이 높은지 계산하는 것은 서비스일 가능성이 있습니다. 확률을 계산하는 것은 광고의 책임도 아니고, 유저의 책임도 아니지만, 도메인 로직에서 중요한 개념이죠. 이렇듯 Entity나 VO에 해당하지 않는 도메인 로직을 도메인 레이어에 나타내는 방법이 바로 Service입니다.
Domain Service is NOT Application Service and Service-Oriented Architecture
먼저 서비스에 대한 고정관념을 버릴 필요가 있습니다. 서비스는 무거운 오퍼레이션을 포함하거나, 외부 호출을 의미하는게 아닙니다. 그저 하나의 오퍼레이션일 뿐이에요!
Service는 여러 계층에 존재할 수 있습니다. 그러나 우리는 Domain Service를 언급하고 있습니다. 그렇다면 Domain Service가 아닌 것들은 어떤게 있을까요? 대표적으로는 Application Service가 있습니다. Application Service는 도메인 모델의 직접적인 클라이언트로, 도메인 모델을 사용하여 유즈케이스를 지원합니다. DDD에서 유즈케이스를 관리하는 것이 바로 이 Application Service입니다. 우리 회사에서는 Application Service라는 이름 대신 UseCase라는 객체를 사용하고 있죠. UseCase는 Domain Service의 직접적인 클라이언트가 됩니다. 또한 UseCase는 도메인 서비스 뿐만 아니라 모든 Domain Object의 클라이언트입니다.
그래서 정말로 어떤 점이 다를까요? UseCase는 Application Layer이고, Service는 도메인 레이어라는 점이 다릅니다. 어떤 행동이나 프로세스가 도메인에서 중요한 의미를 지녀 도메인 모델이 적합하다면 Service로 사용하고, 그게 아니라 단순한 로직이라면 UseCase에 두는게 좋습니다. Mediation Service에서 Batch로 요청을 보내는 것은 중요한 도메인 프로세스이고, 상태가 없는 행동이기에 Service로 모델링 하였습니다. Domain Layer와 Application Layer의 차이라고 생각하셔도 좋습니다. 그러나 DDD에서는 Application Layer를 최대한 얇게 유지하는 것이 바람직 하며, CA에선 Application Layer에 어느정도는 로직을 넣어도 괜찮다는 태도를 취하고 있습니다. 이는 프로젝트마나 선택할 수 있는 방법일 것 같습니다.
Service Layer와도 다릅니다. Service Layer는 Application Layer의 다른 이름입니다. Domain Service는 도메인 모델이지만, Service Layer는 계층을 나타내는 이름이죠. 즉, DDD에는 Domain Service, Application Service, Service Layer가 존재합니다. 그러나 도메인 레이어에서 존재할 수 있는 Service는 Domain Service뿐이며, 우리 회사 코드에서 실체가 있으면서 Service라는 이름을 쓸 수 있는 것은 Domain Service뿐입니다. (마이크로 서비스의 서비스와도 아예 다른 개념이에요)
서비스 중독
서비스를 너무 많이 사용하게 되면 Anemic Domain Model이 될 가능성이 높습니다. 서비스는 얼핏 보면 매우 활용성이 높고 모델링 문제를 해결하는 묘책으로 여겨질 수도 있습니다. 대부분의 로직이 엔티티나 ValueObject로 흩어지지 않고 서비스에만 몰리게 됩니다. 그렇기에 서비스 사용에는 신중을 가하여 사용해야합니다.
UseCase에 모든 로직이 들어가고 모든 도메인 객체가 Data Class가 되는 문제점이 Anemic Domain Model이듯, UseCase에 있는 로직을 Service로 옮기고 Service를 UseCase가 사용하는 것도 마찬가지로 Anemic Domain Model입니다.
IDDD에서는 “서비스가 필요한지 확인하자”라는 챕터가 존재하며 서비스를 지나치게 사용하게 된다면 Anemic Domain Model이 만들어질 수도 있다고 경고합니다. 이는 Entity나 VO에 있어야 할 메서드들이 모두 service로 옮겨지고 service에서 Entity의 책임을 떠맡는 경우 발생할 수 있습니다. IDDD에서는 이 챕터에서 어디까지가 service에서 해야하고 어디까지 entity가 해야하는 것인지 user인증 프로세스를 예시로 들어 설명하고 있습니다.
언제 서비스를 써야하나?
- 중요한 비즈니스 프로세스를 수행할 때
- 어떤 컴포지션에서 다른 컴표지션으로 도메인 객체를 변경할 때
- 하나 이상의 도메인 객체에서 필요로 하는 입력 값을 계산할 때
자세한 예시는 IDDD을 참고하는게 좋습니다.
잘 만들어진 서비스
잘 만들어진 서비스에는 3가지 특징이 있다고합니다.
- 연산이 원래부터 entity / VO에 일부를 구성하는 것이 아니라 도메인 개념과 관련되어있다.
- 인터페이스가 도메인 모델의 외적인 요소의 측면에서 정의된다.
- 상태를 갖지 않는다.
또한 service는 상태를 갖지 않기에 singletone으로 구현하거나 분리된 인터페이스를 사용하지 않을 수도 있습니다. 분리된 인터페이스는 의존성 역행 원리와 같은 것들을 사용할 수 있게 하지만, 구현이 여러개 존재하지 않고 상태를 가지지 않는다면 주입을 할 필요성이 없을 수도 있습니다. 따라서 인터페이스없이 구현 클래스를 직접 사용하는 것도 고려할만한 일입니다. 언급하긴 싫지만 이러한 점은 application service와 비슷한 점입니다.
상태를 갖지 않은 다는 의미는 repository나 다른 서비스를 사용하지 않는다는 의미는 아닙니다. 그러나 중요한 점은 service자체는 상태가 없어야한다는 점입니다. 이러한 점이 entity와의 차이점인데요, entity는 자기자신을 캡슐화 하고 상태를 관리합니다. 예를들어 game player의 이름을 바꾼다고 하면 las.changeName(“magical las”)처럼 할 수 있습니다. 그러나 서비스는 이러한 상태가 없습니다.
service.changeRepository(mysql)이나 service.changeLogic(logic.BASIC)같은 코드가 나오지 않습니다. 이러한 상태를 관리하거나 변경하지 않죠. 그렇다고 해서 항상 같은 동작은 한다는 의미는 아닙니다.