실제 TypeScript 예제로 어댑터 디자인 패턴을 살펴보세요. 호환되지 않는 API를 연결하고, 레거시 코드를 리팩터링하며, 확장 가능한 시스템을 구축하는 방법을 배우세요.
January 7, 2026 (3mo ago)
클린 코드의 어댑터 디자인 패턴 가이드
실제 TypeScript 예제로 어댑터 디자인 패턴을 살펴보세요. 호환되지 않는 API를 연결하고, 레거시 코드를 리팩터링하며, 확장 가능한 시스템을 구축하는 방법을 배우세요.
← Back to blog
The Adapter 디자인 패턴은 본질적으로 중개자입니다. 두 개의 호환되지 않는 시스템이 아무 문제 없이 서로 대화할 수 있게 해주는 번역가라고 생각하세요. 핵심 목적은 기존 클래스를 원본 소스 코드를 건드리지 않고 협력하게 만드는 것입니다. 마치 캐나다 전자기기를 유럽 콘센트에 꽂을 수 있게 해주는 여행용 어댑터의 소프트웨어 버전이라고 볼 수 있습니다.
이 패턴은 특히 서드파티 라이브러리를 통합하거나 레거시 시스템을 다룰 때, 깔끔하고 유지보수하기 쉬운 코드의 초석입니다.1
왜 코드베이스에 어댑터 패턴이 필요한가

상황을 떠올려보세요: 유럽에 있는데 캐나다 노트북을 콘센트에 꽂으려고 합니다. 그냥 맞지 않죠. 충전기는 잘 작동하고, 벽면 소켓도 문제없지만 인터페이스가 완전히 다릅니다. 소프트웨어 개발에서도 설계상 전혀 만나도록 만들어지지 않은 두 시스템을 갑자기 함께 작동시켜야 할 때 이와 같은 문제가 빈번히 발생합니다.
그럴 때 어댑터 패턴이 등장합니다. 코드용 범용 여행 어댑터처럼, 불일치하는 구성 요소들 사이에 원활한 다리를 만들어 줍니다.
현대 개발에서의 간극 연결
오늘날 우리는 거의 모든 것을 처음부터 제작하지 않습니다. 서드파티 라이브러리, 외부 API, 레거시 시스템을 조합해 솔루션을 만듭니다. 이는 배달 속도를 높이지만 호환되지 않는 인터페이스와 장기적으로 문제를 일으키는 유혹 많은 지름길도 함께 가져옵니다:
- 코드 중복: 동일한 변환 로직이 앱 전반에 흩어집니다.
- 높은 결합도: 비즈니스 로직이 외부 서비스의 세부사항과 얽힙니다.
- 증가하는 기술 부채: API 변경이 있을 때마다 깨지기 쉬운 통합 코드를 찾는 수색전이 벌어집니다.
어댑터 패턴은 변환을 한곳에서 처리하는 전용 어댑터 클래스를 제공해 핵심 애플리케이션 로직을 보호하고 통합을 더 유지보수하기 쉽게 만듭니다. 이러한 접근법은 모듈화된 아키텍처와 점진적 현대화를 우선시하는 팀에서 일반적입니다.2
어댑터 패턴은 단순히 작동시키기 위한 해킹이 아닙니다. 외부 시스템의 지저분한 세부사항으로부터 핵심 애플리케이션 로직의 단순성과 무결성을 보호합니다.
어댑터는 깔끔하고 확장 가능한 코드를 촉진한다
어댑터는 전체 재작성 없이 변화할 수 있는 유연한 아키텍처를 구축하는 전략입니다. 클라이언트 코드를 건드리지 않고 어댑터를 추가하거나 교체함으로써 서비스를 바꾸거나 레거시 시스템을 단계적으로 폐기할 수 있습니다. 이런 안정성은 기능 개발 속도를 높이고 통합 리스크를 줄입니다.2
어댑터 패턴은 실제로 어떻게 동작하는가
핵심적으로 이 패턴은 시스템의 부분들 사이에 깔끔한 분리를 만들어 각 부분이 독립적으로 진화할 수 있게 합니다. 네 가지 플레이어가 이 패턴을 이해하기 쉽게 만듭니다:
- 클라이언트: 어떤 작업이 필요하고 단순하고 안정적인 인터페이스를 기대하는 코드.
- 타겟(Target): 클라이언트가 말하는 깔끔한 인터페이스.
- 어댑티(Adaptee): 인터페이스가 호환되지 않는 기존 컴포넌트(종종 변경 불가).
- 어댑터(Adapter): 타겟을 구현하고 호출을 어댑티로 번역하는 역할.

클라이언트는 오직 타겟 인터페이스와만 대화하고, 어댑터는 조용히 어댑티를 위해 번역합니다. 이 설계는 개방/폐쇄 원칙(Open/Closed Principle)과 잘 맞습니다: 새로운 서비스를 통합하려면 새로운 어댑터를 작성하면 되고 기존 클라이언트 코드는 건드릴 필요가 없습니다.1
어댑터 패턴의 주요 목적은 클래스의 인터페이스를 클라이언트가 기대하는 다른 인터페이스로 변환하는 것입니다. 어댑터는 인터페이스 불일치 때문에 함께 작동할 수 없던 클래스들이 함께 작동하도록 합니다.3
실제 예제로 보는 TypeScript에서의 어댑터 구축

아래에는 실제 상황에서 흔히 마주치는 문제를 반영한 두 가지 실용적인 TypeScript 예제가 있습니다: 레거시 XML API 적응과 서드파티 결제 게이트웨이 표준화.
예제 1: 레거시 XML API를 JSON으로 적응시키기
시나리오: 모던한 React 프론트엔드는 JSON을 기대하지만 유일한 데이터 소스는 XML을 반환하는 레거시 서비스뿐입니다. 클라이언트는 깔끔한 IUserService 인터페이스를 사용해야 하고 LegacyUserService는 XML을 사용합니다. 어댑터가 이 둘을 연결합니다.
불호환 어댑티
// Adaptee: The old service with an incompatible interface
class LegacyUserService {
fetchUsersXML(): string {
return `
<users>
<user id="1">
<name>Alice</name>
<email>alice@example.com</email>
</user>
<user id="2">
<name>Bob</name>
<email>bob@example.com</email>
</user>
</users>
`;
}
}
타겟 인터페이스와 어댑터
interface IUser {
id: number;
name: string;
email: string;
}
interface IUserService {
getUsers(): Promise<IUser[]>;
}
class UserServiceAdapter implements IUserService {
private adaptee: LegacyUserService;
constructor(legacyService: LegacyUserService) {
this.adaptee = legacyService;
}
async getUsers(): Promise<IUser[]> {
const xmlData = this.adaptee.fetchUsersXML();
// Use a robust XML parser in production (e.g., xml2js).
console.log("Translating XML to JSON...");
return [
{ id: 1, name: "Alice", email: "alice@example.com" },
{ id: 2, name: "Bob", email: "bob@example.com" },
];
}
}
이 어댑터를 사용하면 클라이언트 코드는 IUserService와만 대화하고 XML에 대해 알 필요가 없습니다.
예제 2: 서드파티 결제 게이트웨이 표준화
시나리오: 애플리케이션은 표준 IPaymentProcessor 인터페이스를 사용하지만 PayWizard 게이트웨이는 startTransaction과 verifyPaymentStatus 같은 메서드를 가집니다. 어댑터는 표준 호출을 PayWizard의 API로 매핑합니다.
불일치 어댑티
class PayWizard {
startTransaction(amount: number, cardDetails: string): string {
console.log(`PayWizard: Initiating transaction for $${amount}.`);
const transactionId = "pw_" + Math.random().toString(36).substr(2, 9);
return transactionId;
}
verifyPaymentStatus(transactionId: string): boolean {
console.log(`PayWizard: Verifying status for ${transactionId}.`);
return true;
}
}
타겟 인터페이스와 어댑터
interface IPaymentProcessor {
processPayment(amount: number, cardInfo: string): Promise<string>;
checkStatus(id: string): Promise<boolean>;
}
class PayWizardAdapter implements IPaymentProcessor {
private payWizard: PayWizard;
constructor() {
this.payWizard = new PayWizard();
}
async processPayment(amount: number, cardInfo: string): Promise<string> {
console.log("Adapter: Translating 'processPayment' to 'startTransaction'.");
return this.payWizard.startTransaction(amount, cardInfo);
}
async checkStatus(id: string): Promise<boolean> {
console.log("Adapter: Translating 'checkStatus' to 'verifyPaymentStatus'.");
return this.payWizard.verifyPaymentStatus(id);
}
}
어댑터를 사용하면 다양한 제공자에 걸쳐 애플리케이션 코드를 깔끔하고 일관되게 유지할 수 있습니다.
어댑터로 레거시 코드 리팩터링하기

모든 프로젝트는 결국 레거시 코드에 직면합니다. 어댑터 패턴을 사용하면 오래된 시스템을 현대적인 타겟 인터페이스로 래핑하고 클라이언트 코드를 점진적으로 마이그레이션함으로써 위험한 대대적 재작업을 피할 수 있습니다. 이 접근법은 리스크를 줄이고 통제된 현대화 플랜을 지원합니다.2
단계별 마이그레이션 계획
- 이상적인 타겟 인터페이스를 정의하세요.
- 해당 인터페이스를 구현하고 레거시 어댑티를 수용하는 어댑터 클래스를 만드세요.
- 어댑터 내부에 변환 로직을 구현하세요.
- 클라이언트 코드를 점진적으로 어댑터를 사용하도록 마이그레이션하세요.
AI로 리팩터링 가속화하기
현대의 AI 도구는 보일러플레이트를 빠르게 생성해 중요한 매핑 로직에 집중할 수 있게 도와주지만, 아키텍처 결정은 팀의 책임으로 남아 있습니다. 도구를 사용해 어댑터를 스캐폴딩하고, 그 다음 매핑을 검증하는 테스트들을 작성하세요.
어댑터 사용은 단순한 임시 방편이 아니라 점진적 현대화를 가능하게 하는 아키텍처 건강에 대한 전략적 투자입니다.2
어댑터와 다른 패턴 사이의 선택
올바른 패턴을 선택하는 것이 중요합니다. 어댑터(Adapter), 데코레이터(Decorator), 프록시(Proxy), 파사드(Façade)는 비슷해 보일 수 있지만 의도가 다릅니다. 인터페이스를 변경하려면 어댑터를 사용하세요; 동작을 추가하려면 데코레이터를 사용하세요; 접근을 제어하려면 프록시를 사용하세요; 복잡한 서브시스템을 단순화하려면 파사드를 사용하세요.
어댑터 vs 데코레이터
어댑터는 인터페이스를 변환합니다. 데코레이터는 원래 인터페이스를 유지하면서 책임을 추가합니다.
어댑터 vs 프록시
프록시는 같은 인터페이스를 유지하며 접근을 제어하거나 지연 초기화, 캐싱, 로깅을 추가합니다. 어댑터는 클라이언트가 사용할 수 있도록 인터페이스를 변경합니다.
어댑터 vs 파사드
파사드는 단일 인터페이스 뒤에 서브시스템을 단순화합니다. 어댑터는 두 컴포넌트가 상호 운용 가능하도록 한 객체의 인터페이스를 변환하는 데 초점을 맞춥니다.
| Pattern | Primary Intent | When to Use |
|---|---|---|
| Adapter | Convert one interface to another | When an existing class must work with an incompatible client. |
| Decorator | Add responsibilities | When you want to extend behavior dynamically. |
| Proxy | Control access | When you need lazy loading, access control, or logging. |
| Façade | Simplify a subsystem | When you want a single entry point to complex behavior. |
어댑터 패턴을 팀에 도입하기
과용을 피하려면 어댑터 생성에 대한 명확한 가드레일을 설정하세요:
- 문서화: 각 어댑터는 어댑티, 타겟 인터페이스, 매핑을 명시한 README가 필요합니다.
- 테스트: 변환 로직을 검증하는 단위 테스트를 요구하세요.
- 성능 모니터링: 핫 패스에 있는 주요 어댑터는 벤치마크를 수행하세요.
CI에 이러한 규칙을 강제하는 자동화 검사를 추가하고 코드베이스 전반에 어댑터 일관성을 유지하세요. 내부 문서 사이트에 예제와 템플릿을 제공하거나 modernizing legacy systems 같은 가이드에 링크하세요.
질문 있나요? 어댑터에 대해 이야기해봅시다
Q&A
Q: 언제 어댑터가 전체 재작성보다 나은가요?
A: 기존 컴포넌트가 제대로 작동하지만 인터페이스가 요구사항과 맞지 않을 때 어댑터를 사용하세요—특히 안정적인 레거시 시스템이나 여러분이 제어할 수 없는 서드파티 API의 경우 그렇습니다. 컴포넌트가 버그가 많거나 필수 기능이 부족하다면 재작성도 고려해야 합니다.
Q: 어댑터가 눈에 띄는 성능 오버헤드를 추가하나요?
A: 어댑터는 약간의 오버헤드(추가 메서드 호출이나 변환 단계)를 추가하지만 대부분의 비즈니스 애플리케이션에서는 네트워크 또는 I/O 비용에 비해 무시할 수 있습니다. 지연에 민감한 시스템의 경우 어댑터를 벤치마크하세요.
Q: 우리 팀은 어댑터를 어떻게 테스트해야 하나요?
A: 타겟과 어댑티 간의 매핑에 초점을 맞춘 단위 테스트를 작성하세요. 적절한 경우 어댑티를 Mock 처리하고 실제 의존성과 함께 어댑터가 올바르게 동작하는지 확인하는 통합 테스트도 포함하세요.
AI가 코드를 작성합니다.당신이 그것을 지속시킵니다.
AI 가속 시대에 클린 코드는 단순히 좋은 관행이 아닙니다 — 확장되는 시스템과 자체 무게로 붕괴되는 코드베이스의 차이입니다.