어댑터 패턴
Coddy C++ 여정의 객체 지향 프로그래밍 섹션에 포함된 레슨 — 104개 중 97번째.
어댑터 패턴(Adapter pattern)은 호환되지 않는 인터페이스들이 함께 작동할 수 있도록 해줍니다. 이 패턴은 두 클래스 사이의 가교 역할을 하며, 한 클래스의 인터페이스를 클라이언트가 기대하는 인터페이스로 변환합니다. 이는 시스템의 인터페이스와 일치하지 않는 레거시 코드나 서드파티 라이브러리를 통합할 때 특히 유용합니다.
필요한 기능을 수행하는 기존 클래스가 있다고 가정해 봅시다. 하지만 그 인터페이스가 여러분의 코드에서 기대하는 것과 일치하지 않습니다. 어느 한 쪽을 수정하는 대신, 기존 클래스를 감싸고 호출을 변환하는 어댑터를 만듭니다:
#include <iostream>
#include <string>
// 호환되지 않는 인터페이스를 가진 기존 클래스
class LegacyPrinter {
public:
void printMessage(const std::string& msg) {
std::cout << "Legacy: " << msg << "\n";
}
};
// 클라이언트가 기대하는 타겟 인터페이스
class Printer {
public:
virtual void print(const std::string& text) = 0;
virtual ~Printer() = default;
};
// 어댑터는 LegacyPrinter를 감싸고 Printer 인터페이스를 구현합니다.
class PrinterAdapter : public Printer {
LegacyPrinter& legacy;
public:
PrinterAdapter(LegacyPrinter& lp) : legacy(lp) {}
void print(const std::string& text) override {
legacy.printMessage(text); // 호출을 변환합니다.
}
};
int main() {
LegacyPrinter oldPrinter;
PrinterAdapter adapter(oldPrinter);
// 클라이언트는 기대하는 인터페이스를 사용합니다.
adapter.print("Hello World");
}어댑터는 LegacyPrinter에 대한 참조를 유지하며 Printer 인터페이스를 구현합니다. print()가 호출되면, printMessage()로 작업을 위임합니다. 클라이언트 코드는 내부의 레거시 구현에 대해 알 필요 없이 Printer 인터페이스를 통해 작동합니다.
기존 클래스를 사용하고 싶지만 해당 인터페이스가 필요한 인터페이스와 일치하지 않을 때, 또는 호환되지 않는 인터페이스를 가진 클래스들과 협력하는 재사용 가능한 클래스를 생성해야 할 때 어댑터(Adapter)를 사용하세요.
챌린지
쉬움서로 다른 오디오 라이브러리 인터페이스를 연결하는 미디어 플레이어 어댑터(Media Player Adapter) 시스템을 구축해 보겠습니다. 레거시 오디오 라이브러리를 현대적인 미디어 플레이어 애플리케이션에 통합한다고 가정해 보세요. 기존 라이브러리는 완벽하게 작동하지만, 그 인터페이스가 플레이어가 기대하는 것과 일치하지 않습니다. 이것이 바로 어댑터 패턴이 빛을 발하는 지점입니다.
코드는 다음 네 개의 파일로 구성됩니다:
MediaPlayer.h: 현대적인 미디어 플레이어가 기대하는 타겟 인터페이스를 정의합니다.다음 순수 가상 메서드들을 포함하는 추상
MediaPlayer클래스를 생성하세요:play(const std::string& filename)— 지정된 파일을 재생합니다.stop()— 재생을 중지합니다.setVolume(int level)— 볼륨을 설정합니다 (0-100).
가상 소멸자를 포함하세요.
LegacyAudioLib.h: 호환되지 않는 인터페이스를 가진 기존 "레거시" 오디오 라이브러리를 생성합니다.LegacyAudioLib클래스는 플레이어가 기대하는 방식과 다르게 작동하는 오래된 라이브러리를 나타냅니다. 다음 메서드들을 가져야 합니다:loadAudio(const std::string& path)—Legacy: Loading [path]를 출력합니다.startPlayback()—Legacy: Playback started를 출력합니다.stopPlayback()—Legacy: Playback stopped를 출력합니다.adjustVolume(double percentage)— 소수점 값(0.0에서 1.0)을 인자로 받아Legacy: Volume set to [percentage]를 출력합니다 (소수점 값을 표시하세요).
AudioAdapter.h: 두 인터페이스를 연결하는 어댑터를 구축합니다.AudioAdapter클래스는MediaPlayer를 상속받아야 하며LegacyAudioLib객체에 대한 참조를 보유해야 합니다. 어댑터는 현대적인 인터페이스의 호출을 레거시 인터페이스로 변환합니다:play()는 레거시 라이브러리의loadAudio()와startPlayback()을 모두 호출해야 합니다.stop()은stopPlayback()으로 기능을 위임해야 합니다.setVolume()은 정수(0-100)를 받지만 레거시 라이브러리는 소수점 값(0.0-1.0)을 기대하므로, 이를 변환해야 합니다.
main.cpp: 어댑터가 작동하는 모습을 보여줍니다.두 개의 입력을 읽습니다:
- 재생할 파일 이름 (문자열)
- 볼륨 레벨 (정수, 0-100)
LegacyAudioLib인스턴스와 이를 감싸는AudioAdapter를 생성하세요. 그런 다음MediaPlayer인터페이스를 통해(즉,MediaPlayer포인터로 저장하여) 어댑터를 사용하여 다음을 수행하세요:- 볼륨을 입력받은 레벨로 설정합니다.
- 입력받은 파일 이름을 재생합니다.
- 재생을 중지합니다.
예를 들어, 입력값이 song.mp3와 75인 경우:
Legacy: Volume set to 0.75
Legacy: Loading song.mp3
Legacy: Playback started
Legacy: Playback stopped입력값이 podcast.wav와 50인 경우:
Legacy: Volume set to 0.5
Legacy: Loading podcast.wav
Legacy: Playback started
Legacy: Playback stopped클라이언트 코드가 전적으로 MediaPlayer 인터페이스만 사용하여 작동하는 방식에 주목하세요. 클라이언트는 내부적으로 레거시 라이브러리가 실제 작업을 수행하고 있다는 사실을 전혀 모릅니다. 어댑터는 정수 볼륨을 소수점으로 변환하는 것을 포함하여 모든 변환을 처리합니다. 이것이 바로 어댑터 패턴의 본질입니다. 즉, 어느 한 쪽도 수정하지 않고 호환되지 않는 인터페이스들이 함께 작동하도록 만드는 것입니다.
치트 시트
어댑터 패턴(Adapter pattern)은 두 클래스 사이의 가교 역할을 하여 호환되지 않는 인터페이스들이 함께 작동할 수 있도록 합니다. 이는 한 클래스의 인터페이스를 클라이언트가 기대하는 인터페이스로 변환합니다.
기존 클래스를 사용하고 싶지만 그 인터페이스가 필요에 맞지 않을 때, 또는 레거시 코드나 서드파티 라이브러리를 통합할 때 어댑터 패턴을 사용하세요.
구조
이 패턴은 세 가지 주요 구성 요소로 이루어집니다:
- Target Interface (타겟 인터페이스): 클라이언트가 기대하는 인터페이스
- Adaptee (어댑티): 호환되지 않는 인터페이스를 가진 기존 클래스
- Adapter (어댑터): 어댑티를 감싸고 타겟 인터페이스를 구현하여, 둘 사이의 호출을 변환함
예제
#include <iostream>
#include <string>
// 호환되지 않는 인터페이스를 가진 기존 클래스 (Adaptee)
class LegacyPrinter {
public:
void printMessage(const std::string& msg) {
std::cout << "Legacy: " << msg << "\n";
}
};
// 클라이언트가 기대하는 타겟 인터페이스
class Printer {
public:
virtual void print(const std::string& text) = 0;
virtual ~Printer() = default;
};
// 어댑터는 LegacyPrinter를 감싸고 Printer 인터페이스를 구현합니다
class PrinterAdapter : public Printer {
LegacyPrinter& legacy;
public:
PrinterAdapter(LegacyPrinter& lp) : legacy(lp) {}
void print(const std::string& text) override {
legacy.printMessage(text); // 호출을 변환합니다
}
};
int main() {
LegacyPrinter oldPrinter;
PrinterAdapter adapter(oldPrinter);
// 클라이언트는 기대하는 인터페이스를 사용합니다
adapter.print("Hello World");
}어댑터는 어댑티에 대한 참조를 유지하고 타겟 인터페이스를 구현합니다. 어댑터에서 메서드가 호출되면, 어댑터는 이를 어댑티의 메서드로 위임하여 필요에 따라 호출을 변환합니다. 클라이언트 코드는 레거시 구현에 대해 알지 못한 채 타겟 인터페이스를 통해 작동합니다.
직접 해보기
#include <iostream>
#include <string>
#include "MediaPlayer.h"
#include "LegacyAudioLib.h"
#include "AudioAdapter.h"
using namespace std;
int main() {
// 입력 읽기
string filename;
int volumeLevel;
cin >> filename;
cin >> volumeLevel;
// TODO: LegacyAudioLib 인스턴스 생성
// TODO: 레거시 라이브러리를 래핑하는 AudioAdapter 생성
// TODO: 어댑터를 MediaPlayer 포인터로 저장
// TODO: MediaPlayer 인터페이스를 사용하여 다음을 수행:
// 1. 볼륨을 입력된 수준으로 설정
// 2. 입력된 파일 이름 재생
// 3. 재생 중지
return 0;
}
이 레슨에는 짧은 퀴즈가 포함되어 있습니다. 레슨을 시작해 문제를 풀고 진행 상황을 기록하세요.