4장. 유스케이스 구현하기

Seungjae·2022년 6월 9일
0

우아한 스터디

목록 보기
5/10
  • 헥사고날 아키텍처에서 유스케이스를 구현하기 위해 이 책에서 제시하는 방법을 설명한다.
    • 도메인 중심의 아키텍처에 적합하기 때문에 도메인 엔티티를 만드는 것으로 시작한 후 도메인 엔티티를 중심으로 유스케이스 구현!

도메인 모델 구현하기


  • 예제에서 한 계좌에서 다른 계좌로 송금하는 유스케이스를 구현!

  • Account 엔티티 → 실제 계좌의 현재 스냅숏 제공

  • Activity 엔티티 → 계좌에 대한 모든 입금과 출금을 포착

  • 입금과 출금은 새로운 Activity를 추가하는 것에 불과하다.

  • 출금하기 전에는 잔고를 초과하는 금액은 출금할 수 없도록 하는 비즈니스 규칙을 검사한다.

유스케이스 둘러보기


  • 유스케이스의 일반적 단계는 아래와 같다.
  1. 입력을 받는다.
  2. 비즈니스 규칙을 검증한다.
  3. 모델 상태를 조작한다.
  4. 출력을 반환한다.
  • 유스케이스 코드는 도메인 로직에만 신경 써야 한다. → 입력 유효성 검증으로 오염되면 안된다! → 책임의 분리!

  • 이때, 유스케이스는 비즈니스 규칙을 검증할 책임이 있다! → 도메인 엔티티와 이 책임을 공유하게 된다.

유스케이스에서 일어나는 일...

  • 비즈니스 규칙을 만족하면 → 유스케이스는 입력을 기반으로 모델의 상태를 변경 → 영속성 어댑터를 통해 구현된 포트로 이 상태를 전달 → 저장 → 아웃고잉 어댑터에서 온 출력을 유스케이스를 호출한 어댑터로 반환할 출력 객체로 변환 → 반환

입력 유효성 검증


  • 입력 유효성 검증은 유스케이스 클래스의 책임이 아니다! → 하지만 애플리케이션 계층의 책임이다!

  • 호출하는 어댑터(인커밍 어댑터)가 유스케이스에 입력을 전달하기 전에 입력 유효성을 검증하면?

    • 유스케이스 입장에서 필요로하는 것을 모두 검증했다고 믿을 수 있나?
    • 하나 이상의 어댑터에서 호출되는 유스케이스의 경우, 유효성 검증을 각 어댑터에서 전부 구현해야 하나?
    • 그 과정에서 실수가 나올 수 있지 않나?
  • 그럼 애플리케이션 계층 책임 OK, 근데 유스케이스 클래스의 책임이 아니면 어디서 검증하지..?

    • “입력 모델”
  • 더 정확히는 입력 모델의 생성자 내에서 입력 유효성을 검증하도록 한다!

    • 이때 조건을 위배하면 예외를 던져서 객체 생성을 막는다!
    • 또한 필드는 final로 하여, 불변으로 한다! → 검증이 끝나면 그 상태는 계속해서 유효하다는 것을 보장할 수 있다!
  • 이때, 입력 모델은 사실상 유스케이스 API의 일부이기 때문에 인커밍 포트 패키지에 위치시킨다!

    • 이로 인해, 유효성 검증이 애플리케이션 코어에 남아 있지만, 유스케이스 코드를 오염시키지는 않는다!
  • 이러한 유효성 검증은 직접 구현하는 방법 외에도, Bean Validation API를 통해 해결할 수 있다!

생성자의 힘


  • 입력 모델은 생성자에서 유효성 검증을 수행한다.

    • 즉, 유효하지 않은 상태의 객체를 만드는 것은 불가능하다!
  • 이때 입력 모델의 파라미터가 많다면..?

    • 빌더 패턴을 활용하면 되지 않을까?
  • 하지만 빌더의 경우, 필드를 새로 추가하는 상황에 놓여있을 때, 그것을 추가하는 것을 쉽게 잊게 된다!

    • 이때 컴파일러도 경고를 해주지 않는다!
    • 런타임이 되어서야, 유효성 검증 로직을 통해 누락된 것을 알게 된다!
  • 필자는 빌더를 쓰는 것보다 긴 생성자를 쓰는 편이 더 좋다고 주장한다!

    • Why?
      • 새로운 필드를 추가하거나 필드를 삭제할 때마다 컴파일 에러를 따라 나머지 코드에 변경사항을 반영할 수 있다!
      • 또한 IDE의 도움을 받을 수 있다!

흠... 이게 맞나?

  • 사실 생성자의 인자는 최대한 적은 것이 맞다고 생각한다.
  • 물론 빌더 패턴의 단점도 인지하였다.
  • 하지만 그렇다고 긴 생성자를 사용하자! 빌더보다는 좋다! 보다는 최대한 관련 값들끼리 래핑하여 인자를 줄여 가독성을 높이는 방향이 좀 더 맞다고 생각한다...!

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


  • 다른 유스케이스에서 같은 입력 모델을 공유할 경우?

    • 특정 경우에 대해서, 입력 모델의 값 중 필요 없는 것, 또는 null 이어야하는 것이 발생할 수 있다.
  • 불변 객체의 필드에 대해서 null을 유효한 상태로 받아들이는 것은 그 자체로 code smell(코드 악취)이다!

  • 물론 Cost가 들지만, 각 유스케이스 전용 입력 모델은 유스케이스를 훨씬 더 명확하게 만들고 다른 유스케이스와의 결합도 제거해서 불필요한 부수효과가 발생하지 않게 한다!

비즈니스 규칙 검증하기


  • 비즈니스 규칙 검증은 유스케이스 로직의 일부다!

  • 비즈니스 규칙은 애플리케이션의 핵심이기 때문이다!

  • 비즈니스 규칙 vs 입력 유효성 검증

    • 비즈니스 규칙 → 도메인 모델의 현재 상태에 접근 필요, 유스케이스 맥락 속에서 의미적인 유효성을 검증
    • 입력 유효성 검증 → 그럴 필요 X, 구문 상의 유효성을 검증
  • 사실 위의 구분이 매우 모호하기에, 도메인 모델의 현재 상태 접근이라는 보다 명확한 구분을 지어놓은 듯 하다!

  • 이로 인해, 특정 유효성 검증 로직을 코드 상의 어느 위치에 둘 지 결정하고 나중에 그것이 어디에 있는지 더 쉽게 찾는데 도움이 된다!

  • 그럼 비즈니스 규칙은 어디에..?

    1. 도메인 엔티티에 넣는다! → 비즈니스 로직 바로 옆에 규칙이 위치하기에, 위치를 정하기도 쉽고, 추론하기 쉽다!
    2. 유스케이스 코드에서 도메인 엔티티를 사용하기 전에 넣는다!

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


풍부한 도메인 모델

  • 코어에 있는 엔티티에서 가능한 한 많은 도메인 로직이 구현된다!
  • 엔티티들은 상태를 변경하는 메서드를 제공하고, 비즈니스 규칙에 맞는 유효한 변경만을 허용한다.
  • 유스케이스는 도메인 모델의 진입점으로 동작한다.
  • 유스케이스는 사용자의 의도만을 표현하면서 이 의도를 실제 작업을 수행하는 체계화된 도메인 엔티티 메서드 호출로 변환한다!
  • 많은 비즈니스 규칙이 유스케이스 구현체 대신 엔티티에 위치한다.

빈약한 도메인 모델

  • 엔티티 자체가 굉장히 얇다.
  • 상태를 표현하는 필드와 getter, setter만 포함!
  • 도메인 로직을 가지고 있지 않다!
  • 도메인 로직이 유스케이스 클래스에 구현돼 있다!
  • 비즈니스 규칙을 검증하고, 엔티티의 상태를 바꾸고, 데이터베이스 저장을 담당하는 아웃고잉 포트에 엔티티를 전달할 책임 모두 유스케이스 클래스에 있다!
  • 풍부함이 유스케이스로!

그래서 나는..?

  • 나는 개인적으로 빈약한 도메인 모델을 선호한다.
  • Why?
    • 물론 저 방식처럼 풍부함을 유스케이스로 모두 넣기보다는, 각각을 다 클래스로 나눌 것 같다.
    • 그로인해, 테스트가 더 용이해지고 책임이 조금 더 분리된다고 생각한다.
    • 도메인 모델이 풍부해지면, 그리고 로직이 복잡해지면 결국 독립적인 도메인 모델이 아니라, 어딘가 의존 관계가 발생할 가능성도 높아지고, 가독성도 떨어지지 않을까?
    • 풍부한 도메인 모델은 그 상태를 변경하기 위해 setter를 허용해야한다는 단점이 있지만, 빈약한 도메인 모델은 불변 객체를 가져갈 수 있다.
    • 예를 들어 송금 유스케이스를 조금 더 복잡하게 가정해보자.
      • A가 B에게 돈을 보낼 수 있다.
      • 이때 A와 B에게 각각 입금, 출금 되었다는 메세지가 전달된다.
    • 이때 송금하기와 메세지를 보낸다는 유스케이스가 분리되어야할까? 나는 이것은 동일한 유스케이스 내에 존재하는 비즈니스 로직들이라고 생각한다.
    • 그렇기에 각각은 MoneySender, MessageSender 같은 각 비즈니스 로직들에 대한 클래스로 뽑아내고 SendMoneyUsecase에서는 이 로직들을 순서에 맞게 오케스트레이션만 해주면 된다고 생각한다.

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


  • 입력과 비슷하게 출력도 가능하면 각 유스케이스에 맞게 구체적인 것이 좋다!

    • 출력은 호출자에게 꼭 필요한 데이터만 들고 있어야 한다!
  • 유스케이스들 간에 같은 출력 모델을 공유하게 되면 유스케이스들도 강하게 결합된다.

  • 공유 모델은 결국 여러 이유로 점점 커지게 돼 있다!

    • SRP를 적용하고 모델을 분리해서 유지하는 것은 유스케이스에 결합을 제거하는데 도움이 된다!
  • 같은 이유로 도메인 엔티티를 출력 모델로 사용하고 싶은 유혹도 견뎌야 한다!

    • 도메인 엔티티를 변경할 이유를 필요 이상으로 늘어나는 것을 원치 않을 것이다.

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


  • 애플리케이션 코어의 관점에서 그저 읽기 전용, 유스케이스라고 언급하기에 조금 이상한 경우, 그냥 간단한 데이터 쿼리라고 취급한다!

    • 물론 전체 프로젝트 맥락에서 이러한 작업이 유스케이스로 분류된다면 유스케이스처럼 구현하고 처리!
  • 인커밍 전용 포트를 만들고 이를 ‘쿼리 서비스’에서 구현한다.

    • 유스케이스 서비스와 동일한 방식으로 동작한다!

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


  • 입출력 모델을 독립적으로 모델링 함으로써 원치 않는 부수효과를 피할 수 있다.

  • 물론 이로 인해 많은 입출력모델 클래스 + 매핑하는 Cost가 생기게 된다!

  • 하지만 장기적으로보면, 유스케이스를 명확하게 이해할 수 있고, 유지보수 측면에서도 매우 큰 장점을 얻을 수 있다!

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

profile
코드 품질의 중요성을 아는 개발자 👋🏻

0개의 댓글

관련 채용 정보