HomeAboutMeBlogGuest
© 2025 Sejin Cha. All rights reserved.
Built with Next.js, deployed on Vercel
📖
공부한 책
/
🕛
만들면서 배우는 클린 아키텍처
/
3장. 코드 구성하기

3장. 코드 구성하기

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