육각형 아키텍처에서는 외부 세계와의 모든 커뮤니케이션은 어댑터를 통해 이루어진다.
의존성 역전
웹 어댑터는 인커밍 어댑터이다. 외부로부터 요청을 받아 애플리케이션 코어를 호출하고 무슨 일을 해야 할지 알려준다.
애플리케이션 계층은 웹 어댑터가 통신할 수 있는 특정 포트를 제공한다. 서비스는 이 포트를 구현하고 웹 어댑터는 이 포트를 호출할 수 있다.
그림처럼 웹 어댑터가 유스케이스를 직접 호출할 수 있지만 의존성 역전 원칙을 적용한 이유는 뭘까
애플리케이션 코어가 외부 세계와 통신할 수 있는 곳에 대한 명세가 포트이기 때문이다. 포트를 적절한 곳에 위치시키면 외부와 어떤 통신이 일어나고 있는지 정확히 알 수 있고, 유지보수할 때 유용하다.
웹 소켓을 통해 실시간 데이터를 사용자의 브라우저로 보내는 경우 웹 어댑터에 능동적으로 알림을 줘야 하기 때문에 의존성 방향을 유지하기 위해선 아웃고잉 포트를 통과해야 한다. 이 경우 웹 소켓 컨트롤러는 인커밍 어댑터인 동시에 아웃고잉 어댑터가 된다.
웹 어댑터의 책임
웹 어댑터가 하는 일
1. HTTP 요청을 자바 객체로 매핑
URL, 경로, HTTP 메서드, 콘텐츠 타입과 같이 특정 기준을 만족하는 HTTP 요청을 수신해야 한다. 이후 HTTP 요청의 파라미터와 객체로 역직렬화해야 한다.
2. 권한 검사
인증과 권한 부여를 수행하고 실패할 경우 에러를 반환한다.
3. 입력 유효성 검증
웹 어댑터의 입력 모델에 대해 유효성 검증을 해야한다. 유스케이스의 입력 모델과는 구조나 의미가 다를 수 있기 때문에 또 다른 유효성 검증이 필요하다.
웹 어댑터의 입력 모델을 유스케이스의 입력 모델로 변환할 수 있다는 것을 검증해야 한다.
4. 입력을 유스케이스의 입력 모델로 매핑
5. 유스케이스 호출
6. 유스케이스의 출력을 HTTP로 매핑
7. HTTP 응답을 반환
이 과정에서 문제가 생기면 예외를 던지고, 웹 어댑터는 에러를 호출자에게 보여줄 메시지로 변환해야 한다.
이 책임들은 애플리케이션 계층이 신경 쓰면 안 되는 것들이다. HTTP와 관련된 것은 애플리케이션 계층으로 침투해서는 안 된다. 침투하게 되면 HTTP를 사용하지 않는 또 다른 인커밍 어댑터의 요청에 대해 동일한 도메인 로직을 수행할 수 있는 선택지를 잃게 된다.
특정 인커밍 어댑터를 생각할 필요 없이 유스케이스를 먼저 구현하면 경계를 흐리게 만들지 않을 수 있다.
컨트롤러 나누기
웹 어댑터는 한 개 이상의 클래스로 구성해도 된다. 각 컨트롤러가 가능한 한 좁고 다른 컨트롤러와 가능한 한 적게 공유하는 웹 어댑터 조각을 구현해야 한다.
AccountController를 하나 만들어서 계좌와 관련된 모든 요청을 받는 방법
package buckpal.adapter.web;
@RestController
@RequiredArgsConstructor
class AccountController {
private final GetAccountBalanceQuery getAccountBalanceQuery;
private final ListAccountsQuery listAccountsQuery;
private final LoadAccountQuery loadAccountQuery;
private final SendMoneyUseCase sendMoneyUseCase;
private final CreateAccountUseCase createAccountUseCase;
@GetMapping("/accounts")
List<AccountResource> listAccounts(){
...
}
@GetMapping ("/accounts/{accountId}")
AccountResource getAccount(@PathVariable("accountId") Long accountId){
...
}
@GetMapping("/accounts/{accountId}/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) {
...
}
}
단점
- 클래스는 코드가 적을수록 좋다. 시간이 지나면서 메서드로 깔끔하게 분리돼 있더라도 코드를 파악하는 것이 어려워진다.
- 컨트롤러에 코드가 많으면 테스트 코드도 많아진다. 테스트 코드는 더 추상적이기 때문에 코드를 파악하는 것이 더더욱 어렵다. 특정 프로덕션 코드에 해당하는 코드를 찾기도 쉽지 않다.
- 모든 연산을 단일 컨트롤러에 넣는 것은 데이터 구조의 재활용을 촉진한다. 예제에서 많은 연산들이 AccountResource 모델 클래스를 공유하는데 어떤 연산에서는 필요하지 않은 필드가 있을 수 있다.
별도의 패키지 안에 별도의 컨트롤러를 만드는 방법
package buckpal.adapter.in.web;
@RestController
@RequiredArgsConstructor
class SendMoneyController {
private final SendMoneyUseCase sendMoneyUseCase;
@PostMapping("/accounts/send/{sourceAccountId}/{targetAccountId}/{amount}")
void sendMoney(
@PathVariable("sourceAccountId") Long sourceAccountId,
@PathVariable ("targetAccountId") Long targetAccountId,
@PathVariable("amount") Long amount) {
SendMoneyCommand command = new SendMoneyCommand (
new AccountId(sourceAccountId),
new AccountId(targetAccountId),
Money.of(amount));
sendMoneyUseCase.sendMoney(command);
}
}
각 컨트롤러가 CreateAccountResource, UpdateAccountResource 같은 컨트롤러 자체의 모델을 갖고 있거나, 예제처럼 원시값을 받아도 된다.
컨트롤러의 패키지에 대해 private으로 선언할 수 있기 때문에 실수로 다른 곳에서 재사용될 일이 없다.
또 다른 장점은 서로 다른 연산에 대한 동시 작업이 쉬워진다는 것이다.
유지보수 가능한 소프트웨어를 만드는 데 어떻게 도움이 될까?
웹 어댑터를 구현할 때는 어떤 도메인 로직도 수행하지 않는 어댑터를 만들어야 한다. 애플리케이션 계층은 HTTP와 관련된 작업을 해서는 안된다. 이렇게 하면 필요할 경우 웹 어댑터를 다른 어댑터로 쉽게 교체할 수 있다.
웹 컨트롤러를 나눌 때는 모델을 공유하지 않는 작은 클래스들을 만드는 것이 좋다.
더 파악하기 쉽고, 테스트하기 쉬우며, 동시 작업을 지원한다.
'📙 > Study' 카테고리의 다른 글
[만들면서 배우는 클린 아키텍처] 07. 아키텍처 요소 테스트하기 (0) | 2024.10.15 |
---|---|
[만들면서 배우는 클린 아키텍처] 06. 영속성 어댑터 구현하기 (0) | 2024.10.14 |
[만들면서 배우는 클린 아키텍처] 04. 유스케이스 구현하기 (0) | 2024.10.07 |
[만들면서 배우는 클린 아키텍처] 03. 코드 구성하기 (2) | 2024.10.06 |
[만들면서 배우는 클린 아키텍처] 02. 의존성 역전하기 (2) | 2024.10.05 |