3장 : 코드 구성하기

계층으로 구성하기

계층형 코드 구조

문제

  • 애플리케이션의 기능 조각(functional slice)이나 특성(feature)을 구분 짓는 패키지 경계가 없다.

  • 애플리케이션이 제공하는 유스케이스를 파악할 수 없다. AccountService와 AccountController가 어떤 유스케이스를 구현했는지 파악할 수 있겠는가 ? 특정 기능을 찾기 위해서는 어떤 서비스가 이를 구현했는지 추측해야 하고, 해당 서비스 내의 어떤 메서드가 그에 대한 책임을 수행하는지 찾아야 한다.

  • 패키지 구조를 통해 목표로 하는 아키텍처를 파악할 수 없다.

기능으로 구성하기

기능 중심 코드 구조

특징

  • 각 기능을 묶은 새로운 그룹은 account와 같은 레벨의 새로운 패키지로 들어가고, 패키지 외부에서 접근되면 안 되는 클래스들에 대해 package-private 접근 수준을 이용해 패키지 간의 경계를 강화할 수 있다. 패키지 경계를 package-private 접근 수준과 결합하면 각 기능 사이의 불필요한 의존성을 방지할 수 있다.

  • AccountService의 책임을 좁히기 위해 SendMoneyService로 클래스명을 바꿨다.

문제

  • 기능에 의핸 패키징 방식은 가시성이 더 떨어짐(어댑터를 나타내는 패키지명 없음, 인커밍/아웃고잉 포트 확인 불가)

  • 의존성을 역전시켰음에도 불구하고, package-private 접근 수준을 이용해 도메인 코드가 영속성 코드에 의존하고 있다.

💡애플리케이션의 기능을 코드를 통해 볼 수 있게 만드는 것을 가리켜 로버트 마틴이 '소리치는 아키텍처'라고 명명한 바 있다. 왜냐하면 코드가 그 의도를 우리에게 소리치고 있기 때문이다.

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

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

💡 육각형 아키텍처에서 구조적으로 핵심적인 요소는 엔티티, 유스케이스, 인커밍/아웃고잉 포트, 인커밍/아웃고잉(혹은 주도하거나 주도되는) 어댑터이다.

account

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

domain

  • 도메인 모델이 속함

application

  • 도메인 모델을 둘러싼 서비스 계층 포함

  • 인커밍 포트 인터페이스인 SendMoneyUseCase를 구현한 SendMoneyService

  • 아웃고잉 포트 인터페이스이자 영속성 어댑터에 의해 구현된 LoadAccounrPort, UpdateAccountStatePort

adapter

  • 인커밍 어댑터 : 애플리케이션 계층의 인터밍 포트호출

  • 아웃고잉 어댑터 :애플리케이션 계층의 아웃고잉 포트에 대한 구현을 제공

패키지간 접근 허용 수준

  • 어댑터 패키지 들어 있는 모든 클래스들은 application 패키지 내에 있는 포트 인터페이스를 통하지 않고는 바깥에서 호출되지 않기 때문에 package-private 접근 수준으로 둬도 된다. 그러므로 애플리케이션 계층에서 어댑터 클래스로 향하는 우발적인 의존성은 있을 수 없다.

  • application 패키지와 domain 패키지 내의 일부 클래스들은 public으로 지정해야 한다.

  • public

    • 의도적으로 어댑테에서 접근 가능해야 하는 포트

    • 서비스(잠재적으로는 어댑터)에서 접근 가능하도록 해야하는 도메인 클래스

  • 서비스는 인커밍 포트 인터페이스 뒤에 숨겨질 수 있기 때문에 public이 아니어도 됨

장점

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

  • DDD 개념에 직접적으로 대응시킬 수 있다.

의존성 주입의 역할

클린 아키텍처의 본질 : 애플리케이션 계층이 인커밍/아웃고잉 어댑터에 의존성을 갖지 않는다.

인커밍 어댑터

  • 예제코드의 웹 어댑터와 같이 인커밍 어댑터에 대해서는 그렇게 하기가 쉽다. 제어 흐름의 방향이 어댑터와 도메인 코드 간의 의존성 방향과 같은 방향이기 때문이다. 어댑터는 그저 애플리케이션 계층에 위치한 서비스를 호출할 뿐이다. 그럼에도 불구하고 애플리케이션 계층으로의 진입점을 구분 짓기 위해 실제 서비스를 포트 인터페이들 사이에 숨겨두고 싶을 수 있다.

아웃고잉 어댑터

  • 예제 코드의 영속성 어댑터와 같이 아웃고잉 어댑터에 대해서는 제어 흐름의 반대 방향으로 의존성을 돌리기 위해 의존성 역전 원칙을 이용해야 한다.

  • 애플리케이션 계층에 인터페이스를 만들고 어댑터에 해당 인터페이스를 구현한 클래스를 두면 된다. 육각형 아키텍처에서는 이 인터페이스가 포트다.

포트 인터페이스를 구현한 실제 객체를 누가 애플리케이션 계층에 제공해야 할까 ? 포트를 애플리케이션 계층 안에서 수동으로 초기화하고 싶지는 않다. 애플리케이션 계층에 어댑터에 대한 의존성을 추가하고 싶지는 않기 때문이다.

의존성 주입이란..?

  • 모든 계층에 의존성을 가진 중립적인 컴포넌트

  • 이 컴포넌트는 아키텍처를 구성하는 대부분의 클래스들을 초기화하는 역할을 한다.

예를 들면
  • 중립적인 의존성 주입 컴포넌트

AccountController, SendMoneyService, AccountPersistenceAdapter 클래스의 인스턴스를 만들 것이다. AccountController가 SendMoneyUserCase 인터페이스를 필요로 하기 때문에 의존성 주입을 통해 SendMoneyService 클래스의 인스턴스를 주입한다. 컨트롤러는 인터페이스만 알면 되기 때문에 자신이 SendMoney 인스턴스를 실제로 가지고 있는지도 모른다.

이와 비슷하게 SendMonwyService 인스턴스를 만들 때도 의존성 주입 메커니즘이 LoadAccoutPort 인터페이스로 가장한 AccountPersistenceAdapter 클래스의 인스턴스를 주입할 것이다.

DISCUSSION

  • 규모에 따라서 각 아키텍처(계층형, 헥사고날)의 가치가 다른데, 만약 본인이 아키텍처를 선택한다고 가정했을 때 아키텍처의 선택 기준은 무엇입니까 ?

    • 내 답변 : 팀원의 규모와, 프로젝트의 규모, 팀원들의 수준과 (러닝커브 고려), 프로젝트 기한을 고려하여 선정할 것 같습니다. 과거 프로젝트 규모를 선택하는 과정에 있어서 도메인 + 계층형 구조로 디렉터리 구조를 설정할지, 헥사고날 + 도메인 구조로 디렉터리 구조를 설정할지 고민을 했던 경험이 있습니다. 그때 팀의 인원이 소수이고, 프로젝트 규모도 크지 않고, 당시 팀원들 중 헥사고날에 대해 잘 알고있는 사람이 없어 초기에는 계층적 + 도메인으로 구현을 하고, 추후 리팩터링이 필요하면 확장하기로 하였습닏.

  • 헥사고날로 구현할 때 공통 모듈에 대해서는 어떻게 구현하실 생각입닊싸 ?

Last updated