유스케이스
- 인커밍 어댑터로부터 입력을 받는다.
- 비즈니스 규칙 검증
- 모델 상태 조작
- 변경된 상태를 아웃고잉 어댑터를 통해 전달
- 인커밍 어댑터로 출력 반환
- 유스케이스 코드는 도메인 로직에만 신경써야 하므로, 입력 유효성 검증은 유스케이스에서 담당하지 않는 것이 바람직
- 유스케이스에서는 비즈니스 규칙을 검증할 책임이 존재
@RequiredArgsConstructor
@UseCase
@Transactional
public class SendMoneyService implements SendMoneyUseCase {
private final LoadAccountPort loadAccountPort;
private final AccountLock accountLock;
private final UpdateAccountStatePort updateAccountStatePort;
private final MoneyTransferProperties moneyTransferProperties;
@Override
public boolean sendMoney(SendMoneyCommand command) {
checkThreshold(command);
LocalDateTime baselineDate = LocalDateTime.now().minusDays(10);
Account sourceAccount = loadAccountPort.loadAccount(
command.getSourceAccountId(),
baselineDate);
Account targetAccount = loadAccountPort.loadAccount(
command.getTargetAccountId(),
baselineDate);
AccountId sourceAccountId = sourceAccount.getId()
.orElseThrow(() -> new IllegalStateException("expected source account ID not to be empty"));
AccountId targetAccountId = targetAccount.getId()
.orElseThrow(() -> new IllegalStateException("expected target account ID not to be empty"));
accountLock.lockAccount(sourceAccountId);
if (!sourceAccount.withdraw(command.getMoney(), targetAccountId)) {
accountLock.releaseAccount(sourceAccountId);
return false;
}
accountLock.lockAccount(targetAccountId);
if (!targetAccount.deposit(command.getMoney(), sourceAccountId)) {
accountLock.releaseAccount(sourceAccountId);
accountLock.releaseAccount(targetAccountId);
return false;
}
updateAccountStatePort.updateActivities(sourceAccount);
updateAccountStatePort.updateActivities(targetAccount);
accountLock.releaseAccount(sourceAccountId);
accountLock.releaseAccount(targetAccountId);
return true;
}
private void checkThreshold(SendMoneyCommand command) {
if(command.getMoney().isGreaterThan(moneyTransferProperties.getMaximumTransferThreshold())){
throw new ThresholdExceededException(moneyTransferProperties.getMaximumTransferThreshold(), command.getMoney());
}
}
}
- 서비스는 인커밍 포트 인터페이스인 SendMoneyUseCase를 구현
- 또한 계좌를 불러오기 위한 LoadAccountPort와, 데이터베이스의 계좌 상태를 업데이트하기 위한 UpdateAccountStatePort를 Injection 받음
- 하나의 서비스가 하나의 유스케이스를 구현하고, 도메인 모델을 변경한다. 이후 변경된 상태를 저장하기 위해 아웃고잉 포트를 호출
즉, 유스케이스는 논리적인 단위 기능에 대한 진입점을 나타내는 인터페이스이고, 이를 구현체로 구현한 것이 서비스이다.

입력 유효성 검증
- 유스케이스에서는 입력 유효성 검증을 수행하지 않음
- 그렇다면 어댑터에서 입력 유효성을 검증하도록 한다면, 각 어댑터를 구현할 때마다 모든 입력 유효성을 구현해야 함
- 이 과정에서 일부 유효성 검사가 누락되거나 잘못되는 경우도 발생
- 이를 입력 모델(Input Model)을 통해 다룸
- 위의 SendMoneyCommand
- 각 유스케이스 별로 입력 모델을 만들고 해당 입력 모델 생성자 내에 유효성 검증 로직을 넣으면 됨
- 주로 Bean Validate API를 사용해 구현
- 유스케이스 간에 입력 모델을 공유하는 것은 바람직하지 않음
- 일부 모델 입력에 Null값을 허용하는 형태가 발생
- 각 유스케이스 별 하나의 입력 모델에서 어느 부분이 사용되는지 알아보기가 어려움
- 각각 입력 모델을 만들게 되면 들어오는 데이터를 각 유스케이스에 해당하는 입력 모델에 매핑해야 하는 비용이 발생 → 8장의 매핑 전략에 따라 선택
비즈니스 규칙 검증
- 비즈니스 규칙은 유스케이스 로직의 일부
- 입력 유효성과 비즈니스 규칙의 차이점
- 비즈니스 규칙은 도메인 모델의 현재 상태에 접근해야 함
- 의미적인 유효성 검증
- ex) 출금 계좌는 초과 출금되어서는 안 된다.
- 현재 모델의 상태에 따라 검증이 달라짐
- 입력 유효성은 도메인 모델의 현재 상태와 무관
- 구문상의 유효성 검증
- ex) 송금되는 금액은 0보다 커야 한다.
- 현재 모델 상태와 상관없이 입력된 값으로만 판단이 가능
- 비즈니스 규칙은 도메인 모델의 현재 상태에 접근해야 함
- 이러한 비즈니스 규칙 검증은 도메인 엔티티 내에서 수행(풍부한 도메인 모델) 또는 서비스(빈약한 도메인 모델)에서 수행할 수 있다.
풍부한 도메인 모델 vs 빈약한 도메인 모델
- 풍부한 도메인 모델 : 애플리케이션 코어에 있는 엔티티에서 가능한 많은 도메인 로직을 구현
- 빈약한 도메인 모델 : 도메인 로직이 유스케이스 클래스 내에 구현
풍부한 도메인 모델 (Rich Domain Model)
특징
- 비즈니스 로직 포함: 엔티티 클래스 자체에 비즈니스 로직과 규칙이 포함되어 있습니다.
- 행동 중심: 데이터뿐만 아니라 데이터와 관련된 행동(메서드)이 포함됩니다.
- 응집도 높음: 데이터와 해당 데이터에 대한 로직이 한곳에 모여 있습니다.
- 상태 유효성 검사: 객체 내부에서 자신의 상태를 유효하게 유지합니다.
장점
- 응집도 향상: 데이터와 비즈니스 로직이 함께 있으므로 코드의 응집도가 높아집니다.
- 유지보수성 향상: 객체가 스스로 자신의 상태를 관리하므로 상태 유효성 검사가 일관되게 이루어집니다.
- 캡슐화: 내부 상태를 외부에서 직접 조작할 수 없도록 하여 객체의 무결성을 유지할 수 있습니다.
- 재사용성: 도메인 객체가 독립적으로 비즈니스 로직을 수행할 수 있어 재사용성이 높아집니다.
단점
- 복잡성 증가: 객체가 비즈니스 로직을 포함하므로 클래스가 복잡해질 수 있습니다.
- 테스트 어려움: 객체의 상태와 행동을 모두 테스트해야 하므로 테스트가 어려울 수 있습니다.
- 개발 난이도 증가: 객체 지향 프로그래밍에 익숙하지 않은 개발자에게는 설계와 구현이 어려울 수 있습니다.
빈약한 도메인 모델 (Anemic Domain Model)
특징
- 비즈니스 로직 부재: 엔티티 클래스는 데이터를 저장하기 위한 속성만 가지고 있으며, 비즈니스 로직은 서비스 클래스에 포함됩니다.
- 데이터 중심: 데이터 구조(필드)만 포함하고 있습니다.
- 서비스 클래스 사용: 모든 비즈니스 로직은 서비스 클래스에서 처리됩니다.
- 상태 유효성 검사 부재: 상태 유효성 검사는 주로 서비스 클래스에서 처리됩니다.
장점
- 단순성: 엔티티 클래스가 단순하여 이해하고 작성하기 쉽습니다.
- 테스트 용이: 서비스 클래스를 독립적으로 테스트할 수 있어 단위 테스트가 용이합니다.
- 개발 난이도 감소: 객체 지향 프로그래밍에 대한 깊은 이해가 필요하지 않으므로 쉽게 개발할 수 있습니다.
단점
- 응집도 저하: 데이터와 비즈니스 로직이 분리되어 있어 응집도가 낮습니다.
- 유지보수성 저하: 비즈니스 로직이 분산되어 있어 코드를 유지보수하기 어렵습니다.
- 객체 캡슐화 부족: 객체가 자신의 상태를 관리하지 않으므로 객체의 무결성이 쉽게 깨질 수 있습니다.
- 재사용성 감소: 서비스 클래스에 의존하는 로직이 많아 재사용성이 낮습니다.
결론
- 풍부한 도메인 모델은 고도의 응집성과 캡슐화를 통해 객체 지향 설계의 장점을 극대화할 수 있지만, 복잡성이 증가하고 개발과 유지보수가 어려울 수 있습니다.
- 빈약한 도메인 모델은 단순성과 개발의 용이성을 제공하지만, 응집도와 캡슐화가 부족하여 유지보수성과 재사용성이 떨어질 수 있습니다.
유스케이스 별 다른 출력 모델
- 입력과 비슷하게 출력도 유스케이스 별 다르게 만드는 것이 좋다.
- 반환 데이터는 항상 최소한의 정보만 반환해야 한다.
- 유스케이스 간 같은 출력 모델을 공유하면 유스케이스 간에 강하게 결합되게 됨
- 다른 유스케이스에서 출력 모델의 특정 필드를 수정해야 하면, 나머지 유스케이스에서도 변경이 발생
- 단일 책임 원칙에서 변경되는 이유가 추가되는 것
읽기 전용 유스케이스
- 읽기만 수행하는 로직의 경우에도 별도의 유스케이스를 만들어 관리하는 것이 좋다.
- ex) UI에 계좌의 잔액을 표시해야 하는 경우
- 읽기 전용 유스케이스는 쓰기가 가능한 유스케이스와 코드 상에서 명확하게 분리가 됨
- CQS나 CQRS 개념과 잘 맞아 떨어짐
'Architecture(아키텍처)' 카테고리의 다른 글
| [만들면서 배우는 클린 아키텍처] 06. 영속성 어댑터 구현하기 (0) | 2024.07.20 |
|---|---|
| [만들면서 배우는 클린 아키텍처] 05. 웹 어댑터 구현하기 (0) | 2024.07.20 |
| [만들면서 배우는 클린 아키텍처] 03. 코드 구성하기 (0) | 2024.07.20 |
| [만들면서 배우는 클린 아키텍처] 02. 의존성 역전하기 (0) | 2024.07.20 |
| [만들면서 배우는 클린 아키텍처] 01. 계층형 아키텍처의 문제는 무엇일까? (1) | 2024.07.20 |