728x90

만들면서 배우는 클린 아키텍처 책을 읽고 정리하며 소감을 적는 포스트입니다.

 

서론


오늘날의 애플리케이션은 대부분 웹 인터페이스 같은 것을 제공한다. 웹 브라우저를 통해 상호작용할 수 있는 UI나 다른 시스템에서 우리 애플리케이션으로 호출하는 방식으로 상호작용하는 HTTP API가 여기에 해당한다.

헥사고날 아키텍처에서 외부 세계와의 모든 커뮤니케이션은 어댑터를 통해 이뤄진다.

웹 인터페이스를 제공하는 어댑터의 구현 방법을 살펴보자

 

의존성 역전


웹 어댑터는 주도하는 혹은 인커밍 어댑터다. 외부로부터 요청을 받아 애플리케이션 코어를 호출하고 무슨 일을 해야 할지 알려준다. 이때 제어 흐름은 웹 어댑터에 있는 컨트롤러에서 애플리케이션 계층에 있는 서비스로 흐른다.

애플리케이션 계층은 웹 어댑터가 통신할 수 있는 특정 포트를 제공한다. 서비스는 이 포트를 구현하고, 웹 어댑터는 이 포트를 호출할 수 있다.

 

 

  • 웹 어댑터와 관련된 아키텍처 요소에 초점을 맞추었다.
  • 인커밍 어댑터는 애플리케이션 서비스에 의해 구현된 인터페이스인 전용포트를 통해 애플리케이션 계층과 통신한다.

 

자세히 살펴보면 의존성 역전 원칙이 적용된 것을 발견할 수 있다. 그런데 위의 그림을 보면 제어 흐름이 왼쪽에서 오른쪽으로 흐르기 때문에 웹 어댑터가 유스케이스를 직접 호출할 수 있는데 왜 사이에 간접 계층을 넣어야 할까?

 

 

그 이유는 애플리케이션 코어가 외부 세계와 통신할 수 있는 곳에 대한 명세가 포트이기 때문이다. 포트를 적절한 곳에 위치시키면 외부와 어떤 통신이 일어나고 있는지 정확힐 알 수 있고, 이는 유지보수 측면에서 장점이 있다.

 

 

하지만 만약 애플리케이션이 웹 어댑터에 능동적으로 알림을 줘야 한다면 올바른 방향으로 유지하기 위해 아웃고잉 포트를 통과해야 한다.

  • ex) 웹 소켓을 통해 실시간 데이터를 사용자의 브라우저로 보낸다고 가정
  • 포트가 반드시 필요
  • 해당 포트는 웹 어댑터에서 구현하고 애플리케이션 코어에서 호출해야 한다.
  • 엄밀히 말하면 해당 포트는 아웃고잉 포트이기 때문에 이제 웹 어댑터는 인커밍 어댑터인 동시에 아웃고잉 어댑터가 된다.

 

웹 어댑터의 책임


애플리케이션이 REST API를 제공한다고 하면 웹 어댑터의 책임은 보통 다음과 같다.

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

 

웹 어댑터는 특정 기준을 만족하는 HTTP 요청을 수신해야 하며 요청의 파라미터와 컨텐츠를 객체로 역직렬화 해야한다.

또한 웹 어댑터가 인증과 권한 부여를 수행하고 실패할 경우 에러를 반환한다.

 

그러고 나면 객체의 상태 유효성 검증을 할 수 있다. 여기서의 입력 유효성 검증은 유스케이스의 입력 모델이 책임지는 검증을 말하는 것이 아니라 웹 어댑터의 입력 모델에 대해 검증을 말하는 것이다.

  • 유스케이스의 입력 모델과는 구조과 의미가 다를 수 있으므로 또 다른 유효성 검증을 수행해야 한다.
  • 유스케이스 입력 모델에서 했던 검증을 똑같이 구현해야하는 것은 아니다.
  • 웹 어댑터의 입력 모델을 유스케이스의 입력 모델로 변환할 수 있다는 것을 검증해야한다.
  • 해당 변환을 방해는 모든 것이 유효성 검증 에러다.

 

이는 자연스럽게 웹 어댑터의 다음 책임인 변환된 입력 모델로 특정 유스케이스를 호출하는 것으로 연결된다. 어댑터는 유스케이스의 출력을 반환하고, HTTP 응답으로 직렬화해서 호출자에게 전달한다.

 

해당 과정에서 한 군데서라도 문제가 생기면 예외를 던지고, 웹 어댑터는 에러를 호출자에게 보여줄 메시지로 변환해야 한다. HTTP와 관련된 것은 애플리케이션 계층으로 침투해서는 안 된다.

 

웹 어댑터와 애플리케이션 계층 간의 이 같은 경계는 웹 계층에서부터가 아닌 도메인과 애플리케이션 계층부터 개발하기 시작하면 자연스럽게 생긴다. 특정 인커밍 어댑터를 생각할 필요 없이 유스케이스를 먼저 구현하면 경계를 흐리게 만들 유혹에 빠지지 않을 수 있다.

 

컨트롤러 나누기


웹 어댑터는 한 개 이상의 클래스로 구성해도 되지만 클래스들이 같은 소속이라는 것을 표현하기 위해 같은 패키지 수준에 놓아야 한다.

컨트롤러 갯수는 너무 적은 것보다는 너무 많은 게 낫다. 가능한 좁고 다른 컨트롤러와 가능한 적게 공유하는 웹 어댑터 조각을 구현해야 한다.

 

저자는 각 연산에 대해 가급적이면 별도의 패키지 안에 별도의 컨트롤러를 만드는 방식을 선호한다. 또한 가급적 메서드와 클래스명은 유스케이스를 최대한 반영해서 지어야 한다고 말한다.

package buckpal.adapter.in.web;

@RestController
@RequiredArgsConstructor
class SendMoneyController {

  private final SendMoneyUseCase sendMoneyUseCase;

  @PostMapping(path = "/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);
  }
}

 

요약


애플리케이션의 웹 어댑터를 구현할 때는 HTTP 요청을 애플리케이션의 유스케이스에 대한 메서드 호출로 변환하고 결과를 다시 HTTP로 변환하고 어떤 도메인 로직도 수행하지 않는 어댑터를 만들고 있다는 점을 염두에 둬야 한다.

반면 애플리케이션 계층은 HTTP에 대한 상세 정보를 노출시키지 않도록 웹 계층과 관련된 작업을 해서는 안된다.

 

웹 컨트롤러를 나눌 때는 모델을 공유하지 않는 것을 두려워 해서는 안된다. 작은 클래스들이 더 파악하기 쉽고, 더 테스트하기 쉬우며, 동시 작업을 지원한다. 처음에는 조금 더 공수가 들겠지만 유지보수 측면에서는 분명 더 좋은 효과를 낼 수 있을 것이다.

 

 

 

728x90

+ Recent posts