유스케이스

💡 어댑터 → 입력 모델 클래스 → 유스케이스 → 도메인 모델 → 유스케이스 → 출력 모델 클래스 → 어댑터

도메인 모델

  • 비즈니스 로직의 중심이 되는 도메인 엔티티를 가장 먼저 구성한다.

  • 이 때 엔티티는 영속성 계층(JPA 등)의 엔티티가 아니다!

  • Account 엔티티 예시

    • 계좌 엔티티이므로 입금과 출금을 할 수 있는 메서드를 가지고 있어야 한다.

    • ActivityWindow라는 필드를 가져 지난 며칠 혹은 주간의 범위에 해당하는 활동을 보유해둔다.

    • baselineBalance 는 ActivityWindow의 첫 활동 이전에 있던 잔고 금액을 나타낸다.

    • 현재 잔고는 기준 잔고에 ActivityWindow의 모든 활동 잔고를 합하면 된다.

입력 모델 클래스

입력 유효성 검증

  • 유스케이스에 입력을 전달하기 전 어댑터에서 검증하는 방식은 각 어댑터에서 전부 유효성 검증을 구현해야 하기 때문에 실수할 수 있다.

  • 애플리케이션 계층에서 입력 유효성을 검증하지 않으면 바깥쪽으로부터 유효하지 않은 입력 값을 전달받아 모델의 상태를 해칠 수 있다.

입력 모델 클래스 특징

  • 유스케이스의 입력으로 들어오는 클래스

  • 이 클래스의 생성자에서 입력 유효성 검증을 하도록 한다

  • 필드는 모두 final로 지정해 불변 필드로 만들어 상태 변경할 수 없도록 한다.

  • 유스케이스 API의 일부이므로 인커밍 포트 패키지에 위치해야 한다.

  • Bean Validation API 사용해 유효성 검증할 수도 있다.

  • 사실상 잘못된 입력을 호출자에게 돌려주는 오류 방지 계층을 만든 것

생성자의 힘

  • 생성자에 많은 책임을 두고 있다면 빌더 사용하는 것보다는 생성자를 사용하는 게 좋다.

  • Builder를 사용하면 build() 메서드 내부에 생성자 호출을 숨기고 직관적으로 어떤 필드에 어떤 값을 넣는지 알 수 있다.

  • 새로운 필드를 추가하기 위해 생성자와 빌더에 필드를 추가했는데, 빌더를 호출하는 코드에 새로운 필드를 추가하지 않으면 유효하지 않은 상태가 된다

  • 필드에 null이 들어가게 되는데 알아차리지 못할 수 있지만 컴파일러는 유효하지 않은 상태의 불변 객체를 만드는 것까지는 경고해주지 못한다.

  • 생성자를 직접 사용하면 이런 불상사를 막을 수 있고, IDE에서 어떤 필드에 어떤 값을 넣는지 표시해주기 때문에 훨씬 좋을 수 있다.

입력 모델의 공유

  • 두 유스케이스에 비슷한 정보가 필요할 경우 하나의 입력 모델을 사용하려 할 수 있다.

  • 예를 들어 같은 입력 모델을 공유하면 계좌 정보 업데이트하기는 소유자 ID에 null을, 계좌 등록하기에서는 계좌 ID에 null을 허용해야 할 수 있다.

  • 불변 커맨드 객체의 필드에 대해 null을 유효한 상태로 받아들이는 것은 code smell이므로 지양해야 한다.

  • 또한 입력 유효성 검증 로직도 각 요구사항에 맞춰 달라져야 한다.

  • 유스케이스 전용 입력 모델을 만들어 명확하게, 결합을 제거해 불필요한 부수효과가 발생하지 않도록 한다.

유스케이스

  • 역할

    1. 인커밍 어댑터로부터 입력을 받음

    2. 비즈니스 규칙 검증

    3. 모델 상태 조작 (도메인 객체 상태 변경 후 영속성 어댑터로 구현된 포트로 상태를 전달해 저장하도록 함)

    4. 출력을 반환 (아웃고잉 어댑터에서 온 출력값을 알맞게 변환 후 인커밍 어댑터로 반환)

  • 입력 값에 대한 유효성 검증은 유스케이스가 아닌 다른 부분에서 진행

  • 유스케이스를 한 서비스 클래스에 넣지 않고 분리된 각각의 서비스로 만들면 넓은 서비스 문제를 피할 수 있다.

  • 유스케이스는 interface 형태이다.

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

비즈니스 규칙 검증

  • 유스케이스 로직의 일부

  • 입력 규칙 검증과 달리 도메인 모델 현재 상태에 접근해야 한다.

  • 유스케이스 맥락 속에서 의미적인 유효성을 검증해야 한다.

  • 예를 들어 출금 계좌는 초과 출금 되면 안된다 라는 규칙은 도메인 모델의 현재 상태에 접근해 검증이 필요하다.

  • 비즈니스 규칙의 위치

    • 도메인 엔티티 안에 넣으면 이 규칙을 지켜야 하는 비즈니스로직 바로 옆에 규칙이 위치하게 되므로 위치 정하는 것도 쉽고 추론이 쉽다.

    • 혹은 유스케이스 코드에서 도메인 엔티티 사용 전에 검증해주어도 된다.

  • 유효성 검증이 실패할 경우 유효성 검증 전용 예외를 어댑터로 던지고, 어댑터는 적절한 방법으로 처리해야 한다.

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

  • 풍부한 도메인 모델

    • 애플리케이션의 코어에 있는 엔티티에서 가능한 한 많은 도메인 로직 구현

    • 상태 변경 메서드를 제공

    • 비즈니스 규칙에 맞는 유효한 변경만 허용

    • 이 때 유스케이스는 도메인 모델의 진입점으로만 사용된다. 사용자의 의도만 표현하고, 실제 작업을 수행하는 체계화된 도메인 엔티티 메서드를 호출

  • 빈약한 도메인 모델

    • 상태를 표현하는 필드와 상태 조회/변경 메서드(getter/setter)만 가짐

    • 도메인 로직이 유스케이스에 구현되어 있다. 비즈니스 규칙 검증, 엔티티 상태 변경, 아웃고잉 포트에 엔티티 전달 등의 책임을 모두 가진다.

유스케이스의 출력 모델

  • 가능한 한 적은 데이터를 반환

  • 유스케이스 간 출력 모델도 입력 모델처럼 따로따로 가져가야 객체 지향적이다.

  • 도메인 엔티티를 출력 모델로 사용하지 않도록 한다.

읽기 전용 유스케이스

  • 사실상 간단한 데이터 쿼리 작업이지만, 전체 프로젝트 맥락에서 유스케이스로 간주된다면 다른 유스케이스와 비슷한 방식으로 구현해야 한다.

  • 작업이 간단하여 프로젝트 맥락에서 유스케이스로 간주되지 않는다면, 실제 유스케이스와 구분하기 위해 쿼리로 구현할 수 있다.

  • 간단한 쿼리 요청이라면 인커밍 전용 포트를 만들고 쿼리 서비스에 로직을 구현한다.

  • 쿼리 서비스는 쿼리 인커밍 포트를 구현하고 DB로부터 데이터를 로드하기 위해 아웃고잉 포트를 호출한다.

  • 이렇게 구분해서 사용할 경우 쓰기 가능 유스케이스와 코드 상에서 명확히 구분할 수 있다.

CQS: Command-Query Separation / CQRS: Command-Query Responsibility Segregation

  • 변경 작업이 발생하는 커맨드와 단순히 조회하는 쿼리를 구분하도록 하는 디자인 패턴

  • 읽기와 쓰기는 하나의 메서드에 동시에 발생하면 안된다.

  • 읽기와 쓰기가 동시에 발생하지 않아 성능 최적화와 유지보수에 도움을 준다.

Last updated