5. 웹 어댑터 구현하기

의존성 역전

웹 어댑터는 인커밍 어댑터로 외부로부터 요청을 받아 애플리케이션 코어를 호출하여 어떤 일을 해야할지 알려준다. 이때 제어의 흐름은 웹 어댑터 계층의 컨트롤러에서 애플리케이션 계층의 서비스로 흐른다. 애플리케이션 계층은 서비스와 통신할 수 있는 특정 포트를 중간 계층으로 구현하고 웹 어댑터는 이러한 포트를 호출할 수 있다.

여기서 가만히 보면 의존성 역전 원칙이 적용된 걸 알수 있는데 제어의 흐름이 왼쪽에서 오른쪽 즉, 웹 어댑터가 유스케이스를 직접 호출할수도 있다.

컨트롤러에서 서비스를 직접 호출하지 않고 포트를 두는 이유는 애플리케이션 코어가 외부와 통신할 수 있는 곳의명세가 바로 포트이기 때문이다.

포트를 적절한 위치에 구현하게 되면 외부와 어떤 통신을 하는지 명확하게 알기 쉬워 유지보수에 큰 도움이 된다.

웹 어댑터의 책임

일반적으로 웹 어댑터는 다음과 같은 일을 한다.

  1. HTTP 요청을 자바 객체로 매핑
  2. 권한 검사
  3. 입력 유효성 검증
  4. 입력을 유스케이스의 입력 모델로 매핑
  5. 유스케이스 호출
  6. 유스케이스의 출력을 HTTP로 매핑
  7. HTTP 응답을 반환

이 과정에서 한가지라도 예외가 발생하면 웹 어댑터는 오류를 반환단다.

웹 어댑터에 많은 기능과 책임이 들어간거 같지만 이것들은 모두 애플리케이션 계층에서 신경쓰면 안되는 것들이다.

HTTP와 관련된 로직을 애플리케이션 계층에서 알게 된다면 HTTP가 아닌 다른 통신방식을 사용하는 인커밍 포트를 구현할 때 동일한 도메인 로직을 수행할 수 없게 된다.

컨트롤러 나누기

스프링 MVC 프레임워크에서는 위에서 언급한 책임들을 수행할 컨트롤러 클래스를 구현할 수 있다. 웹 어댑터를 구성할땐 하나의 클래스에 많은 기능을 담기보단 가능하면 최대한 기능을 작게 분할하여 여러 개의 어댑터로 구성하는 편이 좋다.

계좌와 관련된 REST API를 설계한다고 할때 보통 AccountController 클래스를 만들게 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@RestController
@RequiredArgsConstructor
public class AccountController{
//...
@GetMapping("/accounts")
List<AccountResource> listAccounts() {}

@GetMapping("/accounts/{id}")
AccountResource getAccount(@PathVariable("accountId") Long accountId) {}

@GetMapping("/accounts/{id}/balance")
long getAccountBalance(@PathVariable("accountId") Long accountId) {}

@PostMapping("/accounts")
AccountResource createAccount(@RequestBody AccountResource account) {}

@PostMapping("/accounts/send/{sourceAccountId}/{targetAccountId}/{amount}")
void sendMoney(
@PathVariable("sourceAccountId") Long sourceAccountId;
@PathVariable("targetAccountId") Long targetAccountId;
@PathVariable("amount") Long amount) {}
}

Account와 관련된 모든 기능이 하나의 컨트롤러에서 처리되서 효율적이고 괜찮게 보일 수 있지만 몇가지 단점이 있다.

  1. 클래스의 코드는 적을수록 좋다.
    하나의 클래스에 너무 많은 코드가 있다면 시간이 지나면서 늘어나는 코드 그 이상으로 파악하는 것에 난이도가 높아진다. 이것은 메서드를 아무리 깔끔하게 분리해놔도 쉽지 않다.
    또한 테스트 코드를 작성하더라도 프로덕션 코드에 비해 더 추상적인 테스트 코드의 특성상 클래스 단위를 작게 해야 찾기 쉬워진다.
  2. 가장 중요한 부분은 모델의 재사용을 촉진한다는 점이다.
    위 코드는 AccountResource 라는 단일 모델 클래스를 공유하는데 여기에 연산에 필요한 모든 데이터를 담다보면 특정 연산에선 필요없는 필드도 생길수 밖에 없다.
    새로운 기능을 추가하거나 기존 기능을 수정할때 불필요한 필드에 대한 고민이 들어가면서 난이도만 늘어날뿐이다.

따라서 가급적 별도의 패키지 안에 별도의 컨트롤러를 만드는 방식이 좋다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@RestController
@RequiredArgsConstructor
public class SendMoneyController {
private final SendMoneyUseCase;

@PostMapping("/accounts/send/{sourceAccountId}/{targetAccountId}/{amount}")
void sendMoney(
@PathVariable("sourceAccountId") Long sourceAccountId,
@PathVariable("targetAccountId") Long targetAccountId,
@PathVaribale("amount") Long amount) {

SendMoneyCommand command = new SendMoneyCommand(new AccountId(sourceAccountId), new AccountId(targetAccountId), Money.of(amount));
sendMoneyUseCase.sendMoney(command);
}
}

각 컨트롤러는 Create, Update와 같은 컨트롤러 자체의 모델을 가지고 있거나, 원시값을 받아도 된다.

이러한 전용모델 클래스는 컨트롤러의 패키지에 private 으로 선언할 수 있어서 다른 곳에서 재사용될 일이 없고 다른 컨트롤러에서 사용할때도 한번 더 생각해볼 수 있게 된다.

웹어댑터를 구현할 땐 HTTP요청이 어플리케이션의 유스케이스 호출로 변환하고 결과를 다시 HTTP로 변환하여 어떠한 도메인 로직도 수행하지 않도록 구현해야 한다.

Author

Jaeyong Yoo

Posted on

2023-03-15

Updated on

2023-06-10

Licensed under

댓글