ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 유스케이스 구현하기
    Architecture/Clean Architecture 2024. 12. 2. 21:36

    도메인 모델 구현

    package study.cleanarchitecture.acount.domain;
    
    import lombok.Getter;
    import lombok.Value;
    
    import java.time.LocalDateTime;
    
    @Getter
    public class Account  {
    
        private AccountId id;
        private Money baselineBalance;
        private ActivityWindow activityWindow;
    
        public Money calculateBalance() {
            return Money.add(
                    this.baselineBalance,
                    this.activityWindow.calculateBalance(this.id)
            );
        }
    
        public boolean withdraw(Money money, AccountId targetAccountId) {
            if (!mayWithdraw(money)) {
                return false;
            }
    
            Activity withdrawal = new Activity(
                    this.id,
                    this.id,
                    targetAccountId,
                    LocalDateTime.now(),
                    money
            );
    
            this.activityWindow.addActivity(withdrawal);
            return true;
        }
    
        private boolean mayWithdraw(Money money) {
            return Money.add(
                    this.calculateBalance(),
                    money.negate()
            ).isPositive();
        }
    
        public boolean deposit(Money money, AccountId sourceAccountId) {
            Activity deposit = new Activity(
                    this.id,
                    sourceAccountId,
                    this.id,
                    LocalDateTime.now(),
                    money
            );
            this.activityWindow.addActivity(deposit);
            return true;
        }
    
        @Value
        public static class AccountId {
            Long value;
        }
    }
    

     

     

    도메인 모델인 Account 객체는 deposit과 withdrawal 기능을 가지고 있다. 

    유스케이스 둘러보기

    package study.cleanarchitecture.acount.application.service;
    
    import study.cleanarchitecture.acount.application.port.in.SendMoneyUseCase;
    
    public class SendMoneyService implements SendMoneyUseCase {
    
        @Override
        public boolean sendMoney(SendMoneyCommand command) {
            // TODO: 비즈니스 규칙 검증
            // TODO: 모델 상태 조작
            // TODO: 출력 값 반환
            return false;
        }
    
    }
    

    유스케이스는 도메인 로직에만 신경을 써야하지만 비즈니스 검증할 의무가 있다.

    그래서 입력 어댑터에서 받은 파라미터를 제일 먼저 검증한다. 

    하나의 서비스가 하나의 유즈케이스를 구현하고, 도메인 모델을 변경하고, 변경된 상태를 저장하기 위해 아웃고잉 포트를 호출한다.

    입력 유효성 검증

    import io.reflectoring.buckpal.account.domain.Account.AccountId;
    import io.reflectoring.buckpal.account.domain.Money;
    import io.reflectoring.buckpal.common.SelfValidating;
    import lombok.EqualsAndHashCode;
    import lombok.Value;
    
    import javax.validation.constraints.NotNull;
    
    @Value
    @EqualsAndHashCode(callSuper = false)
    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;
            this.validateSelf();
        }
    }
    

     

    위와 같이 유스케이스에서 하는 것보다 하나 이상의 어댑터에서 호출하게 될텐데 유효성은 각 어댑터에서 전부 구현해야 한다.

    하지만 이렇게 하다보면 사람의 실수가 발생하기 마련이다.

    그래서 입력 모델에서 해당 입력 유효성을 검사하는 것이 효과적이다.

    그리고 각 필드를 final로 불변 필드를 만들고 잘못된 상태로 변경할 수 없다는 사실을 보장한다.

    import javax.validation.ConstraintViolation;
    import javax.validation.ConstraintViolationException;
    import javax.validation.Validation;
    import javax.validation.Validator;
    import javax.validation.ValidatorFactory;
    import java.util.Set;
    
    public abstract class SelfValidating<T> {
    
      private Validator validator;
    
      public SelfValidating() {
        ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
        validator = factory.getValidator();
      }
    
      protected void validateSelf() {
        Set<ConstraintViolation<T>> violations = validator.validate((T) this);
        if (!violations.isEmpty()) {
          throw new ConstraintViolationException(violations);
        }
      }
    }

    생성자의 힘

    생성자의 힘의 파트를 읽고 여태 개발을 진행하면서 빌더 패턴을 이용하여 객체를 초기화를 많이 하였다.

    하지만 해당 파트를 읽고 객체 필드에 null 값이 들어가는 경우는 객체의 유효성이 판별이 안되어서 버그를 초래할 수 있다는 사실을 알았다.

    객체를 만들 때에는 생성자를 활용하여 최대한 유효성을 검사하고 컴파일 단에서 에러를 발견하는 식으로 진행하는 편이 좋을 것 같다.

    비즈니스 검증

    입력 유효성 : 구문상의

    비즈니스 규칙 : 의미적인 유효성 

    public class Account {
    	public boolean withdraw(Money money, AccountId targetAccountId) {
    
        	if (!mayWithdraw(money)) {
           		return false;
        }
     }

    이렇게 하면 이 규칙을 지켜야 하는 비즈니스 로직 바로 옆에 규칙이 위치하기 때문에 위치를 정하는 것도 쉽고 추론하기 쉽다.

    풍부한 도메인 모델 vs 빈약한 도메인 모델

    • 풍부한 도메인 모델
      • 어플리케이션의 코어에 있는 엔티티에서 가능한 한 많은 도메인 로직이 구현된다.
      • 엔티티들은 상태를 변경하는 메서드를 제공
      • 비즈니스 규칙에 맞는 유효한 변경만을 허용
    • 빈약한 도메인 모델
      • 일반적으로 엔티티는 상태를 표현하는 필드와 이 값을 읽고 바꾸기 위한 getter, setter 메더드만 포함하고 어떤 도메인 로직도 가지고 있지 않다.
      • 도메인 로직이 유스케이스 클래스에 구현돼 있다.

    읽기 전용 유스케이스는?

    import java.time.LocalDateTime;
    
    import io.reflectoring.buckpal.account.application.port.in.GetAccountBalanceQuery;
    import io.reflectoring.buckpal.account.application.port.out.LoadAccountPort;
    import io.reflectoring.buckpal.account.domain.Account.AccountId;
    import io.reflectoring.buckpal.account.domain.Money;
    import lombok.RequiredArgsConstructor;
    
    @RequiredArgsConstructor
    class GetAccountBalanceService implements GetAccountBalanceQuery {
    
        private final LoadAccountPort loadAccountPort;
    
        @Override
        public Money getAccountBalance(AccountId accountId) {
           return loadAccountPort.loadAccount(accountId, LocalDateTime.now())
                 .calculateBalance();
        }
    }
    

     

    UI에 계좌의 잔액을 표시해야 하는 경우

    쿼리를 위한 인커밍 전용 포트를 만들고 이를 쿼리 서비스에 구현한다.

    이러한 방식을 쓰기가 가능한 유스케이스 또는 커맨드와 코드 상에서 명확하게 구분된다.

    이런 방식은 CQS 패턴과 CQRS 패턴과 아주 잘 맞는다.

     

    ref: https://github.com/thombergs/buckpal

     

     

     

    'Architecture > Clean Architecture' 카테고리의 다른 글

    영속성 어댑터 구현하기  (1) 2024.12.10
    웹 어댑터 구현  (1) 2024.12.03
    코드 구성하기  (0) 2024.11.28
    의존성 역전하기  (1) 2024.11.27
    Layered architecture 문제  (1) 2024.11.25
Designed by Tistory.