[클린 아키텍처] Ch 04. 유스케이스 구현하기

.·2022년 5월 9일
0

육각형 아키텍처는 도메인 중심의 아키텍처에 적합하므로 도메인 엔티티를 만든 후 유스케이스를 구현한다.

송금하기 예제

4-1. 도메인 모델 구현하기

  • 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)
        );
    }
}
  • 입출금 = 새로운 활동으로 활동창에 추가됨

4-2. 유스케이스 둘러보기

유스케이스 처리 방법

  1. 인커밍 어댑터로 입력을 받는다.
  2. 입력이 비즈니스 규칙을 충족하면
  3. 도메인 객체의 상태를 바꾼다.
  4. 아웃고잉 포트를 통해 영속성 어댑터로 전달해서 저장한다.
  5. 아웃고잉 어댑터에서 온 출력값을 유스케이스를 호출한 인커밍 어댑터로 반환할 출력 객체로 변환한다.
  • 유스케이스 구현 ∋ 도메인 로직 구현 + 비즈니스 규칙 검증 ≠ 입력 유효성 검증

SendMoneyService class

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;
    }
}

4-3. 입력 유효성 검증

입력 모델에서 입력 유효성 검증하기

  • 유스케이스 클래스(서비스)의 책임은 아니지만 애플리케이션 계층의 책임은 맞다.
  • 애플리케이션 계층이 아닌 웹 계층에서 입력 유효성 검증을 한다면?
    • 하나 이상의 인커밍 어댑터에서 하나의 유스케이스를 사용한다. 모든 어댑터에서 입력 유효성 검증을 구현해야 한다. (실수하거나 빼먹을 확률 ↑)

입력 모델

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)

4-4. 생성자의 힘

빌더 사용을 지양하자

Builder를 사용한다면?

@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)
);
  • 새로운 필드를 추가하거나 삭제할 때, 컴파일 에러가 코드를 수정할 수 있게 도와준다.

4-5. 유스케이스마다 다른 입력 모델

다른 유스케이스에서 같은 입력 모델을 사용할 경우

code smell

  1. 재사용한 입력 모델에서 필요하지 않은 필드를 null을 유효한 상태로 받아들여야한다.
  2. 다른 입력 유효성 검증이 필요할 경우 유스케이스 코드에 들어갈 수 있게 된다.

유스케이스마다 전용 입력 모델을 사용할 경우

  1. 다른 유스케이스와의 결합 제거
  2. 유스케이스를 훨씬 명확하게 만든다.
  3. 장기적으로 유지보수하기 더 쉽다.

(-) 더 많은 비용이 든다.

4-6. 비즈니스 규칙 검증하기

유스케이스 구현 ∋ 도메인 로직 구현 + 비즈니스 규칙 검증 ≠ 입력 유효성 검증

입력 유효성 검증 vs 비즈니스 규칙 검증

현재 모델의 상태에 접근해야하는가?

  • 입력 유효성 검증
    • 도메인 모델의 현재 상태에 접근하지 않는다.
      • ex. 송금되는 금액은 0보다 커야 한다.
    • 선언적(ex. @NotNull)으로 구현 가능
    • 구문상의(syntactical) 유효성을 검증
  • 비즈니스 규칙 검증
    • 도메인 모델의 현재 상태에 접근한다.
      • ex. 출금 계좌는 초과 출금되어서는 안 된다. 계좌가 존재해야 한다.
    • 유스케이스 맥락 속에서 의미적인(semantical) 유효성을 검증

비즈니스 규칙의 위치

  1. 비즈니스 로직이 있는 도메인 엔티티

    package com.woowa.cleanarchitecture.account.domain;
    
    public class Account {
        public boolean withdraw(Money money, AccountId targetAccountId) {
            if (!mayWithdraw(money)) {   // 초과 출금 불가능
                return false;
            }
    
    				// business logic
        }
    }
  2. 유스케이스 코드에서 도메인 엔티티 사용 전

4-7. 풍부한 도메인 모델 vs. 빈약한 도메인 모델

각자 필요에 맞는 방법을 자유롭게 사용하라.

풍부한 도메인 모델

  • DDD
  • 엔티티에서 가능한 한 많은 도메인 로직과 비즈니스 규칙을 구현
  • 유스케이스 = 도메인 모델의 진입점
    • 도메인 엔티티의 메서드 호출

빈약한 도메인 모델

  • 얇은 엔티티
    • 필드, getter, setter
    • 도메인 로직 포함 X → 유스케이스에 구현됨
  • 풍부한 유스케이스 클래스

4-8. 유스케이스마다 다른 출력 모델

출력 모델은 각 유스케이스에 맞게 구체적이고 꼭 필요한 데이터만 들고 있어야 한다.

가능한 한 적게 반환하자.

public boolean deposit(Money money, AccountId sourceAccountId) {
    Activity deposit = new Activity(null, id, sourceAccountId, id, now(), money);
    activityWindow.addActivity(deposit);

    return true;
}

출력 모델을 공유한다면

  1. 유스케이스간의 강한 결합
  2. SRP 위반

4-9. 읽기 전용 유스케이스는 어떨까?

읽기 전용 유스케이스 = 간단한 데이터 쿼리

  • 읽기 전용 쿼리 서비스와 유스케이스 서비스를 분리한다. (CQRS, Command-Query Responsibility Segregation)
    1. 쿼리를 위한 인커밍 전용 포트
    2. 쿼리 서비스 구현
    3. 데이터베이스를 로드하는 아웃고잉 포트 호출

4-10. 유지보수 가능한 소프트웨어를 만드는 데 어떻게 도움이 될까?

  • 도메인 로직은 원하는대로 구현해도 된다.
  • 입출력 모델은 각 유스케이스별로 독립적으로 모델링하자.
    1. 장기적으로 유지보수하기 더 쉽다.
    2. 유스케이스를 더 이해하기 쉽다.
    3. 같은 모델을 건드리지 않아도 되기 때문에 다른 유스케이스 동시 작업이 가능하다.

느낀점

  • 새로운 게시글을 생성하는 예제에서 post article을 한 반환값으로 게시글이 잘 생성되었다는 표시와 더불어 생성된 게시글을 화면에 표시하기 위해 그 게시글의 내용들을 포함해서 돌려주었다. 만약 이 경우에 최소한의 반환을 위해서는 게시글이 문제없이 생성되었다는 boolean 값 하나와, 클라이언트 쪽에서 해당 값이 true일 경우 다시 post의 내용을 재요청해야한다.
    • 예로 들어준 송금하기 예제는 모델의 상태를 변경하는 update이기 때문에 출력 모델을 공유하면 안 된다는 저자의 말에 수긍이 갔지만, create를 할 경우에는 전체 모델을 읽어오는 과정이 필요할 것이라고 생각된다.

질문 & 논의

  1. p33 2문단 세 계층이 현재 아키텍처에서 아주 느슨하게 결합돼 있기 때문에 필요한 대로 도멩니 코드를 자유롭게 모델링할 수 있다.
  2. p49 만약 의심스럽다면 가능한 한 적게 반환하자.
    • 모델을 생성하는 경우 출력 모델을 공유하지 않고 게시글 생성 유스케이스의 마지막에 읽어오기 유스케이스를 추가하는 방법

0개의 댓글