Adapter 패턴: 인터페이스가 다른 두 시스템을 잇는 법
레거시 SDK와 신규 인터페이스 사이의 불일치를 흡수하는 Adapter 패턴의 구조, Java 구현, 운영·보안 고려사항을 정리한다.
2026-08-13 · 최초 발행 2025-10-14
코드를 고치지 않고 인터페이스를 맞추는 법
이기종 시스템, 레거시 코드, 서드파티 라이브러리 사이의 인터페이스 차이를 최소 비용으로 흡수해야 하는 상황은 반복해서 생긴다. Adapter 패턴은 기존 코드를 바꾸지 않고 새 인터페이스에 맞춰 재사용성을 높이는 대표적인 방법이다. 서로 다른 인터페이스를 가진 객체 사이에 중간 변환 계층(Adapter)을 두어, 클라이언트가 기대하는 인터페이스(Target)를 만족하도록 매핑하는 것이 핵심 아이디어다. 클라이언트는 Target에만 의존하고, Adaptee(기존 구현체)의 변경이나 차이는 캡슐화된다. 객체지향 설계 원칙을 지키면서 결합도를 최소화하고 교체 가능성을 극대화하는 것이 목표다.
Target·Adaptee·Adapter·Client가 나누는 역할
Target은 클라이언트가 기대하는 표준 인터페이스를 정의한다. 호출 규약, 예외 모델, 데이터 계약의 단일 기준점 역할을 한다. Adaptee는 기존 구현체나 외부 라이브러리로, 인터페이스 불일치·데이터 형식 차이·호출 순서 제약을 갖고 있다. Adapter는 Target을 구현하면서 Adaptee에 호출을 위임하고 변환을 수행한다 — 매핑 로직, 기본값 보정, 예외 변환, 리소스 관리를 책임진다. Client는 Target만 의존하며, DI나 팩토리와 결합하면 런타임에 구현을 교체할 수 있고 테스트할 때는 Mock Target으로 쉽게 대체할 수 있다.
객체 어댑터와 클래스 어댑터
객체 어댑터(Object Adapter)는 합성(Composition) 기반이다. Adapter가 Adaptee 인스턴스를 보유하고 위임을 수행하며, 다형성과 확장성이 우수하다. 클래스 어댑터(Class Adapter)는 상속(Inheritance) 기반으로, 다중 상속이 필요해(C++ 등) 언어 제약 때문에 채택 빈도가 낮다 — Java 등에서는 애초에 불가능하다.
요청이 변환되는 절차
입력은 클라이언트가 Target API를 호출하는 것으로 시작하며, 파라미터로 도메인 모델·원시 타입·컨텍스트 메타데이터를 받는다. Adapter는 유효성을 검증하고 데이터를 변환한 뒤 호출 순서를 보정하고, Adaptee 호출을 위임해 결과를 받은 다음 예외·결과를 매핑한다. 출력은 Target 규약에 맞춘 결과이거나 표준화된 예외다.
에러 핸들링에서는 Null이나 옵셔널 입력값을 기본값으로 처리하고, Adaptee 전용 예외를 도메인 표준 예외로 변환한다. 부분 실패가 생기면 재시도 정책, 타임아웃, 서킷브레이커와 결합해 처리하는 것도 고려 대상이다. 일관성 측면에서는 양방향 변환 시 정보 손실 여부를 명시하는 불변 변환 원칙을 지키고, 멱등성이 필요하면 요청 식별자·캐시 키를 유지한다.
Java로 구현한 결제 게이트웨이 어댑터
환경·전제조건은 Java 17+, 외부 라이브러리는 필요 없고 단위테스트는 JUnit 5를 권장한다.
// Target: 클라이언트가 기대하는 인터페이스
public interface PaymentGateway {
PaymentResult pay(PaymentRequest req);
}
// Adaptee: 기존/외부 결제 SDK
public class LegacyPaySdk {
public LegacyResponse submit(String cardNo, int amountCents) {
// 가정: 블로킹 I/O 호출
if (amountCents <= 0) throw new IllegalArgumentException("Invalid amount");
return new LegacyResponse(true, "OK");
}
}
public record PaymentRequest(String cardNumber, long amount) {}
public record PaymentResult(boolean success, String message) {}
public record LegacyResponse(boolean ok, String desc) {}
// Adapter: 변환·예외 매핑·위임
public class LegacyPayAdapter implements PaymentGateway {
private final LegacyPaySdk sdk;
public LegacyPayAdapter(LegacyPaySdk sdk) {
this.sdk = sdk;
}
@Override
public PaymentResult pay(PaymentRequest req) {
// 유효성 및 단위 변환: 원화 단위→센트(예시)
if (req == null || req.cardNumber() == null) {
return new PaymentResult(false, "Invalid request");
}
long cents = Math.multiplyExact(req.amount(), 100); // 오버플로우 검출
try {
LegacyResponse resp = sdk.submit(req.cardNumber(), (int) cents);
return new PaymentResult(resp.ok(), resp.desc());
} catch (IllegalArgumentException e) {
return new PaymentResult(false, "Bad input: " + e.getMessage());
} catch (Exception e) {
// 표준화된 예외/메시지로 변환
return new PaymentResult(false, "Payment gateway error");
}
}
}
// 사용 예
public class Demo {
public static void main(String[] args) {
PaymentGateway gw = new LegacyPayAdapter(new LegacyPaySdk());
PaymentResult r = gw.pay(new PaymentRequest("4111111111111111", 1000));
System.out.println(r.success() + " - " + r.message());
}
}
테스트와 운영에서 놓치기 쉬운 지점
테스트에서는 0원·음수 금액·최대 금액(오버플로우) 같은 경계값을 반드시 확인해야 한다. Adaptee마다 던지는 예외가 도메인 표준 예외·메시지로 일관되게 매핑되는지도 검증 대상이고, 멱등성이 필요하면 요청 ID 기반 재시도 시나리오까지 확인해야 한다.
운영에서는 네트워크로 연결되는 Adaptee라면 타임아웃·재시도·서킷브레이커 설정이 필요하다. 카드번호 같은 PII·민감정보는 마스킹·토큰화하고 로깅은 최소화해야 한다. 스레드 안전성 측면에서는 Adapter 내부 상태를 불변으로 설계하고 공용 객체를 공유할 때 주의해야 한다.
레거시 마이그레이션부터 포맷 변환까지
레거시 시스템을 교체하는 단계에서는 기존 API는 그대로 두고 내부 구현만 새 라이브러리로 바꿔 다운타임을 최소화하고 롤백을 쉽게 만들 수 있다. 서드파티 SDK나 벤더 API를 통합할 때는 벤더별로 Adapter를 두어 캡슐화하면 전략적으로 교체하기 쉬워지고, 계약 테스트로 호환성을 자동 검증할 수 있다. 인터페이스가 진화하거나 버전이 바뀔 때는 v1→v2 전환기에 Adapter로 쌍방향 호환 계층을 제공하고, API 게이트웨이에서 요청·응답 스키마를 변환하는 데도 쓴다. 메시지나 데이터 포맷 변환에서는 JSON↔XML, Avro↔Protobuf 같은 포맷을 브릿징하고, 손실이 있는 변환이라면 스키마 진화 규칙을 문서화해야 한다.
인접 패턴과 구별하기
| 구분 | 객체 어댑터(Composition) | 클래스 어댑터(Inheritance) |
|---|---|---|
| 성능 | 경량 위임 오버헤드 소폭 존재(수십 ns~수 µs) | 직접 상속으로 오버헤드 최소화 |
| 확장성 | 다형성·구성 교체 용이, 다수 Adaptee 지원 용이 | 언어 제약(다중 상속 필요), 확장 제약 |
| 일관성 | 명시적 변환 계층으로 규약 일관성 유지 용이 | 상속 트리 복잡 시 규약 누수 가능 |
| 안정성 | 캡슐화로 변경 영향 최소화 | 상위 클래스 변경 영향 큼 |
| 운영 편의 | 테스트·모킹 용이, DI 친화 | 상속 기반 테스트 복잡도 증가 |
인접한 패턴과는 목적으로 구별한다. Facade는 복잡성을 숨기는 것이 목적이고 인터페이스 변환은 부수적이다. Decorator는 기능 확장이 목적이며 인터페이스는 동일하게 유지한다. Proxy는 접근 제어나 원격 호출이 목적이고 인터페이스도 동일하게 유지한다. Adapter만이 호환성 확보, 즉 인터페이스 변환 자체를 핵심으로 삼는다.
코드 수정량으로 보는 효과
정량적으로는 주요 엔드포인트 기준으로 호출부 수정 라인 수가 60~90% 줄어드는 사례가 가능한데, 시스템 규모와 분리도에 따라 상이하다. 성능 오버헤드는 메서드 위임 수준으로 경미해 SLA에 미치는 영향은 최소한이다. 정성적으로는 결합도가 낮아져 교체·확장 비용이 줄고, 테스트가 쉬워지며 계약 기반으로 품질을 확보할 수 있다. 인터페이스가 안정되면서 릴리스 리스크도 줄어든다. 객체 어댑터를 기본 선택으로 삼되, 성능·언어 제약·조직 표준에 따라 변형을 적용하는 것이 실무적인 접근이다.