Programming/C++

C++ 브릿지 패턴

_SYPark 2021. 5. 15. 18:04
728x90

우리가 클래스를 설계할 때 외부 인터페이스와 내부 구현으로 나누어 구현을 해주게 됩니다. 그리고 사용자가 사용할 때는 인터페이스를 통해 접근해 사용합니다. 이러면 실제 클래스의 구현이 바뀌어도 사용자는 인터페이스 그대로 사용하면 되기 때문에 변경의 국지화가 됩니다. 이런 장점은 하나의 클래스가 여러 환경, 플랫폼에 따라 각기 구현해야 한다고 할 때 인터페이스는 하나여도 구현은 다르게 해줘야 합니다. 

 

어떤 제품이 윈도우에도 돌아가고 리눅스에도 돌아가야 한다고 할 때 하나의 인터페이스로 사용한다 하면 아래와 같이 구현해야 합니다. 만약 클래스를 각각 운영체제에 따라 구현한다고 하면 사용하려는 운영체제에 따라 프로그램을 각기 사용해야 합니다.

class Product {
public:
	void Run();
};

void Product::Run() {
#ifdef __WIN32__
	cout << "Windows Run";
#else
    cout << "Linux Run";
#endif
}

위와 같이 #ifdef 을 이용한다면 가독성도 떨어질 뿐 아니라 수정도 어려워집니다. 

 

다른 방법으로 Product를 추상 클래스로 상속받는 각 운영체제마다 클래스를 만들어 줍니다.

위 방법을 사용하면 Product를 상속받아 사용하기 때문에 수정도 더 용이할 뿐 아니라 인터페이스도 가져갈 수 있습니다. 하지만 Product를 상속받는 또 다른 제품이 나온다 하면 각 하위 Product에서 또 상속받는 각 운영체제 클래스를 만들어줘야 합니다. 

 

플랫폼 별로 하위 클래스를 정의하는 방식은 논리적 관점과 구현 플랫폼 두 가지 상속을 복합적으로 사용하고 있기 때문에 상속 관계가 복잡해집니다. 이런 문제를 해결하기 위해 논리적 관점에 따른 상속 구조와 플랫폼에 따른 상속 구조를 별개로 아래와 같이 정의합니다.

위처럼 플랫폼 별 상속 구조와 구현을 위한 상속 구조를 독립적으로 정의하여 참조하는 방식으로 설계된 패턴을 브릿지 패턴이라고 합니다.

사용자들은 기본적으로 Product를 상속받는 각 모델에서 인터페이스를 사용하지만 실질적인 구현은 모두 Imp 클래스에 정의되어 있습니다. 새로운 플랫폼, 환경이 추가되면 Imp을 상속받는 새로운 클래스를 추가로 정의하면 되고 새로운 모델이 나오게 되면 Product를 상속받는 새로운 클래스를 추가 정의하면 됩니다.

class ProductImp {
public:
    virtual void RunModel1() = 0;
    virtual void RunModel2() = 0;    
};

class WindowsProductImp : public ProductImp {
public:
    void RunModel1() { "Windows Product Model1 Run"; }
    void RunModel2() { "Windows Product Model2 Run"; }
};

class LinuxProductImp : public ProductImp {
public:
    void RunModel1() { "Linux Product Model1 Run"; }
    void RunModel2() { "Linux Product Model2 Run"; }
};

class Product {
public:
    virtual void Run() = 0;
protected:
    ProductImp* getImp() {
        if ( pImp == 0 ) {
#ifdef __WIN32__
            pImp = new WindowsProductImp;
#else
            pImp = new LinuxProductImp;
#endif
        }
        return pImp;
    }
private:
    ProductImp* pImp;
};

class Model1 : public Product {	void Run() { getImp()->RunModel1(); } };
class Model2 : public Product {	void Run() { getImp()->RunModel2(); } };

int main()
{
    Model1 model;
    model.Run();
}
    

위처럼 각 모델은 똑같이 Product 클래스의 Run을 통해 접근하지만 플랫폼 별 구현은 Imp클래스로 나누고 Imp클래스에서 각 Model에 사용되는 Run을 정의합니다. 브릿지 패턴 안에서도 플랫폼 별 Imp 객체를 생성하기 위해서는 #ifdef을 사용하고 있습니다. 

 

여기서 Product 클래스를 인터페이스 클래스라고 불리고 실제 구현을 담당하는 Imp 클래스를 구현 클래스라고 합니다. 

브릿지 패턴을 사용할 때 어떤 구현 클래스를 사용할 것인가를 결정하는 방법과 시기도 중요합니다. 인터페이스 클래스의 생성자나 특정 멤버 함수 내에서 구현 클래스를 생성하여 사용하는 방법과 인터페이스 클래스에서 기준을 정해 그 기준을 체크하여 다른 구현 클래스를 생성하여 사용하는 방법이 있습니다. 위 두 가지 방법을 섞어 다른 객체에 생성을 맡기는 방법도 있습니다. 

 

구현 클래스의 객체가 여러 인터페이스 객체에 공유될 경우 구현 객체의 소멸 시점을 어떻게 판단할지도 중요합니다. 적절한 방법 중 하나로 더 이상 자신을 참조하는 인터페이스 객체가 없을 때 소멸시켜주는 방법이 있는데 Reference Counting 기법을 통해 구현 클래스 내부에 자신을 참조하는 인터페이스 객체 수를 Counting 할 수 있는 변수를 두어 그 수를 통해 조절하고 0이 되면 자기 자신을 소멸하게 만드는 방법이 있습니다. 

 

정리해보면 브릿지 패턴은 인터페이스와 구현에 대한 방식을 구분하여 사용합니다. 서로 구분이 되어 있기 때문에 독립적으로 확장이 가능합니다. 제품이 여러 가지가 있고 그 제품들이 여러 플랫폼에서 지원한다고 할 때 동일한 인터페이스를 통해 접근할 수 있습니다. 

 

728x90