책 일지/만들면서 배우는 클린 아키텍쳐

4장 유스케이스 구현하기

worldi 2024. 1. 7. 14:06

육각형 아키텍쳐의 장점은 애플리케이션, 웹, 영속성 계층이 아주 느슨하게 결합돼 있다는 점이다. 이를 통해, 도메인 코드를 자유롭게 모델링 하고, DDD, 풍부하거나 빈약한 도메인 모델을 구현할 수 있다.

예제에 나와 있는 Account 도메인 값객체인 ActivityWindow에 대한 설명

 

유스케이스

  • 입력의 유효성 검증 책임은 없다.
  • 비즈니스 규칙을 검증할 책임이 있다. 도메인 엔티티와 이 책임을 공유한다. 유스케이스는 입력을 기반으로 어떤 방법으로든 모델의 상태를 변경한다.
  • 아웃고잉 어댑터를 호출한다. 아웃고잉 어댑터에서 온 출력값을, 유스케이스를 호출한 어댑터로 반환할 출력 객체로 변환한다.

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

입력 유효성 검증

애플리케이션 계층에서 이를 검증한다. 이는, 애플리케이션 코어의 바깥쪽으로 부터 유효한 입력값을 보장하기 위함이다. 이를 위해 입력 모델에서 검증한다.

유효성 검증에 Bean Validation API를 사용할 수 있다

@Getter
public class SendMoneyCommand extends SelfValidating<SendMoneyCommand>{

	public SendMoneyCommand(
		Account.AccountId sourceAccoundId, 
		Account.AccountId targetAccoundId){
		
		this.validateSelf();
	}
}

SelfValidating 추상 클래스는 validateSelf 메서드를 통해 생성자 코드의 마지막 문장에서 이메서드를 호출한다. 이는 Bean Validataion 애너테이션을 검증하고, 유효성 검증 규칙을 위반하면 예외를 던진다.

이를 통해, 오류 방지 계층을 만든다. 하위계층을 호출하는 계층형 아키텍처에서의 계층이 아니라 잘못된 입력을 호출자에게 돌려주는 유스케이스 보호막을 의미한다.

생성자의 힘

만약 파라미터가 많다면, 생성자를 Private 메서드로 만들고, 빌더 패턴을 이용할 수 있다.

하지만, 새로운 필드를 추가하는 것을 까먹고 유효하지 않은 불변 객체에 대해선 컴파일러는 에러를 취하지 않는다. 반대로 생성자는 이에 대해 컴파일 에어를 통해 변경 사항을 확인할 수 있을 것이다.

취향차이가 아닐까 한다.. 생성자는 반대로 IDE 에 의존적이게 되지 않을까..?

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

유스케이스마다, 다른 유효성 검증이 필요하다. 다른 입력 모델이 필요하다.

이는, 당연이 null 허용과 더불어 다른 유스케이스의 결합이 생길 수 있는 부작용을 막는다.

비즈니스 규칙 검증하기

입력 유효성 검증은 유스케이스 로직의 일부가 아닌 반면, 비즈니스 규칙 검증은 분명히 유스케이스 로직의 일부다.

입력 유효성을 검증하는 것은 구문상의 유효성을 검증하는 것이고, 비즈니스 규칙은 유스케이스 맥락 속에서 의미적인 유효성을 검증하는 일이다.

출금 계좌는 초과 출금 되어 서는 안된다. → 모델의 상태에 접근한다는 측면해서, 비즈니스 규칙이다.

송금되는 금액은 0보다 커야 한다. → 모델에 접근하지 않고도 검증될 수 있다. 입력 유효성 검증이다.

비즈니스 규칙 검증은 어디에서 해야할까?

  • 도메인 엔티티 → 비즈니스 로직 바로 옆에 규칙이 위치하여, 추론이 쉽다.
  • 유스케이스 코드 → 도메인 엔티티 사용하기 전에

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

  • 육각형 아키텍처는 도메인 모델을 구현하는 방법에 대해서는 열려있다.
  • 만약 풍부한 도메인 모델을 추구한다면, 엔티티에서 가능한 한 많은 도메인 로직이 구현된다.
  • 반대로 빈약한 도메인 모델을 추구한다면, 도메인 로직이 유스케이스 클래스에 구현된다.
    • 비즈니스 규칙 검증, 엔티티 상태 바꾸고, 데이터 베이스 저장 담당하는 아웃고잉 포트에 엔티티를 전달하는 책임도 유스케이스 클래스에 있다.

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

유스케이스 들 간에 출력 모델을 공유하게 되면 유스케이스들도 강하게 결합된다. 단일 책임 원칙을 적용하고, 모델을 분리해서 유지하는 것은 유스케이스의 결합을 제거하는데 도움이 된다.

핵심은 가능한 한 적게, 필요한 값만 반환한다.

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

간단한 데이터 쿼리일 경우, 유스케이스로 간주되지 않는다면 쿼리로 표현한다.

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

@RequiredArgsConstructor
class GetAccountBalanceService implements GetAcccountBalanceQuery {
	private final LoadAccountPort loadAccountPort;
	
	@Override
	public Money getAccountBalance (AccountId accountId) {
	return loadAccountPort.loadAccount(accountId, LocalDateTime.now().calcaulateBalance(););	
}
}

읽기 전용 쿼리는 쓰기가 가능한 유스케이스(커맨드)와 구분된다. 이는 CQS와 CQRS 같은 개념과 잘 맞는다.

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

유스케이스 별로 모델을 만들면 유스케이스를 명확하게 이해할 수 있고, 장기적으로 유지보수하기도 더 쉽다.

꼼꼼한 입력 유효성 검증, 유스케이스 별 입출력 모델은 지속가능한 코드를 만드는데 도움이 된다.