📖 상세 설명
Anti-Corruption Layer(ACL)는 Eric Evans의 "Domain-Driven Design" (2003)에서 소개된 전략 패턴입니다. "Corruption(부패)"이란 외부 시스템의 개념, 용어, 데이터 구조가 자신의 도메인 모델에 침투하여 도메인의 순수성과 의미를 훼손하는 것을 의미합니다. ACL은 이러한 오염을 방지하는 방화벽 역할을 합니다.
레거시 시스템과의 통합은 불가피하지만, 레거시의 구조적 문제나 잘못된 추상화를 그대로 새 시스템에 가져오면 기술 부채가 전파됩니다. ACL은 레거시 시스템의 "나쁜" 모델을 받아들이되, 이를 내부적으로 의미 있는 도메인 객체로 변환합니다. 외부 API가 변경되어도 ACL만 수정하면 되므로 변경의 영향을 최소화합니다.
ACL의 핵심 구성요소는 세 가지입니다. Facade는 외부 시스템에 대한 단순화된 인터페이스를 제공합니다. Adapter는 외부 시스템의 프로토콜을 내부에서 사용하는 형식으로 변환합니다. Translator는 외부 시스템의 데이터를 도메인 객체로 매핑합니다. 이 세 요소가 협력하여 완전한 격리를 제공합니다.
실무에서 ACL은 마이크로서비스 간 통합, 레거시 모놀리스 분해, 외부 SaaS 연동, M&A로 인한 시스템 통합 등에서 필수적입니다. 특히 Strangler Fig 패턴으로 레거시를 점진적으로 대체할 때 ACL은 새 시스템과 레거시 시스템 사이의 공존을 가능하게 하는 핵심 패턴입니다.
ACL의 핵심 구성요소
Facade (파사드)
복잡한 외부 시스템을 단순한 인터페이스로 추상화. 내부 코드는 Facade만 알면 됨.
Adapter (어댑터)
프로토콜 변환 담당. REST↔gRPC, XML↔JSON 등 기술적 변환 처리.
Translator (번역기)
개념적 매핑 담당. 외부 모델의 엔티티/값 객체를 도메인 모델로 변환.
Domain Service
ACL을 사용하는 도메인 로직. 순수한 도메인 언어로만 작성됨.
💻 코드 예제
TypeScript - 레거시 결제 시스템 ACL
// ===== 레거시 시스템의 모델 (외부, 변경 불가) =====
// 레거시는 "트랜잭션"이라는 모호한 개념을 사용
interface LegacyTransaction {
txn_id: string;
txn_type: 'P' | 'R' | 'C'; // Payment, Refund, Chargeback
amt: number; // 센트 단위
ccy: string; // 통화 코드
cust_no: string;
status_cd: 'A' | 'P' | 'D' | 'F'; // Approved, Pending, Declined, Failed
created_ts: string; // 'YYYYMMDDHHMMSS' 형식
}
// 레거시 API 클라이언트
class LegacyPaymentAPI {
async getTransaction(txnId: string): Promise {
// 레거시 SOAP/XML API 호출
const response = await fetch(`${LEGACY_URL}/txn/${txnId}`);
return this.parseXmlResponse(response);
}
async processPayment(data: any): Promise {
// 복잡한 레거시 API 호출
return {} as LegacyTransaction;
}
}
// ===== 우리 도메인 모델 (순수한 비즈니스 개념) =====
// 결제(Payment)라는 명확한 개념
interface Payment {
id: PaymentId;
type: PaymentType;
amount: Money;
customerId: CustomerId;
status: PaymentStatus;
createdAt: Date;
}
type PaymentType = 'PAYMENT' | 'REFUND' | 'CHARGEBACK';
type PaymentStatus = 'APPROVED' | 'PENDING' | 'DECLINED' | 'FAILED';
// 값 객체 (Value Object)
class Money {
constructor(
public readonly amount: number, // 원 단위 (정수)
public readonly currency: Currency
) {}
static fromCents(cents: number, currencyCode: string): Money {
const currency = Currency.fromCode(currencyCode);
return new Money(cents / 100, currency);
}
add(other: Money): Money {
if (!this.currency.equals(other.currency)) {
throw new Error('Currency mismatch');
}
return new Money(this.amount + other.amount, this.currency);
}
}
class PaymentId {
constructor(public readonly value: string) {}
}
class CustomerId {
constructor(public readonly value: string) {}
}
// ===== Anti-Corruption Layer =====
// 1. Translator: 개념적 매핑
class PaymentTranslator {
// 레거시 → 도메인
toDomain(legacy: LegacyTransaction): Payment {
return {
id: new PaymentId(legacy.txn_id),
type: this.translateType(legacy.txn_type),
amount: Money.fromCents(legacy.amt, legacy.ccy),
customerId: new CustomerId(legacy.cust_no),
status: this.translateStatus(legacy.status_cd),
createdAt: this.parseTimestamp(legacy.created_ts),
};
}
// 도메인 → 레거시 (필요시)
toLegacy(payment: Payment): Partial {
return {
txn_type: this.reverseType(payment.type),
amt: payment.amount.amount * 100, // 센트로 변환
ccy: payment.amount.currency.code,
cust_no: payment.customerId.value,
};
}
private translateType(code: string): PaymentType {
const mapping: Record = {
'P': 'PAYMENT',
'R': 'REFUND',
'C': 'CHARGEBACK',
};
return mapping[code] || 'PAYMENT';
}
private translateStatus(code: string): PaymentStatus {
const mapping: Record = {
'A': 'APPROVED',
'P': 'PENDING',
'D': 'DECLINED',
'F': 'FAILED',
};
return mapping[code] || 'PENDING';
}
private reverseType(type: PaymentType): 'P' | 'R' | 'C' {
const mapping: Record = {
'PAYMENT': 'P',
'REFUND': 'R',
'CHARGEBACK': 'C',
};
return mapping[type];
}
private parseTimestamp(ts: string): Date {
// 'YYYYMMDDHHMMSS' → Date
const year = parseInt(ts.substring(0, 4));
const month = parseInt(ts.substring(4, 6)) - 1;
const day = parseInt(ts.substring(6, 8));
const hour = parseInt(ts.substring(8, 10));
const min = parseInt(ts.substring(10, 12));
const sec = parseInt(ts.substring(12, 14));
return new Date(year, month, day, hour, min, sec);
}
}
// 2. Adapter: 프로토콜 변환 + 에러 핸들링
class LegacyPaymentAdapter {
constructor(
private legacyApi: LegacyPaymentAPI,
private translator: PaymentTranslator
) {}
async getPayment(id: PaymentId): Promise {
try {
const legacyTxn = await this.legacyApi.getTransaction(id.value);
return this.translator.toDomain(legacyTxn);
} catch (error) {
// 레거시 에러를 도메인 예외로 변환
if (error.code === 'TXN_NOT_FOUND') {
return null;
}
throw new PaymentSystemError('결제 조회 실패', error);
}
}
async processPayment(payment: CreatePaymentCommand): Promise {
const legacyData = this.translator.toLegacy(payment);
try {
const result = await this.legacyApi.processPayment(legacyData);
return this.translator.toDomain(result);
} catch (error) {
throw new PaymentProcessingError('결제 처리 실패', error);
}
}
}
// 3. Facade: 단순화된 인터페이스
// 도메인 서비스가 사용하는 인터페이스 (Port)
interface PaymentGateway {
getPayment(id: PaymentId): Promise;
processPayment(command: CreatePaymentCommand): Promise;
refundPayment(paymentId: PaymentId, amount: Money): Promise;
}
// ACL Facade 구현
class LegacyPaymentGateway implements PaymentGateway {
constructor(private adapter: LegacyPaymentAdapter) {}
async getPayment(id: PaymentId): Promise {
return this.adapter.getPayment(id);
}
async processPayment(command: CreatePaymentCommand): Promise {
return this.adapter.processPayment(command);
}
async refundPayment(paymentId: PaymentId, amount: Money): Promise {
// 레거시 환불 로직을 도메인 메서드로 추상화
const refundCommand = { originalPaymentId: paymentId, amount };
return this.adapter.processRefund(refundCommand);
}
}
// ===== 도메인 서비스 (ACL 사용) =====
// 도메인 서비스는 레거시의 존재를 전혀 모름
class PaymentService {
constructor(private paymentGateway: PaymentGateway) {}
async getPaymentDetails(paymentId: string): Promise {
const id = new PaymentId(paymentId);
const payment = await this.paymentGateway.getPayment(id);
if (!payment) {
throw new PaymentNotFoundError(paymentId);
}
// 순수한 도메인 모델로 작업
return this.toDto(payment);
}
async processNewPayment(request: PaymentRequest): Promise {
const command = this.toCommand(request);
const payment = await this.paymentGateway.processPayment(command);
return this.toDto(payment);
}
}
Java - 외부 API 통합 ACL
// ===== 외부 배송 API 모델 (3rd party) =====
@JsonIgnoreProperties(ignoreUnknown = true)
public class ExternalShipmentResponse {
private String shipment_id;
private String carrier_code;
private String tracking_num;
private String ship_status; // "IN_TRANSIT", "DELIVERED", "EXCEPTION"
private List events;
}
// ===== 우리 도메인 모델 =====
public record Shipment(
ShipmentId id,
Carrier carrier,
TrackingNumber trackingNumber,
ShipmentStatus status,
List events,
LocalDateTime estimatedDelivery
) {}
public enum ShipmentStatus {
PREPARING, SHIPPED, IN_TRANSIT, OUT_FOR_DELIVERY, DELIVERED, EXCEPTION
}
public record TrackingEvent(
LocalDateTime occurredAt,
String location,
EventType type,
String description
) {}
// ===== Anti-Corruption Layer =====
// Translator
@Component
public class ShipmentTranslator {
public Shipment toDomain(ExternalShipmentResponse external) {
return new Shipment(
new ShipmentId(external.getShipmentId()),
Carrier.fromCode(external.getCarrierCode()),
new TrackingNumber(external.getTrackingNum()),
translateStatus(external.getShipStatus()),
translateEvents(external.getEvents()),
calculateEstimatedDelivery(external)
);
}
private ShipmentStatus translateStatus(String externalStatus) {
return switch (externalStatus) {
case "IN_TRANSIT" -> ShipmentStatus.IN_TRANSIT;
case "DELIVERED" -> ShipmentStatus.DELIVERED;
case "EXCEPTION" -> ShipmentStatus.EXCEPTION;
case "OUT_FOR_DELIVERY" -> ShipmentStatus.OUT_FOR_DELIVERY;
default -> ShipmentStatus.SHIPPED;
};
}
private List translateEvents(List events) {
return events.stream()
.map(this::translateEvent)
.sorted(Comparator.comparing(TrackingEvent::occurredAt))
.toList();
}
}
// Adapter
@Component
public class ExternalShippingAdapter {
private final ExternalShippingClient client;
private final ShipmentTranslator translator;
private final CircuitBreaker circuitBreaker;
public ExternalShippingAdapter(
ExternalShippingClient client,
ShipmentTranslator translator,
CircuitBreakerRegistry registry
) {
this.client = client;
this.translator = translator;
this.circuitBreaker = registry.circuitBreaker("shipping");
}
public Optional getShipment(ShipmentId id) {
return circuitBreaker.executeSupplier(() -> {
try {
ExternalShipmentResponse response = client.getShipment(id.value());
return Optional.of(translator.toDomain(response));
} catch (ShipmentNotFoundException e) {
return Optional.empty();
} catch (ExternalApiException e) {
throw new ShippingServiceException("배송 조회 실패", e);
}
});
}
}
// Facade (Port 구현)
@Component
public class ExternalShippingGateway implements ShippingGateway {
private final ExternalShippingAdapter adapter;
@Override
public Optional findById(ShipmentId id) {
return adapter.getShipment(id);
}
@Override
public Shipment createShipment(CreateShipmentCommand command) {
return adapter.createShipment(command);
}
}
// 도메인 서비스 - 외부 시스템의 존재를 모름
@Service
public class OrderFulfillmentService {
private final ShippingGateway shippingGateway; // Port
private final OrderRepository orderRepository;
public ShipmentStatusDto getOrderShipmentStatus(OrderId orderId) {
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new OrderNotFoundException(orderId));
Shipment shipment = shippingGateway.findById(order.getShipmentId())
.orElseThrow(() -> new ShipmentNotFoundException(order.getShipmentId()));
// 순수한 도메인 로직
return ShipmentStatusDto.from(shipment);
}
}
아키텍처 다이어그램
┌─────────────────────────────────────────────────────────────┐
│ Our Bounded Context │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Domain Layer │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────────┐ │ │
│ │ │ Domain │ │ Domain │ │ Domain │ │ │
│ │ │ Model │ │ Service │ │ Events │ │ │
│ │ │ (Payment, │ │ (uses Port) │ │ (PaymentMade) │ │ │
│ │ │ Money...) │ │ │ │ │ │ │
│ │ └─────────────┘ └──────┬──────┘ └─────────────────┘ │ │
│ │ │ uses │ │
│ │ ▼ │ │
│ │ ┌─────────────────────────────────────────────────────┐│ │
│ │ │ Port (Interface) ││ │
│ │ │ PaymentGateway ││ │
│ │ │ - getPayment(id): Payment ││ │
│ │ │ - processPayment(cmd): Payment ││ │
│ │ └─────────────────────────────────────────────────────┘│ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ════════════════════════════╪═══════════════════════════════│
│ │ implements │
│ ┌───────────────────────────▼─────────────────────────────┐ │
│ │ Anti-Corruption Layer (ACL) │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────────────────┐ │ │
│ │ │ Facade │→ │ Adapter │→ │ Translator │ │ │
│ │ │ (Gateway │ │ (Protocol│ │ (LegacyTxn→Payment) │ │ │
│ │ │ impl) │ │ convert)│ │ │ │ │
│ │ └──────────┘ └──────────┘ └──────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────┘ │
└───────────────────────────────│──────────────────────────────┘
│ calls
▼
┌─────────────────────────────────────────────────────────────┐
│ External / Legacy System │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Different model, different language │ │
│ │ (LegacyTransaction, txn_type: 'P'|'R'|'C', ...) │ │
│ └─────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘