서비스 중개자 패턴은 서비스를 구현한 구체 클래스는 숨긴 채로 어디에서나 서비스에 접근할 수 있게 한다.

싱글턴 패턴(6장) 과 비슷하기 때문에 둘 중 어느 쪽이 더 필요에 맞는지 판단하여 적용해야 한다.

 

소리를 출력하려 하는 경우

// 정적 클래스를 사용할 수도 있고

AudioSystem::playSound(VERY_LOUD_BANG);

 

// 싱글턴을 쓸 수도 있다.

AudioSystem::instance()->playSound(VERY_LOUD_BANG);

 

유니티의 GetComponent<Transform>() 구문도 Transform 컴포넌트(서비스)를 반환하는 서비스 중개자 패턴으로 볼 수 있다.

 

 

16.3 패턴

서비스는 여러 기능을 추상 인터페이스로 정의한다.

구체 서비스 제공자는 이런 서비스 인터페이스를 상속받아 구현한다.

서비스 중개자는 서비스 제공자의 실제 자료형과 이를 등록하는 과정은 숨긴채 적절한 서비스 제공자를 찾아 서비스에 대한 접근을 제공한다.

 

16.4 언제 쓸 것인가?

싱글턴과 유사한데 서비스를 구현한 구체 클래스 자료형이 무엇인지(AudioSystem), 어디에 있는지(클래스 인스턴스를 어떻게 얻을지)를 몰라도 되게 해준다.

16.5 주의사항

서비스가 실제로 등록되어 있어야 한다.

싱글턴이나 정적 클래스에서는 인스턴스가 항상 준비되어 있지만 서비스 중개자 패턴에서는 서비스 객체를 등록해야 하기 때문에 필요한 객체가 없을 때를 대비해야 한다.

서비스는 누가 자기를 가져다가 놓는지 모른다.

서비스 중개자는 전역에서 접근 가능하기 때문에 특정 상황에서만 실행되어야 하는 클래스가 있다면 정해진 곳에서만 실행되는 것을 보장할 수 없기 때문에 서비스로는 적합하지 않다.

 

16.6 예제 코드

// 서비스 (추상 인터페이스로 정의)
class Audio {
public:
    virtual ~Audio() {}
    virtual void playSound(int soundID) = 0;
    virtual void stopSound(int soundID) = 0;
    virtual void stopAllSounds() = 0;
};

// 서비스 제공자
class ConsoleAudio : public Audio {
public:
    virtual void playSound(int soundID) {
    	// 콘솔의 오디오 API를 이용해 사운드를 출력한다.
    }
    virtual void stopSound(int soundID) {
    	// 콘솔의 오디오 API를 이용해 사운드를 중지한다.
    }
    virtual void stopAllSounds() {
    	// 콘솔의 오디오 API를 이용해 모든 사운드를 중지한다.
    }
};

// 서비스 중개자
class Locator {
public:
    static Audio* getAudio() { return service_; }
    static void provide(Audio* service) { service_ = service; }
    
private:
    static Audio* services_;
};
// 외부 코드에서 서비스 제공자를 중개자에 등록
ConsoleAudio *audio = new ConsoleAudio();
Locator::provide(audio);

// Audio 서비스 인스턴스 사용 예시
Audio *audio = Locator::getAudio();
audio->playSound(VERY_LOUD_BANG); 
// playSound를 호출하는 쪽에서는 Audio라는 추상 인터페이스만 알 뿐 
// ConsoleAudio라는 구체 클래스에 대해서는 전혀 모른다.

NULL 서비스

서비스 제공자가 서비스를 등록하기 전에 서비스를 사용하려고 시도하면 NULL을 반환한다.

서비스가 NULL이면 같은 인터페이스(Audio)를 구현한 특수 객체 NullAudio를 대신 반환하도록 한다.

의도적으로 특정 서비스의 기능을 막고 싶을 때에도 유용하다.

 

class NullAudio : public Audio {
public:
    virtual void playSound(int soundID) { }
    virtual void stopSound(int soundID) { }
    virtual void stopAllSounds() { }
};

class Locator {
public:
    static void initialize() {
        service_ = &nullService_;
    }
    static Audio& getAudio() { return *service_; }
    static void provide(Audio* service) {
        if (service == NULL) {
            // 널 서비스로 돌려놓는다.
            service_ = &nullService_;
        } else {
            service_ = service;
        }
    }
private:
    static Audio* service_;
    static NullAudio nullService_;
};

getAudio()가 서비스를 포인터가 아닌 레퍼런스로 반환하게 바꾼 것은 NULL이 될수 없기 때문에 항상 객체를 받을 수 있다고 개대해도 된다는 의미이다.

분기문을 getAudio()가 아닌 provide()에 둠으로써 매번 서비스에 접근할 때마다 분기문에 접근하지 않아도 되지만 Locator가 기본값을 null 객체로 초기화할 수 있도록 initalize() 함수를 먼저 호출해야 한다.

 

로그 데커레이터

데커레이션으로 감싼 서비스

class LoggedAudio : public Audio {
public:
    LoggedAudio(Audio &wrapped) : wraped_(wrapped) {}
    virtual void playSound(int soundID) {
        log("사운드 출력");
        wrapped_.playSound(soundID);
    }
    virtual void stopSound(int soundID) {
        log("사운드 중지");
        wrapped_.stopSound(soundID);
    }
    virtual void stopAllSounds() {
        log("모든 사운드 중지");
        wrapped_.stopAllSounds();
    }
private:
    void log(const char* message) {
        //로그를 남기는 코드...
    }
    Audio &wrapped_;
};

//사용 예시
void enableAudioLogging() {
    // 기존 서비스를 데커레이트한다.
    Audio *service = new LoggedAudio(Locator::getAudio());
    // 이 값으로 바꿔치기 한다.
    Locator::provide(service);
}

데커레이터 패턴을 널 서비스에 적용하면 사운드는 비활성화해놓고도 정상적으로 사운드가 활성화되었다면 어떤 사운드 가 출력되었을지를 로그로 확인할 수 있다.

 

16.7 디자인 결정

서비스는 어떻게 등록되는가?

외부 코드에서 등록 (가장 일반적인 방법)

ㄴ 예제 코드에서 서비스를 등록하는 방식

ㄴ 서비스 제공자를 어떻게 만들지 제어할 수 있다. (로그를 찍는 오디오 서비스라던지, 소리 크기를 줄인 오디오 서비스 라던지 ...)

ㄴ 게임 실행 도중에 서비스를 교체할 수 있다.

ㄴ 서비스 중개자가 외부 코드에 의존한다는 단점이 있다. 외부 코드에서 초기화를 제대로 안해주면 문제가 생긴다.

 

컴파일할 때 바인딩

ㄴ 전처리기 매크로를 이용해 서비스를 컴파일할 때 등록한다.

ㄴ 빠르다. (모든 작업이 컴파일에 끝나기 때문에 런타임에 따로 할 일이 없다.)

ㄴ 서비스를 쉽게 변경할 수 없다.

class Locator {
public:
    static Audio& getAudio() { return service_; }
private:
#if DEBUG
    static DebugAudio service_;
#else
    static ReleaseAudio service_;
#endif
};

 

런타임에 설정 값 읽기

ㄴ 서비스 제공자 설정 파일을 로딩한 뒤에, 리플렉션(프로그램이 실행시간에 자기 자신을 조사하는 기능)으로 원하는 서비스 제공자 클래스 객체를 런타임에 생성한다.

ㄴ 다시 컴파일하지 않고도 서비스를 교체할 수 있다. 바꾼 설정 값을 적용하려면 게임을 재시작해야 하기 때문에 실행중에 서비스를 교체할 수 있는 방법보다는 덜 유연하다.

ㄴ 서비스 등록에 시간이 걸린다. (런타임에 설정 값을 사용하려면 파일을 읽어야 하기 때문에 )

ㄴ 복잡하다 (파일을 로딩해서 파싱한 뒤에 서비스를 등록하는 설정 시스템을 만들어야 하기 때문에)

 

서비스를 못 찾으면 어떻게 할 것인가?

사용자가 알아서 처리하게 한다

ㄴ 실패했을 때 어떻게 처리할지를 사용자 쪽에서 정할 수 있다.

게임을 멈춘다. (단언문 사용)

ㄴ getAudio() return문 앞에 assert(service != NULL) 구문을 넣으면 서비스를 찾지 못했을 때 게임이 중단된다.

ㄴ 사용자 측에서 서비스가 없는 경우를 처리하지 않아도 된다.

NULL 서비스를 반환한다. (규모가 큰 팀에서 좋다)

ㄴ 위의 예제에서 사용한 방법이다.

ㄴ 사용자 측에서 서비스가 없는 경우를 처리하지 않아도 된다.

ㄴ 서비스를 사용할 수 없을 때에도 게임을 계속 진행할 수 있다.

ㄴ 디버깅하기 쉽지 않다. (널 서비스라 기능이 제공이 안되는 것인지 서비스의 기능이 문제인지 파악이 힘들다)

 

서비스의 범위는 어떻게 잡을 것인가?

전역에서 접근 가능한 경우

ㄴ 전체 코드에서 같은 서비스를 쓰도록 한다.

 

접근이 특정 클래스에 제한되는 경우

ㄴ 커플링을 제어할 수 있다. 서비스를 특정 클래스를 상속받는 클래스들에게만 제한함으로써 다른 시스템으로부터는 디커플링 상태를 유지할 수 있다.

+ Recent posts