- 실제 코드 구조를 최대한 우리가 목표로 하는 아키텍처에 가깝게 만들어 주는 육각형 아키텍처의 패키지 구조 살펴보기
- 새 프로젝트에서 가장 먼저 제대로 만들려고 하는 것은 패키지 구조임. 처음에는 계속 사용할 괜찮아 보이는 구조를 잡는다
- 그리고 나서 프로젝트가 진행될수록 점점 바빠지고 패키지 구조는 짜임새 없는 엉망진창 코드를 그럴싸하게 보이게 만드는 껍데기일 뿐이라는 점을 깨닫게 됨…
- 소리치는 아키텍처 : 애플리케이션의 기능을 코드를 통해 볼 수 있게 만드는 것
- eg) AccountService → SendMoneyService : 송금하기 유스케이스를 구현한 코드는 클래스명 만으로도 찾을 수 있음
육각형 아키텍처에 맞는 패키지 구조- 클린 아키텍처의 가장 본질적인 요건은 어플리케이션 계층이 인커밍/아웃고잉 어댑터에 의존성을 갖지 않는 것임
흰색 화살표는 구현. 검은색 화살표는 DirectAssociation(즉, 해당 클래스를 가지고 있음. 이용함)package io.reflectoring.buckpal.account.application.service;
import io.reflectoring.buckpal.account.application.port.in.SendMoneyCommand;
import io.reflectoring.buckpal.account.application.port.in.SendMoneyUseCase;
import io.reflectoring.buckpal.account.application.port.out.AccountLock;
import io.reflectoring.buckpal.account.application.port.out.LoadAccountPort;
import io.reflectoring.buckpal.account.application.port.out.UpdateAccountStatePort;
import io.reflectoring.buckpal.common.UseCase;
import io.reflectoring.buckpal.account.domain.Account;
import io.reflectoring.buckpal.account.domain.Account.AccountId;
import lombok.RequiredArgsConstructor;
import javax.transaction.Transactional;
import java.time.LocalDateTime;
@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());
}
}
}