Why
여러 엔드포인트, DB, 외부 의존성 등 같은 인터페이스에 다양한 구현이 올 수 있습니다. 이러한 구현들은 어떻게 정리하는 게 좋은 아키텍처라 할 수 있을까요? 헥사고날 아키텍처는 이러한 고도화된 아키텍처에 대한 훌륭한 표준을 제시합니다. 그렇다면 헥사고날 아키텍처는 과연 무엇이고, 어떻게 사용하는 것이 좋은지 알아봅시다.
What
기술적 독립성과 의존성 역전
헥사고날 아키텍처에서 핵심 비즈니스 로직은 외부 요인에 절대적으로 독립적입니다. 즉, 클라이언트에게 어떤 통신 방식(REST API, GraphQL 등)으로 서비스할지, 혹은 데이터를 어디서 읽어올지(데이터베이스, 다른 MSA의 gRPC/REST API, 단순 CSV 파일 등)와 같은 기술적 세부 사항을 전혀 몰라야 합니다. 이 아키텍처는 다음과 같은 구조적 특징을 가집니다. - 안쪽을 향하는 의존성: 외부의 전송 계층은 내부의 인터랙터(Interactor)를 어떻게 호출할지만 알며, 외부의 데이터 소스는 내부에 추상화된 리포지토리(Repository) 인터페이스를 어떻게 구현할지만을 압니다. - 아키텍처 수준의 DIP 적용: 이는 객체 지향 설계의 의존성 역전 원칙(DIP, Dependency Inversion Principle)을 아키텍처에 적용한 것입니다. 전통적인 구조에서는 비즈니스 로직이 데이터베이스(영속성 계층)에 직접 의존했지만, 헥사고날은 도메인 계층 내부에 인터페이스를 두어 외부 데이터 소스가 도메인 내부를 향해 의존하도록 그 방향을 역전시킵니다.
이러한 구조가 가져다주는 이점
이처럼 철저하게 분리된 구조 덕분에 시스템은 외부 환경의 불가피한 변경에 매우 유연하게 대비할 수 있으며, 필요 시 데이터 소스를 교체하는 작업도 간단해집니다. 가장 큰 장점은 프레임워크, 웹 서버, DB 등 무거운 외부 의존성 없이 순수한 비즈니스 로직(Interactor, Entity)만을 대상으로 빠르고 독립적인 단위 테스트(Unit Test)가 가능하다는 것입니다.
헥사고날 아키텍처의 통신 매개체: 포트와 어댑터
헥사고날 아키텍처는 내부(비즈니스 로직)와 외부(인프라)의 경계를 명확히 하기 위해 포트와 어댑터라는 개념을 사용합니다. - 포트 (Port): 비즈니스 로직이 외부와 소통하기 위해 열어둔 인터페이스입니다. - 인바운드 포트: 외부에서 내부 로직을 호출할 때 사용하는 명세입니다. (내부의 인터랙터가 이를 구현합니다.) - 아웃바운드 포트: 내부 로직이 외부로 데이터를 요청할 때 사용하는 명세입니다. (내부의 리포지토리 인터페이스가 이에 해당합니다.) - 어댑터 (Adapter): 포트라는 규격에 맞춰 외부 기술과 핵심 로직을 연결해 주는 실제 구현체입니다. - 인바운드 어댑터: 외부의 요청을 받아 내부로 전달하는 전송 계층입니다. (예: REST Controller, Event Listener) - 아웃바운드 어댑터: 내부의 요청을 받아 외부 시스템과 통신하는 데이터 소스입니다. (예: SQL DB Adapter, 외부 API Client)
내부 영역: 비즈니스 로직을 구성하는 3가지 핵심 요소
시스템의 가장 깊숙한 곳에 위치하며, 프레임워크나 DB에 의존하지 않는 순수한 도메인 영역입니다. 이 세 가지 요소를 잘 조합하면 데이터의 저장 방식이나 외부 환경에 신경 쓰지 않고 비즈니스 규칙 그 자체에만 집중할 수 있습니다. 1. 엔티티 (Entity) - 핵심 도메인 객체로, 자신이 DB의 어디에 어떻게 저장되는지 전혀 알지 못합니다. - 단순히 데이터를 담아두는 바구니가 아니라, 비즈니스 규칙과 행위를 스스로 캡슐화합니다. - 설계 관점에서는 도메인의 무결성을 지키는 애그리거트 루트(Aggregate Root) 역할을 하거나, 상태 변이가 없는 값 객체(Value Object) 등을 포괄하는 최상위 모델로 기능합니다. 2. 리포지토리 (Repository) - 엔티티를 조회하거나 변경하기 위한 인터페이스(명세)입니다. - 데이터 소스와 통신하여 단일 엔티티나 엔티티 목록을 반환하는 메서드들의 시그니처만 정의하며, 실제 구현 코드는 포함하지 않습니다. 3. 인터랙터 (Interactor) - 도메인 작업을 조율하고 실행하는 클래스로, 일반적으로 Service나 Use Case 객체로 불립니다. - 특정 도메인 작업에 필요한 복잡한 비즈니스 규칙의 흐름을 제어하고 검증 로직을 수행합니다.
외부 영역: 비즈니스 로직을 감싸는 바깥 계층
외부 세계와 맞닿아 있는 영역으로, 언제든지 다른 기술로 교체될 수 있는 세부 구현 기술들이 위치합니다. - 전송 계층 (Transport Layer) - 시스템의 입력(Input)을 담당하는 인바운드 어댑터입니다. - 사용자나 외부 시스템의 요청을 받아 내부의 인터랙터를 호출함으로써 비즈니스 로직을 실행시킵니다. - 비즈니스 로직이 인터랙터로 철저히 분리되어 있기 때문에 특정 컨트롤러 기술에 얽매이지 않습니다. 즉, REST API 컨트롤러뿐만 아니라 메시지 큐의 이벤트, Cron 스케줄러, CLI 명령어 등을 통해서도 완전히 동일한 인터랙터(비즈니스 로직)를 실행할 수 있습니다. - 데이터 소스 (Data Source) - 다양한 저장소 및 외부 시스템에 연결하기 위한 출력(Output), 즉 아웃바운드 어댑터입니다. - 내부에 정의된 리포지토리 인터페이스를 실제로 구현하여, 데이터를 읽고 쓰는 구체적인 작업을 수행합니다. - SQL DB나 Elasticsearch 같은 실제 데이터베이스용 어댑터는 물론, 로컬 테스트를 위한 단순 Hash Map 형태의 인메모리 어댑터로도 쉽게 교체하여 구현할 수 있습니다.
How
헥사고날 아키텍처를 실제 프로젝트에 적용할 때 가장 중요한 핵심 원칙은 "안에서 밖으로(Inside-out)" 개발하는 것입니다. 데이터베이스 스키마나 웹 프레임워크의 설정을 먼저 고민하는 전통적인 방식에서 벗어나, 가장 핵심이 되는 순수한 도메인 로직부터 시작해야 합니다.
1단계: 순수한 도메인 모델(Entity) 설계
- 외부 기술 요소(예: JPA의
@Entity어노테이션, JSON 직렬화 라이브러리 등)가 전혀 섞이지 않은 순수한 클래스로 도메인 객체를 생성합니다. - 데이터베이스에 어떻게 저장될지 고민하지 말고, 이 객체 안에 핵심 비즈니스 규칙과 상태 변경 로직을 캡슐화하는 데 집중합니다.
2단계: 비즈니스 흐름(Interactor)과 포트(Port) 정의
- 인터랙터(Use Case) 구현: 도메인 객체를 활용하여 실제 비즈니스 요구사항의 흐름을 제어하고 검증하는 클래스를 작성합니다.
- 아웃바운드 포트 도출: 로직을 수행하다가 데이터를 저장하거나 외부 시스템 조회가 필요한 시점에, 구체적인 기술 대신 추상화된 인터페이스(Repository)를 정의합니다. 내부의 인터랙터는 이 인터페이스의 명세에만 의존하도록 코드를 작성합니다.
3단계: 외부와 통신할 어댑터(Adapter) 구현
이제 비로소 바깥 계층의 기술적인 부분들을 구현합니다. - 아웃바운드 어댑터: 2단계에서 만든 리포지토리 인터페이스를 실제로 구현하는 클래스(예: Spring Data JPA 어댑터, 외부 결제 API 연동 클래스 등)를 작성합니다. - 인바운드 어댑터: 시스템 밖에서 들어오는 요청을 받아 내부의 인터랙터를 호출하는 진입점을 만듭니다. (예: REST API Controller, 메시지 큐 Listener, CLI 진입점 등)
4단계: 의존성 주입(DI)을 통한 조립(Wiring)
- 애플리케이션의 시작점(프레임워크의 영역)에서 제어의 역전(IoC) 컨테이너나 의존성 주입(DI)을 활용해 객체들을 조립합니다.
- 런타임에 구현된 아웃바운드 어댑터(DB 로직)를 내부의 아웃바운드 포트(인터페이스)에 주입해 줍니다.
- 도메인 영역은 프레임워크의 존재를 모르지만, 바깥쪽의 프레임워크가 안쪽으로 필요한 부품을 알아서 끼워 맞춰주는 형태가 완성됩니다.
한눈에 보는 헥사고날 패키지(디렉토리) 구조 예시 아키텍처의 의도를 명확히 하기 위해 패키지부터 명시적으로 분리하는 것이 좋습니다.
domain/: 핵심 Entity들이 위치 (외부 기술 의존성 0%)application/:port/in/: 인바운드 포트 (유스케이스 인터페이스)port/out/: 아웃바운드 포트 (리포지토리 인터페이스)interactor/: 실제 비즈니스 로직이 담긴 Service 구현체adapter/:in/web/: REST Controller 등 인바운드 어댑터out/persistence/: DB 연동을 담당하는 아웃바운드 어댑터