본문으로 건너뛰기
Leo Jung
뒤로 가기

[DDD Intro #20] Hexagonal Architecture: 도메인을 중심에 두는 구조

페이지 수정

계층형 아키텍처는 도메인 모델을 보호하기 위한 중요한 출발점이다. 하지만 실제 코드에서는 계층이 한 방향으로만 흐르는 것처럼 보이면서도, 도메인이 여전히 프레임워크나 데이터베이스에 강하게 묶이는 경우가 많다.

Hexagonal Architecture, 또는 Ports and Adapters Architecture는 이 문제를 더 분명하게 바라보게 해준다. 핵심은 간단하다. 도메인을 중심에 두고, 외부 세계와의 연결은 포트와 어댑터를 통해 처리한다.

이 구조는 DDD와 잘 어울린다. DDD가 도메인 모델의 언어와 규칙을 중요하게 본다면, Hexagonal Architecture는 그 모델이 외부 기술에 끌려가지 않도록 코드 구조를 제공한다.

중심에는 도메인이 있다

Hexagonal Architecture를 그림으로 보면 가운데에 애플리케이션과 도메인이 있고, 바깥에 여러 어댑터가 붙어 있다.

Web Controller  →  Application / Domain  →  Database Adapter
CLI Command     →  Application / Domain  →  Payment Adapter
Message Listener→  Application / Domain  →  Event Publisher

중요한 것은 외부 세계가 도메인을 직접 지배하지 않는다는 점이다. 웹 요청이든 메시지든 배치 작업이든, 모두 애플리케이션의 유스케이스를 호출하는 하나의 진입 방식일 뿐이다.

마찬가지로 데이터베이스, 결제 API, 메시지 브로커도 도메인의 중심이 아니다. 도메인이 필요로 하는 기능을 바깥에서 제공하는 구현체일 뿐이다.

이 관점은 기술을 중심으로 코드를 나누는 방식과 다르다.

controller
service
repository
entity

이 구조가 항상 나쁜 것은 아니다. 하지만 시간이 지나면 웹 프레임워크와 DB 구조가 중심이 되기 쉽다. Hexagonal Architecture는 중심을 다시 도메인과 유스케이스로 돌려놓는다.

Port는 의도이고 Adapter는 구현이다

Hexagonal Architecture의 핵심 단어는 Port와 Adapter다.

Port는 애플리케이션이 외부 세계와 상호작용하기 위해 정의하는 경계다. Adapter는 그 Port를 실제 기술로 연결하는 구현이다.

예를 들어 구독 갱신 유스케이스가 결제를 요청해야 한다고 해보자. 애플리케이션 입장에서는 “결제를 요청하고 결과를 받는다”는 의도가 중요하다. PG사가 어디인지, HTTP API를 쓰는지, 어떤 JSON을 주고받는지는 세부사항이다.

public interface PaymentPort {
    PaymentResult pay(PaymentRequest request);
}

이것이 Port다. 그리고 실제 PG사를 호출하는 구현체가 Adapter다.

public class PgPaymentAdapter implements PaymentPort {
    public PaymentResult pay(PaymentRequest request) {
        PgResponse response = pgClient.requestPayment(toPgRequest(request));
        return paymentTranslator.toPaymentResult(response);
    }
}

도메인과 애플리케이션은 PaymentPort만 알면 된다. 외부 API의 세부사항은 Adapter 안에 갇힌다.

Inbound Adapter와 Outbound Adapter

Adapter는 크게 두 종류로 볼 수 있다.

Inbound Adapter는 외부 요청을 애플리케이션 안으로 들여보내는 역할을 한다. REST Controller, GraphQL Resolver, 메시지 리스너, CLI 명령, 배치 스케줄러가 여기에 해당한다.

Outbound Adapter는 애플리케이션이 외부 시스템을 사용할 때 호출하는 구현체다. 데이터베이스 Repository 구현, 결제 API 클라이언트, 이메일 발송기, 메시지 발행기 등이 여기에 해당한다.

구독 해지 유스케이스를 예로 들면 다음과 같다.

CancelSubscriptionController
→ CancelSubscriptionUseCase
→ SubscriptionRepository
→ JpaSubscriptionRepository

Controller는 Inbound Adapter다. CancelSubscriptionUseCase는 애플리케이션의 진입 포트 또는 서비스다. SubscriptionRepository는 저장소 접근을 위한 Port이고, JpaSubscriptionRepository는 Outbound Adapter다.

이 구조에서 웹도, DB도 중심이 아니다. 중심에는 유스케이스와 도메인 모델이 있다.

테스트하기 쉬운 구조

Hexagonal Architecture의 큰 장점 중 하나는 테스트하기 쉽다는 것이다. 외부 기술을 Port 뒤로 밀어내면, 애플리케이션과 도메인을 가짜 구현으로 테스트할 수 있다.

FakePaymentPort paymentPort = new FakePaymentPort(PaymentResult.succeeded());
InMemorySubscriptionRepository repository = new InMemorySubscriptionRepository();

RenewSubscriptionUseCase useCase = new RenewSubscriptionUseCase(repository, paymentPort);

이렇게 하면 실제 PG사나 데이터베이스 없이도 유스케이스를 검증할 수 있다. 중요한 것은 테스트 편의 자체가 아니다. 테스트가 쉽다는 것은 도메인과 애플리케이션이 외부 기술에 덜 묶여 있다는 신호다.

물론 모든 테스트를 가짜로만 해서는 안 된다. Adapter 자체는 별도로 통합 테스트가 필요하다. PG API 변환, JPA 매핑, 메시지 발행은 실제 기술과 함께 검증해야 한다.

핵심은 테스트의 대상과 목적을 분리할 수 있다는 점이다.

DDD와 잘 맞는 이유

DDD에서 중요한 것은 도메인 모델의 언어와 규칙을 보호하는 것이다. Hexagonal Architecture는 이 목표를 구조적으로 돕는다.

외부 API 모델은 Adapter에서 내부 모델로 번역된다. 데이터베이스 매핑은 Repository Adapter에서 처리된다. 웹 요청 DTO는 Application Command로 변환된다. 메시지 포맷은 메시지 Adapter에서 해석된다.

결과적으로 Domain Layer는 다음과 같은 것들을 몰라도 된다.

HTTP 요청 형식
JSON 필드명
DB 테이블 이름
PG사 응답 코드
메시지 브로커 토픽 이름
프레임워크 어노테이션

도메인 모델은 자신이 알아야 할 언어만 알면 된다.

subscription.renew(paymentResult, now);
subscription.cancelByMember(reason, now);
subscription.changePlan(targetPlan, policy, now);

이것이 DDD와 Hexagonal Architecture가 만나는 지점이다.

계층형 아키텍처와 무엇이 다른가

Layered Architecture와 Hexagonal Architecture는 서로 대립하는 개념이 아니다. 많은 경우 함께 사용할 수 있다.

Layered Architecture는 계층의 책임을 설명하는 데 유용하다. Presentation, Application, Domain, Infrastructure로 나누면 코드의 큰 구조를 이해하기 쉽다.

Hexagonal Architecture는 외부 세계와 내부 애플리케이션의 경계를 더 강조한다. 특히 입력과 출력의 다양한 Adapter를 명시적으로 다룬다.

계층형 구조에서는 종종 Application → Infrastructure 의존이 자연스럽게 생긴다. Hexagonal 관점에서는 이를 Port로 뒤집어 볼 수 있다.

Application은 PaymentPort를 의존한다.
Infrastructure의 PgPaymentAdapter가 PaymentPort를 구현한다.

이렇게 하면 애플리케이션의 의도와 기술 구현이 분리된다.

과한 추상화가 될 수도 있다

Hexagonal Architecture도 언제나 정답은 아니다. 작은 CRUD 서비스에서 모든 Repository, 모든 외부 호출, 모든 입력 경로를 Port와 Adapter로 나누면 코드가 불필요하게 복잡해질 수 있다.

중요한 것은 복잡성을 다루기 위한 경계가 필요한지다.

다음과 같은 경우에는 Hexagonal 구조가 특히 도움이 된다.

Core Domain을 외부 기술로부터 보호해야 한다.
외부 시스템이 자주 바뀔 가능성이 있다.
같은 유스케이스를 여러 입력 방식에서 호출한다.
테스트에서 외부 의존성을 쉽게 대체해야 한다.
레거시 또는 외부 모델과 내부 모델의 언어 차이가 크다.

반대로 단순한 관리자 CRUD 기능까지 모두 완전한 Hexagonal 구조로 만들 필요는 없다. 아키텍처는 문제에 맞아야 한다.

마치며

Hexagonal Architecture는 도메인을 중심에 두는 구조다. 외부 세계는 포트와 어댑터를 통해 연결된다. 웹, DB, 메시지 브로커, 외부 API는 중요하지만 중심은 아니다.

DDD 관점에서 이 구조의 가치는 명확하다. 도메인 모델이 외부 기술의 언어에 끌려가지 않게 해준다. 외부 요청은 Application Command로 번역되고, 외부 API 응답은 내부 도메인 결과로 번역되며, 저장소 구현은 Repository Port 뒤에 숨는다.

하지만 Hexagonal Architecture는 목적이 아니라 수단이다. 모든 코드를 포트와 어댑터로 감싸는 것이 목표가 아니다. 목표는 도메인 모델을 보호하고, 유스케이스를 명확하게 만들며, 외부 기술 변화에 덜 흔들리는 구조를 만드는 것이다.

좋은 아키텍처는 도메인을 더 잘 말하게 만든다. Hexagonal Architecture는 그 목표를 위한 강력한 선택지다.


페이지 수정
공유하기:

이전 글
[DDD Intro #21] ORM과 DDD: JPA Entity는 도메인 모델인가
다음 글
[DDD Intro #19] Layered Architecture: 도메인 계층을 보호하기