f8b21af4be
10_Wiki/Topics 대규모 정리: - 오류 캡처/미완성 stub 문서 227개 제거 - 교차폴더 중복 43클러스터 병합 (63파일 → redirect) - 링크명 정규화: 깨진 링크 수정·redirect 직결·개념 매핑 ~2,400건 - 카테고리 MOC 6개 신규 생성 - Graph 섹션 미해결 related-keyword 링크 10,058건 제거 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
8.6 KiB
8.6 KiB
id, title, category, status, canonical_id, aliases, duplicate_of, source_trust_level, confidence_score, verification_status, tags, raw_sources, last_reinforced, github_commit, tech_stack
| id | title | category | status | canonical_id | aliases | duplicate_of | source_trust_level | confidence_score | verification_status | tags | raw_sources | last_reinforced | github_commit | tech_stack | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| wiki-2026-0508-hexagonal-architecture | Hexagonal Architecture | 10_Wiki/Topics | verified | self |
|
none | A | 0.96 | applied |
|
2026-05-10 | pending |
|
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.
매 응용
- Backend service.
- Domain-rich app.
- Testable codebase.
- 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)
// 매 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<void>;
findById(id: string): Promise<Order | null>;
}
// 매 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)
const orderRepo = new PostgresOrderRepo(db);
const payments = new StripePaymentGateway(stripeKey);
const placeOrder = new PlaceOrderUseCase(orderRepo, payments);
const controller = new OrderController(placeOrder);
Test (no DB)
// 매 in-memory adapter for fast test
class InMemoryOrderRepo implements OrderRepository {
private store = new Map<string, Order>();
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)
# 매 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)
// 매 domain
public class Order {
public void place() { ... }
}
// 매 port
public interface OrderRepository {
void save(Order order);
Optional<Order> 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
// 매 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)
// 매 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)
// 매 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) · 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 |