--- id: wiki-2026-0508-hexagonal-architecture title: Hexagonal Architecture category: 10_Wiki/Topics status: verified canonical_id: self aliases: [hexagonal, ports and adapters, clean architecture, onion architecture, Cockburn] duplicate_of: none source_trust_level: A confidence_score: 0.96 verification_status: applied tags: [architecture, hexagonal, ports-adapters, clean-architecture, ddd, software-design] raw_sources: [] last_reinforced: 2026-05-10 github_commit: pending tech_stack: language: TypeScript / Python / Java applicable_to: [Backend, DDD, Testable] --- # Hexagonal Architecture (Ports and Adapters) ## 매 한 줄 > **"매 core domain 의 의 의 outside infrastructure 의 separate"**. Alistair Cockburn 2005. 매 inside (domain) → 매 ports (interfaces) → 매 adapters (DB, HTTP, ...). 매 testability + 매 swappable infrastructure 의 핵심. 매 vs Clean Architecture (Uncle Bob): 매 same idea. ## 매 핵심 ### 매 layers - **Core domain**: 매 entity, value object, business rule. - **Application** (use case): 매 orchestration. - **Ports**: 매 interface (input port = use case interface, output port = repo interface). - **Adapters**: - **Driving** (primary): HTTP controller, CLI. - **Driven** (secondary): DB repo, email gateway. ### 매 dependency rule - 매 Outer 매 Inner 의 depend. - 매 Inner 매 Outer 의 know X (DI). - 매 모든 dependency 매 inward. ### 매 응용 1. **Backend service**. 2. **Domain-rich app**. 3. **Testable codebase**. 4. **Multi-frontend** (web + mobile + CLI). ### 매 vs others - **Clean Architecture** (Uncle Bob): 매 same idea, 매 different name. - **Onion**: 매 layer 의 emphasize. - **CQRS**: 매 read/write 의 separate, 매 hexagonal 와 compatible. ## 💻 패턴 ### TypeScript (NestJS-style) ```typescript // 매 1. Domain (innermost) export class Order { constructor(public id: string, public items: Item[], private status: 'NEW' | 'PLACED') {} place() { if (this.items.length === 0) throw new Error('Empty order'); this.status = 'PLACED'; } } // 매 2. Port (interface) export interface OrderRepository { save(order: Order): Promise; findById(id: string): Promise; } // 매 3. Use case (application) export class PlaceOrderUseCase { constructor(private orderRepo: OrderRepository, private payments: PaymentGateway) {} async execute(orderId: string) { const order = await this.orderRepo.findById(orderId); if (!order) throw new Error('Not found'); order.place(); await this.payments.charge(order); await this.orderRepo.save(order); } } // 매 4. Adapter (driven — Postgres) export class PostgresOrderRepo implements OrderRepository { async save(order: Order) { await db.query('INSERT INTO orders ...', [...]); } async findById(id: string) { const row = await db.query('SELECT * FROM orders WHERE id = $1', [id]); return row ? toDomain(row) : null; } } // 매 5. Adapter (driving — HTTP) @Controller('orders') export class OrderController { constructor(private placeOrder: PlaceOrderUseCase) {} @Post(':id/place') async place(@Param('id') id: string) { await this.placeOrder.execute(id); return { status: 'placed' }; } } ``` ### Folder structure ``` src/ ├── domain/ │ ├── order.ts │ └── ports/ │ ├── order-repository.ts │ └── payment-gateway.ts ├── application/ │ └── place-order.use-case.ts ├── infrastructure/ │ ├── persistence/ │ │ └── postgres-order-repo.ts │ ├── http/ │ │ └── order-controller.ts │ └── external/ │ └── stripe-payment-gateway.ts └── main.ts ← wire everything ``` ### DI wiring (composition root) ```typescript const orderRepo = new PostgresOrderRepo(db); const payments = new StripePaymentGateway(stripeKey); const placeOrder = new PlaceOrderUseCase(orderRepo, payments); const controller = new OrderController(placeOrder); ``` ### Test (no DB) ```typescript // 매 in-memory adapter for fast test class InMemoryOrderRepo implements OrderRepository { private store = new Map(); async save(o: Order) { this.store.set(o.id, o); } async findById(id: string) { return this.store.get(id) ?? null; } } class FakePayments implements PaymentGateway { async charge(o: Order) { /* no-op */ } } it('places order', async () => { const repo = new InMemoryOrderRepo(); await repo.save(new Order('1', [item], 'NEW')); const uc = new PlaceOrderUseCase(repo, new FakePayments()); await uc.execute('1'); expect((await repo.findById('1'))!.status).toBe('PLACED'); }); ``` ### Python (FastAPI) ```python # 매 domain class Order: def __init__(self, id, items): self.id, self.items = id, items def place(self): self.status = 'placed' # 매 port from abc import ABC, abstractmethod class OrderRepository(ABC): @abstractmethod async def save(self, o: Order): pass @abstractmethod async def find_by_id(self, id: str) -> Order: pass # 매 use case class PlaceOrder: def __init__(self, repo: OrderRepository, payments): self.repo = repo; self.payments = payments async def execute(self, order_id): order = await self.repo.find_by_id(order_id) order.place() await self.payments.charge(order) await self.repo.save(order) # 매 adapter (HTTP) @app.post('/orders/{id}/place') async def place(id: str, uc: PlaceOrder = Depends(get_use_case)): await uc.execute(id) return {'status': 'placed'} ``` ### Java (Spring) ```java // 매 domain public class Order { public void place() { ... } } // 매 port public interface OrderRepository { void save(Order order); Optional findById(String id); } // 매 use case @Service public class PlaceOrderUseCase { private final OrderRepository orderRepo; private final PaymentGateway payments; public PlaceOrderUseCase(OrderRepository r, PaymentGateway p) { this.orderRepo = r; this.payments = p; } public void execute(String id) { Order order = orderRepo.findById(id).orElseThrow(); order.place(); payments.charge(order); orderRepo.save(order); } } ``` ### Anti-corruption layer ```typescript // 매 external API → 매 domain DTO class StripeAdapter implements PaymentGateway { async charge(order: Order) { const stripeRequest = this.toStripeFormat(order); const stripeResp = await this.stripe.charges.create(stripeRequest); if (stripeResp.status !== 'succeeded') throw new PaymentFailed(); } private toStripeFormat(order: Order) { // 매 isolate domain from Stripe schema return { amount: order.totalCents(), currency: 'usd', ... }; } } ``` ### Multiple adapters (frontend) ```typescript // 매 same use case — 매 web + mobile + CLI const useCase = new PlaceOrderUseCase(repo, payments); new HttpAdapter(useCase).listen(3000); new GraphQLAdapter(useCase).listen(4000); new CliAdapter(useCase).run(); ``` ### Eval architecture (lint check) ```javascript // 매 ESLint rule: 매 domain 매 infrastructure 의 import X { rules: { 'import/no-restricted-paths': ['error', { zones: [ { target: 'src/domain', from: 'src/infrastructure' }, { target: 'src/application', from: 'src/infrastructure' }, ], }], } } ``` ## 매 결정 기준 | 상황 | Approach | |---|---| | Domain-rich service | Hexagonal | | CRUD-only | Lighter (no full hex) | | Multi-frontend | Hexagonal benefit ↑ | | Long-lived | Worth the upfront | | Throwaway | Skip | **기본값**: 매 domain-rich = hexagonal + 매 lint enforced + 매 in-memory test adapter + 매 composition root + 매 anti-corruption layer for external. ## 🔗 Graph - 부모: [[Architecture]] - 변형: [[Clean-Architecture]] · [[Onion-Architecture]] - 응용: [[Domain-Driven-Design]] · [[CQRS]] · [[Testability]] - Adjacent: [[Encapsulation-of-Domain-Invariants]] · [[Dependency_Injection_(DI)|Dependency-Injection]] · [[Anti-Corruption-Layer]] ## 🤖 LLM 활용 **언제**: 매 backend service. 매 domain-rich. 매 long-lived. **언제 X**: 매 simple CRUD. 매 prototype. ## ❌ 안티패턴 - **Anemic domain**: 매 domain has no logic. - **Leak infrastructure into domain**: 매 violate dependency rule. - **Single-impl interfaces**: 매 over-abstraction. - **No lint enforcement**: 매 architecture decay. - **Skip composition root**: 매 wiring chaos. ## 🧪 검증 / 중복 - Verified (Cockburn 2005, Uncle Bob Clean Architecture). - 신뢰도 A. ## 🕓 Changelog | 날짜 | 변경 | |---|---| | 2026-04-20 | Auto | | 2026-05-08 | Phase 1 | | 2026-05-10 | Manual cleanup — layers + 매 TS / Python / Java / lint / ACL code |