어댑터 adapter 패턴 - 자바스크립트 예제
요약
어댑터 adapter 패턴을 자바스크립트 코드와 함께 알아봅니다. 정보처리기사 대비 문제가 포함되어있습니다.
어댑터 패턴 요약
패턴 종류 | 핵심 키워드 |
---|---|
어댑터 (Adapter) | 서로 다른 인터페이스 연결 |

어댑터 (Adapter) 패턴
어댑터 패턴은 이름 그대로, 호환되지 않는 인터페이스를 가진 클래스들을 함께 동작하도록 변환해주는 디자인 패턴입니다. 현실 세계의 '돼지코' 어댑터(110V를 220V로)를 생각하면 이해하기 쉽습니다. 기존 코드를 수정하지 않고도 새로운 클래스나 외부 라이브러리를 시스템에 통합하고 싶을 때 매우 유용합니다.
기본 구조
어댑터 패턴은 세 가지 주요 요소로 구성됩니다.
- Target: 클라이언트가 사용하려는 목표 인터페이스입니다.
- Adaptee: 호환되지 않는 인터페이스를 가진 기존 클래스입니다. (우리가 사용하고 싶은 기능)
- Adapter:
Adaptee
의 인터페이스를Target
인터페이스에 맞게 변환해주는 역할을 합니다.
예시: 웹 스토리지 API 호환시키기
웹 브라우저에는 localStorage
와 sessionStorage
라는 두 가지 데이터 저장소가 있습니다. 두 API는 setItem(key, value)
, getItem(key)
등 유사한 인터페이스를 가지고 있지만, 만약 sessionStorage
에만 데이터를 JSON 형태로 자동 변환하여 저장하는 특별한 기능이 필요하다고 가정해봅시다.
우리의 애플리케이션은 Storage
라는 표준 인터페이스(Target)를 통해 데이터 저장을 처리하도록 설계되었습니다.
// Target 인터페이스
class Storage {
setData(key, data) {
throw new Error("setData()는 반드시 구현되어야 합니다.");
}
getData(key) {
throw new Error("getData()는 반드시 구현되어야 합니다.");
}
}
기본적으로 localStorage
는 이 Storage
인터페이스를 그대로 사용할 수 있습니다.
// Concrete Target
class LocalStorageManager extends Storage {
setData(key, data) {
localStorage.setItem(key, data);
}
getData(key) {
return localStorage.getItem(key);
}
}
이제 sessionStorage
를 사용하고 싶은데, 이 클래스는 데이터를 저장할 때 자동으로 JSON.stringify
를 수행하는 saveObject
와, 가져올 때 JSON.parse
를 수행하는 loadObject
라는 다른 메서드를 가지고 있다고 가정해봅시다. 이것이 우리의 Adaptee
입니다.
// Adaptee (호환되지 않는 인터페이스를 가진 클래스)
class SessionStorageHandler {
saveObject(key, obj) {
sessionStorage.setItem(key, JSON.stringify(obj));
}
loadObject(key) {
const item = sessionStorage.getItem(key);
return item ? JSON.parse(item) : null;
}
}
SessionStorageHandler
를 우리 시스템의 Storage
인터페이스에 맞추기 위해 어댑터를 만듭니다.
// Adapter
class SessionStorageAdapter extends Storage {
constructor() {
super();
this.handler = new SessionStorageHandler();
}
// Target의 setData()를 Adaptee의 saveObject()로 연결
setData(key, data) {
// Adaptee는 객체를 받으므로, 문자열 데이터를 객체로 감싸줍니다.
this.handler.saveObject(key, { value: data });
}
// Target의 getData()를 Adaptee의 loadObject()로 연결
getData(key) {
const obj = this.handler.loadObject(key);
return obj ? obj.value : null;
}
}
// 클라이언트 코드
function saveUserData(storage, userData) {
storage.setData("user", userData);
console.log("데이터가 저장되었습니다. ✅");
}
// LocalStorage 사용 (기존 방식)
const localManager = new LocalStorageManager();
saveUserData(localManager, "John Doe");
console.log("LocalStorage에서 읽음:", localStorage.getItem("user")); // "John Doe"
// SessionStorage 사용 (어댑터 활용)
const sessionAdapter = new SessionStorageAdapter();
saveUserData(sessionAdapter, "Jane Doe");
console.log("SessionStorage에서 읽음:", sessionStorage.getItem("user")); // {"value":"Jane Doe"}
// 클라이언트는 getData를 호출할 뿐이지만, 어댑터가 내부적으로 JSON 파싱을 처리합니다.
console.log("어댑터를 통해 읽음:", sessionAdapter.getData("user")); // "Jane Doe"
클라이언트 코드(saveUserData
)는 Storage
라는 동일한 인터페이스를 사용하므로, 데이터가 localStorage
에 저장되는지 sessionStorage
에 JSON 형태로 저장되는지 알 필요가 없습니다. 이처럼 어댑터 패턴을 사용하면 서로 다른 인터페이스를 가진 객체들을 손쉽게 통합하여 사용할 수 있습니다.
어댑터 패턴 중요 키워드
- 호환되지 않는 인터페이스를 연결한다.
- 기존 코드를 수정하지 않고 재사용한다.
- 'Wrapper' 클래스라고도 불린다.
- 상속을 이용하는 클래스 패턴과 위임을 이용하는 인스턴스 패턴이 존재한다.
클래스 어댑터와 인스턴스 어댑터
어댑터 패턴은 구현 방식에 따라 두 종류로 나뉩니다.
- 클래스 어댑터 (Class Adapter): 상속을 사용합니다. 어댑터가
Target
클래스를 상속하고Adaptee
의 기능을 구현합니다. 앞서 본SessionStorageAdapter extends Storage
예시가 여기에 해당합니다. - 인스턴스 어댑터 (Instance Adapter): 위임(Delegation) 을 사용합니다. 어댑터가
Adaptee
의 인스턴스를 내부에 품고,Target
의 요청이 들어오면Adaptee
에게 작업을 위임합니다. 이 방식이 더 유연하여 일반적으로 더 많이 사용됩니다.
인스턴스 어댑터 예시: 새로운 로깅 라이브러리 통합
우리 시스템은 간단히 메시지만 받는 log(message)
인터페이스를 사용하는데, 새로 도입할 라이브러리는 타임스탬프까지 함께 받는 logMessage(timestamp, message)
인터페이스를 사용한다고 가정해봅시다.
// Adaptee: 우리가 통합하려는 새로운 로거.
class NewLogger {
logMessage(timestamp, message) {
console.log(`[${new Date(timestamp).toISOString()}] - ${message}`);
}
}
// Target: 우리 시스템이 기대하는 로거 인터페이스.
// 여기서는 'log(message)' 메서드를 가진 객체로 가정합니다.
// const logger = { log: (message) => { console.log(message) } };
// Instance Adapter
class LoggerAdapter {
constructor(newLogger) {
// Adaptee의 인스턴스를 내부에 품습니다. (Composition)
this.adaptee = newLogger;
}
// Target 인터페이스에 맞는 log 메서드를 구현합니다.
log(message) {
// Adaptee가 요구하는 형식에 맞춰 데이터를 가공한 뒤,
const timestamp = Date.now();
// 실제 작업은 Adaptee에게 위임합니다.⭐️
this.adaptee.logMessage(timestamp, message);
}
}
// 클라이언트 코드
function logSystemEvent(logger, message) {
logger.log(message);
}
const newLogger = new NewLogger();
const adapter = new LoggerAdapter(newLogger);
logSystemEvent(adapter, "사용자가 로그인했습니다.");
// 출력: [2025-07-02TXX:XX:XX.XXX] - 사용자가 로그인했습니다.
위 예시에서 LoggerAdapter
는 어떤 클래스도 extends
하지 않습니다. 대신 생성자에서 NewLogger
의 인스턴스를 받아 this.adaptee
에 저장(구성)합니다. 클라이언트가 adapter.log()
를 호출하면, 어댑터는 실제 로깅 작업을 adaptee
에게 '위임'합니다. 이처럼 인스턴스 어댑터는 구성을 사용하므로 더 유연한 구조를 만들 수 있습니다.
정처기 기출 문제
기출 | |
문제 | 서로 다른 인터페이스를 가진 클래스들을 연결해 사용 가능하게 하는 디자인 패턴을 보기에서 고르시오 |
보기 | |
답변 | |
정답 | 정답 보기 |