유스케이스
💡 어댑터 → 입력 모델 클래스 → 유스케이스 → 도메인 모델 → 유스케이스 → 출력 모델 클래스 → 어댑터
도메인 모델
비즈니스 로직의 중심이 되는 도메인 엔티티를 가장 먼저 구성한다.
이 때 엔티티는 영속성 계층(JPA 등)의 엔티티가 아니다!
Account 엔티티 예시
계좌 엔티티이므로 입금과 출금을 할 수 있는 메서드를 가지고 있어야 한다.
ActivityWindow라는 필드를 가져 지난 며칠 혹은 주간의 범위에 해당하는 활동을 보유해둔다.
baselineBalance 는 ActivityWindow의 첫 활동 이전에 있던 잔고 금액을 나타낸다.
현재 잔고는 기준 잔고에 ActivityWindow의 모든 활동 잔고를 합하면 된다.
입력 모델 클래스
입력 유효성 검증
유스케이스에 입력을 전달하기 전 어댑터에서 검증하는 방식은 각 어댑터에서 전부 유효성 검증을 구현해야 하기 때문에 실수할 수 있다.
애플리케이션 계층에서 입력 유효성을 검증하지 않으면 바깥쪽으로부터 유효하지 않은 입력 값을 전달받아 모델의 상태를 해칠 수 있다.
입력 모델 클래스 특징
유스케이스의 입력으로 들어오는 클래스
이 클래스의 생성자에서 입력 유효성 검증을 하도록 한다
필드는 모두 final로 지정해 불변 필드로 만들어 상태 변경할 수 없도록 한다.
유스케이스 API의 일부이므로 인커밍 포트 패키지에 위치해야 한다.
Bean Validation API 사용해 유효성 검증할 수도 있다.
사실상 잘못된 입력을 호출자에게 돌려주는 오류 방지 계층을 만든 것
생성자의 힘
생성자에 많은 책임을 두고 있다면 빌더 사용하는 것보다는 생성자를 사용하는 게 좋다.
Builder를 사용하면 build() 메서드 내부에 생성자 호출을 숨기고 직관적으로 어떤 필드에 어떤 값을 넣는지 알 수 있다.
새로운 필드를 추가하기 위해 생성자와 빌더에 필드를 추가했는데, 빌더를 호출하는 코드에 새로운 필드를 추가하지 않으면 유효하지 않은 상태가 된다
필드에 null이 들어가게 되는데 알아차리지 못할 수 있지만 컴파일러는 유효하지 않은 상태의 불변 객체를 만드는 것까지는 경고해주지 못한다.
생성자를 직접 사용하면 이런 불상사를 막을 수 있고, IDE에서 어떤 필드에 어떤 값을 넣는지 표시해주기 때문에 훨씬 좋을 수 있다.
입력 모델의 공유
두 유스케이스에 비슷한 정보가 필요할 경우 하나의 입력 모델을 사용하려 할 수 있다.
예를 들어 같은 입력 모델을 공유하면 계좌 정보 업데이트하기는 소유자 ID에 null을, 계좌 등록하기에서는 계좌 ID에 null을 허용해야 할 수 있다.
불변 커맨드 객체의 필드에 대해 null을 유효한 상태로 받아들이는 것은 code smell이므로 지양해야 한다.
또한 입력 유효성 검증 로직도 각 요구사항에 맞춰 달라져야 한다.
유스케이스 전용 입력 모델을 만들어 명확하게, 결합을 제거해 불필요한 부수효과가 발생하지 않도록 한다.
유스케이스
역할
인커밍 어댑터로부터 입력을 받음
비즈니스 규칙 검증
모델 상태 조작 (도메인 객체 상태 변경 후 영속성 어댑터로 구현된 포트로 상태를 전달해 저장하도록 함)
출력을 반환 (아웃고잉 어댑터에서 온 출력값을 알맞게 변환 후 인커밍 어댑터로 반환)
입력 값에 대한 유효성 검증은 유스케이스가 아닌 다른 부분에서 진행
유스케이스를 한 서비스 클래스에 넣지 않고 분리된 각각의 서비스로 만들면 넓은 서비스 문제를 피할 수 있다.
유스케이스는 interface 형태이다.
하나의 서비스는 하나의 유스케이스를 구현하고, 도메인 모델을 변경하고, 변경된 상태를 저장하기 위해 아웃고잉 포트를 호출한다.
비즈니스 규칙 검증
유스케이스 로직의 일부
입력 규칙 검증과 달리 도메인 모델 현재 상태에 접근해야 한다.
유스케이스 맥락 속에서 의미적인 유효성을 검증해야 한다.
예를 들어 출금 계좌는 초과 출금 되면 안된다 라는 규칙은 도메인 모델의 현재 상태에 접근해 검증이 필요하다.
비즈니스 규칙의 위치
도메인 엔티티 안에 넣으면 이 규칙을 지켜야 하는 비즈니스로직 바로 옆에 규칙이 위치하게 되므로 위치 정하는 것도 쉽고 추론이 쉽다.
혹은 유스케이스 코드에서 도메인 엔티티 사용 전에 검증해주어도 된다.
유효성 검증이 실패할 경우 유효성 검증 전용 예외를 어댑터로 던지고, 어댑터는 적절한 방법으로 처리해야 한다.
풍부한 vs 빈약한 도메인 모델
풍부한 도메인 모델
애플리케이션의 코어에 있는 엔티티에서 가능한 한 많은 도메인 로직 구현
상태 변경 메서드를 제공
비즈니스 규칙에 맞는 유효한 변경만 허용
이 때 유스케이스는 도메인 모델의 진입점으로만 사용된다. 사용자의 의도만 표현하고, 실제 작업을 수행하는 체계화된 도메인 엔티티 메서드를 호출
빈약한 도메인 모델
상태를 표현하는 필드와 상태 조회/변경 메서드(getter/setter)만 가짐
도메인 로직이 유스케이스에 구현되어 있다. 비즈니스 규칙 검증, 엔티티 상태 변경, 아웃고잉 포트에 엔티티 전달 등의 책임을 모두 가진다.
유스케이스의 출력 모델
가능한 한 적은 데이터를 반환
유스케이스 간 출력 모델도 입력 모델처럼 따로따로 가져가야 객체 지향적이다.
도메인 엔티티를 출력 모델로 사용하지 않도록 한다.
읽기 전용 유스케이스
사실상 간단한 데이터 쿼리 작업이지만, 전체 프로젝트 맥락에서 유스케이스로 간주된다면 다른 유스케이스와 비슷한 방식으로 구현해야 한다.
작업이 간단하여 프로젝트 맥락에서 유스케이스로 간주되지 않는다면, 실제 유스케이스와 구분하기 위해 쿼리로 구현할 수 있다.
간단한 쿼리 요청이라면 인커밍 전용 포트를 만들고 쿼리 서비스에 로직을 구현한다.
쿼리 서비스는 쿼리 인커밍 포트를 구현하고 DB로부터 데이터를 로드하기 위해 아웃고잉 포트를 호출한다.
이렇게 구분해서 사용할 경우 쓰기 가능 유스케이스와 코드 상에서 명확히 구분할 수 있다.
CQS: Command-Query Separation / CQRS: Command-Query Responsibility Segregation
변경 작업이 발생하는 커맨드와 단순히 조회하는 쿼리를 구분하도록 하는 디자인 패턴
읽기와 쓰기는 하나의 메서드에 동시에 발생하면 안된다.
읽기와 쓰기가 동시에 발생하지 않아 성능 최적화와 유지보수에 도움을 준다.
Last updated