Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

4장 유스케이스 구현하기 #6

Open
wants to merge 2 commits into
base: mychum1
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
143 changes: 143 additions & 0 deletions docs/ch04.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
다른 계좌로 송금하는 유스케이스 구현해보기

## 도메인 모델 구현하기

1. ActivityWindow에 지난 활동들을 포착한다.
2. 출금 전에 잔고를 초과하는지 체크하는 비즈니스 규칙을 검사한다.

## 유스케이스 둘러보기

### 단계

1. 입력을 받는다.
2. 비즈니스 규칙을 검증한다.
3. 모델 상태를 조작한다.
4. 아웃고잉 어댑터에서 온 출력을 유스케이스를 호출한 어댑터로 반환한다.

입력 유효성 검증은 다른 곳에서 처리한다.

유스케이스는 비즈니스 규칙을 검증하고, 도메인 엔티티와 이 책임을 공유한다.(잔고 초과 체크)

## 입력 유효성 검증

유스케이스는 하나 이상의 어댑터에서 호출되므로 유효성 검증을 각 어댑터에서 전부 구현하면서 휴먼 에러가 발생할 수 있다. 따라서 **입력 모델** 의 생성자 내에서 입력 유효성을 검증한다.

```java
package io.reflectoring.buckpal.account.application.port.in;

import io.reflectoring.buckpal.account.domain.Account.AccountId;
import io.reflectoring.buckpal.account.domain.Money;
import io.reflectoring.buckpal.common.SelfValidating;
import lombok.EqualsAndHashCode;
import lombok.Value;

import javax.validation.constraints.NotNull;

@Value
@EqualsAndHashCode(callSuper = false)
public
class SendMoneyCommand extends SelfValidating<SendMoneyCommand> {

@NotNull
private final AccountId sourceAccountId;

@NotNull
private final AccountId targetAccountId;

@NotNull
private final Money money;

public SendMoneyCommand(
AccountId sourceAccountId,
AccountId targetAccountId,
Money money) {
this.sourceAccountId = sourceAccountId;
this.targetAccountId = targetAccountId;
this.money = money;
this.validateSelf();
}
}
```

1. final 로 불변 필드로 지정해서 생성 이후 변경할 수 없다는 것을 보장한다.
2. 유스케이스 API 일부이기 때문에 incoming 포트 패키지에 위치한다.
3. @NotNull 과 같이 Bean Validation API 를 사용하면 유효성을 쉽게 체크할 수 있다.
4. SelfValidating 추상 클래스를 확장해서 어노테이션 외에 유효성 검증을 체크한다. 이는 계층형의 계층이 아니라 유스케이스의 보호막 계층이다.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SelfValidating 추상 클래스를 확장한건 아니고, 추상 클래스를 상속받고, 부모클래스의 validateSelf()메서드만 사용하더라구요.

추상클래스로 만든건 단순 인스턴스화 못하게 하기위해서 제약을 둔거 같은데, 좋은 방법 일까여??


## 생성자의 힘

생성자를 private으로 한 후 build() 메소드에서 구현해서 많은 파라미터를 받을 수도 있다.

다만 필드가 많아지면 컴파일러가 잡아내지 못하기 때문에 오히려 휴먼에러가 발생하기 쉽다.

```java
new SentMoneyCommandBuilder()
.sourceAccountId(new AccountId(~))
.targetAccountId(new AccountId(~))
.build();
```

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

각기 다른 유스케이스에 동일한 입력 모델을 사용해야할 때가 있다. 그럴 때 각기 다른 필드에 대한 유효성 검사가 필요해지게 된다. 이는 8장에서..

## 비즈니스 규칙 검증하기

구문상의 유효성을 검증하는 입력 유효성 검증과는 다르게 비즈니스 규칙은 의미적인 유효성을 검증한다. ‘출금 계좌에서 초과출금되면 안된다’ 라는 규칙과 비슷하다.

가장 좋은 방법은 이 규칙을 도메인 엔티티 안에 넣는 것이고 여의치 않다면 유스케이스 코드에서 도메인 엔티티를 사용하기 전에 해도 된다. 데이터베이스에서 가져와야 검증이 가능한 경우처럼 좀 복잡한 경우에는 도메인 엔티티 내에서 구현해야 한다.

```java
public boolean withdraw(Money money, AccountId targetAccountId) {

if (!mayWithdraw(money)) {
return false;
}
}
```

실패할 경우 검증 전용 예외를 던지거나 에러메시지로 사용자에게 보여준다.

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

**풍부한 도메인 모델** 에서는 코어에 있는 엔티티에 가능한 많은 도메인 로직이 구현된다. 유스케이스는 도메인 모델의 진입점으로 동작하고, 많은 비즈니스 규칙이 유스케이스 구현체가 아니라 엔티티에 위치한다.

**빈약한 도메인 모델**에서는 getter, setter 만 있고 어떤 도메인 로직도 가지고 있지 않다.

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

유스케이스가 반환하는 값은 필요로 하는지, 필요하다면 다른 호출자도 사용할 수 있는지 확인해야 한다. 의심스러운 경우에는 가능한 적게 반환한다. 출력 모델을 공유하면 유스케이스들이 강하게 결합된다.

## 읽기 전용 유스케이스

간단한 데이터 쿼리라서 프로젝트 맥락에서 유스케이스로 간주되지 않는다면 유스케이스와 구분하기 위해 쿼리로 구현할 수 있고 이를 쿼리 서비스에 구현한다.

```java
package io.reflectoring.buckpal.account.application.service;

import java.time.LocalDateTime;

import io.reflectoring.buckpal.account.application.port.in.GetAccountBalanceQuery;
import io.reflectoring.buckpal.account.application.port.out.LoadAccountPort;
import io.reflectoring.buckpal.account.domain.Account.AccountId;
import io.reflectoring.buckpal.account.domain.Money;
import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
class GetAccountBalanceService implements GetAccountBalanceQuery {

private final LoadAccountPort loadAccountPort;

@Override
public Money getAccountBalance(AccountId accountId) {
return loadAccountPort.loadAccount(accountId, LocalDateTime.now())
.calculateBalance();
}
}
```

이는 CQS(Command-Query Separation) 나 CORS(Command-Query Responsibility Segragation)과 개념이 유사하다.

## 유지보수 가능한 소프트웨어를 만드는 데 어떻게 도움이 될까?

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

우리 어드민 비즈니스들이 입력 모델 분리가 필요를 많이 느낌 ㅠㅠ

2 changes: 2 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# 목차
## 1. 계층형 아키텍쳐의 문제는 무엇일까?