Notice
Recent Posts
Recent Comments
Link
«   2024/07   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30 31
Archives
Today
Total
관리 메뉴

csct3434

[만들면서 배우는 클린 아키텍처] 05. 웹 어댑터 구현하기 본문

개발 서적/만들면서 배우는 클린 아키텍처

[만들면서 배우는 클린 아키텍처] 05. 웹 어댑터 구현하기

csct3434 2024. 3. 3. 21:11

서문

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

의존성 역전

웹 어댑터 관련 아키텍처 요소

  • 자세히 보면 의존성 역전 원칙이 적용된 것을 발견할 수 있다.

  • 포트 인터페이스를 거치지 않고 어댑터가 서비스를 직접 호출할 수도 있다.
  • 그럼에도 불구하고 어댑터와 유스케이스 사이에 포트 인터페이스를 넣는 이유는 애플리케이션 코어가 외부 세계와 통신할 수 있는 곳에 대한 명세가 코어이기 때문이다.
    • 포트를 적절한 곳에 위치시키면 외부와 어떤 통신이 일어나는지 정확히 파악할 수 있고, 이는 유지보수에 있어 매우 소중한 정보이다
  • 포트 인터페이스가 필요한 이유는 상호작용이 많이 일어나는 애플리케이션에서 더욱 명확해진다.
    • 애플리케이션에서 웹 소켓을 통해 실시간 데이터를 사용자의 브라우저로 보내는 시나리오에서는 반드시 포트가 필요하다
    • (이해가 잘 안되서 패스, p.55)

웹 어댑터의 책임

웹 어댑터의 책임

  • 웹 어댑터의 책임이 많기는 하지만, 이 책임들은 애플리케이션 계층이 신경 쓰면 안 되는 것들이다.
  • HTTP와 관련된 것은 애플리케이션 계층으로 침투해서는 안 된다.
    그렇지 않으면, HTTP를 사용하지 않는 다른 인커밍 어댑터의 요청에 대해 동일한 도메인 로직을 수행할 수 있는 선택지를 잃게 된다.
    좋은 아키텍처에서는 선택의 여지를 남겨둔다.
  • 웹 어댑터와 애플리케이션 계층 간의 경계는 웹 계층에서부터 개발을 시작하는 대신 도메인과 애플리케이션 계층부터 개발을 시작하면 자연스럽게 생긴다.
    특정 인커밍 어댑터를 생각할 필요 없이 유스케이스를 먼저 구현하면 경계를 흐리게 만들 유혹에 빠지지 않을 수 있다.

유효성 검증

  • 웹 어댑터는 들어오는 객체의 상태 유효성을 검증할 수 있다.
  • 웹 어댑터에서 수행하는 유효성 검증은 웹 어댑터의 입력 모델을 유스케이스의 입력 모델로 변환할 수 있다는 것을 검증한다
  • 유스케이스의 입력 모델에서 수행하는 유효성 검증과는 맥락이 다르다

컨트롤러 나누기

  • 각 컨트롤러는 다른 컨트롤러와 최소한으로 공유하는, 가능한 좁은 웹 어댑터 조각을 구현해야 한다
  • Buckpal 애플리케이션의 Account 엔티티의 연산들을 예로 들어보자.
package buckpal.adapter.web;

@RestController
@RequiredArgsConstructor
public class AccountController {
    
    private final GetAccountBalanceQuery getAccountBalanceQuery;
    private final ListAccountQuery listAccountQuery;
    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
    ) {
        ...
    }
    
}
  • 자주 사용하는 방식은 계좌와 관련된 모든 요청을 받는 컨트롤러를 하나 만드는 것이다.
  • 단일 컨트롤러 방식의 단점
    • 클래스에 코드가 많아지면 코드를 파악하는 난이도가 높아지고, 그에 해당하는 테스트 코드 또한 파악하기 어려워진다.
      (보통 테스트 코드는 프로덕션 코드에 비해 추상적이어서 파악하기가 더 어렵다)
    • 모든 연산을 단일 컨트롤러에 넣는 것은 데이터 구조의 재활용(모델 공유)을 촉진한다
package buckpal.adapter.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);
  }
}
  • 가급적이면 별도의 패키지 안에 별도의 컨트롤러를 만드는 방식을 추천한다.
  • 각 컨트롤러가 컨트롤러 전용의 모델을 가지는 것도 좋은 방식이다.
    • 이러한 전용 모델 클래스는 컨트롤러 패키지에 대해 private으로 선언할 수 있기 때문에 다른 곳에서 실수로 사용하는 것을 방지할 수 있다
    • 컨트롤러끼리 모델을 공유할 수 있지만 다른 패키지에 위치한 덕분에, 공유해서 사용하기 전에 다시 한번 생각해 볼 수 있다
  • 메서드와 클래스명은 유스케이스를 최대한 반영해서 지어야 한다.
    • 예를 들어, CreateAccount보다는 RegisterAccount가 문맥을 더욱 반영한다.
    • Create, Update, Delete 이러한 단어를 사용하기 전에 한번 더 숙고해보자
  • 이러한 스타일의 또 다른 장점은 서로 다른 연산에 대한 동시 작업이 쉬워진다는 것이다.
    • 두 명의 개발자가 서로 다른 연산에 대한 코드를 짜고 있다면 병합 충돌이 발생하지 않을 것이다.

정리

  • 웹 어댑터를 구현할 때는 어떠한 도메인 로직도 수행하지 않는 어댑터를 만들고 있다는 점을 염두에 둬야 한다.
  • 반면 애플리케이션 계층은 HTTP와 관련된 작업을 해서는 안된다.
  • 웹 컨트롤러를 나눌 때는 모델을 공유하지 않는 여러 작은 클래스들로 나누자.
    작은 클래스들은 더 파악하기 쉽고, 테스트하기 쉬우며, 동시 작업을 지원한다.
    세분화된 컨트롤러를 만드는 것은 처음에는 조금 더 공수가 들겠지만, 유지보수하는 동안에는 분명히 빛을 발할 것이다.