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

Jimin Lim·2023년 6월 4일
1

Architecture

목록 보기
4/23
post-thumbnail

✅ 04. 유스케이스 구현하기

🔗 도메인 모델 구현하기

한 계좌에서 다른 계좌로 송금하는 유스케이스를 구현하고 있다.

Account 전체 코드

  • baselineBalance: 첫번째 활동 바로 전의 베이스 잔고, 기준 잔고에 activityWindow의 모든 활동들의 잔고를 합한 값이 현재 총 잔고가 된다.
  • Activity: Activity(ownerAccountId, sourceAccountId, targetAccountId, timestamp, money) 을 가진다.
  • activityWindow: 값 객체에 특정 범위에 해당하는 활동만 가지도록 한다.

🔗 유스케이스 둘러보기

일반적으로 유스케이스는 다음과 같은 단계를 따른다.

  1. 입력을 받는다 --> incoming adapter (입력 유효성 검증은 다른 곳에서)
  2. 비즈니스 규칙을 검증한다
  3. 모델 상태를 조작한다 --> 상태 변경, 영속성 어댑터를 통해 구현된 포트로 상태 전달, 아웃고잉 어댑터 호출
  4. 출력을 반환한다 --> 아웃고잉 어댑터에서 온 출력값을 출력 객체로 반환

  • SendMoneyService는 SendMoneyuseCase(인커밍 포트 인터페이스)를 구현한다.
  • 아웃고잉 포트 인터페이스인 LoadAccountPort, UpdateAccountStatePort 을 호출 한다.

🔗 입력 유효성 검증

호출자가 유효성 검증하고 넘기는지 확신할 수 없고, 호출자마다 유효성 검사를 해야하기에 애플리케이션 계층에서 해야한다.

input model

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

SendMoneyUseCase에서 넘기는 SendMoneyCommand의 생성자에서 유효성 검증을 하도록 한 예시다. SendMoneyCommand는 api의 일부이기에 port - in에 위치하도록 하여 애플리케이션의 코어에 남긴다.

🔗 생성자의 힘

위에서 살펴본 SendMoneyCommand는 불변이기에 생성자가 모든 파라미터를 가지고 있고, 생성자에서 유효성 검증까지 하므로 유효하지 않은 상태의 객체를 만드는 것은 불가능하다.

만약 더 많은 파라미터를 가진다면, 생성자를 private으로 만들고 build 메서드 내에서 생성자를 호출하도록 하여 builder 패턴으로 만들 수 있다. 하지만 새로운 필드가 추가된다면 생성자와 빌더에 새로운 필드를 추가해야하는데, 이는 컴파일 시점에 유효하지 않은 상태의 불변 객체를 만드는 것을 막지 못한다.

빌더 뒤에 숨기는 대신 생성자를 직접 사용한다면, 필드가 수정될 때마다 컴파일 에러로 유효하지 않은 상태의 불변 객체를 잡을 수 있을 것이다.

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

  • 계좌 등록하기: 소유자의 id 필요
  • 계좌 정보 업데이트하기: 계좌 id 필요

위의 유스케이스가 같은 입력 모델을 공유한다면, 각각의 id를 null로 가져야 한다. 하지만 불변 커맨드 객체의 필드에 대해 null을 유효한 상태로 둔다면 서로 다른 유효성 검증 로직을 넣고, 비즈니스 코드를 입력 유효성 검증과 관련된 관심사로 오염시킨다.

즉, 각각 따로 만들어 결합도를 제거하는 편이 낫다!

🔗 비즈니스 규칙 검증하기

입력 유효성 검증은 유스케이스 로직의 일부가 아니지만, 비즈니스 규칙 검증은 유스케이스 로직의 일부다.

입력 유효성 검증비즈니스 규칙 검증
@NotNull 과 같이 선언적 구현맥락 필요
현재 상태 접근 x현재 도메인 모델의 상태 접근

요약하자면,

  • 출금 계좌는 초과 출금되어서는 안된다 --> 현재 상태 접근, 비즈니스 규칙 검증
  • 송금되는 금액은 0보다 커야 한다 --> 모델에 접근하지 않음, 입력 유효성 검증

비즈니스 규칙 검증은 아래와 같이 도메인 엔티티 안에 넣는게 가장 좋다.

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

만약, 도메인 엔티티에서 검증하는게 여의치 않다면 유스케이스 코드에서 도메인 엔티티를 사용하기 전에 사용해도 된다.

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

Rich Domain Model

애플리케이션의 코어에 있는 엔티티에서 가능한 한 많은 도메인 로직이 구현된다. 이는 예제의 Account 엔티티에서 따랐던 방식처럼, 엔티티들이 상태를 변경하는 메서드를 제공하고, 비즈니스 규칙에 맞는 유효한 변경을 허용하는 것이다.

유스케이스 구현체 대신, 엔티티에 로직이 주로 위치하는 것이다. 예를 들어 송금하기 유스케이스 서비스는 출금, 입금 계좌 엔티티를 로드하고 엔티티에 위치하는 withdraw(), deposit() 메서드를 호출한 후 DB에 결과를 보낸다.

Anemic Domain Model

엔티티는 getter, setter 메서드만 포함하고 도메인 로직은 유스케이스 클래스에 구현하는 것이다.

위의 두 방식은 스타일에 맞게 선택해서 사용하는 편이다.

🔗 유스케이스마다 다른 출력 모델

입력 모델과 비슷하게 유스케이스에 맞게 구체적일수록 좋으며, 호출자에게 필요한 데이터만 들고 있어야 한다.

🔗 읽기 전용 유스케이스는 어떨까?

계좌의 잔액을 조회하는 읽기 전용 유스케이스는 애플리케이션 코어의 관점에서 간단한 데이터 쿼리다. 그렇기에 유스케이스로 간주되지 않는다면, 실제 유스케이스와 구분하기 위해 쿼리로 구현할 수 있다.

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

@RequiredArgsConstructor
class GetAccountBalanceService implements GetAccountBalanceQuery { //유스케이스가 아닌 쿼리로 생성 

	private final LoadAccountPort loadAccountPort;

	@Override
	public Money getAccountBalance(AccountId accountId) {
		return loadAccountPort.loadAccount(accountId, LocalDateTime.now())
				.calculateBalance();
	}
}

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

결론적으로 유스케이스 간 입출력 모델을 독립적으로 모델링하자!

✨ 참고자료

https://github.com/wikibook/clean-architecture

profile
💻 ☕️ 🏝 🍑 🍹 🏊‍♀️

0개의 댓글