Programming/Clean Architecture

헥사고날(Hexagonal) 아키텍처란 무엇인가?!

JaeHoney 2022. 12. 27. 08:57

헥사고날 아키텍처는 전통적인 계층형 아키텍처의 단점을 보완하기 위해 생겼다.

  • 도메인 중심 아키텍처의 일종으로 클린 아키텍처를 일반화한 구조 중 하나이다.
  • (포트와 어댑터(Ports and Adaters) 아키텍처라고도 말한다.)

계층형 아키텍처의 문제

1. 데이터베이스(+ 영속성)에 대한 의존이 퍼지게 된다.

전통적인 계층형 아키텍처의 도메인 계층은 영속성에 의존한다.

 

즉, 도메인 계층이 자연스레 데이터 베이스에 의존하게 되어서 데이터 베이스에 변화가 일어난다면 도메인 계층도 변화가 생긴다.

 

추가로 아래와 같이 서비스 계층에서도 영속성 모델을 도메인 모델 처럼 사용하게 된다.

결과적으로 해당 도메인 모델을 사용하는 서비스 계층에서도 즉시 로딩, 지연 로딩, 트랜잭션, 플러시 등을 고려해야 하고, 영속성에 대한 의존이 프로젝트 전체적으로 퍼지게 되고 변경에 취약해진다.

2. 아키텍처 경계를 강제할 수 없다.

전통적인 계층형 아키텍처에서는 상위 계층에 있는 컴포넌트를 접근할 목적으로 해당 컴포넌트를 하위 계층으로 내려버릴 수 있다.

해당과 같은 행위가 쌓이면, 점점 경계가 모호해지다가 결국 경계가 허물어지게 된다. 위 그림처럼 모든 계층에서 헬퍼나 유틸리티에 의존하게 될 수도 있다.

 

중요한 것은 부적절한 선택지를 닫아야 한다는 점이다. 선택지가 열려 있으면 누군가는 반드시 그렇게 하기 마련이다.

3. 계층을 Skip 할 수 있다.

계층형 구조에서 많이 나타나는 형태로 계층을 건너뛰는 것이 가능하다.

 

가령, 구현이 간단한 경우 Controller에서 바로 도메인을 참조해서 로직을 작성할 수 있다.

이 경우 첫 번째 문제는 기능이 확장이다. '기능이 추가되면 해당 로직을 서비스 계층으로 로직을 옮기겠지..?' 라고 생각하지만, 다른 동료는 그렇게 생각하지 않을 수 있다.

 

두 번째 문제는 테스트가 복잡해진다는 것이다. 위 그림의 경우 Controller가 Application Layer를 건너뛰고 있다. 그래서 Controller의 단위 테스트를 하는데 영속성 계층도 Mocking을 하면서 복잡한 처리를 해야만 테스트가 가능하게 된다.

4. 유스케이스를 숨긴다.

개발자들은 새로운 유스케이스를 구성하는 새로운 코드를 짜는 것을 선호하지만, 실제로는 기존 코드를 바꾸는 데 더 많은 시간을 소모한다.

  • 유스케이스 로직의 존재 여부, 적절한 위치 등을 파악 등

 

개발자는 유스케이스를 웹 계층에도 생성할 수 있고, 영속성 계층에도 생성할 수 있고 자유롭다.

 

즉, 개발자가 해당 유스케이스가 존재하는 지 여부를 파악하기 어려워서 동일한 로직을 다른 위치에 새롭게 구현하여 코드를 더럽힐 수 있게 된다.

5. 서비스의 크기를 강제할 수 없다.

계층형 구조에서는 서비스의 크기를 강제하지 않는다. 그래서 PostService에 수십개의 서비스 로직을 전부 때려박는 것도 가능해진다.

 

이 경우 Service는 너무 많은 의존을 가져버리고, 수많은 Web 계층이 해당 Service를 의존하게 된다.

 

결국 서비스를 테스트하기 어려워지고 작업해야 할 유즈케이스를 찾기도 힘들어진다.

클린 아키텍처

아래는 로버트 C. 마틴이 소개하는 Clean Architecture의 그림이다.

그는 클린 아키텍처에서는 비즈니스 규칙이 외부(Data-base, Framework, UI, 외부 System)로부터 독립적으로 만들어 테스트를 용이하게 하고, 비즈니스 규칙이 외부의 영향을 받지 않는다.

 

이는 도메인 코드가 바깥으로 향하는 어떠한 의존성도 없어야 함을 의미한다. DIP의 도움으로 모든 의존성이 안쪽(도메인)을 향하고 있다.

  • 도메인은 더 이상 어떤 영속성 프레임워크가 사용되는 지 알 수 없다.
    • 특정 프레임워크에서 분리된 안정적인 도메인 코드를 작성할 수 있다.
  • 즉, 영속성 계층이 도메인 계층에 의존하고, 영속성 엔터티를 도메인 엔터티로 변환하는 과정이 필요하다.

도메인 밖에는 Use Cases가 있고, 이는 단일 책임을 갖기 위해 조금 더 세분화 되어 있다.

  • UserService가 아닌 RegisterUserService
  • (이는 비대한 서비스 문제를 피할 수 있다.)

헥사고날 아키텍처

헥사고날 아키텍처(육각형 아키텍처)는 알레스테어 콕번이 만든 용어로 클린 아키텍처를 일반화한 구조 중 하나이다.

  • (육각형 모양은 사실 아무 의미가 없다. 애플리케이션이 다른 시스템이나 어댑터와 연결되는 4개의 면을 나타내기 위해 육각형을 사용했다고 한다.)

헥사고날 아키텍처에서는 사용자 인터페이스나 데이터베이스 모두 비즈니스 로직으로부터 분리해야 하는 외부 요소로 취급한다.

  • 이는 비즈니스 로직이 외부 요소에 의존하지 않고 프레젠테이션 계층과 데이터 소스 계층이 도메인 계층에 의존하도록 만들어야 한다는 것이다.

육각형 안에는 도메인 엔터티와 이와 상호작용하는 비즈니스 로직(UseCase)가 있다. 이는 육각형 외부로 향하는 의존성이 없기 때문에 클린 아키텍처에서 제시한 의존성 규칙이 그대로 적용된다. 모든 의존성은 코어를 향한다.

 

서론에서 헥사고날 아키텍처는 포트와 어댑터 아키텍처로 부르기도 한다고 했다. 육각형 바깥에는 애플리케이션과 상호작용하는 어댑터가 있다. 일부 어댑터는 외부 시스템과 상호작용하며, 데이터베이스와 상호 작용하는 어댑터도 있다.

  • 왼쪽에 있는 어댑터는 애플리케이션 코어를 호출하는 어댑터이고, 오른쪽에 있는 어댑터는 애플리케이션 코어에 의해 호출되는 어댑터이다.
  • 어댑터와 애플리케이션 코어 간 통신을 할 때는 각각의 포트를 사용해야 한다.
    • 왼쪽에 있는 어댑터의 포트는 유스케이스에 의해 구현되고 호출되는 인터페이스가 된다.
    • 오른쪽에 있는 어댑터의 포트는 어댑터에 의해 구현되고 코어에 의해 호출되는 인터페이스가 된다.

정리하자면 클린 아키텍처와 헥사고날 아키텍처 모두 의존성을 역전시켜 도메인 코드가 바깥쪽 코드에 의존하지 않게 함으로써 외부로 부터의 도메인 로직의 결합을 제거한다. 변경할 이유가 적을수록 유지보수성이 높은 코드가 된다.

패키지 구조

다음은 '만들면서 배우는 클린 아키텍처'라는 책에서 제시한 헥사고날 아키텍처의 패키지 구조이다.

payment-system
        ㄴ account
            ㄴ adapter
                ㄴ in
                    ㄴ web
                        ㄴ AccountController
                ㄴ out
                    ㄴ persistence
                        ㄴ AccountPersistenceAdapter
                        ㄴ SpringDataAccountRepository
            ㄴ domain
                ㄴ Account
                    ㄴ Activity
            ㄴ application
                ㄴ SendMoneyService
                ㄴ port
                    ㄴ in
                        ㄴ SendMoneyUseCase
                    ㄴ out
                        ㄴ LoadAccountPort
                        ㄴ UpdateAccountStatePort

최상위에는 Account와 관련된 UseCase를 구현하는 모듈인 account 패키지가 있고, 다음 레벨에는 domain 패키지와 application 패키지가 있다.

 

application 패키지는 도메인 모델을 둘러싼 서비스를 포함한다.

  • SendMoneyService는 인커밍 포트 인터페이스인 SendMoneyUseCase를 구현한다.
  • SendMoneyService는 아웃고잉 포트 인터페이스이자 영속성 어댑터에 의해 구현되는 LoadAccountPort와 UpdateAccountStatePort를 사용한다.

 

adapter 패키지application 계층의 인커밍 포트(SendMoneyUseCase)를 호출하는 인커밍 어댑터아웃고잉 포트(LoadAccountPort, UpdateAccountStatePort)에 대한 구현을 제공하는 아웃고잉 어댑터를 포함한다.

 

이러한 아키텍처의 추가적인 장점은 아키텍처와 코드 간의 갭을 줄인다는 것이다. 가령, 고수준으로 표현된 아키텍처 그림이 있다고 했을 때, 해당 그림을 보고 원하는 패키지(adapter/out/어댑터 이름, ...)를 찾아가는 것이 간단하다.

 

이처럼 표현력 있는 구조는 아키텍처에 대한 적극적인 사고를 촉진한다. 현재 작업 중인 코드를 어떤 패키지에 넣을 지 고민하게 되기 때문이다.

접근 제한자

패키지의 수가 아주 많아진다는 것이 모든 것을 public으로 만들어서 패키지 간의 접근을 허용해야 한다는 것은 아닐까? 걱정스럽다.

 

하지만 그렇지 않다!

 

이 패키지에 들어 있는 모든 클래스 들은 application 패키지 내에 있는 포트 인터페이스를 통하지 않고는 바깥에서 호출될 수 없다. 그래서  모든 어댑터를 package-private(Java 기준 default) 접근 수준으로 둘 수 있다.

  • 이러한 구조는 애플리케이션 계층에서 어댑터 계층으로 향하는 의존을 막는다!

다형성

어댑터 패키지를 따로 두는 것은 다형성으로 인해 DIP 원칙을 지키는 데에 큰 도움을 준다.

 

예를 들어 RDB에서 Key-Value DB로 변경을 할 때 새로운 아웃고잉 포트들만 어댑터 패키지에 새로 구현하고, 기존 어댑터를 지우면 된다.

  • 변경의 내용이 도메인 및 안정적인 것에 전파되지 않는다.

DDD

추가로 위 헥사고날 패키지의 최상위에는 account가 있었다. 이는 통신할 전용 진입점과 출구(포트)를 포함하는 바운디드 컨텍스트에 해당한다.

 

그래서 DDD의 도구들을 이용해서 도메인 모델 설계가 가능하다.

참고

해당 패키지 구조 및 소스 코드는 아래 링크에서 확인할 수 있다.

 

Reference