시작하며
안녕하세요. 개발자 stark입니다.
저는 최근 1년간 사내에서 헥사고날 아키텍처로 구성된 MSA 프로젝트를 개발했습니다. 그리고 이제 어느 정도 안정화 단계가 되어 지속적으로 프로젝트 구조를 리펙토링 하고 있습니다. 이를 위해 팀 내에서 회의를 9차 정도 진행했는데 그 과정에서 흥미로운 주제로 이야기를 했던 것이 있어서 포스팅을 작성하였습니다.
저희는 시간 날 때마다 아키텍처 관련 얘기를 하곤 하는데 어느 날 선배님께서 이런 주제를 던져주셨습니다. 프로젝트에 헥사고날 아키텍처와 DDD를 적용하기 위해서는 계층 간 역할을 명확하게 분리하여 서로의 침범이 없도록 하는 것이 정말 중요한데 과연 우리는 제대로 구성했을까? 이 주제로 대화를 진행하다 저희의 프로젝트를 다시 분석해 보기 시작했고 그중에서 가장 논란이 있었던 것은 application, domain 두 곳에 모두 관련이 있어 보이는 port(인터페이스)였습니다.
용어 사전
아래 용어들은 이번 글에서 자주 등장하는 개념들이므로 먼저 짚고 넘어가겠습니다.
Port
헥사고날 아키텍처에서 외부 세계와 내부 애플리케이션을 연결하는 인터페이스입니다.
- Input Port: 외부에서 들어오는 요청을 받아들이는 규격 (ex. UseCase 인터페이스)
- Output Port: 내부에서 외부로 나가는 요청을 추상화한 규격 (ex. Repository, 외부 API 호출 인터페이스)
Adapter
Port를 구현한 실제 클래스입니다.
- In Adapter: Controller, Listener처럼 외부 요청을 받아 Input Port를 호출하는 구현체
- Out Adapter: Repository, API Client처럼 Output Port를 구현해 실제 외부 시스템(DB, 메시지 브로커, 외부 API 등)과 통신하는 구현체
Aggregate
- DDD에서 하나의 트랜잭션 단위로 묶이는 도메인 객체 집합입니다. 항상 루트 엔티티(Aggregate Root)를 통해서만 접근합니다.
Domain Service
- 하나의 Aggregate로 해결되지 않는 복잡한 비즈니스 규칙을 담당하는 순수 서비스 클래스입니다. 여러 Aggregate를 조합하는 역할을 합니다.
Application Service
- 외부 요청을 받아 도메인 모델을 불러오고, Domain Service나 Aggregate에 비즈니스 처리를 위임하는 계층입니다. 오케스트레이터 역할을 하며 트랜잭션 경계를 관리합니다.
먼저 port에 대해 이해해 봅시다.
헥사고날 아키텍처에서 port(인터페이스)의 역할은 단순 인터페이스가 아닙니다. 하단의 그림을 봅시다.

헥사고날 아키텍처에서 port의 역할은 의존관계를 역전시키는 핵심입니다. 인터페이스를 선언하고 스프링 빈으로 등록할 구현체가 이 port 인터페이스를 구현함으로써 시스템이 매우 유연해집니다.
예를 들어 Rest전용 controller를 사용하는 시스템과 kafka를 사용하는 이벤트 리스너가 있을 때 이 2가지는 모두 외부에서 서버로 들어오는 요청입니다. 이것을 In Adapter라고 부르는데 이 어댑터는 모두 인터페이스가 아닌 클래스로 생각해 주시면 됩니다.
아래 그림과 같이 Rest 전용인 controller 클래스에서는 input port인 usecase를 의존(필드에 선언, Import)합니다. Kafka 전용으로 선언된 Listener 클래스 또한 같은 input port인 usecase를 의존할 수 있습니다.

이것을 통해 알 수 있는 것은 서버로 들어오는 input Adapter가 어떤 형태(rest, kafka, rpc)이든 input port(인터페이스)의 규격(메서드 시그니처)만 맞춰준다면 클라이언트는 언제든 바꿀 수 있다는 것입니다. 매우 유연하죠? output port 또한 input port와 동일한 원리로 구성되기 때문에 설명은 생략하도록 하겠습니다.
도메인 모델에 대해서도 이해해 봅시다.
port에 대한 이해도가 생기셨다면 다음으로는 도메인 계층을 이해해 볼 차례입니다.
제가 생각한 도메인 모델의 구성은 아래와 같이 3가지(Aggregate, Factory, Service)를 구성요소로 만들어집니다.

이 구성은 DDD를 구성할 때 필수가 되는 요소이며 특히 이 중에서 Aggregate(애그리거트)는 트랜잭션 단위로 설계해 주시는 것이 중요합니다. 또한 Aggregate를 구성하실 때는 가능한 작게 만들고자 노력해 주셔야 heavy 한 모델이 만들어지지 않습니다. DDD에서 선호하는 풍부한 모델(Rich domain)과 모든 것이 합쳐져서 비대해진(God model)은 정말 큰 차이가 있기 때문에 구성하실 때 많은 주의가 필요합니다.
다음으로 Domain Factory는 도메인 객체를 생성할 때 사용하는 공장이라고 생각해 주시면 됩니다. Aggregate 내부에 Factory method를 두고 선언하는 방식도 좋지만 생각보다 방대한 생성 메서드가 필요한 경우가 있기 때문에(단위 테스트 포함) 도메인 펙토리 클래스를 선언해 주시면 좋습니다.
마지막으로 Domain Service입니다. 이 클래스는 도메인 모델이 스스로 처리할 수 없는 복잡한 비즈니스 규칙을 처리하기 위해 존재합니다. 예를 들어 은행에서 누군가 입금한 것을 처리하는 상황을 떠올려봅시다. 이 경우 2명의 유저가 상호작용을 해야만 입금이라는 비즈니스가 완료될 것입니다. 그렇다면 이것을 처리하기 위해 서버 측의 비즈니스 로직에서는 여거개의 도메인 Aggregate를 사용해야만 합니다.

여러 개의 도메인 Aggregate가 존재한다는 것과 이 Aggregate들을 사용해서 2명의 돈과 관련된 값들이 모두 변경되어야 한다는 것 (한 명은 돈 감소 한 명은 돈 증가) 이 작업을 단 1개의 Aggregate 내부에서 처리할 수 있을까요? 물론.. 한다면 가능이야 하겠지만 그렇게 되면 도메인 비즈니스가 정말 복잡하고 헷갈리게 구성될 것입니다.
이렇게 작업한 것에 대한 return은 추후 프로젝트 운영에 큰 리스크로 작용하게 되겠지요.. 이런 경우를 미리 방지하기 위해 N개의 도메인이나 서로 다른 도메인 Aggregate 간 비즈니스가 존재하는 경우 그 비즈니스를 쉽게 처리할 수 있도록 Domain Service라는 클래스를 선언하는 것입니다.
참고로 제일 중요한 것은 이 모든 것이 합쳐져서 1개의 Domain Model이 구성되는 것이므로 꼭 POJO로 만들어주셔야 합니다.
Application Service는 무엇일까?
Application Service는 프로젝트의 오케스트레이터입니다.
외부(in Adapter)에서 들어온 요청을 받아 필요한 Aggregate를 out port(Repository)를 통해 불러오고, 이 도메인 객체들에게 적절한 비즈니스 동작을 위임합니다.
만약 하나의 Aggregate로 끝나는 간단한 작업이라면 Application Service가 직접 도메인에 요청을 전달하고, 여러 Aggregate가 협력해야 하는 복잡한 시나리오라면 Domain Service를 호출하여 처리하도록 위임합니다.
이때 중요한 점은 Application Service는 어디까지나 트랜잭션 경계를 관리하고 필요한 객체를 준비한 뒤, 비즈니스 로직 자체는 Domain 계층에 위임한다는 것입니다.
즉, Application Service는 스스로 비즈니스를 구현하지 않고, 도메인이 제 역할을 다할 수 있도록 무대를 마련해 주는 조율자의 역할을 수행하는 계층이라고 볼 수 있습니다.
port는 왜 논란이 되었을까?
제가 생각한 port의 역할은 헥사고날 아키텍처에서 application 계층의 service인 ApplicationService(도메인 서비스와 다릅니다)를 위한 것으로 판단했습니다. 그래서 프로젝트에서 port 패키지의 위치 또한 application 패키지 내부에 두어야 한다고 생각했습니다.
하단의 그림을 봐주시면 실제로 input port와 output port를 사용하고 있는 위치는 모두 Application Service라는 것을 알 수 있습니다.

그러나 제 생각이 모두 옳은 것은 아니기 때문에 또 다른 관점에서 봐주신 선배님의 말을 경청했습니다. 선배님의 의견은 다음과 같았습니다.
1. 도메인이 out port를 가져야 하는 것 아닌가?
- input port에 대해서는 도메인과 연관이 없기에 가질 필요가 없다고 생각하셨지만 output port는 단순 도메인을 외부로 던지는 역할만을 수행하니 domain 계층이 가져도 되는 것이 아닌지에 대한 얘기였습니다.

2. 도메인 서비스 내부에서는 output port를 의존하고 사용해도 되는 것 아닌가?
- 위의 관점에서 이어진 내용인 것 같습니다. output port가 도메인 영역이니 domain service가 output port를 사용해서 DB에서 도메인 데이터를 받아오고 이것을 통해 도메인 서비스가 비즈니스 작업을 수행해도 문제없는 것 아닌가? 다른 곳에서도 이렇게 하는 곳이 있던데..?

제 생각은 다음과 같습니다.
먼저 "도메인이 out port를 가져야 하는 것 아닌가?"에 대한 생각입니다.
- 일부 문헌이나 실무 사례에서는 Output Port를 Domain Layer에 두기도 한다는 것은 알고 있습니다. (Eric Evans의 Repository는 Domain의 개념이라는 기록) 그렇다 하더라도 저는 반대의 입장이었습니다. 왜냐하면 output port를 사용할 수 있는 위치는 도메인 모델이 아닌 application service 단 1곳이어야만 한다고 생각했기 때문입니다. 또한 도메인 모델이 어떤 방법이든 외부로 나가는 방향성을 가져서는 안 된다고 생각하기 때문에 외부와 통신하기 위해서 선언된 output port를 가질 이유가 전혀 없다고 생각합니다. (아무리 POJO 인터페이스라 하더라도 말이죠)
다음으로"도메인 서비스 내부에서는 output port를 의존하고 사용해도 되는 것 아닌가?"에 대한 생각입니다.
- 저는 이것 또한 위와 마찬가지로 반대의 입장이었습니다. 왜냐하면 헥사고날 아키텍처에서는 이미 output port를 사용하기 위해 선언한 오케스트레이션 클래스인 application service가 존재하기 때문입니다. 만약 domain service가 output port를 사용한다면 그때부터 도메인 서비스는 application service와 다른 것이 하나도 없다고 생각합니다. 오히려 application과 domain 간의 계층 분리가 명확하지 않게 될 것이고 아키텍처가 망가질 수 있다고 생각했습니다. 그래서 저는 Application Service에서만 output port를 호출하는 게 맞다고 생각합니다.
또한 도메인 서비스 클래스 내부에서 out port 인터페이스에 대한 의존성을 가지는 것이 맞는지에 대해서도 고민이 되었습니다. (한때는 인터페이스가 외부 라이브러리에 대한 의존성을 가지지 않는 순수 pojo객체로 구성된다면 괜찮을 것 같다는 생각도 들었지만 그래도 비즈니스 규칙만 담는다는 것이 out port를 의존하는 것으로 인해 깨지는 것이 아닌지.. 만약 이게 허용된다면 Aggregate 내부에서도 output port를 의존할 수 있어야 하는 게 아닌지 생각이 들어 반대의 입장이었습니다.)
이 2가지 상황을 그림으로 표현하면 다음과 같습니다.

마무리하며
지금까지 열심히 제 생각을 정리해 봤습니다. 당연히 정답은 없습니다!
이번 포스팅을 작성하게 된 이유는 저 스스로도 이런 아키텍처 구성에 대한 것을 더 고민해 볼 기회가 생겼기 때문입니다. 또한 다른 개발자분들의 의견은 어떤지 알고 싶기도 했습니다. 그러니 혹시라도 이 글을 읽게 되신 개발자분께서는 어떤 생각을 가지고 계신지 댓글에 적어주셔서 이것에 대해 같이 얘기해 볼 수 있었으면 좋겠습니다. 긴 글 읽어주셔서 감사합니다!
'DDD' 카테고리의 다른 글
| [DDD] 카카오 로그인 써도 일반 서브도메인이 아닌 이유 (0) | 2025.10.26 |
|---|---|
| [DDD] 도메인 모델 (Domain Model) 이해하기 (2) | 2025.07.07 |
| [DDD] 도메인 주도 설계: 전략적 설계 (Strategic Design) (1) | 2025.06.08 |
| DDD와 헥사고날 아키텍처는 왜 완벽한 조합일까? (0) | 2025.06.01 |
| [DDD] 왜 외부 애그리거트는 ID로 참조하는것이 좋을까? (0) | 2025.05.31 |