브릿지 패턴(Bridge Pattern)

  • 브릿지 패턴은 객체의 확장성을 향상하기 위한 패턴입니다.
  • 객체의 동작을 처리하는 구현부와 확장을 위한 추상부를 분리합니다.
  • 브릿지 패턴은 유지보수를 간결하게 하기 위한 유용한 패턴입니다.
  • 실제 개발 상황에서 자주 발생하는 불행한 일을 조금이나마 방지하고, 해결할 수 있습니다.

일반적인 문제 상황

  • 매우 간단한 상황을 상상해보겠습니다.
  • 고객의 첫 요구사항에 의해 인사를 건네는 클래스를 작성하였습니다.
1
2
3
4
5
6
7
8
class Hello
{
public:
    std::string greeting()
    {
        return "Hello";
    }
};
  • 작성한 클래스를 다음과 같이 잘 사용하고 있었습니다.
1
2
3
4
5
6
7
int main(const int argc, const char* argv[])
{
    std::shared_ptr<Hello> hello = std::make_shared<Hello>();
    std::cout << hello->greeting() << std::endl;

    return 0;
}
  • 며칠 뒤 고객의 추가 요구사항이 접수되었습니다.
  • 인사를 건네는 대상의 언어를 파악하고, 적절한 인사를 하도록 수정 요구사항입니다.
  • 다음과 같이 클래스의 메서드 자체를 변경하면 된다고 생각하기 쉽습니다.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
enum class Language
{
    Korean,
    English
};

class Hello
{
public:
    std::string greeting(Language lang)
    {
        if (lang == Language::Korean)
        {
            return "Hello Korean";
        }
        else
        {
            return "Hello";
        }
    }
};
  • 클래스의 메서드가 변경되었으니, 이를 사용하는 코드도 변경이 필요합니다.
1
2
3
4
5
6
7
int main(const int argc, const char* argv[])
{
    std::shared_ptr<Hello> hello = std::make_shared<Hello>();
    std::cout << hello->greeting(Language::Korean) << std::endl;

    return 0;
}
  • 고객의 요구사항을 만족하고, 잘 동작하는 코드이지만 문제가 있습니다.
  • 만일 이런 요구사항이 빈번하게 접수되는 상황이라면 매번 메서드가 변경되고 클래스의 역할이 커지는 상황이 발생합니다.
  • 이런 과정이 반복되면 사이드 이펙트 가능성이 커지고 유지 보수가 어려운 코드로 변질됩니다.
  • 끔찍한 경험을 하기 전 브릿지 패턴을 적용하여 유지 보수가 간단한 코드로 설계합니다.

브릿지 패턴의 구조

  • 위와 같은 문제 상황에서 Hello 클래스를 상속하는 별도의 클래스를 작성하여 문제를 해결할 수 있습니다.
  • 하지만 클래스를 상속한다는 것은 부모 클래스의 모든 메서드를 자식 클래스가 상속받는 것이며 이는 강력한 결합 관계로 변질됩니다.
  • 이러한 상속의 문제를 해결하고자 브릿지 패턴을 복합 구조로 작성합니다.
  • 브릿지 패턴은 4개의 구성 요소를 갖고 있습니다.
    • Abstract
      • 기능 계층의 최상위 클래스입니다.
    • refinedAbstract
      • Abstract 클래스를 상속받으며 기능 계층에서 새로운 부분을 확장한 클래스입니다.
    • Implementor
      • Abstract의 기능을 구현하기 위한 인터페이스 클래스입니다.
    • concreteImplementor
      • Implementor에서 정의한 기능을 실제로 구현합니다.

잘못된 브릿지 패턴의 예제

  • 위 문제를 브릿지 패턴을 적용하여 재설계를 합니다.
  • 브릿지 패턴을 처음 작성할 때 다음과 같은 실수가 자주 발생합니다.
  • 먼저 Hello 클래스를 인터페이스화 하고 Korean 클래스와 English 클래스로 구현부를 분리합니다.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// Implementor
class Hello
{
public:
    virtual std::string greeting() = 0;
};

// concreteImplementor
class Korean :
    public Hello
{
public:
    std::string greeting() override
    {
        return "Hello Korean";
    }
};

// concreteImplementor
class English :
    public Hello
{
public:
    std::string greeting() override
    {
        return "Hello English";
    }
};
  • 브릿지 패턴은 복합 구조이므로 Language 클래스를 작성합니다.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// Abstract & refinedAbstract
class Language
{
public:
    void setEnglish(std::shared_ptr<English> english)
    {
        m_english = english;
    }

    void setKorean(std::shared_ptr<Korean> korean)
    {
        m_korean = korean;
    }

    std::shared_ptr<English> m_english;
    std::shared_ptr<Korean> m_korean;
};
  • 이와 같은 코드는 기능의 구현부가 하나의 계층으로 구성됩니다.
  • 브릿지 패턴은 복합 객체를 다시 재정의하여 추상 계층화된 구조로 작성해야 합니다.
  • 추상 계층화된 구조가 무엇인지 잘 이해하기 힘드니 코드를 확인해보겠습니다.

브릿지 패턴: 구현

  • Language 클래스를 추상화하여 재설계합니다.
1
2
3
4
5
6
7
8
9
// Abstract
class Language
{
public:
    virtual std::string execute() = 0;

protected:
    std::shared_ptr<Hello> m_Impl;
};
  • Language 클래스를 인터페이스화 하여 선언과 구현부를 분리합니다.
  • Language 클래스를 상속받는 Message 클래스를 작성하고 실제 구현을 진행합니다.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// refinedAbstract
class Message :
    public Language
{
public:
    Message(std::shared_ptr<Hello> language)
    {
        m_Impl = language;
    }

    std::string execute() override
    {
        return m_Impl->greeting();
    }
};
  • Language 클래스와 Message 클래스가 서로 분리되었습니다.
  • Language 클래스의 인터페이스 정보를 제공하고 Message 클래스는 라이브러리 파일로 제공하면 실제로 Message 클래스가 어떤 작업을 수행하는지 정보를 은닉합니다.
  • 또한 프로그램이 동작하는 환경에 따라 실제 구현은 Message 클래스에서 해당 부분을 처리하기 때문에 인터페이스 클래스는 변경될 일이 없습니다.

브릿지 패턴: 실행 코드

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
int main(const int argc, const char* argv[])
{
    std::shared_ptr<Hello> korean = std::make_shared<Korean>();
    std::shared_ptr<Hello> english = std::make_shared<English>();

    std::shared_ptr<Message> message = std::make_shared<Message>(korean);
    std::cout << "Result: " << message->execute() << std::endl;

    korean.reset();
    english.reset();
    message.reset();

    return 0;
}

브릿지 패턴과 어댑터 패턴의 차이

  • 브릿지 패턴은 종종 어댑터 패턴과 헷갈리기 쉽습니다.
  • 브릿지 패턴과 어댑터 패턴은 모두 인터페이스의 실제 구현을 감추는 구조에서는 동일합니다.
  • 하지만 다음과 같은 점이 서로 다릅니다.
    • 어댑터 패턴은 완성된 코드에서 인터페이스가 변경될 때 이를 연결하는 패턴입니다.
    • 브릿지 패턴은 프로그램의 초기 설계 단계에서 추상화 및 구현 단계를 분리하여 확장성을 확보하는 패턴입니다.
  • 또한 브릿지 패턴은 프로그램의 실행 시점에서 기능을 선택할 수 있는 장점이 있습니다. 이는 운영체제의 환경에 따라 실행 코드를 분기할 경우 매우 유용합니다.
1
2
3
4
5
6
7
#ifdef _LINUX_
    std::shared_ptr<Application> application = std::make_shared<LinuxApplication>();
#elif _WINDOWS_
    std::shared_ptr<Application> application = std::make_shared<WindowsApplication>();
#endif

    std::shared_ptr<ApplicationManager> manager = std::make_shared<ApplicationManager>(application);