---
id: arch-anti-corruption-layer
title: Anti-Corruption Layer — legacy / external 격리
category: Coding
status: draft
source_trust_level: B
verification_status: conceptual
created_at: 2026-05-09
updated_at: 2026-05-09
tags: [architecture, ddd, vibe-coding]
tech_stack: { language: "TS / generic", applicable_to: ["Architecture"] }
applied_in: []
aliases: [anti-corruption layer, ACL, adapter, bridge, translator, facade pattern]
---

# Anti-Corruption Layer

> Legacy / 외부 system 의 model 이 새 system 에 침투 = 큰 부패. **ACL 가 변환 + isolation**. DDD 의 핵심 패턴.

## 📖 핵심 개념
- 외부 model 가 비즈니스 model 와 다름.
- ACL = adapter / translator.
- 변경 가 외부 → ACL 만 (비즈니스 보호).
- Domain 가 깨끗하게.

## 💻 코드 패턴

### 문제 (ACL 없음)
```ts
// ❌ Domain 가 legacy field 노출
import { LegacyUser } from './legacy';

class OrderService {
  process(legacyUser: LegacyUser, ...) {
    if (legacyUser.usr_typ === 'P') {  // 'P' 가 무엇?
      // ...
    }
    if (legacyUser.eml_addr.endsWith('@admin.com')) {  // weird field name
      // ...
    }
  }
}
```

→ 비즈니스 로직 가 legacy 의 weird name / format 가 흐른다.

### ACL 적용
```ts
// Domain
class User {
  id: UserId;
  email: Email;
  type: UserType;  // enum
  
  isAdmin(): boolean { return this.email.domain === 'admin.com'; }
}

// ACL — legacy → domain
class LegacyUserAdapter {
  toDomain(raw: LegacyUser): User {
    return new User(
      new UserId(raw.user_id),
      new Email(raw.eml_addr),
      this.mapType(raw.usr_typ),
    );
  }
  
  private mapType(t: string): UserType {
    switch (t) {
      case 'P': return UserType.Premium;
      case 'F': return UserType.Free;
      case 'A': return UserType.Admin;
      default: throw new Error('unknown');
    }
  }
}

// Service 가 깨끗
class OrderService {
  process(user: User, ...) {
    if (user.type === UserType.Premium) { ... }
    if (user.isAdmin()) { ... }
  }
}
```

### 외부 API ACL
```ts
// Stripe 의 model
interface StripeCharge {
  id: string;
  amount: number;
  currency: string;
  status: string;
  metadata: Record<string, string>;
}

// 비즈니스 model
class Payment {
  id: PaymentId;
  amount: Money;
  status: PaymentStatus;
  
  isSuccessful(): boolean { return this.status === PaymentStatus.Succeeded; }
}

class StripeAdapter {
  toDomain(charge: StripeCharge): Payment {
    return new Payment(
      new PaymentId(charge.id),
      new Money(charge.amount, charge.currency),
      this.mapStatus(charge.status),
    );
  }
}
```

### Two-way ACL
```ts
class StripeAdapter {
  toDomain(charge: StripeCharge): Payment { ... }
  
  fromDomain(payment: Payment): StripeChargeRequest {
    return {
      amount: payment.amount.cents,
      currency: payment.amount.currency,
      // ...
    };
  }
}
```

→ Domain → external 도 ACL.

### Bounded context (DDD)
```
Sales BC: Customer = "구매한 사람"
Support BC: Customer = "ticket 가진 사람"
Marketing BC: Customer = "lead"

같은 사람 — 다른 model.
ACL 가 BC 간 변환.
```

```ts
// Sales → Support
class SalesCustomerToSupportCustomer {
  translate(c: Sales.Customer): Support.Customer {
    return new Support.Customer(
      c.id,
      c.email,
      // sales 가 가진 일부
    );
  }
}
```

### 외부 event ACL
```ts
// Kafka topic 의 다른 system event
interface ExternalOrderEvent {
  ord_id: string;
  cust: { id: string };
  itms: Array<{ pid: string; qty: number }>;
}

class OrderEventAdapter {
  toDomain(raw: ExternalOrderEvent): OrderPlaced {
    return new OrderPlaced(
      new OrderId(raw.ord_id),
      new CustomerId(raw.cust.id),
      raw.itms.map(i => new OrderLine(new ProductId(i.pid), i.qty)),
    );
  }
}

eventBus.on('external.order', (raw) => {
  const event = adapter.toDomain(raw);
  service.handle(event);
});
```

### Hexagonal architecture (port + adapter)
```ts
// Port (domain interface)
interface PaymentGateway {
  charge(amount: Money, card: CardToken): Promise<Payment>;
}

// Adapter (Stripe)
class StripeGateway implements PaymentGateway {
  async charge(amount: Money, card: CardToken): Promise<Payment> {
    const stripeReq = this.toStripe(amount, card);
    const stripeRes = await stripe.charges.create(stripeReq);
    return this.toDomain(stripeRes);
  }
}

// Service 만 port 알아
class CheckoutService {
  constructor(private gateway: PaymentGateway) {}
  
  async checkout(...) {
    const payment = await this.gateway.charge(...);
    // ...
  }
}
```

→ Stripe 교체 → 새 adapter, service 변경 X.

### Test 친화
```ts
class FakePaymentGateway implements PaymentGateway {
  async charge(): Promise<Payment> {
    return new Payment(...);  // success
  }
}

const service = new CheckoutService(new FakePaymentGateway());
// → Test 가 외부 호출 X
```

### Event sourcing 의 schema 변경
```
Event v1: { type: 'OrderPlaced', orderId, total }
Event v2: { type: 'OrderPlaced', orderId, total, currency }

Upcaster (ACL):
v1 → v2: { ...v1, currency: 'USD' }

→ 옛 event 도 새 schema 로 처리.
```

### 외부 webhook
```ts
app.post('/webhook/stripe', async (req, res) => {
  const event = adapter.fromStripeWebhook(req.body);
  await service.handle(event);
  res.sendStatus(200);
});

class StripeWebhookAdapter {
  fromWebhook(body: any): DomainEvent {
    switch (body.type) {
      case 'charge.succeeded': return new PaymentSucceeded(...);
      case 'charge.failed': return new PaymentFailed(...);
      // ...
    }
  }
}
```

### ACL 가 보호
```
변경 영향:
- Stripe API 가 변경 → ACL 만
- Legacy field rename → ACL 만
- 새 backend → 새 ACL, domain 0 변경
```

### Translator vs Adapter vs Facade
```
Adapter: interface 변환 (port → external)
Translator: data 변환 (DTO → domain)
Facade: 복잡 system 의 simple front

→ 비슷. ACL = 모두 포함.
```

### 작은 system 의 함정
```ts
// ❌ 5 개 endpoint — adapter 가 boilerplate
// 그냥 legacy 사용? 
// → 1년 후 비즈니스 로직 가 entangled.

// ✅ 작아도 minimal ACL.
// Field rename / type convert 만이라도.
```

### 큰 ACL 함정
```ts
// ❌ ACL 가 비즈니스 logic 가짐
class StripeAdapter {
  toDomain(charge) {
    const payment = new Payment(...);
    
    // ❌ "Premium 사용자 면 X" — 비즈니스
    if (payment.user.isPremium && payment.amount > 1000) {
      payment.bonus = true;
    }
    
    return payment;
  }
}
```

→ ACL = pure 변환. 비즈니스 = service.

### Cost 인지
```
ACL = boilerplate 비용.
- 매 변경 두 곳 (legacy + ACL)
- 새 field 추가 = ACL update

→ 외부 model 가 안정 + 1-2 곳 만 = 직접 OK.
큰 / 변경 잦은 = ACL.
```

### Code organization
```
src/
├── domain/             # 비즈니스 model + service
├── infrastructure/
│   ├── stripe/
│   │   └── stripe.adapter.ts   # ACL
│   └── legacy/
│       └── legacy.adapter.ts
└── application/        # use case
```

→ `infrastructure` = adapter / external.

### LLM 활용
```
- 외부 schema → domain mapping = LLM 가 잘 작성
- "이 OpenAPI spec → TypeScript domain model" prompt
- ACL boilerplate 자동
```

## 🤔 의사결정 기준
| 외부 | 추천 |
|---|---|
| Legacy DB | ACL + adapter |
| 외부 API (Stripe) | Port + adapter |
| 다른 BC | Bounded context translator |
| Webhook | Webhook adapter |
| 매우 작은 1-2 호출 | 직접 OK |
| Schema 잦은 변경 | ACL 강력 |
| Test | Fake adapter |

## ❌ 안티패턴
- **외부 model 가 service 직접**: corruption.
- **ACL 가 비즈니스 logic**: 분리 X.
- **모든 거 ACL**: boilerplate, 작은 system.
- **One-way ACL (read 만)**: write 가 leaky.
- **ACL 없이 hexagonal 만 가정**: port 의 의미 사라짐.
- **ACL test 없음**: silent translation bug.
- **외부 type 가 domain 노출**: leak.

## 🤖 LLM 활용 힌트
- ACL = port + adapter 의 변환 layer.
- 외부 mess 가 domain 침투 차단.
- LLM 가 schema mapping 작성 강함.
- BC 간 translator 가 DDD 의 핵심.

## 🔗 관련 문서
- [[Arch_Hexagonal_Clean]]
- [[Arch_DDD_Bounded_Context]]
- [[Arch_Strangler_Fig]]
