최근 프로젝트에서 헥사고날 아키텍처(Hexagonal Architecture)를 기반으로 개발을 진행하게 되었다.
처음에는 생소한 구조와 복잡함 때문에 적응하는 데 시간이 걸렸지만, 구현을 거듭할수록 파편화되어 있던 개념들이 하나둘 정리되기 시작했다.
그동안 고민하며 익힌 내용들을 잊기 전에 기록으로 남겨보려 한다.
❓ 기존 구조 vs 헥사고날 아키텍처
- 기존에 익숙했던 구조는 전형적인 계층형 아키텍처(Layered Architecture)로,
Controller - Service - Mapper의 3단계 계층 구조였다.
헥사고날 아키텍처는 여기에 Port / Adapter / Domain 계층이 추가되면서 구조가 더 세분화된다.
단순히 계층이 늘어난 것이 아니라, 역할과 의존성을 기준으로 책임을 분리하여 '비즈니스 로직'을 외부 기술로부터 격리하는 것이 핵심이다.
장점
- 유연성 : 외부 시스템이나 인프라와의 의존성이 낮아 구성요소를 쉽게 교체하거나 업데이트가 가능
- 테스트 용이성 : 비즈니스 로직을 독립적으로 테스트할 수 있음
- 유지보수성 : 책임이 분리되어 있어 코드의 이해와 수정이 용이
단점
- Port / Adapter 구조로 인한 설계 및 관리 복잡도 증가
- 초기 개발 시간 증가
- 단순 CRUD에서도 구조가 과해질 수 있음
- 여러 계층을 거치며 디버깅 시 추적 비용 증가
📁 패키지 구조
🗂️ post | 도메인 기준 패키지
- 📂
adapter | 외부와의 연결
- 📂
in | 입력 어댑터
- 📂
web
- 📂
dto
- PostRequest.java : 요청 DTO
- PostResponse.java : 응답 DTO
- PostController.java : @RestController
- 📂
out | 출력 어댑터
- 📂
persistence
- 📂
entity
- PostEntity.java : DB 매핑 객체
- 📂
mapper
- PostPersistenceMapper.java : Entity ↔ Domain 모델 변환
- 📂
repository
- PostMapper.java : @Mapper / MyBatis Mapper 인터페이스
- PostPersistenceAdapter.java : DB 접근 (RepositoryPort 구현체)
- 📂
application | 유스케이스 계층
- 📂
port
- 📂
in
- PostUseCase.java : 입력 포트 (Controller가 의존, Service가 구현)
- 📂
out
- PostRepositoryPort.java : 출력 포트 (DB 접근 추상화, PersistenceAdapter가 구현)
- 📂
service
- PostService.java : @Service (Application Service, UseCase 구현체)
🔄️ 전체 흐름
- 모든 의존성은 Controller → Domain 방향으로만 흐르며, Domain은 외부 계층(DB, 프레임워크 등)에 대해 알지 못한다.
- 서비스 계층은 DB 구현 기술(MyBatis, JPA 등)을 알 필요가 없다. 단지 정의된 RepositoryPort를 호출할 뿐이며, 실제 구현체인 PersistenceAdapter가 그 요청을 처리한다.
1. Controller (Web Adapter)
⬇️ [호출]
2. UseCase (In Port / Interface)
⬆️ [구현]
3. Service (Application Service)
⬇️ [호출]
4. RepositoryPort (Out Port / Interface)
⬆️ [구현]
5. PersistenceAdapter (Persistence Adapter)
⬇️ [호출] ↘️ [사용]
6. Mapper (MyBatis / Interface) | 7. PersistenceMapper (Entity ↔ Domain 변환 Mapper)
⬇️ [SQL 매핑]
8. Mapper.xml
데이터 흐름
-
adapter > in > web > dto > PostRequest/Response.java
: 클라이언트와 데이터를 주고받는 요청/응답 객체
-
adapter > out > persistence > entity > PostEntity.java
: DB 스키마에 맞춘 객체
-
domain > Post.java
: 비즈니스 로직을 담는 핵심 객체
-
adapter > out > persistence > mapper > PostPersistenceMapper.java
: Entity ↔ Domain 변환. 각 계층은 직접 의존하지 않고, 변환을 통해 데이터를 전달한다.
- PostEntity와 Post의 차이?
- PostEntity.java : DB 스키마에 종속적인 객체
- Post.java : 비즈니스 로직 중심을 담고 있는 순수한 자바 객체 (POJO)
- 두 객체의 차이는 책임의 차이다.
- Domain 모델은 특정 기술(DB, ORM 등)에 의존하지 않도록 설계된다.
✍🏻 느낀 점
- 솔직히 개인적으로는 장점보다 단점이 더 크게 느껴졌다.
데이터 관련 클래스(Entity, Domain, DTO 등)를 포함하면 기능 하나를 구현할 때마다 최소 10-12개 클래스를 생성/수정해야 했고, 흐름이 여러 계층으로 나뉘어 있어 복잡도와 작업량이 확실히 증가했다.
다만 요구사항이 복잡해지고 외부 인프라(DB가 바뀌거나, 외부 API가 추가되는 등)의 변화가 잦은 대규모 프로젝트라면, 비즈니스 로직을 보호할 수 있다는 점에서 의미있는 구조라고 느껴졌다.
결국 헥사고날 아키텍처는 정답이라기보다는 선택지에 가깝고, 프로젝트의 규모와 복잡도를 고려해서 적용하는 것이 중요하다고 생각한다.
Reference