헥사고날 아키텍처(Hexagonal Architecture) 적용 후기

류넹·2026년 4월 25일

etc

목록 보기
22/22

최근 프로젝트에서 헥사고날 아키텍처(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 구현체)

  • 📂 domain | 도메인 계층
    • Post.java : 도메인 모델


🔄️ 전체 흐름

  • 모든 의존성은 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

데이터 흐름

  1. adapter > in > web > dto > PostRequest/Response.java
    : 클라이언트와 데이터를 주고받는 요청/응답 객체

  2. adapter > out > persistence > entity > PostEntity.java
    : DB 스키마에 맞춘 객체

  3. domain > Post.java
    : 비즈니스 로직을 담는 핵심 객체

  4. 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

profile
학습용 커스터마이징 간단 개발자 사전

0개의 댓글