본문 바로가기
Theory/Architecture

유스케이스 작성하기

by y.j 2022. 6. 21.
728x90

육각형 아키텍처는 도메인 주임의 아키텍에 적합하기 때문에 도메인 엔티티를 만드는 것으로 시작한 후 해당 도메인 엔티티 중심으로 유스케이스를 구현하겠다.

 

도메인 모델 구현하기

송금하는 유스케이스를 구현해보자. OOP의 관점으로 Account라는 객체를 만들어 현재 잔고를 스냅샷을 하도록 한다. 계좌에 대한 모든 입금과 출금은 Activity 엔티티에 포착하고 모든 Activity에 대해 메모리를 올리는 것은 낭비이기 때문에 ActivityWindow를 통해 일정 기간만 보유한다.

@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class Account {

   /**
    * The unique ID of the account.
    */
   @Getter private final AccountId id;

   /**
    * The baseline balance of the account. This was the balance of the account before the first
    * activity in the activityWindow.
    */
   @Getter private final Money baselineBalance;

   /**
    * The window of latest activities on this account.
    */
   @Getter private final ActivityWindow activityWindow;

   /**
    * Creates an {@link Account} entity without an ID. Use to create a new entity that is not yet
    * persisted.
    */
   public static Account withoutId(
               Money baselineBalance,
               ActivityWindow activityWindow) {
      return new Account(null, baselineBalance, activityWindow);
   }

   /**
    * Creates an {@link Account} entity with an ID. Use to reconstitute a persisted entity.
    */
   public static Account withId(
               AccountId accountId,
               Money baselineBalance,
               ActivityWindow activityWindow) {
      return new Account(accountId, baselineBalance, activityWindow);
   }

   public Optional<AccountId> getId(){
      return Optional.ofNullable(this.id);
   }

   /**
    * Calculates the total balance of the account by adding the activity values to the baseline balance.
    */
   public Money calculateBalance() {
      return Money.add(
            this.baselineBalance,
            this.activityWindow.calculateBalance(this.id));
   }

   /**
    * Tries to withdraw a certain amount of money from this account.
    * If successful, creates a new activity with a negative value.
    * @return true if the withdrawal was successful, false if not.
    */
   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())
            .isPositiveOrZero();
   }

   /**
    * Tries to deposit a certain amount of money to this account.
    * If sucessful, creates a new activity with a positive value.
    * @return true if the deposit was successful, false if not.
    */
   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 {
      private Long value;
   }

}

 

유스케이스 둘러보기

단계

1. 입력을 받는다.
2. 비즈니스 규칙을 검증한다.
3. 모델 상태를 조작한다.
4. 출력을 반환한다.

 

유스케이스는 인커밍 어댑터로부터 입력을 받는다. 저자는 유효성 검증을 도메인 계층에서 하는 것이 옳다고 생각한다. 따라서 입력부분에서는 유효성 검증을 하지 않는다. 하지만, 비즈니스 규칙을 검증할 책임은 있다고 한다. 입력에 필수로 들어와야 하는 값에서는 @NotNull을 통해 검증을 한다. 그 외 검증은 모데인 계층에서 진행한다.

 

SelfValidating class

public abstract class SelfValidating<T> {

  private Validator validator;

  public SelfValidating() {
    ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
    validator = factory.getValidator();
  }

  /**
   * Evaluates all Bean Validations on the attributes of this
   * instance.
   */
  protected void validateSelf() {
    Set<ConstraintViolation<T>> violations = validator.validate((T) this);
    if (!violations.isEmpty()) {
      throw new ConstraintViolationException(violations);
    }
  }
}

 

유스케이스마다 다른 입력 모델

각기 다른 유스케이스에 동일한 입력 모델을 사용하고 싶은 생각이 들 때가 있다. '계좌 등록'와 '계좌 정보 업데이트'라는 두 가지 유스케이스를 보자. '계좌 등록'에는 계좌ID null로 주어야 하고 '계좌 정보 업데이트'에서는 소유자ID를 넣어주어야 한다. 이렇게 서로 가지고 있어야 할 값이 다를 경우에는 유효성 검증이 쉽지 않다. 가장 쉬운 방법은 객체를 2개로 나누어 관리하여 불필요한 부수효과 가능성을 없애는 것이 좋다.

 

비즈니스 규칙 검증하기

비즈니스 규칙은 유스케이스의 맥락 속에서 의미적은(semantical) 유효성을 검증하는 일이라고 할 수 있다. 출금 계좌는 초과 출금되어서는 안된다라는 규칙을 정 할 때 입력과 비즈니스 로직 어느 부분에서 유효성을 검증해야 할지 논란이 있다. 유지보수의 입장에서 생각해보면 도메인 엔티티 안에 넣는 것이 찾기도 추론하기도 쉽다.

 

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

'풍부한' 모데인 모델이란 Account 엔티티처럼 엔티티에 가능한 한 많은 로직이 구현되는 것들이다. 위에서 본 '송금하기' 유스케이스 서비스는 출금 계좌와 입금 계좌 엔티티를 로드하고 withdraw(), desposit() 메서드를 호출한 후, 결과를 다시 데이터베이스로 보낸다. 

'빈약한' 모데인 모델이란 엔티티 상태를 getter, setter 메서드만 포함하고 어떤 모데인 로직도 가지고 있지 않다. 엔티티를 전달할 책임조차도 유스케이스 클래스에 있다.

 

 

 

 

 

728x90

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

영속성 어댑터 구현하기  (0) 2022.07.02
웹 어댑터 구현하기  (0) 2022.06.29
코드 작성하기  (0) 2022.06.20
의존성 역전하기  (0) 2022.06.19
계층형 아키텍쳐의 문제는 문제일까?  (0) 2022.06.16

댓글