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

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

입력

유스케이스는 웹 컨트롤러와 같은 인커밍 어댑터로부터 입력을 받습니다.

애플리케이션 코어에 유효하지 않은 입력값이 전달되면 모델의 상태를 해칠 수 있기 때문에 애플리케이션 계층에서 입력에 대한 검증이 이뤄져야 합니다. 하지만 유스케이스의 책임은 비즈니스 규칙을 검증하는 것이지 입력 유효성 검증이 아닙니다.

그렇다면 입력 유효성 검증은 어디서 해야 할까요? 먼저, 어댑터에서 하는 방법이 있습니다. 하지만 그러면 유스 케이스를 사용하는 모든 어댑터에서 똑같은 유효성을 구현해야 한다는 단점이 있습니다. 그리고 만약 유효성 검증을 까먹고 빠트린다면 검증되지 않은 데이터로 인해 우리 모델의 상태를 해칠 수 있습니다. 다른 방법은 입력 모델(input model)이 하게 하는 것입니다. 더 정확하게는 입력 모델의 생성자 내에서 입력 유효성을 검증합니다.

아래 예시에 따르면 송금액(money)가 0보다 작으면 예외가 발생하고, 아예 객체 생성에 실패하게 됩니다.

class SendMoneyCommand(
    val money: Long,
    val sourceAccountId: Long,
    val targetAccountId: Long,
) {
    init {
        if (this.money < 0) {
            throw NegativeMoneyException(money)
        }
    }
}

// throws exception
val result = sendMoney(SendMoneyCommand(-1, 100L, 1004L))

입력 모델이 입력 유효성 검증 책임을 맡게 됨으로써 유스케이스 구현체 주위에는 사실상 오류 방지 계층(anti corruption layer)가 만들어지게 됐습니다. 잘못된 입력을 호출자에게 돌려주는 유스케이스의 보호막이 생긴 것이죠.

각기 다른 유스케이스에 동일한 입력값이 필요한 경우, 동일한 유스케이스를 사용해도 될까요? 예를 들어 ‘게시글 등록’과 ‘게시글 수정’이라는 두 가지의 유스케이스는 거의 동일한 입력값을 필요로 합니다. 하지만 ‘게시글 수정’은 수정하고자 하는 게시글을 특정 지을 게시글의 식별자가 필요합니다. 만약 두 가지 유스케이스에 같은 입력 모델을 공유하면 ‘게시글 수정’에서는 게시글 식별자가 null이면 안되고, ‘게시글 등록’에서는 게시글 식별자에 null을 허용해야 합니다.

불변 커맨드 객체 필드에 null을 허용하는 것 자체만으로도 코드 스멜이지만, 입력 유효성 검증은 더 큰 문제입니다. ‘게시글 등록’과 ‘게시글 수정’에 각기 다른 유효성 검증 로직이 필요하기 때문입니다. 각기 다른 유효성 검증 로직은 또 유스케이스에서 넣어줘야겠죠. 이는 비즈니스 코드가 유효성 검증에 대한 관심사로 오염되는 결과로 이어집니다.

따라서 각 유스케이스 전용 입력 모델을 사용해야 합니다. 유스케이스 전용 입력 모델은 유스케이스를 훨씬 명확하게 만들고, 다른 유스케이스와의 결합도 제거해 불필요한 부수효과가 발생하지 않게 해줍니다. 물론 각 유스케이스마다 새로운 입력 모델을 만들고 검증하는 비용은 추가되겠죠.

비즈니스 규칙 검증

위에서 말했듯이 유스케이스는 입력 유효성 검증에 대한 책임은 없으나 비즈니스 규칙 검증에 대한 책임은 가집니다. 그런데 때때로 입력 유효성 검증과 비즈니스 규칙을 구분하기 어려울 때가 있습니다.

이 둘을 구분하는 실용적인 방법은 비즈니스 규칙을 검증하는 것은 도메인 모델의 현재 상태에 접근해야 하는 반면, 입력 유효성 검증은 그럴 필요가 없다는 것입니다. 비즈니스 규칙 검증은 도메인의 맥락을 필요로 합니다. 예를 들어서 송금되는 금액이 0보다 커야 한다는 규칙은 모델에 접근할 필요가 없기 때문에 입력 유효성 검증으로 구현할 수 있습니다. 하지만 “출금 계좌는 초과 출금되어서는 안 된다”라는 규칙은 모델의 현재 상태에 접근해야 하기 때문에 비즈니스 규칙 검증으로 구현해야 합니다.

이 둘을 구분하지 않고 유스케이스에서 입력 유효성 검증을 한다거나, 입력 모델에서 비즈니스 규칙을 검증하게 되면 특정 유효성 검증 규칙을 찾기 어렵게 됩니다. 따라서 유스케이스에서는 비즈니스 규칙을 검증하는 책임만 갖도록 해야 합니다.

모델 상태 조작

위 과정을 거쳐 비즈니스 규칙이 충족되면, 유스케이스는 입력을 기반으로 도메인 모델의 상태를 변경합니다. 도메인 모델은 풍부한 도메인 모델과 빈약한 도메인 모델로 나뉩니다.

풍부한 도메인 모델에서의 엔티티들은 가능한 한 많은 도메인 로직을 가지며, 상태를 변경하는 메서드를 제공하고, 비즈니스 규칙에 맞는 유효한 변경만을 허용합니다. 따라서 많은 비즈니스 규칙이 유스케이스 구현체가 아닌 엔티티에 위치합니다. 반면 빈약한 도메인 모델에서의 엔티티들은 상태를 표현하는 필드와 접근자 및 설정자 외에는 어떤 도메인 로직도 가지지 않아서, 유스케이스 클래스에 도메인 로직이 구현돼있습니다.

즉 풍부한 도메인 모델에서는 엔티티가 모델의 상태를 조작하고, 빈약한 도메인 모델에서는 유스케이스 클래스가 모델의 상태를 조작합니다.

출력

출력은 호출자에게 꼭 필요한 데이터만 들고 있어야 합니다. 만약 여러 유스케이스 간에 출력 모델을 공유하게 되면 유스케이스 간에 강한 결합이 생깁니다. 예를 들어서 한 유스케이스에서 새로운 필드가 생겨서 출력 모델에 필드를 추가하게 되면, 이 출력 모델을 사용하고 있는 다른 유스케이스에서는 관련 없는 필드가 추가되는 셈입니다. 따라서 출력 모델에서도 단일 책임 원칙을 적용하여 유스케이스 간에 출력 모델을 분리하는 것이 좋습니다.

도메인 엔티티를 출력 모델로 사용하고 싶을 때도 있는데, 이러면 도메인의 변경할 이유가 필요 이상으로 늘어나기 때문에 추천하지는 않습니다.