어댑터 패턴
어댑터 패턴은 서로 맞지 않는 인터페이스를 가진 객체를 클라이언트가 원하는 인터페이스에 맞춰주는 구조 디자인 패턴이다. 이름처럼 어댑터를 끼워서 호환성 문제를 해결한다. 220V 전기기기를 110V 콘센트에 쓸 때 전압 변환 어댑터를 쓰는 것과 비슷하다고 생각하면 된다.
이미 잘 돌아가던 클래스가 있는데, 새로운 요구사항에서 다른 인터페이스를 요구할 때 문제가 생긴다. 예를 들어, 클라이언트는 Request() 메서드를 기대하는데, 기존 Adaptee 클래스는 SpecificRequest()만 제공한다. 메서드 이름, 매개변수, 반환 타입이 달라서 바로 연결이 안 된다. 새로 클래스를 만들자니 비용과 리스크가 크다. 억지로 기존 코드에 땜질하면 복잡해지고 결합도가 높아진다. 어댑터 패턴을 쓰면 핵심 로직은 그대로 두고 인터페이스만 조정한다.
클라이언트가 원하는 인터페이스(ITarget)와 기존 인터페이스(Adaptee) 사이의 불일치를 어댑터가 중간에서 해결한다. 어댑터는 ITarget을 구현하고, 내부에서 Adaptee를 참조해서 필요한 메서드를 호출하거나 변환 로직을 처리한다. 두 가지 방식이 있다.
Adaptee 인스턴스를 필드로 가지고 위임한다. Adaptee를 상속하고 ITarget을 구현한다.ITarget만 보면 된다.간단 예시
ITarget에 Request()가 있고, Adaptee에 SpecificRequest()가 있다. Adapter가 ITarget을 구현하고, Adaptee를 참조해서 Request() 호출 시 SpecificRequest()를 실행한다. 클라이언트는 ITarget으로 편하게 쓴다.
#include <iostream>
#include <string>
// 클라이언트가 기대하는 인터페이스
class ITarget {
public:
virtual std::string Request() = 0;
virtual ~ITarget() {}
};
// 기존 클래스
class Adaptee {
public:
std::string SpecificRequest() {
return "Adaptee 결과";
}
};
// 어댑터
class Adapter : public ITarget {
private:
Adaptee* _adaptee;
public:
Adapter(Adaptee* adaptee) : _adaptee(adaptee) {}
std::string Request() override {
return _adaptee->SpecificRequest();
}
};
// 사용 예시
int main() {
Adaptee* adaptee = new Adaptee();
ITarget* adapter = new Adapter(adaptee);
std::cout << adapter->Request() << std::endl; // "Adaptee 결과"
delete adapter;
delete adaptee;
return 0;
}
실전 예시: Duck vs Turkey
현실적인 상황으로 가보자. 오리(IDuck)는 Quack()하고 Fly()를 할 수 있다. 칠면조(ITurkey)는 Gobble()하고 FlyShortDistance()만 된다. 클라이언트는 오리 인터페이스를 기대하는데, 칠면조를 주면 안 맞는다. TurkeyAdapter를 만들어서 IDuck을 구현하고, 칠면조를 오리처럼 보이게 한다. Quack()은 Gobble()로, Fly()는 lyShortDistance()를 여러 번 호출로 바꾼다.
#include <iostream>
// 오리 인터페이스
class IDuck {
public:
virtual void Quack() = 0;
virtual void Fly() = 0;
virtual ~IDuck() {}
};
// 칠면조 인터페이스
class ITurkey {
public:
virtual void Gobble() = 0;
virtual void FlyShortDistance() = 0;
virtual ~ITurkey() {}
};
// 오리 구현
class MallardDuck : public IDuck {
public:
void Quack() override { std::cout << "Duck: Quack!" << std::endl; }
void Fly() override { std::cout << "Duck: I'm flying far..." << std::endl; }
};
// 칠면조 구현
class WildTurkey : public ITurkey {
public:
void Gobble() override { std::cout << "Turkey: Gobble gobble!" << std::endl; }
void FlyShortDistance() override { std::cout << "Turkey: I'm flying a short distance..." << std::endl; }
};
// 어댑터: 칠면조를 오리처럼 보이게 함
class TurkeyAdapter : public IDuck {
private:
ITurkey* _turkey;
public:
TurkeyAdapter(ITurkey* turkey) : _turkey(turkey) {}
void Quack() override { _turkey->Gobble(); }
void Fly() override {
for (int i = 0; i < 5; ++i) { _turkey->FlyShortDistance(); }
}
};
// 사용 예시
int main() {
IDuck* duck = new MallardDuck();
duck->Quack(); // "Duck: Quack!"
duck->Fly(); // "Duck: I'm flying far..."
ITurkey* turkey = new WildTurkey();
IDuck* turkeyAdapter = new TurkeyAdapter(turkey);
turkeyAdapter->Quack(); // "Turkey: Gobble gobble!"
turkeyAdapter->Fly(); // "Turkey: I'm flying a short distance..." 5번 출력
delete duck;
delete turkey;
delete turkeyAdapter;
return 0;
}
IDuck은 Quack()과 Fly()가 있고, ITurkey는 Gobble()과 FlyShortDistance()가 있다. TurkeyAdapter가 IDuck을 구현하고, ITurkey를 참조한다. Quack()은 Gobble()로, Fly()는 FlyShortDistance() 여러 번으로 변환한다. 클라이언트는 오리처럼 칠면조를 쓴다.
게임에서 자주 볼 법한 예시다. 새 오디오 시스템 인터페이스 IAudioPlayer는 PlaySound(), StopSound(), SetVolume()을 제공한다. 근데 옛날 시스템 LegacySoundSystem은 StartClip(), StopAllClips(), AdjustMasterVolume()만 있다. 새 인터페이스를 쓰고 싶어도 레거시 코드가 발목을 잡는다. LegacySoundAdapter를 만들어서 새 인터페이스를 구현하고, 레거시 시스템을 호출하게 한다.
#include <iostream>
#include <string>
#include <unordered_map>
// 새로운 오디오 인터페이스
class IAudioPlayer {
public:
virtual void PlaySound(const std::string& soundId) = 0;
virtual void StopSound(const std::string& soundId) = 0;
virtual void SetVolume(float volume) = 0;
virtual ~IAudioPlayer() {}
};
// 레거시 시스템
class LegacySoundSystem {
public:
void StartClip(const std::string& clipPath) {
std::cout << "[LegacySound] Playing clip: " << clipPath << std::endl;
}
void StopAllClips() {
std::cout << "[LegacySound] Stopping all clips." << std::endl;
}
void AdjustMasterVolume(float volumeLevel) {
std::cout << "[LegacySound] Setting master volume to: " << volumeLevel << std::endl;
}
};
// 어댑터
class LegacySoundAdapter : public IAudioPlayer {
private:
LegacySoundSystem* _legacySystem;
std::unordered_map<std::string, std::string> _soundMapping;
public:
LegacySoundAdapter(LegacySoundSystem* legacySystem) : _legacySystem(legacySystem) {
_soundMapping["BGM_Title"] = "Assets/Sounds/BGM/TitleTheme.wav";
_soundMapping["SFX_Explosion"] = "Assets/Sounds/Effects/explosion.mp3";
}
void PlaySound(const std::string& soundId) override {
auto it = _soundMapping.find(soundId);
if (it != _soundMapping.end()) {
_legacySystem->StartClip(it->second);
} else {
std::cout << "[Adapter] Unknown soundId: " << soundId << std::endl;
}
}
void StopSound(const std::string& soundId) override {
_legacySystem->StopAllClips();
}
void SetVolume(float volume) override {
_legacySystem->AdjustMasterVolume(volume);
}
};
// 사용 예시
int main() {
LegacySoundSystem* legacySoundSystem = new LegacySoundSystem();
IAudioPlayer* adapter = new LegacySoundAdapter(legacySoundSystem);
adapter->PlaySound("BGM_Title"); // "[LegacySound] Playing clip: Assets/Sounds/BGM/TitleTheme.wav"
adapter->SetVolume(0.8f); // "[LegacySound] Setting master volume to: 0.8"
adapter->StopSound("BGM_Title"); // "[LegacySound] Stopping all clips."
delete adapter;
delete legacySoundSystem;
return 0;
}
새 인터페이스 IAudioPlayer는 PlaySound(), StopSound(), SetVolume()이 있다. 레거시 LegacySoundSystem은 StartClip(), StopAllClips(), AdjustMasterVolume()이 있다. LegacySoundAdapter가 IAudioPlayer를 구현하고, LegacySoundSystem을 참조해서 메서드를 변환한다. 게임은 IAudioPlayer만 쓴다.
어댑터 패턴은 호환 안 되는 인터페이스를 중간에서 변환해서 클라이언트가 원하는 대로 쓸 수 있게 한다. 어댑터가 새 인터페이스를 구현하고 기존 코드를 참조해서 위임한다. 기존 코드를 안 고치고 재사용할 수 있다.