육각형 아키텍처는 도메인 중심의 아키텍처에 적합하므로 도메인 엔티티를 만든 후 유스케이스를 구현한다.
송금하기 예제
Account
엔티티: 실제 계좌의 현재 스냅샷. 입금과 출금을 할 수 있다.Account.baselineBalance
필드: Account.activityWindow
의 첫번째 활동 바로 전의 잔고ActivityWindow
엔티티: 계좌에 대한 최근 모든 활동Activity
엔티티: 입금, 출금package com.woowa.cleanarchitecture.account.domain;
@AllArgsConstructor
public class Account {
private AccountId id;
private Money baselineBalance; // activityWindow의 첫번째 활동 바로 전의 잔고
private ActivityWindow activityWindow; // 최근 모든 입출금 활동
public boolean withdraw(Money money, AccountId targetAccountId) {
if (!mayWithdraw(money)) return false;
Activity withdrawal = new Activity(id, id, targetAccountId, now(), money);
activityWindow.addActivity(withdrawal); // 새로운 활동을 활동창에 추가
return true;
}
public boolean deposit(Money money, AccountId sourceAccountId) {
Activity deposit = new Activity(id, sourceAccountId, id, now(), money);
activityWindow.addActivity(deposit);
return true;
}
public Money calculateBalance() {
// 현재 잔고 = 기준 잔고 + 최근 활동의 입출금 내역
return Money.add(
baselineBalance,
activityWindow.calculateBalance(id)
);
}
}
package com.woowa.cleanarchitecture.account.application.service;
@RequiredArgsConstructor
@Transactional
public class SendMoneyService implements SendMoneyUseCase {
private final LoadAccountPort loadAccountPort;
private final UpdateAccountStatePort updateAccountStatePort;
private final AccountLock accountLock;
@Override
public boolean sendMoney(SendMoneyCommand command) {
// TODO: 비즈니스 규칙 검증
// TODO: 모델 상태 조작
// TODO: 출력 값 반환
return false;
}
}
입력 모델에서 입력 유효성 검증하기
package com.woowa.cleanarchitecture.account.application.port.in;
import javax.validation.constraints.NotNull;
@Getter
public class SendMoneyCommand extends SelfValidating<SendMoneyCommand> {
@NotNull
private final AccountId sourceAccountId;
@NotNull
private final AccountId targetAccountId;
@NotNull
private final Money money;
public SendMoneyCommand(AccountId sourceAccountId, AccountId targetAccountId, Money money) {
this.sourceAccountId = sourceAccountId;
this.targetAccountId = targetAccountId;
this.money = money;
validateSelf();
}
}
이 객체가 유효하고 잘못된 상태로 변경할 수 없다는 것을 보장한다.
= 오류 방지 계층(anti conrruption layer)
빌더 사용을 지양하자
@Builder
private SendMoneyCommandBuilder(AccountId sourceAccountId, AccountId targetAccountId, Money money) {
this.sourceAccountId = sourceAccountId;
this.targetAccountId = targetAccountId;
this.money = money;
}
SendMoneyCommandBuilder sendMoneyCommand = SendMoneyCommandBuilder.builder()
.sourceAccountId(new AccountId(1L))
.targetAccountId(new AccountId(2L))
// .money(new Money(BigInteger.ONE))
.build();
public SendMoneyCommand(AccountId sourceAccountId, AccountId targetAccountId, Money money) {
this.sourceAccountId = sourceAccountId;
this.targetAccountId = targetAccountId;
this.money = money;
}
SendMoneyCommand sendMoneyCommand = new SendMoneyCommand(
new AccountId(1L),
new AccountId(2L),
// new Money(BigInteger.ONE)
);
code smell
(-) 더 많은 비용이 든다.
유스케이스 구현 ∋ 도메인 로직 구현 + 비즈니스 규칙 검증 ≠ 입력 유효성 검증
현재 모델의 상태에 접근해야하는가?
@NotNull
)으로 구현 가능비즈니스 로직이 있는 도메인 엔티티
package com.woowa.cleanarchitecture.account.domain;
public class Account {
public boolean withdraw(Money money, AccountId targetAccountId) {
if (!mayWithdraw(money)) { // 초과 출금 불가능
return false;
}
// business logic
}
}
유스케이스 코드에서 도메인 엔티티 사용 전
각자 필요에 맞는 방법을 자유롭게 사용하라.
출력 모델은 각 유스케이스에 맞게 구체적이고 꼭 필요한 데이터만 들고 있어야 한다.
가능한 한 적게 반환하자.
public boolean deposit(Money money, AccountId sourceAccountId) {
Activity deposit = new Activity(null, id, sourceAccountId, id, now(), money);
activityWindow.addActivity(deposit);
return true;
}
읽기 전용 유스케이스 = 간단한 데이터 쿼리