[만들면서 배우는 클린 아키텍처] 3. 코드 구성하기

wally·2022년 12월 14일
1

독서시리즈

목록 보기
8/10
  • 패키지 구조를 먼저 작성해야 한다.
  • 한 패키지에 있는 클래스들이 불러오지(import) 말아야 할 다른 패키지에 있는 클래스들을 불러오는 상황을 방지해야 한다.

1. 계층으로 구성하기

buckapl
|--- domain
|    |----- Account
|    |----- Activity
|    |----- AccountRepository
|    |----- AccountService
|--- persistence
|    |----- AccountRepositoryImpl
|--- web
|    |----- AccountController
  • 계층으로 코드를 구성하면, 기능적인 측면들이 섞이기 쉽다

문제점

문제1 : 애플리케이션의 기능 조각(functional slice) 이나 특징(feature) 을 구분 짓는 패키지 경계가 없다

  • 사용자를 관리하는 기능을 추가한다면 web패키지 domain 패키지, persistence 패키지에 관련 클래스가 추가될것이다.
  • 추가적인 구조가 없다면, 아주 빠르게 서로 연관되지 않은 기능들끼리 예상하지 못한 부수효과를 일으킬 수 있는 클래스들의 묶음으로 변모할 수 있다.

문제2 : 애플리케이션이 어떤 유스케이스들을 제공하는지 파악할 수 없다.

  • 특정 기능을 찾기 위해 어떤 서비스가 이를 구현했는지 추측해야 한다.

문제3 : 패키지 구조를 통해서는 우리의 목표로 하는 아키텍처를 파악할 수 없다.

  • 어떤 기능이 웹 어댑터에서 호출되는지, 영속성 어댑터가 도메인 계층에 어떤 기능을 제공하는지 한 눈에 알 수 없다.
  • 인커밍(incoming) 포트와 아웃고인(outgoing) 포트가 코드 속에 숨겨져 있다.

2. 기능으로 구성하기

buckpal
|-- account
    |-- Account
    |-- AccountController
    |-- AccountRepository
    |-- AccountRepositoryImpl
    |-- SendMoneyService
  • 기능을 기준으로 코드를 구성하면 기반 아키텍처가 명확하게 보이지 않는다.
  • account 패키지로 묶고 계층 패키지를 없앴다.

장점

  • 패키지 외부에서 접근하면 안되는 클래스들에 대해 package-private 접근 수준을 이용해 패키지 간의 경계를 강화할 수 있다.
  • 패키지 경계를 package-private 접근 수준과 결합하면 각 기능사이의 불필요한 의존성을 방지할 수 있다.
  • SendMoneyService 와 같이 송금하기 기능을 구현한 클래스를 클래스명으로 바로 찾을 수 있다.(소리치는 아키텍처)

단점

  • 아키텍처의 가시성을 계층방식보다 훨씬 더 떨어뜨린다.
    • 어댑터를 나타내는 패키지명도 없다
    • 인커밍 포트, 아웃고인 포트도 없다.
    • 인터페이스만 알고 구현체를 알수 없게 했지만 패키지 내부 package-private 접근 수준으로 구현체를 자유롭게 접근할 수 있다.(깨진 유리창)

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

buckpal
|-- account
    |-- adapter
    |   |-- in
    |   |   |-- web
    |   |       |-- AccountController
    |   |-- out
    |   |   |-- persistence
    |   |       |-- AccountPersistenceAdapter
    |   |       |-- SpringDataAccountRepository
    |-- domain
    |   |-- Account
    |   |-- Activity
    |-- application
        |-- SendMoneyService
        |-- port
            |-- in
            |   |-- SendMoneyUseCase
            |-- out
            |   |-- LoadAccountPort
            |   |-- UpdateAccountStatePort
  • 육각형 아키텍처의 핵심적인요소 : 엔티티, 유스케이스, 인커밍/아웃고잉 포트, 인커밍/아웃고인 어댑터
  • 최상위에 Account 관련 유스케이스를 구현한 모듈임을 나타내는 account 패키지
  • 도메인 모델에 속한 domain 패키지
  • 서비스 계층(인커밍 포트 구현체)이 속한 application 패키지
  • 인커밍 포트 인터페이스, 아웃고잉 포트 인터페이스가 속한 application 패키지
  • 인커핑 포트를 호출하는 인커밍 어댑터와 아웃고인 포트에 대한 구현을 제공하는 아웃고잉 어댑터가 속한 adapter 패키지

장점

장점1 : 이러한 패키지 구조는 모델-코드 갭(아키텍처-코드 갭)을 효과적으로 다룰 수 있다.

모델-코드 갭(model-code gap) 아키텍처 모델에는 항상 코드에 매핑할 수 없는 추상적인 개념, 기술 선택 및 설계 결정이 혼합되어 있다. 최종 결과는 모델이 정한 구성 요소의 배열과 반드시 일치하지 않는 소스 코드가 될 수 있다.

  • 만약 패키지 구조가 아키텍처를 반영할 수 없다면 시간이 지남에 따라 코드는 점점 목표하던 아키텍처로부터 멀어지게 된다.

장점2 : 패키지간 접근을 제어할 수 있다.

  • 이 패키지에 들어있는 모든 클래스들은 application 패키지 내부에 있는 포트 인터페이스를 통하지 않고는 바깥에서 호출되지 않는다.(애플리케이션 계층에서 어댑터 클래스로 향하는 우발적인 의존성이 있을 수 없다.)
  • 의도적으로 어댑터에서 접근 가능해야 하는 포트들은 public 이어야 한다(application, domain 패키지 내 일부 클래스는 public)
  • 도메인 클래스는 서비스, 잠재적으로 어댑터에서도 접근 가능하도록 public 이어야 한다.
  • 서비스 클래스는 인커밍 포트 인터페이스 뒤에 숨겨질 수 있기 때문에 public 일 필요가 없다.

  • package-private인 adapter 클래스
    • 모든 클래스는 application 패키지 내의 포트 인터페이스를 통해 바깥에 호출되기 때문에 adapter는 모두 package-private 접근 수준으로 둬도 된다.
    • 어플리케이션 계층에서 어댑터로 향하는 우발적 의존성은 있을 수 없다.
  • public이어야 하는 application, domain의 일부 클래스
    • application의 port(in, out)
      • SendMoneyUseCase, LoadAccountPort, UpdateAccountStatePort
    • 도메인 클래스
      • Account, Activity
  • package-private이어도 되는 서비스 클래스
    • 인커밍 포트 인터페이스 뒤에 숨겨지는 서비스는 public일 필요가 없다.
      • GetAccountBalanceService

장점3 : 어댑터 코드를 자체 패키지로 이동시키면 필요시 하나의 어댑터를 다른 구현으로 쉽게 교체할 수 있다.

장점4 : DDD 개념에 직접적으로 대응할 수 있다.

  • 다른 바운디드 컨텍스트(bounded context) 와 통싱할 전용 진입점과 출구(포트)를 포함하는 바운디드 컨텍스트가 account 같은 상위 레벨 패키지다.

의존성 주입의 역할

  • 클린 아키텍처의 본질적인 요건

    • 어플리케이션이 인커밍/아웃고잉 어댑터에 의존성을 갖지 않아야 한다.
  • 이를 지키기 위해 의존성 역전 원칙을 사용한다.

    • 어플리케이션 계층에 인터페이스(port)를 만들고 어댑터에 해당 인터페이스를 구현한 클래스를 둔다.
    • 모든 계층에 의존성을 가진 중립적인 컴포넌트를 하나 두고, 이 컴포넌트가 아키텍처를 구성하는 대부분의 클래스를 초기화하는 역할을 한다.
    • 웹 컨트롤러가 서비스에 의해 구현된 인커밍 포트를 호출한다. 서비스는 어댑터에 의해 구현된 아웃고잉 포트를 호출한다.
  • AccountController

    • SendMoneyUseCase 인터페이스가 필요하므로 의존성 주입을 통해 SendMoneyService 클래스의 인스턴스를 주입
  • SendMoneyService

    • LoadAccount 인터페이스로 가장한 AccountPersistenceAdapter 클래스의 인스턴스 주입
profile
클린코드 지향

1개의 댓글

comment-user-thumbnail
2023년 7월 30일

잘 정리하셨네요 ! 참고가 되었습니다 !!

답글 달기