[클린 아키텍처] Ch 03. 코드 구성하기

정미·2022년 5월 5일
0

송금하기 예제를 사용하여 육각형 아키텍처를 직접적으로 반영하는 표현력 있는 패키지 구조 소개

3-1. 계층으로 구성하기

결론: 간단한 구조의 계층은 가장 적합한 구조가 아닐 수 있다.

웹 계층, 도메인 계층, 영속성 계층의 각 계층 전용 패키지를 만든다.

payment
    ㄴ web
        ㄴ AccountController
    ㄴ domain
        ㄴ Account
        ㄴ Activity
        ㄴ AccountRepository
        ㄴ AccountService
    ㄴ persistence
        ㄴ AccountRepositoryIml

단점

  1. 기능 조각(functional slice)이나 특성(feature)로 구분짓는 경계가 없다.

    만약에 사용자 기능이 추가된다면?

    payment
        ㄴ web
            ㄴ AccountController
            ㄴ UserController
        ㄴ domain
            ㄴ Account
            ㄴ Activity
            ㄴ User
            ㄴ AccountRepository
            ㄴ UserRepository
            ㄴ AccountService
            ㄴ UserService
        ㄴ persistence
            ㄴ AccountRepositoryImpl
            ㄴ UserRepositoryImpl

    연관되지 않은 클래스들의 엉망진창 묶음

  2. 유스케이스 파악이 어렵다.

    특정 기능을 찾기 위해 AccountServiceAccountController로는 한 눈에 파악하기 힘들다. 추측과 찾는 시간이 필요하다.

  3. imcoming port, outgoing port가 코드에 숨겨져 있다.

    헥사고날 아키텍처를 따랐다고 가정했을 때, 웹 어댑터나 영속성 어댑터가 어느 기능을 호출하고 호출당하는지 파악할 수 없다,.

3-2. 기능으로 구성하기

결론: 아키텍처의 가시성을 더 떨어뜨린다.

‘계층으로 구성하기’의 1번 문제를 해결한 방식

payment
    ㄴ account
        ㄴ Account
        ㄴ Activity
        ㄴ AccountRepository
        ㄴ AccountRepositoryIml
        ㄴ SendMoneyService
        ㄴ AccountController
    ㄴ user
        ㄴ User
        ㄴ UserRepository
        ㄴ UserRepositoryIml
        ㄴ UserRegistrationService
        ㄴ UserController

모든 클래스들을 account라는 최상위 디렉토리에 위치시켰다.

장점

  1. package-private 접근 수준을 통해 패키지 경계를 만들어 불필요한 의존성을 방지한다.

  2. AccountServiceSendMoneyService 로 변경 (이는 앞에서도 변경 가능한 부분이었다)

    Screaming Architecture(소리치는 아키텍처) - 클래스명(코드)만으로도 유스케이스(애플리케이션의 기능)를 찾을 수 있다.

단점

  1. 계층별로 나누지 않아서 어느 아키텍처를 기반으로 구성하였는지 알 수 없다.
  2. 한 패키지 안에 모든 코드가 존재하기 때문에 도메인 코드가 영속성 계층에 의존하는 실수가 일어날 수도 있다.
  3. 인커밍, 아웃고잉 포트뿐만 아니라 어댑터를 위한 별도의 패키지도 확인할 수 없다.

3-3. 아키텍처적으로 표현력 있는 패키지 구조

헥사고날 아키텍처의 핵심 요소: 엔티티, 유스케이스, 인커밍/아웃고잉 포트, 인커밍/아웃고잉 어댑터

위 핵심 요소들을 패키지 하나씩 매핑시킨다.

payment
    ㄴ account
        ㄴ adapter
            ㄴ in
                ㄴ web
                    ㄴ AccountController
            ㄴ out
                ㄴ persistence
                    ㄴ AccountPersistenceAdapter
                    ㄴ SpringDataAccountRepository
        ㄴ domain
            ㄴ Account
		        ㄴ Activity
        ㄴ application (서비스 계층)
            ㄴ SendMoneyService
            ㄴ port
                ㄴ in
                    ㄴ SendMoneyUseCase
                ㄴ out
                    ㄴ LoadAccountPort
                    ㄴ UpdateAccountStatePort
    ㄴ user
        ㄴ adapter
        ㄴ domain
        ㄴ application

장점

  1. 패키지 구조(코드)가 아키텍처를 반영한다. → 원하는 코드를 바로 파악 가능하다.
  2. 새로운 코드를 어느 패키지에 넣어야할지 고민하도록 적극적인 사고를 촉진한다.
  3. 어댑터 코드를 달느 구현으로 쉽게 교체 가능하다.
  4. DDD 개념에 직접적인 대응
    • account, user와 같은 상위 레벨 패키지는 bounded context(하나의 도메인 모델이 적용될 수 있는 범위)이며, 다른 bounded context와 통신하는 입출구이다.

package-private

adapter 패키지의 모든 클래스들은 application/port 내의 인터페이스를 통하지 않고서는 호출되지 않기 때문에 package-private 접근 수준으로 설정한다. 애플리케이션 코어에서 바깥 계층으로 향하는 의존성을 막을 수 있다.

package com.woowa.cleanarchitecture.account.adapter.in.web;

@RequiredArgsConstructor
@RestController
@RequestMapping("/accounts")
class AccountController {
    private final SendMoneyUseCase sendMoneyUseCase;

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

3-4. 의존성 주입의 역할

의존성을 역전시켜 도메인 코드가 다른 바깥쪽 코드(영속성/UI)에 의존하지 않게 하자!

영속성

애플리케이션 코드와 영속성 어댑터 같은 아웃고잉 어댑터는 제어 흐름과 의존성 방향이 반대이기 때문에 의존성 역전 원칙을 사용해야 한다.

  • 서비스 uses 영속성 어댑터
  • 아웃고잉 어댑터 depends 서비스

따라서 이 두 계층 사이에 아웃고잉 포트 인터페이스가 필요하다.

‘모든 계층에 의존성을 가진 중립적인 컴포넌트’(ex. 스프링 프레임워크)가 실제 객체를 필요한 다른 계층에 제공한다. (의존성을 주입한다.)

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

코드에서 아키텍처의 특정 요소를 찾기 위해선 아키텍처 다이어그램의 박스 이름을 따라 탐색하면 된다. (챕터2-5 육각형 아키텍처 다이어그램 참고)

의사소통, 개발, 유지보수 모두가 수월해질 것이다.


느낀점 & 깨달은 점

  1. p29 1문단 adapter package: 이 패키지의 모든 클래스들은 application 패키지 내에 있는 포트 인터페이스를 통하지 않고는 바깥에서 호출되지 않기 때문에 package-private 접근 수준으로 둬도 된다.

    → 의존성 역전으로 구현하였기 때문에 인터페이스를 통해 어댑터 구현체가 호출된다.

질문 & 토의

  1. P27 SendMoneyService는 인커밍 포트 인터페이스인 SendMoneyUseCase를 구현하고 ~

    : SendMoneyService implements SendMoneyUseCase

    • 입출력 포트가 인터페이스인 것 같다. 주도하는/주도되는 어댑터의 입장에 따라서 구현되고 사용되는 역할까지는 이해가 간다. 그런데 서비스에 대해서는 꼭 이렇게 중간에 포트(인터페이스)를 두어야 하는 이유가 있나?
      • p30 2문단 마지막: 계층간의 진입점을 구분짓기 위해
    • 인터페이스와 구현체가 1:1이라면 쓸데없이 많고 복잡한 구조를 만드는 것이라고도 생각이 든다.
  2. p29 3문단 어댑터 코드를 자체 패키지로 이동(?)시키면 필요할 경우 하나의 어댑터를 다른 구현으로 쉽게 교체할 수 있다는 장점도 있다.

0개의 댓글