들어가며
이전부터 헥사고날 아키텍처에 대해서 많이 들었었다. 하지만 회사에서 이 아키텍처를 직접 적용하고 있지 않아 크게 관심을 갖지 않았다. 그런데 최근 문득 헥사고날 아키텍처가 떠올라 알아보게 되었다.
헥사고날 아키텍처(Hexagonal Architecture)란?
헥사고날 아키텍처
헥사고날 아키텍처(Hexagonal Architecture)는 소프트웨어의 비즈니스 로직(도메인) 을 입력과 출력(외부 시스템) 으로부터 분리하기 위해 만들어진 소프트웨어 아키텍처 패턴이다.
"포트 앤 어댑터(Ports and Adapters)"라는 별칭을 갖고 있으며, 애플리케이션의 내부 도메인이 외부 시스템(DB, API, 메시지 큐 등)과 직접적으로 의존하지 않도록 만든다.
즉, 핵심 도메인 로직은 "포트(Port)" 를 통해서만 외부와 소통하며, "어댑터(Adapter)" 가 포트와 실제 구현체(DB, API, 파일 시스템 등)를 연결하는 역할을 한다.
주요 개념
헥사고날 아키텍처의 핵심 개념은 포트(Port)와 어댑터(Adapter) 다.
🟢 포트 (Port)
- 입력 포트 (Inbound Port): 애플리케이션이 외부 요청을 받을 때 사용하는 인터페이스 (예: 서비스 인터페이스)
- 출력 포트 (Outbound Port): 애플리케이션이 외부 시스템(DB, API 등)과 통신하기 위한 인터페이스
🔵 어댑터 (Adapter)
- 입력 어댑터 (Inbound Adapter): 컨트롤러(NestJS의 @Controller()) 같은 역할.
- 출력 어댑터 (Outbound Adapter): 외부 시스템과의 실제 통신을 담당하는 구현체(예: TypeORM Repository, API 클라이언트).
📐 헥사고날(Hexagonal)?
내가 공부를 하면서 헥사고날의 가장 중요한 컨셉은 결국 포트와 어댑터를 통해서 핵심 도메인이 외부 시스템에 의존하는 것을 막는 것이라고 이해를 했다. 그런데 왜 헥사고날(육각형)이라는 이름이 붙었을지 궁금했다.
헥사고날 아키텍처(Hexagonal Architecture)라는 이름이 붙은 이유는 이 패턴을 처음 제안한 Alistair Cockburn이 소프트웨어의 구조를 육각형(hexagon) 으로 표현했기 때문이라고 한다.
대단한 무언가가 있을 것이라고 기대했는데... 크게 중요한 내용은 아닌 것 같아 넘어가겠다.
아키텍처 그림
엄밀한 레이어드 혹은 헥사고날 아키텍처를 그리기 보다는 헥사고날 아키텍처가 무엇인지 알아보기 위하여, 대략적인 그림을 그려보겠다.
일반적인 레이어드 아키텍처
아래의 그림은 레이어드 아키텍처를 그린 것이다. 물론 4개의 레이어로 나누는 일반적인 기준이 있지만 여기서는 헥사고날과 비교하기 위해 간소화 하였다.

헥사고날 아키텍처 적용

이전 그림과 비교해 볼 곳은 크게 3개이다.
- 구현체인 Primary Adapter와 Secondary Adapter
- Primary Adapter는 도메인의 인터페이스를 조합하여 외부의 요청을 구현하게 된다.
- Secondary Adapter는 비즈니스 로직에 필요한 세부 데이터베이스 쿼리를 구현한다.
- 인터페이스인 Input Port와 Out Port
- Input Port는 외부의 요청을 받을 때 사용하는 인터페이스이다.
- Out Port는 데이터베이스와 통신하기 위한 인터페이스이다.
- 의존성의 방향
- 도메인 레이어는 API Layer와 Infrastructure Layer에 대해 각각 인터페이스(Port)를 두어, 외부의 변경에 대해 핵심 비즈니스 로직의 변경을 막는다.
- 이를 통해, API Layer와 Infrastructure Layer는 결국 도메인 레이어에 의존하게 된다.
왜 쓰는 것일까?
1️⃣ 핵심 도메인 로직을 외부로부터 보호 (비즈니스 로직 독립성)
- 핵심 도메인 로직이 데이터베이스, 프레임워크, UI 등 특정 기술에 의존하지 않도록 설계 할 수 있다.
- 예를 들어, 특정 데이터베이스(MySQL → PostgreSQL)로 변경하더라도 비즈니스 로직을 수정할 필요가 없다.
- 결국 이 부분이 헥사고날 아키텍처의 핵심이라고 생각한다. 왜냐하면 나머지 장점들도 결국 비즈니스 로직을 완전히 독립시키므로써 얻는 이점이기 때문이다.
🔹 예시
- 아래와 같이 비즈니스 로직은 UserRepository 인터페이스만 참조하고, MySQL, PostgreSQL 등 특정 기술과 분리된다.
export interface UserRepository {
findById(userId: string): Promise<User>;
}
export class MySQLUserRepository implements UserRepository {
async findById(userId: string): Promise<User> {
return this.mysqlClient.query('SELECT * FROM users WHERE id = ?', [userId]);
}
}
2️⃣ 테스트 용이성 (Mocking이 쉬움)
- 비즈니스 로직이 특정 DB, 프레임워크 등에 의존하지 않으므로, 유닛 테스트에서 쉽게 목(Mock) 객체를 사용할 수 있다.
- DB 없이도 서비스 레이어를 독립적으로 테스트할 수 있다.
🔹 예시
- 아래와 같이 mockRepository를 주입하여 실제 DB 없이도 서비스 로직을 검증할 수 있다.
const mockRepository: UserRepository = {
findById: jest.fn().mockResolvedValue(new User('1', 'Alice')),
};
const service = new UserService(mockRepository);
expect(await service.getUser('1')).toEqual(new User('1', 'Alice'));
3️⃣ 확장성 & 유지보수 용이
- 새로운 UI(REST API → GraphQL API, CLI, WebSocket)를 추가할 때 비즈니스 로직을 수정할 필요 없음.
- 새로운 데이터 저장소(MySQL → MongoDB)로 변경해도 포트(인터페이스)만 유지하면 어댑터만 수정하면 됨.
🔹 예시
- 기존에는 REST API만 지원하던 애플리케이션이 GraphQL API 를 추가해야 할 때, 새로운 GraphQL 어댑터만 추가하면 되므로 비즈니스 로직을 수정하지 않아도 됨.
단점
1️⃣ 초기 설계가 복잡하고 비용이 큼
- 작은 프로젝트에서는 Over-engineering(과한 설계)가 될 수 있다.
- 단순 CRUD API만 필요하다면, 굳이 포트-어댑터 패턴을 적용할 필요가 없다.
- 나에게는 이 부분이 가장 어려운 것 같다. 초기 프로젝트는 웬만하면 헥사고날 아키텍처가 오버 엔지니어링이 될 것이라고 생각한다. 그런데 결국 소프트웨어는 발전할 것이기 때문에 미리 헥사고날을 적용하는 것이 좋은지 좋지 않은지 잘 모르겠다. 결국 상황에 맞게 해야겠지.
2️⃣ 추상화로 인해 코드가 많아지고 가독성이 떨어질 수 있음
- Port(인터페이스) + Adapter(구현체) + Service + Controller 등으로 분리되면서 코드량이 많아진다.
- 단순하게
service.findById(id) 하면 될 일을 repository.findById(id) → adapter.findById(id) 로 거쳐야 한다.
- 이 부분도 쉽지 않은 부분이라고 생각한다. 모든 개발자들이 생각이나 관점이 다 같을 수 없다. 그렇다보니 헥사고날 아키텍처를 도입하고 싶은 개발자는 그렇지 않은 개발자를 설득해야하는데 이러한 부분이 설득하기 쉽지 않을 것이라고 생각한다. 그리고 도메인 로직이 외부 로직에 크게 의존하여, 변경이 어려웠던 경험이 개발을 하면서 매우 자주 마주치는 문제는 아닌 경우가 많아서 더 어려운 것 같다.
마무리
헥사고날 아키텍처를 공부하면서 가장 의문인 부분은 입력 포트 (Inbound Port)의 필요성이다.
요구사항이 변경되어 컨트롤러 부분의 변경이 되면 일반적으로 비즈니스 로직의 변경을 동반하는 경우가 대부분일 것이다. 개인적으로 컨트롤러만 변경하고 해결될 만한 변경사항을 본 적은 없다. 그렇다보니 추상화된 입력 포트가 반드시 필요한지 의문이 든다.