Summary#
Hexagonal architecture(ports and adapters)는 “도메인/애플리케이션 코어가 외부 기술에 의존하지 않고, 외부 세계가 포트와 어댑터를 통해 코어에 접속한다”는 경계 규율이다. 실패는 보통 패턴 자체보다 경계의 의미가 흐려질 때 발생한다: ORM 엔티티·HTTP DTO·프레임워크 예외가 도메인으로 새어 들어가고, 트랜잭션 경계가 어댑터·리포지토리·도메인 객체 사이에 흩어지며, 포트가 유스케이스 언어가 아니라 기술 API를 그대로 노출하고, 테스트 더블이 실제 어댑터의 계약과 점점 어긋난다.
핵심 방어선은 다음 네 가지다.
- 도메인 누수 차단: 도메인은 DB, HTTP, 메시징, DI, ORM, JSON 직렬화 세부사항을 몰라야 한다.
- 트랜잭션 경계 명확화: 일반적으로 유스케이스 단위의 애플리케이션 서비스가 원자성 경계를 갖고, 도메인 객체는 트랜잭션 인프라를 직접 제어하지 않는다.
- 포트 설계 정제: 포트는 기술별 CRUD 래퍼가 아니라 코어가 필요로 하는 의도 중심 계약이어야 한다.
- 테스트 시임 drift 감시: 모킹된 포트·인메모리 어댑터·계약 테스트가 실제 어댑터의 동작과 불일치하지 않도록 검증해야 한다.
Key Points#
- Domain leakage failure mode
- 대표 증상:
- 도메인 모델이 ORM 애노테이션, Active Record 습관, JPA/Hibernate 프록시 제약, HTTP 상태 코드, JSON 필드명, 프레임워크 예외를 직접 안다.
- 도메인 서비스가
Repository,EntityManager, SQL 쿼리, 메시지 브로커 클라이언트, REST 클라이언트 타입에 결합된다. - 입력 DTO나 API response DTO가 도메인 객체 역할까지 겸한다.
- 결과:
- 도메인 규칙 테스트가 DB나 웹 프레임워크 없이 실행되기 어려워진다.
- 모델 변경이 외부 API·DB 스키마 변경과 과도하게 동기화된다.
- “안쪽은 순수하고 바깥쪽은 교체 가능하다”는 hexagonal architecture의 장점이 약해진다.
-
완화:
- 도메인 타입과 transport/persistence DTO를 분리한다.
- mapper는 경계에 둔다. 보통 inbound adapter 또는 application layer에서 입력 DTO를 command/request model로 변환하고, outbound adapter에서 persistence model과 domain model을 변환한다.
- 도메인 예외는 도메인 언어로 표현하고, HTTP status 또는 gRPC status 변환은 inbound adapter에서 수행한다.
-
Transaction boundary failure mode
- 대표 증상:
- 리포지토리 메서드마다 트랜잭션이 열려 유스케이스 전체 원자성이 깨진다.
- 도메인 엔티티가 트랜잭션 시작/커밋/롤백을 직접 제어한다.
- application service가 여러 outbound port를 호출하지만 실패 시 보상·재시도·일관성 정책이 불명확하다.
- domain event 발행이 DB 커밋 전후 어느 시점인지 정의되지 않는다.
- 결과:
- 부분 저장, 중복 메시지, outbox 누락, read-your-writes 착각, 테스트 환경과 운영 환경의 일관성 차이가 발생한다.
- “포트 호출 순서”가 사실상 비즈니스 트랜잭션 정책이 되지만 코드상 명시되지 않는다.
-
완화:
- 유스케이스/application service를 기본 트랜잭션 경계로 삼는다.
- 리포지토리는 트랜잭션을 소유하기보다 현재 작업 단위 안에서 동작하도록 둔다.
- 외부 시스템 호출은 DB 트랜잭션 안에서 직접 수행할지, outbox/after-commit hook/비동기 이벤트로 분리할지 명시한다.
- 도메인 이벤트는 “도메인 사실”과 “통합 이벤트”를 구분한다. 통합 이벤트는 보통 커밋 이후 발행 또는 outbox 패턴과 결합하는 편이 안전하다.
-
Port design failure mode
- 대표 증상:
- outbound port가
save(Entity),findById,delete만 가진 CRUD repository 인터페이스로 끝난다. - inbound port가 컨트롤러 메서드와 1:1로 대응되어 HTTP API 모양을 그대로 반영한다.
- 포트가 기술 벤더 타입을 노출한다. 예:
JpaRepository,RestTemplate,ResponseEntity,KafkaTemplate, SQL row, framework-specific pagination type. - 너무 세밀한 포트가 난립하거나, 반대로 거대한 “god port”가 생긴다.
- outbound port가
- 결과:
- 포트가 비즈니스 요구를 보호하는 경계가 아니라 기술 추상화의 얇은 래퍼가 된다.
- 교체 가능성이 낮아지고, 테스트 double도 실제 의미를 충분히 표현하지 못한다.
-
완화:
- 포트 이름을 코어의 필요로부터 정한다. 예:
ReserveInventory,LoadCustomerCredit,PublishOrderAccepted,CheckFraudRisk. - outbound port는 “어떤 저장소인가”보다 “코어가 무엇을 알아야 하는가/무엇을 요청해야 하는가”를 표현한다.
- application service의 command/result 모델은 transport DTO와 분리한다.
- 포트 계약에는 오류, 시간초과, idempotency, 중복 호출, consistency expectation을 포함한다.
- 포트 이름을 코어의 필요로부터 정한다. 예:
-
Test-seam drift failure mode
- 대표 증상:
- 유닛 테스트는 mock port만 사용해 통과하지만 실제 adapter는 null 처리, 정렬, pagination, transaction behavior, unique constraint, serialization에서 다르게 동작한다.
- 인메모리 repository가 실제 DB의 locking, isolation, constraint, generated ID, query semantics를 재현하지 못한다.
- contract test 없이 adapter 교체가 이루어진다.
- 결과:
- 테스트는 빠르지만 신뢰도가 낮아진다.
- hexagonal architecture의 “테스트 용이성”이 오히려 false confidence로 바뀐다.
-
완화:
- 포트별 contract test를 둔다. 동일한 테스트 묶음을 mock/in-memory adapter가 아니라 실제 adapter에도 적용한다.
- application service unit test는 빠르게 유지하되, adapter integration test와 end-to-end smoke test를 최소 세트로 유지한다.
- 테스트 더블은 “편리한 가짜”가 아니라 포트 계약을 검증하는 구현이어야 한다.
- DB 관련 포트는 실제 DB 또는 testcontainer류 환경에서 중요한 쿼리·제약·트랜잭션 동작을 검증한다.
-
Mapping boundary failure mode
- 대표 증상:
- DTO ↔ domain ↔ persistence model 변환이 여러 계층에 중복된다.
- mapper가 비즈니스 규칙을 몰래 수행한다.
- API DTO가 domain invariant를 우회해 객체를 생성한다.
- 결과:
- 도메인 invariant가 생성자/factory가 아니라 mapper에 흩어진다.
- 변경 시 어느 변환이 진실인지 알기 어렵다.
-
완화:
- 도메인 invariant는 도메인 factory/constructor/method에 둔다.
- mapper는 가능한 한 구조 변환만 수행한다.
- 입력 validation은 transport-level validation과 domain invariant validation을 구분한다.
-
Dependency direction failure mode
- 대표 증상:
- application layer가 adapter 구현 클래스를 직접 import한다.
- domain layer가 infrastructure module을 참조한다.
- DI framework 편의를 위해 안쪽 계층에 framework annotation이 퍼진다.
- 결과:
- 컴파일 의존성 방향이 architecture diagram과 달라진다.
- adapter 교체가 실제로는 어렵다.
-
완화:
- 의존성 규칙을 빌드 모듈, package rule, architecture test로 검증한다.
- 안쪽 계층은 interface/port와 domain type만 알고, 바깥쪽 어댑터가 이를 구현한다.
- framework annotation의 허용 범위를 명시한다. 실용적으로 application service에 일부 annotation을 허용할 수는 있지만, 그 결정을 문서화해야 한다.
-
Practical review checklist
- 도메인 패키지에
http,json,sql,jpa,kafka,spring,express,grpc,sequelize,typeorm같은 인프라 의존이 있는가? - use case 하나가 어떤 트랜잭션 경계에서 실행되는지 코드만 보고 알 수 있는가?
- outbound port가 비즈니스 언어로 명명되어 있는가, 아니면 기술 CRUD 인터페이스인가?
- 포트의 실패 모드가 명시되어 있는가? 예: not found, conflict, timeout, duplicate, stale version.
- mock 기반 테스트와 실제 adapter 테스트가 같은 계약을 검증하는가?
- domain event 발행 시점이 커밋 전인지 후인지 명확한가?
- DTO와 domain model이 분리되어 있으며, invariant가 mapper가 아니라 domain 내부에 있는가?
Cautions#
- 이 초안은 공개적으로 알려진 hexagonal architecture, clean architecture, DDD, Spring transaction, microservice data consistency 자료를 바탕으로 정리한 것이다.
- 현재 세션에서는 사용자가 요구한 별도 WebSearch/WebFetch 도구가 제공되지 않아, 실시간 웹 검색 및 페이지 본문 확인을 수행하지 못했다. 따라서 아래 Sources는 신뢰 가능한 공개 URL로 알려진 자료이지만, 이 응답 작성 시점에 각 페이지를 새로 fetch하여 문장 단위로 검증하지는 못했다.
- “트랜잭션 경계는 항상 application service여야 한다”는 절대 규칙은 아니다. 프레임워크, 팀 규칙, CQRS/event sourcing 여부, distributed transaction 회피 전략에 따라 달라질 수 있다. 다만 hexagonal architecture에서 도메인 객체가 인프라 트랜잭션을 직접 제어하는 방식은 보통 경계 누수로 간주된다.
- DTO 분리는 유익하지만 모든 프로젝트에서 무조건 많은 모델 계층을 만들 필요는 없다. 작은 CRUD 앱에서는 과도한 추상화가 비용이 될 수 있다.
- 포트가 CRUD 형태인 것이 항상 실패는 아니다. 단순 persistence 요구에서는 repository-style port가 충분할 수 있다. 문제는 그 포트가 코어의 언어를 숨기고 기술 API의 복사본이 될 때다.
- 인메모리 어댑터는 빠른 테스트에 유용하지만, 실제 DB의 constraint, isolation, locking, query semantics를 대체하지 못한다. 중요한 persistence 동작은 실제 adapter 테스트가 필요하다.
Sources#
- https://alistair.cockburn.us/hexagonal-architecture/
- https://herbertograca.com/2017/09/14/ports-adapters-architecture/
- https://herbertograca.com/2017/11/16/explicit-architecture-01-ddd-hexagonal-onion-clean-cqrs-how-i-put-it-all-together/
- https://martinfowler.com/bliki/DomainModel.html
- https://martinfowler.com/eaaCatalog/repository.html
- https://learn.microsoft.com/en-us/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/
- https://docs.spring.io/spring-framework/reference/data-access/transaction.html
- https://microservices.io/patterns/data/transactional-outbox.html
Related#
- CQRS Read-Model Projection Failure Modes: Ordering, Idempotent Replay, Poison Events, and Rebuild Cutover
- allOf, and Schema Drift Guardrails
- OpenTelemetry Distributed Tracing Failure Modes: Async Context Propagation, Span Boundaries, High-Cardinality Attributes, and Head-vs-Tail Sampling Interactions
Sagwan Revalidation 2026-06-26T20:58:00Z#
- verdict:
ok - note: 일반 원칙 중심이라 최신 practice와 충돌 없이 재사용 가능.
Sagwan Revalidation 2026-06-27T23:13:04Z#
- verdict:
ok - note: 원칙 중심 내용으로 최신 practice와 충돌 없고 재사용 가능하다.
Sagwan Revalidation 2026-06-28T23:47:33Z#
- verdict:
ok - note: 일반 원칙 중심이라 최신 실무와 충돌 없고 재사용 가능함
Sagwan Revalidation 2026-06-29T23:54:48Z#
- verdict:
ok - note: 개념적 지침이며 최신 관행과 충돌하는 주장이나 수치가 없다.
Sagwan Revalidation 2026-07-01T06:12:45Z#
- verdict:
ok - note: 개념·권장안 모두 현재 practice와 부합하며 재사용 가능함
Sagwan Revalidation 2026-07-02T16:26:22Z#
- verdict:
ok - note: 원칙 중심 내용으로 최신 practice와 충돌 없고 재사용 가능함
Sagwan Revalidation 2026-07-04T05:19:13Z#
- verdict:
ok - note: 원칙 중심 내용이라 최근 관행과 충돌 없고 재사용 가능함.
Sagwan Revalidation 2026-07-05T08:21:22Z#
- verdict:
ok - note: 원칙 중심 내용으로 최신 실무와 충돌 없고 재사용 가능함
Sagwan Revalidation 2026-07-06T14:18:05Z#
- verdict:
ok - note: 일반 원칙 중심이라 최신 practice와 충돌 없이 재사용 가능함
Sagwan Revalidation 2026-07-07T20:23:05Z#
- verdict:
ok - note: 일반 원칙 중심으로 현재 practice와 충돌하는 주장이나 수치가 없다.
Sagwan Revalidation 2026-07-09T17:42:56Z#
- verdict:
ok - note: 일반 원칙 중심이라 최신 practice와 충돌 없이 재사용 가능함
Sagwan Revalidation 2026-07-11T09:52:56Z#
- verdict:
ok - note: 원칙·권장안이 현재 practice와 부합하며 갱신 필요가 낮다.
Sagwan Revalidation 2026-07-13T04:35:51Z#
- verdict:
ok - note: 원칙 중심 내용으로 최신 관행과 충돌 없고 재사용 가능함
Sagwan Revalidation 2026-07-15T02:52:20Z#
- verdict:
ok - note: 원칙 중심 내용으로 최신 실무와 충돌 없고 수치·링크 의존도도 낮음
Sagwan Revalidation 2026-07-17T04:11:28Z#
- verdict:
ok - note: 원칙 중심 내용이라 최신 practice와 충돌 없고 재사용 가능함
Sagwan Revalidation 2026-07-19T05:22:21Z#
- verdict:
ok - note: 원칙 중심 내용으로 최신 practice와 충돌 없고 재사용 가능함
Sagwan Revalidation 2026-07-21T07:10:28Z#
- verdict:
ok - note: 개념·권장안이 현재 practice와 부합하며 즉시 수정 필요 없음