한 줄로 이해하기
핵심 비즈니스(도메인)는 가운데 두고 DB, API, 웹 같은 외부 기술은 교체 가능하게 만드는 아키텍처입니다.
회원가입 기능을 만든다고 생각해 보겠습니다. 회원가입을 하려면 웹에서 요청을 받고, 회원 정보를 저장한 다음 응답을 보냅니다.
일반적인 계층형 구조는 다음과 같습니다.
flowchart TD
Controller --> Service
Service --> Repository
Repository --> MySQL[(MySQL)]
이 구조는 간단하지만 나중에 MySQL → MongoDB, JPA → MyBatis로 변경하면 Service도 영향을 받을 수 있습니다. 이러한 결합을 줄이기 위해 헥사고날 아키텍처를 사용합니다.
왜 이름이 헥사고날(육각형)일까?
육각형이라는 모양 자체가 중요한 것은 아닙니다. 여러 방향에서 핵심 비즈니스 로직에 연결될 수 있음을 표현하기 위한 형태입니다.
flowchart TB
Core([핵심 비즈니스])
REST[REST API] --> Core
Web[Web] --> Core
Mobile[Mobile] --> Core
Kafka[Kafka] --> Core
Batch[Batch] --> Core
CLI[CLI] --> Core
REST API, 웹, 모바일, Kafka, Batch, CLI가 모두 같은 비즈니스 로직을 사용할 수 있습니다.
전체 구조
flowchart LR
User[사용자]
Controller[Controller<br/>입력 Adapter]
UseCase[UseCase<br/>비즈니스 로직]
Port[Port<br/>인터페이스]
Adapter[JPA Adapter]
DB[(MySQL)]
User --> Controller --> UseCase --> Port --> Adapter --> DB
요청은 다음 순서로 흐릅니다.
flowchart TD
User[사용자] --> Controller --> UseCase --> Port --> Adapter --> Database[(Database)]
각 역할 이해하기
1. Controller
사용자의 요청을 받습니다.
POST /members
회원가입 요청이 들어오면 Controller는 요청을 애플리케이션에 전달하는 역할까지만 담당합니다.
2. UseCase
핵심 비즈니스 규칙을 수행하는 가장 중요한 영역입니다.
flowchart LR
Check[회원 중복 확인] --> Encrypt[비밀번호 암호화] --> Save[회원 저장]
UseCase는 DB가 MySQL인지 MongoDB인지 알 필요가 없습니다.
3. Port
Port는 애플리케이션이 필요로 하는 기능을 정의한 인터페이스입니다.
public interface MemberPort {
void save(Member member);
}
UseCase는 save() 기능이 필요하다는 사실만 알고, 누가 어떻게 구현하는지는 모릅니다.
4. Adapter
Adapter는 Port를 실제 기술로 구현합니다.
public class MemberJpaAdapter implements MemberPort {
// Port의 기능을 JPA로 구현합니다.
}
Adapter 내부에서는 실제 Repository를 호출합니다.
memberJpaRepository.save(member);
flowchart LR
Port --> Adapter --> JPA --> MySQL[(MySQL)]
왜 Port를 만드는 걸까?
UseCase가 다음 코드를 직접 호출하면 UseCase가 JPA라는 기술을 알게 됩니다.
memberJpaRepository.save(member);
헥사고날 아키텍처에서는 기술 구현체 대신 Port를 호출합니다.
memberPort.save(member);
flowchart LR
UseCase --> MemberPort
MemberPort --> JpaAdapter[JPA Adapter]
UseCase의 관점에서는 “누가 저장하는지는 모르겠고, 저장 기능만 수행해 줘”라고 요청하는 셈입니다.
왜 좋은가?
① DB를 쉽게 교체할 수 있다
flowchart LR
subgraph Before[현재]
U1[UseCase] --> JPA --> MySQL[(MySQL)]
end
subgraph After[교체 후]
U2[UseCase] --> MyBatis --> Oracle[(Oracle)]
end
구현 기술이 바뀌어도 UseCase가 의존하는 Port는 유지되므로 핵심 비즈니스 로직을 수정하지 않아도 됩니다.
② 테스트하기 쉽다
DB Adapter 대신 FakeMemberPort를 주입하면 실제 DB 없이도 UseCase를 테스트할 수 있습니다.
flowchart LR
Test[Test] --> UseCase --> MemberPort
MemberPort --> Fake[FakeMemberPort]
③ 유지보수가 쉽다
비즈니스 영역은 회원가입 규칙을, 기술 영역은 DB 저장과 외부 연동을 담당합니다. 역할과 변경 이유가 명확하게 분리됩니다.
입력과 출력도 따로 생각한다
헥사고날 아키텍처는 입력(Inbound) 과 출력(Outbound) 을 구분합니다.
flowchart LR
Client[사용자]
Controller[입력 Adapter<br/>Controller]
UseCase[UseCase]
Port[출력 Port]
Jpa[JPA Adapter]
Api[외부 API Adapter]
Kafka[Kafka Adapter]
Client --> Controller --> UseCase --> Port
Port --> Jpa
Port --> Api
Port --> Kafka
입력 Adapter에는 Controller, REST API, Batch 등이 올 수 있습니다. 출력 Adapter에는 DB, Redis, Kafka, 외부 API 등이 올 수 있습니다.
실무 프로젝트 구조 예시
src
├── domain
│ ├── Member
│ └── MemberPort
│
├── application
│ └── RegisterMemberUseCase
│
├── adapter
│ ├── in
│ │ └── MemberController
│ │
│ └── out
│ └── MemberJpaAdapter
│
└── infrastructure
└── MemberJpaRepository
시험이나 면접에서 외우기 좋은 핵심
| 구성 요소 | 역할 |
|---|---|
| Controller (Input Adapter) | 사용자 요청을 받는다. |
| UseCase (Application) | 비즈니스 로직을 수행한다. |
| Port (Interface) | 필요한 기능을 인터페이스로 정의한다. |
| Adapter | Port를 실제 기술(JPA, API 등)로 구현한다. |
| Database / 외부 시스템 | 데이터를 저장하거나 외부와 통신한다. |
암기 포인트
헥사고날 아키텍처를 한 문장으로 기억하면 됩니다.
비즈니스 로직은 가운데에 두고, Port(인터페이스)를 통해 외부 기술과 연결하여 DB나 API가 바뀌어도 핵심 로직은 변경되지 않도록 만드는 아키텍처입니다.
이 문장을 이해하면 헥사고날 아키텍처의 핵심 개념을 대부분 이해한 것입니다.