우리가 사용하는 '서비스' 라는 용어는 개발 생태계에서 많이 사용되는 용어입니다. 우리가 알고있는 전통적인 레이어드 아키텍쳐에서도 '서비스'라는 용어가 특히 Application 에서 빈번하게 사용됩니다.
도메인 주도 설계에서 말하는 '서비스'에 대해서 설명하기 전에 전통적인 레이어드 아키텍처에 대해서 설명할 필요가 있다고 생각해 아래와같이 간략히 설명해보려 합니다.
그림 1에서 보이는 것과 같이 흔히 4가지의 계층으로 구성되어, Presentation/Application/Domain/Infra 로 구성되어있고, 의존성은 아래로 향합니다.
Presentation
표현계층에서는 Web 에 관련된 책임을 가지게 됩니다.
객체 변환이나 JSON 변환 등의 대표적으로 Web에 대한 책임을 가지게됩니다.
Application(응용)
애플리케이션은 수행할 작업을 정의하는데, 표현력있는 도메인 객체가 문제를 해결할 수 있도록 하며, 애플리케이션은 그런 표현력있는 도메인 객체를 사용하는 클라이언트가 됩니다. 이곳은 레이어드 아키텍쳐에서 최대한 얇게 유지되어야 하며, 업무 규칙이나 지식은 포함되지 않습니다.
그림 2를 보면 응용계층은 도메인의 Entity와 ValueObject를 사용합니다. 여기 응용 계층에서 서비스라는 용어가 등장합니다. 이 용어에 대해서는 뒤에서 좀 더 설명해볼게요.
Domain
업무개념과 업무상황에 관한 정보, 업무규칙을 표현하는 일을 책임집니다. 기술적인 세부사항은 인프라스트럭쳐에서 구현됩니다. 도메인 주도 설계에서는 도메인이라 하는 부분은 해결해야될 문제점이 있는 영역을 의미합니다.
Domain 을 표현하기 위해 다양한 형태로 존재할 수 있습니다.
그렇다면 위에서 Application(응용) 계층을 왜 얋게 유지해야 하는 걸까요?
먼저 반대로 두껍게 만든건 어떤 것일까요? 제가 생각하는 두꺼운 응용계층은 그림2에서 보여지는 엔티티와 값을 활용해 비지니스 로직을 구현된 형태를 말합니다. '당연하게 그렇게 하는거 아니야?' 라고 생각 한다면, 앞으로 말할 '왜 얋게 유지해야지' 에 대해서 납득이 안 될 수도 있습니다.
1. 응용 계층을 얋게 유지하지 않는다면, Application 계층은 복잡한 비지니스 로직이 몰리게 되어 결합도가 높고, 응잡도가 낮은 절차적인 프로세스 라는 함정에 빠지기 쉽습니다.
두껍다고 말할 수 있는 냄새로는 아래와같은 코드를 맞이할 경우입니다.(spring + java 으로 구성되었습니다)
@Slf4j
@Service
@RequiredArgsConstructor
public class ServiceImpl implements Service {
private final XxxxxxxeRepository xxxxxxxeRepository;
private final XxxxxxxdRepository xxxxxxxdRepository;
private final XxxxxxxcRepository xxxxxxxcRepository;
private final XxxxxxxbRepository xxxxxxxbRepository;
private final XxxxxxxaRepository xxxxxxxaRepository;
private final RedisTemporaryRepository redisRepository;
private final QnaAlarmService qnaAlarmService;
private final EmailSender emailSender;
private final SlackAlarmService slackAlarmService;
private final XxxxxxxService qnaxxxxxxxService;
...
@Override
public void somethingMethod1() {
...
}
@Override
public void somethingMethod2() {
...
}
}
이렇게 여러개의 Repository 를 한 서비스에서 사용하게 된다면, 결합도가 높아지고 응집도가 낮아질 것입니다. 이것을 측정하는 기준은 위 클래스 기준으로 ServiceImpl 클래스에서 의존성 주입된 복수의 클래스가 메서드 하나에서 얼마나 사용되는지? 만약 메서드 하나에 단 2개의 주입된 객체를 사용한다면, 나머지 8개의 주입된 객체를 상요하지 않는다면 그것이 바로 결합도가 높고 응집도가 낮아진다고 말할 수 있습니다.
이런 방식으로 응용계층을 두껍게 가져가게 되면 점점 유지보수하기 어려운 코드를 만들게 됩니다.
2. 테스트 코드를 만들기 어렵고, 회귀적인 자동화 테스트를 실행시키기 어려워집니다.
왜 테스트 코드를 만들기 어렵다고 말할 수 있을까요? 다시 코드로 돌아가서 somethingMethod1 기능을 개발하기 위한 테스트 코드를 만들고 싶다면 어떻게 해야될까요? somethingMethod1 에 사용되는 주입된 객체를 모킹해야될 것입니다. 이때 somethingMethod1에서 사용되지 않는 불필요한 객체들까지 모킹해야만 합니다. 그렇기 때문에 테스트 코드를 만들기 어렵게 만드는 요인이 되고, 설정이 필요한 어떤 객체를 주입받았다면 그것에 맞는 설정까지 해줘야 될 것입니다.
@ExtendWith(SpringExtension.class)
class ServiceImplTest {
@InjectMocks
ServiceImpl sut;
@Mock
XxxxxxRepository repository;
... more and more mocking...
@Test
void name() {
}
}
3. 두껍게 만든 Application(응용) 계층은 쉽게 단일 책임 원칙을 위반하게 됩니다.
단일 책임 원칙을 위반한다는 말이 무슨 의미일까요? 전체적인 아키텍쳐를 바라는 관점이 2가지가 있다고 생각합니다. 외부에서 아키텍쳐를 바라보는 관점과 도메인안에서 밖을 바라보는 관점이 있습니다.
이때 외부에서 아키텍쳐를 바라보는 관점이란? 개발자가 아닌 이해관계자에게 우리의 아키텍쳐를 설명해줘야 할 때 할 수 있는 이야기라고 생각하고, 내부에서 아키텍쳐를 바라보는 관점이란 도메인 전문가와 개발자가 우리의 아키텍쳐를 어떻게 설계할 것인가? 대해서 논의한 후에 만들어진 모델링이라 말할 수 있을 것 같습니다.
그림3 에서 오른쪽에 AS-IS 에서 애플리케이션이 두껍게 유지할 경우, 외부관점과 내부관점이 충돌하게 됩니다. 외부관점에서 우리의 애플리케이션을 설명하는 것과 내부관점에서 설명하는 것이 동일한 것이 무슨 문제가 있는지 조금 더 설명한다면, 외부관점에서 우리의 애플리케이션에 대한 설명을 들어야 할 때, 어디까지 들어야 할까요? Redis, DB 구성 등의 불필요한 내용까지 알아야 될까요? 그렇지 않습니다. 이것은 관심사의 분리와도 관련이 있습니다. 알아야 할 것만 알아야 하며, 불필요한 정보는 제공할 필요가 없습니다.
내부 관점에서는 우리가 흔히 말하는 객체지향 프로그래밍에 대한 이해와 비슷합니다. 1개 이상의 표현력있는 도메인 객체가 서로 협력하는 형태로 서로에게 메세지를 전달합니다. 이렇게 함으로써, 캡슐화/재사용/다형성 등의 이점을 누리게 될 것입니다.
내/외부 관점에서 동일한 위치에 코드가 존재할 경우 SRP 을 쉽게 위반하게 됩니다.
그렇다면 서비스는 무엇일까요?
application(응용)계층에서 말하는 서비스만이 서비스일까요? 그렇다면 왜 서비스 일까요? 응용계층에서 말하는 서비스라는 단어보다 좀 더 큰 범위로 서비스를 생각해봅시다. 서비스란 이름은 객체와 객체간의 관계를 강조하기 위해서 사용됩니다. 단순하게 서비스를 사용하는 클라언트에게 무엇을 제공하는지? 에 있습니다.
응용계층에서 사용되는 서비스는 서비스라는 단어뜻에 맞게 객체와 객체간의 관계를 것이 관심사라는 것을 위한 용어의 뜻으로 사용됩니다. 그렇다면, 도메인과 인프라스럭쳐는 서비스가 없는 것일까요?
사실 그렇지 않습니다. 도메인에도 서비스가 존재할 수 있고, 인프라스트럭쳐에도 서비스가 존재할 수 있습니다. 다만, 그것이 해당 계층의 주 관심사가 아닐 뿐입니다.
도메인 계층에서 서비스는 어떤 의미를 가지게 될까요?
도메인주도설계에서는 도메인서비스라는 용어를 사용됩니다. 도메인서비스는 다음과 같은 특징을 가지게 됩니다.
1. Entity나 ValueObject의 일부로 구성하는 것이 아니라 도메인 개념과 관련되어 있는 객체를 사용합니다. 예를 들어, 계좌이체 기능 구현시 Account간 계좌를 이체할 수 있는 서비스 TransferService 입니다.
2. 도메인 하나로 채워질수 없는 상황에 발생하며, 도메인과 도메인 그 중간의 외적요소의 측면에서 정의되어집니다.
3. 연산이 상태를 가지지 않습니다.
위 3가지 특징을 잘 조합해서 모델의 언어라는 측면에서 인터페이스를 정의하고 연산의 이름을 이해관계자들 사이에 사용되는 언어의 일부가 될 수 있도록 해야 합니다.
그 외도 애플리케이션이나 인프라스트럭쳐에는 서비스가 있습니다. 애플리케이션 서비스의 경우, 도메인/도메인서비스/값객체 등의 표현력있는 도메인 모델의 클라이언트가 되어 도메인간에 메세지를 전달하도록 만들거나- 도메인과는 무관한 프로세스를 가지게 되는데, 대표적으로 트랜잭션 처리가 있습니다.
인프라스트럭쳐 서비스는 외부 API를 사용할 경우 DIP를 활용한 도메인 서비스 인터페이스의 실질적인 구현체가 될 수 있습니다.
서비스란 용어는 포괄적으로 쉽게 사용되지만, 쉽게 사용되는 만큼 주의를 귀기울여야합니다. 또한 도메인주도설계에서 말하는 서비스는 일반적으로 알고 있는 서비스와는 차이가 있습니다. 그 거리감이 이 글을 읽고 조금이라도 좁혀졌으면 좋겠습니다.
'도메인 주도 설계' 카테고리의 다른 글
[특강] 도메인주도설계의 사실과 오해 후기 - 조영호 (0) | 2023.08.18 |
---|---|
메세지와 이벤트의 차이점은 무엇인가? (2) | 2021.10.23 |
Domain-Driven Design: The Identifier Type Pattern[번역] (0) | 2021.05.24 |
Value Object로서 Model Identity를 사용하는 3가지 이유[번역] (0) | 2021.05.17 |
상태 머신 다이어그램은 무엇일까? (0) | 2021.01.30 |
댓글