[Hexagonal Architecture] 2. Use Case 구현하기

2022. 11. 18. 06:42Dev/Clean Architecture

반응형

헥사고날 아키텍처는 도메인 중심의 아키텍처로 적합합니다.
도메인 엔티티를 먼저 만들고, 그 후 유스케이스를 구현하는 순서로 진행합니다.

유스케이스의 동작은 아래와 같은 순서로 진행됩니다.

  1. 입력값에 대한 유효성을 검증하고
  2. 시스템을 동작하기 전에 비즈니스 규칙을 검증한 후
  3. 엔티티 모델을 조작하고
  4. 출력 결과값을 반환합니다

※ 참고로 코드는 kotlin + webflux + coroutine 로 구성되어있습니다.


1. 입력 유효성 검증

1.1. input model

web adapter의 일부분으로, application - port - in 에 위치합니다.

모든 필드는 불변 필드로 지정하고 (final)
유효성 검증 설정하여 잘못된 값 입력시 익셉션 throw 시킵니다.(@NotNull, @Min 등)

입력값의 단순 오류를 체크하는 구문상의(syntactical) 유효성 검증으로, 도메인의 현재 상태와 무관합니다.

data class GetNoticesCommand(
    @Min(0) val page: Int,
    @Min(5) val size: Int,
)
data class RegisterNoticeCommand(
    @NotBlank val title: String,
    @NotBlank val content: String,
)

1.2. Use Case 마다 별도의 input model 사용하기

만일 여러 유스케이스에서 똑같은 입력 모델 구조를 필요로 한다하더라도 각각 별도의 input model을 가질 것을 권장합니다.

입력 모델은 같더라도 각각 유효성 검증이 다를 수도 있고,
당장은 유효성 검증 로직이 같더라도 추후 변경될 수 있기 때문에 SRP 관점에서도 각각 정의하는 것이 좋습니다.

예를들어 A기능과 B기능이 우연히 동일한 입력값을 필요로 하고 있었다고 할때, 두 기능에 동일한 입력 모델을 사용하여 개발을 할 경우,

나중에 A기능에만 입력모델이 변경되었을 때, 함께 사용하고 있던 입력 모델을 변경할 시, B기능에도 영향을 주게되는 문제점이 있습니다.

이는 SRP(단일 책임 원칙)에 위배됩니다.


예를 들면, 아래와 같이 동일한 파라미터를 갖는 input model이 있다고 할때,
정책에 따라 이름은 얼마든지 바뀔수 있으며, 파라미터가 나중에 더 추가되는 등의 일이 일어날 수 있기 때문에
서비스 별로 별도의 입력 모델을 사용하기를 권장합니다.

data class GetNoticesCommand(
    @Min(0) val page: Int,
    @Min(5) val size: Int,
)
data class GetFaqsCommand(
    @Min(0) val page: Int,
    @Min(5) val size: Int,
)

2. Business 규칙 검증

입력 유효성 검증은 단순히 잘못 입력된 값에 대한 검증이기에 유스케이스 로직에 해당되지 않지만
비즈니스 규칙은 실질적인 기능의 검증이기에 유스케이스 로직에 해당됩니다.

따라서 비즈니스 규칙은 도메인 모델의 현재 상태를 고려해야하지만,
입력 유효성 검증은 도메인 모델의 현재 상태를 고려할 필요가 없습니다.

※ tip!
계좌 출금 기능을 예로 들때
비즈니스 규칙에서는 현재 계좌 잔고를 고려해야하지만
input model에서는 현재 계좌 잔고를 확인할 필요가 습니다.


@NotNull, @Min 등의 애너테이션 설정으로 간단히 검증할 수 있는 입력 유효성 검증과 달리
비즈니스 규칙은 현재 도메인 상태에 따른 검증을 해야하기 때문에 기능에 맞춰 메서드를 정의해야합니다.

2.1. domain model에 따른 비즈니스 규칙 위치

비즈니스 규칙은 현재 도메인 상태에 따른 검증을 하는 것이기 때문에 도메인 엔티티 내에 위치하는 것이 적합하지만, 도메인 모델 형태에 따라 위치를 달리할 수 있습니다.

  • anemic domain model
    • 빈약한 도메인 모델
    • 도메인 엔티티는 getter, setter 메서드만 갖고 있습니다.
    • 비즈니스 규칙은 유스케이스에 위치시킵니다
      • 유스케이스에 비즈니스 규칙 뿐만 아니라, 엔티티 상태 변경, persistence 어댑터에 엔티티를 전달 등의 기능을 포함합니다.
  • rich domain model
    • 풍부한 도메인 모델
    • 비즈니스 규칙을 도메인 엔티티에 위치시킵니다.

3. 엔티티 모델 조작

Use Case는 엔티티 모델의 상태를 변경하는 코드를 구현한 곳으로, 실질적인 시스템 동작에 대한 기능을 작성합니다.
incoming port에 위치합니다. (application - port - in)

단순하게 데이터를 읽기위한 작업은(읽기 전용) Suffix에 Query를 붙여 유스케이스와 구분짓습니다. (ex. AccountQuery)

일반 유스케이스(쓰기도 가능한 유스케이스)는 Suffix에 UseCase를 붙입니다. (ex. AccountUseCase)

편의상 이름을 구분지은 것이며, 쿼리 서비스나 유스케이스 서비스 모두 동일하게 동작됩니다.

유스케이스

interface RegisterNoticeUseCase {
    suspend fun registerNotice(command: RegisterNoticeCommand)
}
interface GetNoticeQuery {
    fun getNoticeSummaries(command: GetNoticesCommand): Flow<Notice.Summary>
    suspend fun getNoticeDetail(id: String): Notice.Detail?
}

4. 출력 결과값 반환

output model은 input model과 같이 구체적으로 작성하며, 기능에 맞게 최소한의 데이터만 포함시킵니다.(불필요한 데이터는 내보내지 않습니다.)

input model과 마찬가지로 각 유스케이스별로 별도의 output model을 적용합니다.

SRP(단일책임원칙)에 근거합니다.

728x90
반응형