[PonsWarp 개념 교실 26] 헥사고날 아키텍처 — 외부 기술을 갈아끼우는 구조
쉽게 말하면, 헥사고날 아키텍처는 핵심 업무 로직이 WebRTC·파일시스템·클라우드 같은 외부 기술을 직접 알지 않게 만드는 구조다.

외부 기술을 바꾸더라도 안쪽 유스케이스는 같은 계약(Port)만 사용한다.
안쪽과 바깥쪽
[WebRTC] [Cloud API] [File System]
\ | /
Adapter
↓
Port
↓
[Transfer Use Case]
↓
Domain State
PonsWarp의 transfer/ports.ts는 SignalingPort, PeerPort, WriterPort, ClockPort, CloudDropPort, TransferOutputPort를 정의한다. 유스케이스는 구현체가 아니라 이 인터페이스를 받는다.
왜 테스트가 쉬워지나
실제 WebRTC 연결이나 디스크 writer를 만들지 않고도 Fake Port를 주입해 상태 전이를 테스트할 수 있다. 이벤트를 넣고 reduceTransferState 결과만 비교하면 된다.
기존 SwarmManager 같은 레거시 서비스는 compatibility facade가 감싸서 기존 호출부를 보호한다. 새 구조와 기존 API를 한 번에 끊지 않고 경계를 만든 방식이다.
PonsWarp 적용 지점
| 항목 | 구현 | 증거 |
|---|---|---|
| Port | 6개 외부 계약 | transfer/ports.ts |
| Use Case | runner·쓰기 큐·timeout | transfer/application/useCases.ts |
| Domain State | 순수 reducer | transfer/state.ts |
| Adapter | sender/receiver compatibility facade | transfer/adapters/* |
| Factory | signaling 구현 선택 | services/signaling-factory.ts |
| 레거시 보호 | SinglePeerConnection facade | services/singlePeerConnection.ts |
의존성 방향과 포트의 의미
헥사고날 아키텍처는 폴더를 육각형처럼 배치하는 규칙이 아니라 의존성 방향의 규칙이다. 핵심 유스케이스가 RTCPeerConnection, fs, Cloud SDK 같은 구체 타입을 import하면 바깥 기술의 변경이 안쪽 판단을 흔든다. 대신 안쪽이 필요한 동작을 Port로 선언하고, 바깥 Adapter가 그 계약을 구현한다.
text[WebRTC adapter] [R2 adapter] [File adapter] \ | / implements ports │ [Transfer use case] │ [Domain]
입력은 유스케이스 명령과 domain data, 출력은 domain result 또는 port가 정의한 오류다. Adapter는 외부 오류를 안쪽에서 이해할 수 있는 타입으로 번역하고, 유스케이스는 브라우저 API의 이벤트 이름이나 SDK 응답 구조를 몰라야 한다. ClockPort 같은 작은 포트도 테스트에서 시간을 고정하는 역할을 한다.
그냥 서비스 안에서 SDK를 바로 호출하면 안 되나?
작은 기능에서는 직접 호출이 빠르다. 하지만 WebRTC에서 R2로, 브라우저 파일에서 서버 저장소로 기술이 바뀌는 순간 조건문과 mock이 유스케이스에 퍼진다. 포트는 추상화를 위한 추상이 아니라 변경 가능성이 높은 경계에 이름을 붙이는 장치다. 반대로 모든 함수마다 포트를 만들면 흐름이 보이지 않고 adapter보다 interface가 많아진다.
| 경계 | Port가 표현할 것 | Adapter가 숨길 것 |
|---|---|---|
| peer | connect/send/close | WebRTC 이벤트·상태 |
| writer | append/flush/resume | 파일 API·R2 multipart |
| clock | now/sleep | 실제 시간 |
| signaling | join/route | WebSocket transport |
테스트 경계와 레거시 공존
포트 테스트는 실제 WebRTC를 띄우지 않고 계약을 검증할 수 있어야 한다. 예를 들어 fake writer로 append → flush → resume 순서를 확인하고, adapter 테스트에서는 실제 브라우저·SDK의 변환만 확인한다. 두 테스트를 섞으면 실패가 네트워크인지 유스케이스인지 구분하기 어렵다.
레거시 코드가 이미 외부 API를 직접 호출한다면 한 번에 모든 것을 포트로 옮기지 않는다. 먼저 하나의 유스케이스에서 호출 경계를 감싸고, 새 경로의 결과를 기존 경로와 비교한 뒤 확장한다. 단, 임시 adapter가 영구 API처럼 남지 않도록 책임과 제거 기준을 기록해야 한다.
재구축의 판단 기준은 “추상화가 멋진가”가 아니라 외부 기술을 바꿀 때 domain 규칙과 전송 상태 머신을 그대로 보존할 수 있는가다.
포트가 너무 커지는 신호
TransferPort 하나가 connect, send, persist, billing까지 모두 노출하면 adapter가 모든 책임을 떠안고 테스트도 다시 통합 테스트가 된다. 유스케이스가 실제로 필요로 하는 동작만 작은 port로 나누고, 여러 port를 조합하는 것은 application layer가 맡는다. port의 메서드 이름은 외부 SDK 용어보다 domain 동작을 표현해야 한다.
좋은 경계는 import 방향과 오류 변환을 함께 단순하게 만든다. 나쁜 경계는 타입만 하나 더 만들고 실제 외부 상태를 그대로 안쪽으로 새어 보낸다. 이 기준으로 기존 transfer/ports.ts를 점검하면 포트 추가보다 불필요한 의존성 제거가 먼저인 경우도 있다.
유스케이스를 재현 가능한 상태로 만든다
ClockPort를 고정하면 timeout과 retry를 결정론적으로 재현할 수 있고, FakePeerPort를 사용하면 연결이 끊기는 순서도 원하는 시점에 만들 수 있다. Fake가 실제 adapter의 모든 동작을 흉내 낼 필요는 없다. port 계약에 필요한 성공·실패·취소만 제공하면 된다.
구조를 옮길 때 가장 큰 위험은 이름만 port로 바꾸고 외부 객체를 그대로 전달하는 것이다. domain이 RTCDataChannel이나 SDK response를 받는다면 의존성 방향은 여전히 바깥을 향한다. 경계에서 plain data와 domain error로 번역하는지 확인해야 헥사고날 구조가 실질적으로 작동한다.
한 문장 정리
PonsWarp는 Port를 중심으로 전송 유스케이스를 외부 기술에서 분리하고 Adapter·Facade로 기존 구현을 감싸 테스트와 교체 가능성을 확보한다.