도메인과 도메인 모델에 대해 이야기할 때, 팀 내에서 특정 대상을 명확하고 일관된 단어로 부르는 것이 매우 중요하다. 이를 ‘보편 언어(Ubiquitous Language)’로 발전시키려는 노력이 필요하며, 개발 기술에서도 마찬가지이다. 하나의 기술 개념을 두 가지 이상의 용어로 표현하거나, 하나의 단어가 여러 의미로 사용되는 것은 개발팀 내 커뮤니케이션을 방해하고 상호 이해를 어렵게 만드는 주된 원인 중 하나이다. 명확한 이름을 선택하고 그 정의를 팀 내에서 공유하며, 일종의 ‘기술 보편 언어’를 만드는 것을 개발팀에게 매우 종요한 일이다.
헥사고날 아키텍처의 중심에 있는 육각형, 즉, 도메인 로직을 담은 코어 애플리케이션을 부르는 이름은 다양하다. 헥사곤, (코어) 애플리케이션, 앱, 코어 시스템 등이 있으며, 자주 불리지는 않지만 SUD(System Under Development), SUT(System Under Test)도 있다. 이 글에서는 이 중심부를 앞으로 ‘애플리케이션’이라고 지칭한다
애플리케이션의 외부 세계의 상호작용: 포트와 어댑터
애플리케이션 (The Hexagon)
육각형은 이 글에서 ‘애플리케이션’이다. 이 애플리케이션은 내부에 도메인 로직을 담고 있으며, 외부 세계와 소통한다. 내부와 외부로 구분되며 이들 사이에 상호작용(인터렉션)이 필요하다. 외부에는 다양한 ‘액터(Actor)’가 존재할 수 있다
- 사용자: 애플리케이션과 특정 의도를 가지고 소통하는 가장 대표적인 주체이다.
- 기계 / 로봇, 다른 컴퓨터 시스템: 애플리케이션을 사용하는 또 다른 액터가 될 수 있다
- 테스트 코드: 테스트 목적으로 애플리케이션과 소통하는 액터이다
애플리케이션이 기능을 수행하는 과정에서 외부 액터와의 소통이 필요할 때도 있다
- 데이터베이스 시스템: 데이터를 저장하거나 조회하는 역할
- 클라우드 서비스: 클라우드 환경의 서버나 서비스
- 메시징 시스템: 메일이나 메시지를 처리하는 시스템
- 목 오브젝트(Mock Object) / Mock Server: 테스트를 위해 실제 시스템 대신 사용하는 가상의 액터
이러한 액터들과 애플리케이션이 상호작용 하는 것은 ‘포트(Port)’를 통해서 이루어진다
포트(Port)
컴퓨터의 시리얼 포트, VGA 포트, USB 포트, 이더넷 포트처럼 컴퓨터가 외부 장치와 소통하기 위해 각각의 의도와 목적에 따라 연결할 수 있는 포트를 제공하는 것과 유사하다. 헥사고날 아키텍처에서 ‘포트’는 애플리케이션이 외부 세계와 어떤 의도를 가지고 상호작용하는 아이디어를 캡처한 것이다. 이는 단순히 데이터를 주고 받는다는 의미를 넘어 각 포트가 명확한 목적과 방향을 가지고 외부와 연결된다는 것을 의미한다
육각형 모양의 애플리케이션의 각 면 하나하나가 포트라고 볼 수 있다. 예를 들어, 회원 등록을 처리하기 위한 포트, 구매하기 포트, 장바구니 포트처럼 애플리케이션 경계에서 액터들과 어떤 의도를 가지고 소통할 것인가롤 정의하는 것이 포트이다.
포트는 결국 애플리케이션이 가지고 있는 ‘인터페이스’이며, 크게 두 가지 종류가 있다
Lollipop: Provided Interface (기능 제공 인터페이스)
- 다이어그램에서는 막대사탕 모양(Lollipop)으로 표현된다
- 애플리케이션이 가지고 있는 기능을 외부 액터가 사용할 수 있도록 노출한다
- 예시 – 사용자가 ‘회원 등록’ 포트의 인터페이스를 연결하여 사용하는 경우. 테스트 코드가 애플리케이션의 기능을 이용하는 경우
- 이러한 인터페이스를 통해 애플리케이션의 기능을 사용하는 액터를 ‘Primary Actor(주도 액터)’라고 부른다. Primary Actor는 Provided Interface를 이용해 애플리케이션과 연결한다
Socket: Required Interface (기능 요구 인터페이스)
- 다이어그램에서는 소켓 모양(Socker)으로 표현된다
- 애플리케이션이 동작하다가 어떤 기능이 필요한데, 이 기능을 애플리케이션이 직접 가지고 있지 않을 때 사용한다. 애플리케이션은 “이런 기능이 필요해, 이런 방식으로 연결해 줘”라고 요구하는 인터페이스를 노출한다
- 이 인터페이스는 누군가(외부 액터 또는 어댑터)가 구현해야 한다
- 예시: 메일 서버 연결, 테스트용 메일 발송 목(Mock) 사용
- 이러한 인터페이스와 연결될 수 있는 외부 액터를 ‘Secondary Actor(피동 액터)’라고 부른다. Secondary Actor는 애플리케이션이 요구하는 기능을 담은 Required Interface와 연결된다
애플리케이션은 이 포트를 가지고 외부 세계와 연결된다
어댑터 (Adapter)
액터와 애플리케이션을 연결해야 하는데, 애플리케이션의 포트(인터페이스)를 액터가 직접 연결해 줄 수 없는 경우가 있다(물론 테스트 코드처럼 직접 연결할 수 있는 경우도 있다)
- Provided Interface 연결: 웹 브라우저는 애플리케이션이 노출한 회원 가입 포트(기능 제공 인터페이스)를 직접 사용할 수 없다. 따라서 ‘웹 컨트롤러 어댑터’를 만들어서 이를 통해 기능 제공 인터페이스와 연결해야 한다
- Required Interface 연결: 애플리케이션이 외부 DB와 연결하기 위해 노출하고 있는 기능 요구 인터페이스도 DB라는 액터가 직접 연결할 수는 없다. 이런 경우, 기능 요구 인터페이스를 구현한 ‘리포지토리 어댑터(Repository Adapter)’를 만들 필요가 있다
애플리케이션(헥사곤)은 외부 세계와의 소통을 위해 인터페이스로 만들어진 포트를 외부에 노출한다. 그러나 액터가 연결할 방법이 없는 경우, 이 사이에 어댑터를 만들어 연결을 시켜주는 것이다. 어댑터는 원래 서로 다른 모양이나 규격을 가진 두 대상을 연결할 때 사용되는 도구이다
포트와 어댑터 아키텍처
‘포트와 어댑터 아키텍처’는 헥사고날 아키텍처의 특징을 담은 또 다른 이름이다. 헥사고날 아키텍처의 핵심 특징이 포트와 어댑터이기 때문에 알리스테어 콕번 – Alistair Cockburn(헥사고날 아키텍처 창시자)은 사람들이 육각형 모양에 집착하는 경향을 보고 아키텍처의 본질을 더 명확하게 설명하기 위해 이 이름을 제시했다. 따라서 헥사고날 아키텍처 관련 자료에서 ‘포트와 어댑터 아키텍처’라는 표현이 나오면 동일한 개념을 가리킨다고 이해하면 된다. 이 이름은 잘 사용되지 않고 “헥사고날 아키텍처”라는 우아하고 이쁜 이름으로 계속 불리운다
대칭성과 흐름: Primary/Driving & Secondary/Driven
헥사고날 아키텍처는 기본적으로 대칭성을 통해 아키텍처를 설명한다. 애플리케이션의 ‘내부’와 ‘외부’를 구분하는 것이 가장 중요하지만, 실제 시스템의 사용 흐름을 보면 계층 구조와 유사한 특징을 가진다. 일반적으로 Primary Actor 쪽에서 시작하여 애플리케이션을 거쳐 필요에 따라 Secondary Actor를 사용하는 흐름이다. 이를 부르는 이름도 기억할 필요는 있다
- Primary Actor / Driving Actor: 애플리케이션이 제공하는 기능을 사용하는 액터
- Primary Adapter / Driving Adapter: Primary Actor와 애플리케이션을 연결하는 어댑터 (애플리케이션을 ‘운전하는’, ‘사용하는’ 주체와 어댑터라는 의미)
- Secondary Actor / Driven Actor: 애플리케이션이 동작하는 데 필요한 기능을 제공하는 외부 액터
- Secondary Adapter / Driven Adapter: Secondary Actor와 애플리케이션을 연결하는 어댑터(애플리케이션에 의해 ‘구동되는’, ‘영향을 받는’ 액터와 어댑터라는 의미)
헥사고날 아키텍처에 대한 흔한 오해들
개발자들 사이에서 헥사고날 아키텍처에 대한 몇 가지 오해가 있다고 전해 들었다. 이 오해가 어디에서 시작 되었는지는 모르겠고 오해라고 오해했을 수도 있다
오해 1: 애플리케이션 내부에 도메인 계층을 만들어야 한다
- 헥사고날 아키텍처는 애플리케이션의 내부 구현에 대한 특정 원칙이나 요구사항을 강요하지 않는다. 내부가 스파게티(맛있겠다) 코드이든, 트랜잭션 스크립트 방식이든, 도메인 모델 패턴을 따르지 않더라도, 헥사고날 아키텍처의 기본 요구사항(포트와 어댑터 사용, 외부 기술 의존성 없음 등)만 지킨다면 헥사고날 아키텍처라고 부를 수 있다
- 종종 헥사고날 내부 구조에 도메인 계층을 더 추가한 이미지들을 볼 수 있는데, 이는 ‘클린 아키텍처(Clean Architecture)’와 같은 다른 아키텍처 패턴의 영향을 받은 것이다. 헥사고날 아키텍처와 클린 아키텍처는 목표와 유사한 특징을 공유하지만, 서로 다른 아키텍처 패턴이다. 이점을 명확히 구분해야 한다
오해 2: 헥사고날 아키텍처 패키지 구조를 따라야 한다
- 헥사고날 아키텍처는 특정 패키지 구조를 강요하지 않는다. 스프링 프레임워크에서 흔히 사용하는 패키지 구조를 거의 그대로 사용할 수 있다. 다만, 한 패키지(또는 서브 패키지) 안에 애플리케이션 코드와 어댑터 코드가 혼재되지 않도록 잘 분리하는 것이 바람직하다. 애플리케이션과 어댑터 패키지를 분리하고 애플리케이션의 포트에 해당하는 인터페이스는 별도의 패키지에 두어 빠르게 식별할 수 있도록 하는 것을 권장한다
오해 3: 포트는 UseCase라는 접미사를 사용한다
- 포트에 해당하는 인터페이스를 만들 때 ‘UseCase’라는 접미사를 붙여야 한다고 오해 아닌 오해를 하는 경우가 있다(예: MemberRegistrationUseCase). 헥사고날 아키텍처가 요구하는 것은 포트가 ‘의도’를 담은 이름을 사용하는 것이다
- Alistair Cockburn은 For+~ing 형식(예: ForRegisteringMember)으로 의도를 표현하는 이름을 제안하기도 했지만, 이는 강제 사항이 아니다. 접미사나 접두사가 너무 길거나 특별한 의미를 더해주지 않는다면, 단순히 의도를 명환히 드러내는 간결한 이름을 사용하는 것이 좋다
오해 4: 애플리케이션에는 도메인 모델만 넣고, JPA 엔티티 등은 어댑터에 둬야 한다
- 이 주장은 헥사고날 아키텍처와 직접접인 관련은 없다. 애플리케이션 내부에 순수한 도메인 모델을 넣고 데이터 모델(JPA Entity)을 분리하여 어댑터에서 매핑해야 한다는 내용을 헥사고날 아키텍처의 필수 요구사항이 아니다. 이렇게 구현할지 말지는 헥사고날 아키텍처 적용 여부와는 별개의 문제이다
- 헥사고날 아키텍처가 요구하는 것은 ‘애플리케이션 코드가 외부 기술에 의존하지 않아야 한다’는 것이다. 즉, 애플리케이션 내부의 도메인 로직 코드와 포트 인터페이스가 특정 웹 기술, 데이터베이스 기술, 프레임워크 등에 묶여서는 안 된다는 의미이다
헥사고날 아키텍처가 요구하는 핵심 원칙
- 포트 정의: 애플리케이션은 모든 외부와의 상호작용을 위해 ‘Provided Interface'(제공하는 기능)와 ‘Required Interface'(필요한 기능)을 애플리케이션 자체에서 정의하고 가지고 있어야 한다. 특히 Required Interface(예: Repository, MailSender 인터페이스)는 어댑터 쪽에 두는 것이 아니라 애플리케이션 내부에 정의되어야 한다. 이는 DIP(Dependency Inversion Principle, 의존 관계 역전 원칙)과 연결되는 개념이다
- 런타임 연결: 애플리케이션과 외부 액터는 런타임(소프트웨어 실행 시점)에 연결되어야 한다. 이는 구성(configuration) 또는 주입(injection)을 통해 이루어진다.
- 코드 의존성 제거: 애플리케이션은 외부 액터에 대한 직접적인 코드 의존성을 가지면 안 된다. 예를 들어, 데이터베이스 종류가 바뀌거나 목 오브젝트로 대체되더라도 애플레케이션 내부 코드는 변경되지 않아야 한다. Spring의 Dependency Injection 개념을 이해하면 이 원칙을 따르는 코드를 만드는 데 도움이 된다
- 포트를 통한 상호작용: 외부 액터와의 오직 정의된 포트(인터페이스)를 통해서만 연결해야 한다. 애플리케이션 내부에서 new 키워드를 사용하여 외부 액터와 연결하는 어댑터 같은 객체를 직접 생성해서는 안 된다
- 기술 중립적인 포트: 포트 인터페이스는 오직 ‘의도’를 드러내는 코드만 포함해야 하며, 어떠한 기술 의존적인 이름도 등장해서는 안 된다. 예를 들어, HttpCookie, JwtAccessToken, DatabaseConnection, RestTemplate 등 특정 웹, 보안, DB, 프레임워크 기술에 묶인 용어는 포트 인터페이스에 사용되지 않아야 한다
이러한 원칙들을 잘 지켜서 애플리케이션 안에 도메인 로직을 충실하게 담아내기만 하면, 헥사고날 아키텍처를 올바르게 사용하는 것이 된다
헥사고날 아키텍처와 계층 구조
헥사고날 아키텍처를 계층형 아키텍처가 아니라고 이야기하기도 하지만, 사실 애플리케이션 내부와 외부, 그리고 이를 연결하는 어댑터라는 구분은 일종의 계층 구조로도 설명될 수 있다. 여기에 도메인 모델 패턴을 추가하여 대칭형 계층 구조를 가질 수 있다

출처 – https://herbertograca.com/tag/explicit-architecture/
이 다이어그램을 보면 빨간색 테두리로 된 ‘Application Core'(헥사곤)가 중심에 있다. 그 안에 ‘Domain Layer'(도메인 모델 / 도메인 서비스)를 두고, 그 밖에 ‘Application Layer'(App Services)를 배치하여 외부로 포트(Ports)를 노출한다. 이 애플리케이션 레이어는 도메인 레이어의 절차적인 API 역할을 한다. 그리고 가장 바깥쪽에는 웹 컨트롤러와 같은 ‘Primary/Driving Adapters’와 DB Repository 같은 ‘Secondary/Driven Adapters’가 위치한다. 이 어댑터들도 하나의 얇은 계층을 이룰 수 있고 그 특징은 다음과 같다
- 다층 레이어 구조: 외부에서 내부로 향하는 여러 층의 레이어를 가진다
- 내부 방향 코드 의존성: 가장 중요한 특징은 코드의 의존 방향이 항상 ‘안쪽으로만’ 향한다는 것이다. 즉, 어댑터는 애플리케이션을 의존하고 애플리케이션은 도메인을 의존한다. (어댑터 코드는 애플리케이션이나 도메인의 영향을 받아도 되지만, 그 반대는 안 된다. 어댑터가 바뀌었다고 해서 애플리케이션이나 도메인 코드가 변경되어서는 안 된다)
- 비대칭적 사용 흐름: 사용의 흐름 자체는 Primary Actor에서 시작하여 애플리케이션을 거쳐 Secondary Actor로 향하는 비대칭적인 구조를 가진다
결론적으로, 도메인의 핵심을 담은 도메인 모델은 ‘도메일 계층’안에 두고 이를 감싸는 포트를 가진 ‘애플리케이션 계층'(헥사곤)을 그 밖에 두어 전체가 하나의 애플리케이션이 되도록 만든다. 애플리케이션은 Provided/Required 포트들을 가지고 있으며 필요에 따라 커넥터와의 연결을 돕는 어댑터를 가장 바깥쪽에 만든다. 이 전체를 묶어 하나의 스프링 부트 애플리케이션 등으로 구현할 수 있다. 여기서 가장 중요한 원칙은 ‘코드 의존성의 방향이 항상 밖에서 안으로 향해야 한다”는 것이다
DDD(도메인 주도 개발)의 아키텍처
DDD의 창시자인 Eric Evans는 초기 저서에서 4개의 계층을 가지는 계층형 아키텍처로 DDD를 설명했다. 이후에 도메인 주도 개발 구현(Implementing Domain-Driven Design)이라는 책에서 헥사고날 아키텍처를 DDD의 핵심을 잘 담은 아키텍처로 소개했다. 이 책의 추천사를 Eric Evans가 직접 쓰면서 헥사고날 아키텍처를 인상 깊은 부분 중 하나로 꼽았기에, DDD의 아키텍처를 헥사고날 아키텍처로 이해하는 것은 충분히 합당해 보인다