시작하며
안녕하세요. 개발자 stark입니다.
최근 DDD관련 책을 읽다가 이런 문구를 봤습니다. "모든 기술적 관심사로부터 비즈니스 로직을 분리하는 것이 포트 앤 어댑터 아키텍처(헥사고날 아키텍처)의 목적이라서 DDD의 비즈니스 로직 구현에 매우 적합하다."
저는 지금 실무에서 헥사고날 + DDD로 개발을 진행하고 있다 보니 이 조합이 비즈니스 로직을 구현하기에 매우 적합하다는 것에는 공감을 했습니다. 근데 스스로에게 "그래서 이게 왜 적합한데?"라고 질문해 봤더니 아무리 생각해도 답이 나오지 않았습니다. 당연히 그럴 수밖에 없었던 게 저는 지금까지 이게 왜 적합한지에 대해서는 단 한 번도 생각해 본 적이 없었던 것입니다. 그냥 적합하다고 하니 "MSA + DDD = 헥사고날 아키텍처" 이렇게 생각하고 사용하고 있었습니다.
문득 왜 사용하는지에 대한 이유도 모르고 사용하는 제가 한심하다는 생각이 들었습니다. 그래서 "헥사고날 아키텍처가 왜 DDD를 위한 이상적인 선택"인지 정확히 이해하고 싶다는 의지를 가지고 이번 포스팅을 작성하게 되었습니다.
참고: 이번 포스팅은 제가 공부한 내용과 제 머릿속에 있던 궁금증을 AI(Gemini)와 문답하며 얻은 정보로 작성하였습니다.
헥사고날 아키텍처 (Hexagonal Architecture, 포트와 어댑터)란?
알리스테어 코크번(Alistair Cockburn)이 제안한 헥사고날 아키텍처(또는 포트와 어댑터 아키텍처)는 특히 도메인 주도 설계(DDD)의 원칙을 구현할 때 강력합니다. 이 아키텍처의 근본적인 목표는 명확합니다. 바로 "애플리케이션의 핵심 비즈니스 로직을 외부 세계의 기술적 복잡성과 변화로부터 철저히 분리하고 보호하는 것"입니다. 이를 통해 시스템은 외부 환경의 변화에 흔들리지 않고 안정적으로 핵심 기능을 수행하며, 마치 잘 설계된 요새처럼 내부의 가치를 지켜낼 수 있습니다.
이러한 견고한 분리를 위해 헥사고날 아키텍처는 애플리케이션을 크게 '내부(Inside)'와 '외부(Outside)' 두 영역으로 구분합니다. '내부', 즉 애플리케이션 코어는 시스템의 심장부로서, 순수한 비즈니스 규칙, 도메인 모델, 그리고 이를 활용한 핵심 로직(유스케이스)만을 담고 있습니다. 이 코어는 마치 외부 세계와 단절된 것처럼, 자신이 다루는 비즈니스 문제 해결에만 몰두합니다. 어떤 데이터베이스 기술이 사용되는지, 사용자 인터페이스가 웹으로 구현되는지 아니면 모바일 앱으로 구현되는지, 또는 어떤 메시징 시스템이 연동되어 있는지에 대해서는 전혀 관여하지 않으며, 이러한 무지(ignorance)가 바로 코어의 독립성과 순수성을 지키는 열쇠가 됩니다. DDD의 엔티티, 값 객체, 애그리게잇, 도메인 서비스 등이 바로 이 코어 영역에서 그 생명력을 유지합니다.
그렇다면 이 독립적인 코어는 어떻게 외부 세계와 소통할까요? 바로 여기서 포트(Ports)라는 개념이 등장합니다. 포트는 애플리케이션 코어가 외부와 상호작용하기 위해 마련한 공식적인 통로이자 약속, 즉 인터페이스입니다. 코어는 자신이 외부로부터 어떤 요청을 받아들일 수 있는지(이를 인커밍 포트 또는 드라이빙 포트라 하며, 주로 유스케이스 인터페이스로 표현됩니다), 혹은 외부 시스템으로부터 어떤 기능을 제공받아야 하는지(이를 아웃고잉 포트 또는 드리븐 포트라 하며, 예를 들어 데이터 저장을 위한 리포지토리 인터페이스가 이에 해당합니다)를 포트를 통해 명확히 정의합니다. 중요한 것은, 포트는 '무엇을' 할 수 있고 '무엇이' 필요한지만을 규정할 뿐, '어떻게' 그 일이 처리될지에 대한 구체적인 방식은 언급하지 않는다는 점입니다.
이러한 포트라는 추상적인 약속을 현실 세계의 구체적인 기술과 연결하는 역할은 어댑터(Adapters)가 담당합니다. 어댑터는 마치 능숙한 통역사나 변환기처럼, 특정 기술에 기반한 외부의 요청을 코어가 이해할 수 있는 형태로 변환하여 인커밍 포트(useCase)를 통해 전달하거나, 코어가 아웃고잉 포트를 통해 요청한 작업을 특정 기술(데이터베이스, 메시징 시스템, 외부 API 등)을 사용하여 실제로 수행합니다. 예를 들어, 사용자의 HTTP 요청을 처리하는 웹 컨트롤러는 인커밍 어댑터가 되어 요청을 파싱하고 적절한 인커밍 포트(서비스 메서드)를 호출합니다. 이 외에도 CLI(Command Line Interface) 입력 처리기, 메시지 큐의 컨슈머(Consumer), 스케줄링된 작업을 실행하는 배치(Batch) 컴포넌트 등도 인커밍 어댑터의 예가 될 수 있습니다. 반대로, JPA를 사용하여 데이터베이스와 통신하는 리포지토리 구현체는 아웃고잉 어댑터가 되어, 코어가 정의한 리포지토리 포트(인터페이스)를 실제로 구현하여 데이터를 저장하거나 조회합니다. 또한 이메일 발송 서비스 클라이언트, 결제 게이트웨이 연동 모듈, 파일 시스템 접근 로직 등도 아웃고잉 어댑터로 볼 수 있습니다.
이 모든 구조를 지탱하는 가장 핵심적인 원칙은 바로 의존성의 방향입니다. 헥사고날 아키텍처에서는 모든 의존성이 반드시 외부에서 내부, 즉 애플리케이션 코어를 향해야 합니다. 다시 말해, 애플리케이션 코어는 자신이 정의한 포트 외에는 그 어떤 외부 어댑터에도 직접 의존하지 않습니다. 오히려 어댑터들이 코어가 정의한 포트 인터페이스에 의존하며 그 규약을 따릅니다. 웹 컨트롤러(어댑터)가 서비스 인터페이스(포트: in)를 사용하고, 데이터베이스 연동 코드(어댑터)가 리포지토리 인터페이스(포트: out)를 구현하는 것이죠. 이러한 의존성 역전은 애플리케이션 코어를 외부 기술의 변화로부터 완벽하게 보호하는 방패막이됩니다. UI 프레임워크가 바뀌거나 데이터베이스 시스템이 교체되더라도, 핵심 비즈니스 로직은 그 영향을 받지 않고 안정적으로 유지될 수 있습니다. 또한, 이러한 분리 덕분에 코어 로직만을 독립적으로 테스트하는 것이 훨씬 용이해지며, 시스템 전체의 유연성과 확장성 또한 크게 향상됩니다.
조금 더 이해하기: 헥사고날 아키텍처의 의존성 방향
위에서 설명한 "모든 소스 코드 의존성은 반드시 외부에서 내부, 즉 애플리케이션 코어를 향해야 한다" 이것의 의미를 바로 이해하기는 쉽지 않을 수 있습니다. 왜냐하면 의존성은 DI, IOC 같은 여러 가지 개념들이 적용되어 상호작용하기 때문입니다. 이 말의 핵심은, 애플리케이션의 심장부인 애플리케이션 코어(순수 비즈니스 로직, 도메인 모델, 유스케이스를 포함)가 외부 세계의 구체적인 기술이나 구현 세부 사항에 대해 어떠한 직접적인 코드 의존성도 가져서는 안 된다는 데 있습니다. 다시 말해, 코어는 자신이 정의하고 소유한 추상적인 약속인 포트(Port) 인터페이스 외에는 그 어떤 외부 어댑터에도 눈길을 주지 않습니다.
이것이 실제로 어떻게 작동하는지 구체적인 예시를 통해 살펴보겠습니다. 가령, 주문 처리 시스템을 만든다고 가정해 봅시다.
내부 (애플리케이션 코어)에는 다음과 같은 요소들이 존재합니다.
- OrderUseCase (인커밍 포트 인터페이스): 주문 생성, 조회 등 애플리케이션이 제공하는 기능을 정의합니다.
- OrderService (유스케이스 구현체): OrderUseCase 인터페이스를 실제 비즈니스 로직으로 구현하며, Order 엔티티와 같은 도메인 모델을 사용합니다.
- Order (도메인 모델): 주문과 관련된 상태와 행위를 가지는 핵심 도메인 객체입니다.
- OrderPort (아웃고잉 포트 인터페이스): 주문 데이터를 영속화하거나 조회하기 위한 메서드(예: save(Order order), findById(OrderId id))를 정의합니다.
외부 (어댑터)에는 다음과 같은 요소들이 있습니다.
- OrderController (인커밍 어댑터): HTTP 요청을 받아 OrderUseCase 인터페이스를 호출합니다.
- OrderPersistenceAdapter (아웃고잉 어댑터): OrderPort 인터페이스를 JPA 기술을 사용하여 구현합니다.
이제 "모든 소스 코드 의존성이 외부에서 내부로 향한다"는 원칙이 어떻게 적용되는지 보겠습니다. OrderController(외부 어댑터)는 사용자의 주문 생성 요청을 처리하기 위해 OrderUseCase(내부 인커밍 포트) 인터페이스를 참조하고 호출합니다. 여기서 명확히 외부에서 내부로의 의존성이 발생합니다.
@RestController
@RequestMapping("/api/orders")
@RequiredArgsConstructor
public class OrderController {
private final OrderUseCase orderUseCase; // ★ 핵심: 내부의 OrderUseCase 포트에 의존
@PostMapping
public ResponseEntity<String> placeOrder(@RequestBody OrderCreationRequest request) {
// Controller는 HTTP 요청을 받아 내부 UseCase가 이해할 수 있는 Command 객체로 변환
CreateOrderCommand command = new CreateOrderCommand(request.getCustomerId(), request.getItems());
// 내부 포트(UseCase) 호출
orderUseCase.createOrder(command);
System.out.println("OrderController: 주문 생성 요청이 UseCase로 전달됨 - 고객 ID: " + command.getCustomerId());
return ResponseEntity.ok("주문이 성공적으로 요청되었습니다.");
}
}
코어 내부를 더 들여다보면, OrderService(내부 유스케이스 구현체)은 OrderUseCase 인터페이스를 구현하며, 비즈니스 로직을 처리하기 위해 Order(내부 도메인 모델) 객체를 사용하고, 데이터 영속화가 필요할 때는 OrderPort(내부 아웃고잉 포트) 인터페이스에 정의된 메서드를 호출합니다. 이 모든 상호작용은 코어 내부에서 이루어지거나, 코어 내부의 추상화(포트)에 대한 의존성입니다. OrderService는 자신이 호출하는 OrderPort 인터페이스의 실제 구현체가 OrderPersistenceAdapter인지, 아니면 테스트를 위한 InMemoryOrderRepository인지 전혀 알지 못합니다.
public interface OrderUseCase {
void createOrder(CreateOrderCommand command);
// 다른 유스케이스 메소드들...
}
@RequiredArgsConstructor
@Transactional
@Service
public class OrderService implements OrderUseCase { // ★ 핵심: 내부 인커밍 포트 구현
private final OrderPort orderPort; // ★ 핵심: 내부 아웃고잉 포트에 의존
@Override
public void createOrder(CreateOrderCommand command) {
// 1. Command 객체를 도메인 객체로 변환 (또는 직접 사용)
Order newOrder = Order.from(command);
// 2. 비즈니스 로직 수행 (예: 유효성 검사, 재고 확인 등 - 여기서는 생략)
System.out.println("OrderService: 주문 생성 중 - 주문 ID: " + newOrder.getOrderId());
// 3. 아웃고잉 포트를 통해 도메인 객체 영속화 요청
// OrderService는 orderPort의 실제 구현체가 JPA인지, JDBC인지, InMemory인지 모름!
orderPort.save(newOrder);
System.out.println("OrderService: 주문이 OrderPort를 통해 저장됨.");
// 4. 필요하다면 도메인 이벤트 발행 등의 후속 처리 (여기서는 생략)
}
}
한편, OrderPersistenceAdapter(외부 아웃고잉 어댑터)는 OrderPort(내부 아웃고잉 포트) 인터페이스를 구현해야 합니다. 즉, OrderPersistenceAdapter의 코드는 OrderPort 인터페이스를 알고 이를 참조(import) 해야 합니다. 여기서도 외부 어댑터가 내부 포트에 의존(구현하기 때문에 import)하는 "외부 → 내부" 관계가 성립됩니다.
public interface OrderPort { // 아웃고잉 포트 (데이터 영속성 관련)
Order save(Order order);
Optional<Order> findById(String orderId);
// 기타 데이터 접근 메소드들...
}
public interface OrderJpaRepository extends JpaRepository<Order, String> {
}
@RequiredArgsConstructor
@Repository
public class OrderPersistenceAdapter implements OrderPort { // ★ 핵심: 내부 OrderPort 인터페이스를 구현
private final OrderJpaRepository orderJpaRepository; // ★ 핵심: Spring Data JPA 리포지토리에 의존
@Override
public Order save(Order order) {
Order savedOrder = orderJpaRepository.save(order);
return savedOrder;
}
@Override
public Optional<Order> findById(String orderId) {
Optional<Order> foundOrderOptional = orderJpaRepository.findById(orderId);
return foundOrderOptional;
}
}
결론적으로, 애플리케이션 코어는 자신이 정의한 추상적인 포트 인터페이스에만 의존하고, 외부의 구체적인 어댑터들에 대해서는 완전히 독립적입니다. OrderService나 Order 같은 코어 내부 요소들은 OrderController나 OrderPersistenceAdapter 같은 외부 어댑터의 구체적인 클래스를 직접 참조하는 일이 결코 없습니다. 반대로, 모든 외부 어댑터들은 애플리케이션 코어가 정의한 포트 인터페이스를 바라보고 의존하게 됩니다. 이것이 바로 "모든 의존성이 외부에서 내부로 향한다"는 원칙의 실체입니다. 이러한 의존성의 방향성은 제어의 역전(IoC) 원칙과 의존성 주입(DI) 메커니즘을 통해 런타임에 효과적으로 구현될 수 있으며, 결과적으로 애플리케이션 코어는 외부 기술 변화로부터 안전하게 보호받고, 독립적인 테스트와 진화가 가능한 견고한 구조를 갖추게 됩니다.
즉, 의존성의 방향은 다음과 같이 설계됩니다.
1. controller (외부) → useCase (내부 인커밍 포트)
- OrderController는 OrderUseCase 인터페이스를 참조하고 호출합니다.
- 방향: 외부(어댑터) → 내부(port) (O)
2. service (내부 유스케이스 구현체)는 useCase (내부 인커밍 포트)를 구현합니다
- OrderService implements OrderUseCase.
- OrderService는 OrderUseCase에 정의된 명세를 따릅니다. 이는 내부 컴포넌트 간의 관계입니다. 즉, OrderService가 OrderUseCase 인터페이스를 "알고" 있습니다.
3. service (내부 유스케이스 구현체) → domain (내부 도메인 모델)
- OrderService는 Order 엔티티와 같은 도메인 객체를 참조하고 사용합니다. (도메인 비즈니스 로직 사용)
- 방향: 내부(service) → 내부(domain) (O)
4. service (내부 유스케이스 구현체) → out port (내부 아웃고잉 포트)
- OrderService는 OrderPort 인터페이스를 참조하고 호출합니다. "이런 데이터를 저장/조회해 줘"라고 요청하는 것입니다.
- 방향: 내부(service) → 내부(port) (O)
5. out adapter (외부)는 out port (내부 아웃고잉 포트)를 구현합니다
- OrderPersistenceAdapter implements OrderPort.
- OrderPersistenceAdapter는 OrderPort 인터페이스에 정의된 명세를 따릅니다. 즉, OrderPersistenceAdapter의 소스 코드는 OrderPort 인터페이스를 참조(import) 해야 합니다.
- 방향: 외부(어댑터) → 내부(port) (O)
여기서 핵심은 이것입니다.
- 가장 중요한 것은 useCase, outPort 이 2가지 port 인터페이스가 "내부"에 포함된다는 것을 인지해야 합니다.
- controller (외부)는 useCase (내부)를 알지만, useCase나 service (내부)는 controller (외부)를 모릅니다.
- out adapter (외부)는 out port (내부)를 알지만(구현하므로), service (내부)는 out adapter (외부)의 구체적인 타입을 모릅니다. service는 오직 out port 인터페이스만 압니다.
즉, "모든 의존성이 외부에서 내부로 향한다"는 말은, service나 domain 같은 코어 내부 요소들이 controller나 OrderPersistenceAdapter 같은 외부 어댑터의 구체적인 클래스를 직접 참조하는 일이 없어야 한다는 의미입니다.
DDD와 헥사고날 아키텍처는 왜 환상의 짝꿍일까?
이제 헥사고날 아키텍처의 핵심 목표인 "기술적 관심사로부터 비즈니스 로직 분리"가 제대로 이루어지려면 의존성의 방향이 매우 중요하다는 것을 알게 되셨을 것이라고 생각합니다. 그렇다면 이번에는 헥사고날 아키텍처가 왜 DDD와 잘 어울리는지 알아봅시다.
1. 도메인 모델의 순수성 보장
DDD의 심장은 누가 뭐래도 도메인 모델입니다. 이곳에는 비즈니스가 실제로 어떻게 작동하는지에 대한 복잡한 규칙, 절차, 그리고 상태 변화 로직이 담겨있습니다. 이 도메인 모델은 외부의 기술적인 변화나 유행에 쉽게 흔들려서는 안 됩니다. 기술은 변하지만, 비즈니스의 본질은 쉽게 변하지 않기 때문입니다.
헥사고날 아키텍처는 바로 이 애플리케이션 코어(도메인 모델과 이를 활용하는 애플리케이션 서비스가 위치하는 곳)를 외부 어댑터들로부터 철저히 격리시킵니다. 예를 들어, 처음에는 관계형 데이터베이스인 PostgreSQL에 주문 데이터를 저장하기로 결정했다고 가정해 봅시다. 하지만 사업이 확장되어 비정형 데이터 처리에 더 용이한 MongoDB로 데이터베이스를 변경해야 하는 상황이 올 수 있습니다. 또는, 기존에는 웹 브라우저 클라이언트만을 위해 REST API로 요청을 받았지만, 새롭게 모바일 앱이나 다른 서버 간 통신을 위해 gRPC 인터페이스를 추가해야 할 수도 있습니다.
헥사고날 아키텍처를 잘 적용했다면, 이러한 외부 기술 스택의 변경이 순수한 도메인 코어에는 거의 영향을 미치지 않습니다. 데이터베이스가 PostgreSQL에서 MongoDB로 바뀌더라도, 도메인 모델은 여전히 "주문(Order)을 저장한다"는 자신의 본질적인 역할만 수행하면 됩니다. 어떻게 저장할지는 외부의 "데이터베이스 어댑터"가 알아서 처리할 일이기 때문입니다. REST API 대신 gRPC를 사용하게 되어도, "주문을 생성한다"는 유스케이스는 변하지 않습니다. 마치 외부 세계와는 단절된 구역처럼, 도메인 모델은 기술적인 혼란으로부터 안전하게 보호받으며 자신의 논리에만 집중할 수 있습니다. 이것이 바로 DDD가 꿈꾸는 순수한 도메인 모델의 모습입니다.
2. 명확한 경계 설정 (Ports as Boundaries)
DDD는 바운디드 컨텍스트(Bounded Context)라는 개념을 통해 거대한 도메인을 논리적으로 의미 있는 작은 단위로 나누고, 각 컨텍스트 내에서 모델의 의미와 일관성을 유지하려고 합니다. 마치 각 나라마다 국경선이 있어서 각 나라의 법과 언어가 그 안에서만 유효한 것과 비슷합니다.
헥사고날 아키텍처의 '포트(Port) 인터페이스'는 이러한 논리적인 경계를 실제 코드 수준에서 명확하게 구현할 수 있는 강력한 도구를 제공합니다. 포트는 애플리케이션 코어가 외부 세계와 주고받는 공식적인 약속(인터페이스)입니다.
- 인커밍 포트(Incoming Ports): 외부에서 애플리케이션 코어에게 "이런 일을 해줘!"라고 요청할 수 있는 통로입니다. 주로 애플리케이션이 제공하는 핵심 기능, 즉 유스케이스(Use Case)를 인터페이스 형태로 정의합니다. 예를 들어, 주문생성 UseCase, 상품조회 UseCase 등이 인커밍 포트가 될 수 있습니다.
- 아웃고잉 포트(Outgoing Ports): 애플리케이션 코어가 자신의 임무를 수행하다가 외부 시스템의 도움이 필요할 때 "이런 기능이 필요해!"라고 요청하는 통로입니다. 예를 들어, 데이터베이스에 데이터를 저장하거나(예: 주문 RepositoryPort), 외부 서비스에 알림 메시지를 보내거나(예: 알림 발송 Port), 결제를 요청하는(예: 결제요청 Port) 등의 기능을 추상화합니다.
이렇게 포트를 통해 도메인 로직이 외부와 상호작용하는 모든 지점을 명시적으로 정의함으로써, 각 부분의 역할과 책임이 분명해집니다. 시스템의 어떤 부분이 어떤 외부 기술에 의존하는지, 또는 어떤 기능을 외부에 제공하는지를 파악하기 쉬워지므로 전체 시스템을 이해하고 관리하기가 훨씬 용이해집니다. 이는 마치 잘 정리된 지도처럼 시스템의 구조를 명확하게 보여줍니다.
3. 의존성 역전을 통한 도메인 중심 설계 강화
전통적인 계층형 아키텍처(Layered Architecture)에서는 대부분 상위 계층인 서비스 계층이 하위 계층인 데이터 접근 계층(인프라스트럭처)에 직접 의존(import)하는 구조를 가집니다. 즉, 비즈니스 로직이 특정 데이터베이스 기술이나 프레임워크 코드에 종속될 가능성이 높습니다. 이는 마치 왕(비즈니스 로직)이 특정 신하(기술 구현체)에게 너무 의존해서, 그 신하가 바뀌면 국정 운영(비즈니스 로직 실행)에 차질이 생기는 것과 비슷합니다.
하지만 헥사고날 아키텍처에서는 의존성 역전 원칙(Dependency Inversion Principle, DIP)이 핵심적인 역할을 합니다. 애플리케이션 코어는 자신이 필요로 하는 기능(예: "데이터를 저장해 줘")을 포트라는 추상적인 인터페이스로 정의할 뿐, 그 인터페이스를 실제로 어떻게 구현하는지(예: JPA로 구현하든, JDBC로 구현하든)에 대해서는 전혀 알지 못합니다.
오히려 외부의 어댑터(예: JpaOrderRepositoryAdapter)가 코어가 정의한 포트 인터페이스(예: OrderRepositoryPort)를 구현함으로써, 어댑터가 코어에 의존하게 됩니다. 즉, 구체적인 구현이 추상화에 의존하는 것입니다. 이를 통해 애플리케이션의 주인공은 언제나 변치 않는 도메인 로직이 되며, 외부 기술은 필요에 따라 교체 가능한 부품처럼 취급될 수 있습니다. 이것이 바로 진정한 의미의 도메인 중심 설계를 가능하게 하는 핵심 메커니즘입니다.
4. 바운디드 컨텍스트와의 자연스러운 조화
복잡한 시스템은 보통 하나의 거대한 도메인 모델로 표현하기 어렵기 때문에, DDD에서는 여러 개의 바운디드 컨텍스트로 나누어 관리합니다. 각 바운디드 컨텍스트는 자신만의 모델, 용어, 그리고 비즈니스 규칙을 가집니다.
헥사고날 아키텍처는 이러한 바운디드 컨텍스트를 구현하는 데 매우 자연스럽게 들어맞습니다. 각각의 바운디드 컨텍스트를 그 자체로 하나의 독립적인 헥사곤(애플리케이션 코어 + 포트 + 어댑터)으로 구현할 수 있기 때문입니다. 예를 들어, 전자상거래 시스템이라면 '주문 컨텍스트', '결제 컨텍스트', '배송 컨텍스트', '회원 컨텍스트' 등이 각각 별도의 헥사곤으로 존재할 수 있습니다.
이렇게 독립적으로 개발되고 배포될 수 있는 컨텍스트들은 각자의 포트와 어댑터를 통해 서로 통신합니다. 예를 들어, '주문 컨텍스트'에서 주문이 완료되면, '주문 컨텍스트'의 아웃고잉 어댑터(예: '결제 요청 어댑터' 또는 '이벤트 발행 어댑터')가 '결제 컨텍스트'의 인커밍 포트(예: '결제 처리 유스케이스 포트' 또는 '주문 완료 이벤트 수신 포트')를 호출하거나 메시지를 전달하는 방식으로 안전하게 상호작용할 수 있습니다. 이는 마이크로서비스 아키텍처(MSA)로 시스템을 확장하거나, 각 팀이 특정 컨텍스트에만 집중하여 개발 생산성을 높이는 데 매우 유리한 구조입니다. 각 헥사곤은 독립적인 작은 집과 같고, 이 집들이 모여 하나의 잘 계획된 마을(전체 시스템)을 이루는 모습과 같습니다.
DDD와 헥사고날 아키텍처를 함께 사용했을 때 얻을 수 있는 이점들
도메인 주도 설계(DDD)의 깊이 있는 통찰과 헥사고날 아키텍처의 견고한 구조가 만나면, 여러 가지 실질적인 이점을 누릴 수 있습니다. 단순히 "좋은 설계"라는 말을 넘어, 실제 개발의 질과 속도, 그리고 장기적인 프로젝트의 건강성에 어떤 긍정적인 영향을 미치는지 살펴봅시다.
1. 뛰어난 테스트 용이성 (Enhanced Testability)
헥사고날 아키텍처를 통해 애플리케이션의 핵심 비즈니스 로직이 UI, 데이터베이스 같은 외부 기술로부터 완전히 분리되면 순수한 로직만을 대상으로 명쾌하고 빠른 단위 테스트를 작성할 수 있게 됩니다. 외부 의존성은 '목(Mock) 객체'라는 가짜 대역을 통해 손쉽게 제어할 수 있어, OrderService가 주문을 처리할 때 실제 데이터베이스 대신 목 객체를 사용하여 "주문 저장 성공" 또는 "특정 오류 발생"과 같은 다양한 시나리오를 신속하고 안정적으로 검증할 수 있습니다. 이렇게 테스트가 쉬워지면 개발자는 버그를 조기에 발견하고 코드 변경에 대한 자신감을 얻어, 결과적으로 전체 개발 속도와 코드 품질을 크게 향상시킬 수 있습니다.
2. 향상된 유지보수성 및 유연성 (Improved Maintainability & Flexibility)
각 컴포넌트의 책임이 명확히 분리되는 헥사고날 구조는 시스템의 특정 부분에서 발생한 변경이 다른 곳으로 불필요하게 퍼져나가는 것을 효과적으로 막아줍니다. 예를 들어, 데이터 저장 방식을 기존 관계형 데이터베이스에서 NoSQL로 변경해야 할 때, 핵심 비즈니스 로직은 그대로 둔 채 해당 데이터베이스용 '아웃고잉 어댑터'만 새로 개발하여 교체하면 됩니다. 이는 마치 자동차 부품을 바꾸듯 필요한 부분만 수정할 수 있게 하여 유지보수 비용을 줄입니다. 또한 새로운 알림 채널(예: 슬랙)이나 API 클라이언트(예: 모바일 앱)를 추가해야 할 때도 기존 로직에 영향을 주지 않고 새 '어댑터'만 구현하면 되므로 변화에 매우 유연하게 대처할 수 있는 시스템을 만들어줍니다.
3. 강력한 도메인 집중 (Stronger Focus on the Domain)
헥사고날 아키텍처는 개발자가 복잡한 인프라나 외부 기술의 세부 사항에 얽매이지 않고, 오롯이 애플리케이션의 핵심 가치인 도메인 문제 해결과 비즈니스 로직 구현에만 집중할 수 있는 환경을 제공합니다. 마치 요리사가 오븐의 내부 설계보다는 레시피 자체에 전념하여 최고의 요리를 만들어내듯, 개발자는 "이 데이터가 화면에 어떻게 보일까?" 또는 "어떤 테이블에 저장될까?"와 같은 고민에서 벗어나 순수하게 "이 비즈니스 규칙을 어떻게 코드로 정확히 표현할까?"에 몰두할 수 있습니다. 이렇게 기술적 복잡성으로부터 도메인 로직이 보호되면, DDD의 핵심인 유비쿼터스 언어가 코드에 더 자연스럽게 반영되고, 비즈니스 요구사항 변경 시에도 핵심 로직에만 집중하여 효과적으로 대응할 수 있게 됩니다.
결론: 헥사고날 아키텍처는 DDD의 잠재력을 극대화한다.
이번 포스팅을 작성하며 DDD는 복잡한 비즈니스 문제를 해결하기 위한 강력한 사고방식이지만, 이를 뒷받침하는 구체적인 아키텍처가 없다면 그 빛을 발하기 어렵다는 것을 알게 되었습니다. 저는 멋진 동료, 선배님들께서 이미 경험해 주신 것들을 공유해 주셨기에 별생각 없이 그 내용을 공부하며 따라갔을 뿐이었는데 정신 차리고 왜 사용해야 하는지에 대한 개념을 이해하고 나니 헥사고날 아키텍처는 DDD의 목표를 실현할 수 있도록 도와주는 최상의 아키텍처라는 생각이 들었습니다.
헥사고날 아키텍처는 비즈니스 로직을 모든 기술적 복잡성으로부터 분리하고 보호함으로써 DDD 프로젝트가 진정으로 도메인에 집중하고, 변화에 유연하게 대응하며, 시간이 지나도 유지보수하기 쉬운 시스템을 구축할 수 있도록 도와줍니다. 만약 DDD 도입을 고민 중이거나 기존 DDD 프로젝트의 아키텍처를 개선하고 싶으시다면 헥사고날 아키텍처는 분명 훌륭한 선택이 될 것입니다.
출처
Hexagonal architecture pattern - AWS Prescriptive Guidance
Hexagonal architecture pattern Intent The hexagonal architecture pattern, which is also known as the ports and adapters pattern, was proposed by Dr. Alistair Cockburn in 2005. It aims to create loosely coupled architectures where application components can
docs.aws.amazon.com
https://blog.naver.com/naverfinancial/223155125321
프로젝트에 새로운 아키텍처 적용하기 | 네이버파이낸셜 기술블로그
네이버페이는 외부 쇼핑몰이 주문형페이의 시스템을 이용해서 결제 및 주문을 진행할 수 있는 프로세스를 ...
blog.naver.com
'DDD' 카테고리의 다른 글
[DDD] 도메인 모델 (Domain Model) 이해하기 (2) | 2025.07.07 |
---|---|
[DDD] 도메인 주도 설계: 전략적 설계 (Strategic Design) (1) | 2025.06.08 |
[DDD] 왜 외부 애그리거트는 ID로 참조하는것이 좋을까? (0) | 2025.05.31 |
[DDD] 값 객체(VO)를 활용한 유비쿼터스 언어 기반의 명확한 도메인 표현법 (0) | 2025.05.30 |
[DDD] 단일 테이블 기반 다중 애그리거트(Aggregate) 모델링 전략 (1) | 2025.05.06 |