어댑터 패턴

정완훈·2025년 3월 13일

어댑터(Adapter) 패턴 요점 정리 노트

어댑터 패턴

어댑터 패턴은 서로 맞지 않는 인터페이스를 가진 객체를 클라이언트가 원하는 인터페이스에 맞춰주는 구조 디자인 패턴이다. 이름처럼 어댑터를 끼워서 호환성 문제를 해결한다. 220V 전기기기를 110V 콘센트에 쓸 때 전압 변환 어댑터를 쓰는 것과 비슷하다고 생각하면 된다.

왜 쓰냐면

이미 잘 돌아가던 클래스가 있는데, 새로운 요구사항에서 다른 인터페이스를 요구할 때 문제가 생긴다. 예를 들어, 클라이언트는 Request() 메서드를 기대하는데, 기존 Adaptee 클래스는 SpecificRequest()만 제공한다. 메서드 이름, 매개변수, 반환 타입이 달라서 바로 연결이 안 된다. 새로 클래스를 만들자니 비용과 리스크가 크다. 억지로 기존 코드에 땜질하면 복잡해지고 결합도가 높아진다. 어댑터 패턴을 쓰면 핵심 로직은 그대로 두고 인터페이스만 조정한다.

핵심

클라이언트가 원하는 인터페이스(ITarget)와 기존 인터페이스(Adaptee) 사이의 불일치를 어댑터가 중간에서 해결한다. 어댑터는 ITarget을 구현하고, 내부에서 Adaptee를 참조해서 필요한 메서드를 호출하거나 변환 로직을 처리한다. 두 가지 방식이 있다.

  • 객체 어댑터: Adaptee 인스턴스를 필드로 가지고 위임한다.
  • 클래스 어댑터: Adaptee를 상속하고 ITarget을 구현한다.
    결국 클라이언트는 ITarget만 보면 된다.

간단 예시

ITargetRequest()가 있고, AdapteeSpecificRequest()가 있다. AdapterITarget을 구현하고, 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

실전 예시: 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;
}

IDuckQuack()Fly()가 있고, ITurkeyGobble()FlyShortDistance()가 있다. TurkeyAdapterIDuck을 구현하고, ITurkey를 참조한다. Quack()Gobble()로, Fly()FlyShortDistance() 여러 번으로 변환한다. 클라이언트는 오리처럼 칠면조를 쓴다.

게임 예시:오디오 시스템

게임에서 자주 볼 법한 예시다. 새 오디오 시스템 인터페이스 IAudioPlayerPlaySound(), StopSound(), SetVolume()을 제공한다. 근데 옛날 시스템 LegacySoundSystemStartClip(), 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;
}

새 인터페이스 IAudioPlayerPlaySound(), StopSound(), SetVolume()이 있다. 레거시 LegacySoundSystemStartClip(), StopAllClips(), AdjustMasterVolume()이 있다. LegacySoundAdapterIAudioPlayer를 구현하고, LegacySoundSystem을 참조해서 메서드를 변환한다. 게임은 IAudioPlayer만 쓴다.

장점

  • 기존 코드를 재사용한다.
  • 구조가 유연해진다.
  • 단일 책임 원칙을 지키기 쉬워진다.

단점

  • 클래스 수가 늘어난다.
  • 복잡도가 올라간다.
  • 성능 오버헤드가 생길 수 있다.

주의할 점

  • 변환 로직을 어댑터에 너무 많이 넣지 않는다.
  • 코드 중복을 피한다.
  • 테스트 꼼꼼히 한다.
  • 성능을 고려한다.

요약

어댑터 패턴은 호환 안 되는 인터페이스를 중간에서 변환해서 클라이언트가 원하는 대로 쓸 수 있게 한다. 어댑터가 새 인터페이스를 구현하고 기존 코드를 참조해서 위임한다. 기존 코드를 안 고치고 재사용할 수 있다.

  • Adaptee: 기존에 이미 존재하던 클래스
  • Adapter: 새롭게 만드는 클래스, 클라이언트가 원하는 인터페이스(ITarget)를 구현하고, 내부적으로 Adaptee를 사용, 두 존재 사이에서 중간 다리 역할

0개의 댓글