3장. 코드 구성하기

Seungjae·2022년 5월 16일
0

우아한 스터디

목록 보기
4/10
  • 코드를 구성하는 몇 가지 방법을 소개한다.

  • 소개할 예시들은 송금 서비스 BuckPal에 대한 것들이다.

  • 특히 사용자가 본인의 계좌에서 다른 계좌로 돈을 송금할 수 있는 ‘송금하기’ 유스케이스에 대해 살펴본다.

계층으로 구성하기


buckpal
|
|--- domain
|     |--- Acount
|     |--- Activity
|     |--- AccountRepository
|     |--- AccountService
|
|--- persistence
|     |--- AccountRepositoryImpl
|
|--- web
      |--- AccountController
  • 웹, 도메인, 영속성 계층 각각에 해당하는 전용 패키지인 web, domain, persistence를 둔다.

  • DIP를 이용해 의존성이 domain 패키지에 있는 도메인 코드만을 향하도록 한다.

  • 단 이 패키지는 최적의 구조가 아니다! → Why?

    1. 애플리케이션의 기능 조각이나 특성을 구분 짓는 패키지 경계가 없다.
      1. 즉, 서로 연관되지 않은 기능들끼리 예상하지 못한 부수효과를 일으킬 수 있는 클래스들의 엉망진창 묶음이 될 가능성이 높다!
    2. 애플리케이션의 어떤 유스케이스들을 제공하는지 파악할 수 없다.(개인적으로 이 부분은 패키지 구조 문제라기보다는 네이밍 문제라고 생각한다.)
      1. 특정 기능을 찾기 위해서는 어떤 서비스가 이를 구현했는지 추측해야 하고, 해당 서비스 내의 어떤 메서드가 그에 대한 책임을 수행하는지 찾아야 한다. (→ 2장에서 이야기했던 내가 □□□Service라는 네이밍을 싫어하는 이유 중 하나이기도 하다.)
    3. 패키지 구조를 통해서는 우리가 목표로 하는 아키택처를 파악할 수 없다. (목표로 하는 아키텍처가 헥사고날 아키텍처라고 가정)
      1. 어떤 기능이 웹 어댑터에서 호출되는지, 영속성 어댑터가 도메인 계층에 어떤 기능을 제공하는지 등을 한눈에 알아보기 힘들다.
      2. 인커밍 포트와 아웃고잉 포트가 코드 속에 숨게 된다.

기능으로 구성하기


buckpal
|
|--- account
      |--- Acount
      |--- AccountController
      |--- AccountRepositoryImpl
      |--- AccountRepository
      |--- SendMoneyService
  • 계좌와 관련된 모든 코드를 최상위 account 패키지에 넣었다.

  • 계층 패키지들도 모두 없앴다.

  • 패키지 외부에서 접근되면 안 되는 클래스들에 대해 package-private 접근 수준을 이용해 패키지 간의 경계를 강화할 수 있다.

    • 각 기능 사이의 불필요한 의존성을 방지!
  • AccountService → SendMoneyService로 네이밍 변경!

    • 유스케이스를 구현한 코드를 찾기 쉽게!
  • 애플리케이션의 기능을 코드를 통해 볼 수 있게 만드는 것을 가리켜 ‘소리치는 아키텍처’ → 코드가 그 의도를 우리에게 소리치고 있기 때문

하지만 이 구조도 문제가 있다!

  1. 아키텍처의 가시성을 훨씬 더 떨어뜨린다.
    1. 어댑터를 나타내는 패키지명 존재하지 않고, 인커밍 포트, 아웃고잉 포트를 확인할 수 없다.
    2. 이 문제점은 근데 잘 공감은 못하겠다. 물론 모두가 헥사고날 아키텍처에 대해 완벽하게 이해한경우 아키텍처의 가시성을 높인 구조화된 패키지 구조가 매우 큰 도움이 되겠지만, 전체적인 팀의 학습 난이도를 높이는 단점이 존재한다고 생각한다. 과연 팀에 새로 합류하게된 10명의 개발자가 모두 헥사고날 아키텍처를 이해하고 있을까? 모르는 팀원들의 경우 오히려 코드를 읽고, 원하는 부분을 찾는게 매우 힘들지 않을까?
  2. package-private 접근 수준으로 인해, 도메인 코드가 실수로 영속성 코드에 의존하게 될 수 있다!

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


buckpal
|
|--- account
      |--- adapter
      |     |--- in
      |     |    |--- web
      |     |          |--- AccountController
      |     |--- out
      |          |--- persistence
      |                |--- AcountPersistenceAdapter
      |                |--- SpringDataAccountRepository
      |      
      |--- domain
      |     |--- Acount
      |     |--- Activity
      |     
      |--- application
            |--- SendMoneyService
            |
            |--- port
                  |--- in
                  |     |--- SendMoneyUseCase
                  |
                  |--- out
                        |--- LoadAccountPort
                        |--- UpdateAccountStatePort
  • 패키지의 의존성 방향이 adapter → application → domain 으로 흐른다.

  • 헥사고날 아키텍처에서 구조적 핵심 요소 : 엔티티(도메인 엔티티), 유스케이스, 인커밍/아웃고잉 포트, 인커밍/아웃고잉 어댑터

  • 위 패키지 구조에서는 각 아키텍처 요소들에 정해진 위치가 있다.

  • 최상위에는 Account와 관련된 유스케이스를 구현한 모듈임을 나타내는 account 패키지가 존재

  • 구성

    • 도메인 모델이 속한 domain 패키지
    • 도메인 모델을 둘러싼 서비스 계층을 포함하는 application 패키지
    • 애플리케이션 계층의 인커밍 포트를 호출하는 인커밍 어댑터와 아웃고잉 포트에 구현을 제공하는 아웃고잉 어댑터를 포함하는 adapter 패키지
  • 이 패키지 구조는 ‘아키텍처 - 코드 gap’을 효과적으로 다룰 수 있다!

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

  • 어댑터 패키지의 모든 코드들은 application 패키지 내에 있는 포트 인터페이스를 통하지 않고서는 밖에서 호출되지 않는다.

    • 따라서 package-private으로 두는 것이 적절하다.
    • 이로인해 application 계층에서 adapter 클래스로 향하는 의존성은 존재하지 않게 된다.
  • application 패키지와 domain 패키지 내의 일부 클래스들은 public으로 지정해야 한다.

    • 어댑터에서 접근 가능해야하는 포트들 → public
    • 도메인 클래스들도 서비스, 잠재적으로는 어댑터에서도 접근 가능하게 → public
    • 이때 서비스는 인커밍 포트 인터페이스 뒤에 숨겨질 수 있기 때문에 public일 필요가 없다.
  • 어댑터 코드를 자체 패키지로 이동시킴으로써, 하나의 어댑터를 다른 구현으로 필요에 따라 쉽게 교체 가능하다!

  • 이 패키지 구조는 DDD 개념에 직접적으로 대응시킬 수 있다.

    • 하나의 account 같은 상위 레벨 패키지가 하나의 바운디드 컨텍스트(DDD에서 하나의 경계화된 단위)가 되고, 이 바운디드 컨텍스트는 통신할 전용 진입점과 출구도 포함하고 있게 된다!
  • 물론 완벽한 방법은 없다!

    • 하지만 표현력 있는 패키지 구조는 적어도 코드와 아키텍처 간의 갭을 줄여준다!

그러나..!

  • 학습 난이도의 문제도 분명 존재한다고 생각한다.

  • 또한 인커밍 포트의 경우 진입점이라는 아키텍처 상의 명목을 제외하고는 실제로 그 필요성이 의문이다.

    • 굳이 인커밍 포트가 존재하지 않아도 의존성 방향의 문제가 전혀 없고 아무리 봐도 인커밍 포트는 아키텍처의 구조를 맞추기 위해서만 쓰이지, 실제로 실효성이 없어보인다. 그저 패키지 구조를 더 복잡하게 만들고 불필요한 인터페이스 늘어나기만 할 것이라고 생각한다. 또한 그로 인해 오히려 유지보수하기 더 힘들어지기만 하지 않을까..!

의존성 주입의 역할


  • 클린 아키택처의 본질적인 요소 → 애플리케이션 계층이 인커밍/아웃고잉 어댑터에 의존성을 갖지 않는다!

    • 이를 지키기 위해, 헥사고날 아키텍처에서는 포트 인터페이스를 이용!
  • 근데 누가 포트 인터페이스를 구현한 실제 객체를 애플리케이션에 제공 하는가..?

    • 이때 의존성 주입이 활용된다!
  • 모든 계층에 의존성을 가진 중립적인 컴포넌트를 하나 도입한다!

    • → 의존성 주입을 통해 아키텍처를 구성하는 대부분의 클래스를 초기화하는 역할을 한다!
    • 의존성을 주입받은 클래스는 해당 인터페이스만 알고 있을 뿐, 그 인터페이스의 실제 구현체(인스턴스)에 대해서는 알지 못한다.

유지보수 가능한 SW를 만드는 데 어떻게 도움이 될까?


  • 실제 코드 구조를 목표하는 아키텍처에 가깝게 만들어주는 헥사고날 아키텍처의 패키지 구조를 살펴봤다.

  • 헥사고날 아키텍처의 패키지 구조를 사용함으로써, 의사소통, 개발, 유지보수가 모두 조금 더 수월해진다.

    • 학습 난이도를 충족한 경우라고 판단된다.
profile
코드 품질의 중요성을 아는 개발자 👋🏻

0개의 댓글

관련 채용 정보