헥사고날 아키텍처란?
- 헥사고날 아키텍처(Hexagonal Architecture)는 핵심 비즈니스 로직(도메인)을 데이터베이스나 UI 같은 외부 시스템으로부터 격리시켜 독립적으로 유지하고 테스트하기 쉽게 만드는 소프트웨어 설계 패턴
- ‘포트와 어댑터 아키텍처’라고도 부릅니다.
주요 구성 요소
- 도메인 (Domain): 외부 기술에 의존하지 않는 순수한 핵심 비즈니스 로직
- 포트 (Port): 도메인과 외부를 연결하는 인터페이스 (입출력 규칙)
- 어댑터 (Adapter): 포트를 구현해 실제 외부 시스템(웹, DB, API 등)과 통신하는 코드
장단점
- 장점
- [관심사 분리] 외부 기술(DB나 프레임워크)이 바뀌어도 비즈니스 로직은 안전
- 확장 용이성. 외부 시스템이 추가되거나 수정됨에 따라 Adapter만 교체/구현하하면 된다.
- 단위 테스트 작성이 매우 쉬워진다.
- DB, 외부 서버를 직접 띄우지 않아도 포트(interface) 를 Mocking 하여 순수 비즈니스 로직에 대해 빠르게 단위 테스트를 할 수 있다.
- 코드가 각자의 역할대로 깨끗하게 분리됩니다.
- [관심사 분리] 외부 기술(DB나 프레임워크)이 바뀌어도 비즈니스 로직은 안전
- 단점
- 포트와 어댑터를 만들면서 코드가 늘어나고 구조가 복잡해집니다.
- 단순한 프로그램에는 오히려 과도한 설계가 될 수 있습니다.
구현
- 프로젝트 구성
com.example.orderservice ├── domain // [Core] 순수 도메인 모델 및 비즈니스 로직 │ ├── model │ │ └── Order.java │ ├── application // [Core] 유스케이스 및 포트 인터페이스 │ ├── port │ │ ├── in // Inbound Port (유스케이스 인터페이스) │ │ │ ├── CreateOrderUseCase.java │ │ │ └── OrderQueryUseCase.java │ │ └── out // Outbound Port (영속성/외부 연동 인터페이스) │ │ ├── SaveOrderPort.java │ │ ├── LoadOrderPort.java │ │ └── CacheOrderPort.java │ └── service │ └── OrderService.java │ └── adapter // [Infrastructure] 기술 구현체 ├── in // Primary / Driving Adapters │ ├── web // REST Controller │ │ ├── OrderController.java │ │ └── dto │ └── listener // Event / Message Listener │ ├── OrderEventListener.java │ └── dto └── out // Secondary / Driven Adapters ├── persistence // MySQL (Spring Data JPA) │ ├── OrderPersistenceAdapter.java │ ├── OrderJpaRepository.java │ └── OrderEntity.java └── cache // Redis (Spring Data Redis) ├── OrderCacheAdapter.java └── OrderRedisRepository.java - Inbound Adapter
@RestController @RequestMapping("/orders") @RequiredArgsConstructor public class OrderController { private final CreateOrderUseCase createOrderUseCase; // Inbound Port 의존 @PostMapping public ResponseEntity<OrderResponse> create(@RequestBody CreateOrderRequest request) { Order order = createOrderUseCase.createOrder(request.toCommand()); return ResponseEntity.ok(OrderResponse.from(order)); } } // Event Listener Adapter (Kafka/RabbitMQ 등) @Component @RequiredArgsConstructor public class OrderEventListener { private final CreateOrderUseCase createOrderUseCase; // Inbound Port 의존 @KafkaListener(topics = "order-create-topic", groupId = "order-group") public void handleOrderCreatedEvent(OrderCreateEventDto event) { createOrderUseCase.createOrder(event.toCommand()); } } - Outbound Adapter
// MySQL Persistence Adapter @Component @RequiredArgsConstructor public class OrderPersistenceAdapter implements SaveOrderPort, LoadOrderPort { private final OrderJpaRepository orderJpaRepository; @Override public Order saveOrder(Order order) { // Domain Model -> JPA Entity 변환 후 저장 OrderEntity entity = OrderEntity.from(order); OrderEntity savedEntity = orderJpaRepository.save(entity); return savedEntity.toDomain(); } } // Redis Cache Adapter @Component @RequiredArgsConstructor public class OrderCacheAdapter implements CacheOrderPort { private final RedisTemplate<String, Object> redisTemplate; @Override public void cacheOrder(Order order) { String key = "order:" + order.getId(); redisTemplate.opsForValue().set(key, order, Duration.ofMinutes(30)); } @Override public Optional<Order> getCachedOrder(Long orderId) { Object data = redisTemplate.opsForValue().get("order:" + orderId); return Optional.ofNullable((Order) data); } } - Port
// [Inbound Port] 외부(Web, Message)에서 애플리케이션을 호출할 때 public interface CreateOrderUseCase { Order createOrder(CreateOrderCommand command); } // [Outbound Port] 애플리케이션이 외부 저장소(MySQL, Redis 등)에 접근할 때 public interface SaveOrderPort { Order saveOrder(Order order); } public interface CacheOrderPort { void cacheOrder(Order order); Optional<Order> getCachedOrder(Long orderId); } - Service
@Service @RequiredArgsConstructor @Transactional public class OrderService implements CreateOrderUseCase { // Inbound Port 구현 // Outbound Port 인터페이스들만 주입받음 (기술 구현체에 직접 의존 X) private final SaveOrderPort saveOrderPort; private final CacheOrderPort cacheOrderPort; @Override public Order createOrder(CreateOrderCommand command) { // 1. 도메인 객체 생성 및 비즈니스 로직 수행 (순수 도메인 규칙) Order order = Order.create( command.getCustomerId(), command.getProducts(), command.getTotalAmount() ); // 2. Outbound Port를 통해 MySQL에 저장 Order savedOrder = saveOrderPort.saveOrder(order); // 3. Outbound Port를 통해 Redis에 캐싱 cacheOrderPort.cacheOrder(savedOrder); return savedOrder; } }
클린 아키텍처와 헥사고날 아키텍처의 차이점이 있다면?
- 대칭적 구조(포트/어댑터) vs 계층적 구조(동심원)
- 헥사고날 아키텍처:내부(Core)와 외부(Adapters)라는 2개의 커다란 구역으로 나뉩니다.
- 진입점(REST API, MQ Listener 등)이든 출력점(MySQL, Redis 등)이든 상관없이, 도메인과 통신하려면 반드시 포트(Port)라는 통로를 거쳐야 합니다.
- 외부 시스템들을 평면적으로(대칭적으로) 다루는 데 초점을 맞춥니다.
- 클린 아키텍처:안쪽부터 Entities > Use Cases > Interface Adapters > Frameworks & Drivers라는 4개 이상의 세분화된 계층(Layer)을 명확하게 규정합니다.
- 의존성 규칙(Dependency Rule)을 매우 엄격하게 강조합니다. (모든 의존성은 무조건 안쪽(고수준 도메인)을 향해서만 들어가야 함)
- 헥사고날 아키텍처:내부(Core)와 외부(Adapters)라는 2개의 커다란 구역으로 나뉩니다.
- 용어와 세부 계층의 명확성
- 헥사고날 아키텍처는 포트와 어댑터 개념을 강조하지만, 애플리케이션 내부(Core)를 얼마나 세부적으로 쪼갤지에 대해서는 구체적으로 규정하지 않고 자율에 맡깁니다.
- 클린 아키텍처는 애플리케이션 내부를 다음과 같이 세분화하여 명확히 명칭을 부여합니다.
- Entities: 전사적인 핵심 비즈니스 규칙 (가장 안쪽)
- Use Cases: 애플리케이션에 특화된 비즈니스 규칙 및 흐름 제어
- Controllers / Presenters / Gateways: 외부 요청을 Use Case에 맞게 변환하거나, Use Case의 결과를 UI/DB에 맞게 변환하는 인터페이스 어댑터
- 실무적 결론:
- 차이점보다 통일된 적용이 중요실무에서는 두 아키텍처를 엄격히 구분하여 따로 쓰기보다는, 두 개념을 섞어서 적용하는 경우가 대부분입니다.
- 구조적 개념: 클린 아키텍처의 Entity - Use Case - Adapter 계층 구조를 가져옵니다.
- 구현 기법: 헥사고날 아키텍처의 Inbound/Outbound Port와 Adapter 인터페이스 패턴을 활용해 패키지를 구성합니다.
- 따라서 두 아키텍처의 용어가 조금 다를 뿐, “비즈니스 로직에 DB나 API 같은 외부 기술이 침범하지 못하도록 DIP(의존성 역전 원칙)를 적용한다”는 본질은 같습니다.