Outdated/Column

[Design Pattern] 브릿지 패턴(bridge pattern)

해달 2020. 4. 11. 08:00

목차

  1. 브릿지 패턴

  2. Pimpl 관용구

  3. 정리하며

  4. 참고자료



1. 브릿지 패턴

개요

브릿지 패턴(bridge pattern)은 구현부에서 추상층을 분리해 각자 독립적으로 변형과 확장이 가능하도록 하는 패턴이다. 브릿지 패턴을 적용하면 두 계층 모두 추상화된 상위 타입을 가지게 되고, 의존성은 상위 타입 간에만 이뤄지게 된다.


이를 통해 실제 의존성이 발생하더라도 서로의 구체 타입은 알 수 없도록 한다. 이렇게 되면 두 계층의 결합도가 약화되어, 양쪽 모두가 독립적으로 변경과 확장이 가능한 상태가 된다. 따라서  컴포넌트 간 다양한 조합이 가능할 때 효과적이다.


구조

구조는 다음과 같다. 실제 구현은 모두 인터페이스에 위임(delegation)하는 형태가 된다.



2. Pimpl 관례

Pointer to Implementation

C++의 프로그래밍 기술 중 Pimpl 관용구가 있다. Pimpl은 Pointer to Implementation의 줄임말로 구현부를 포인터로 참조하며, 대표적인 브릿지 패턴이라고 할 수 있다. 예시를 통해 브릿지 패턴과 Pimpl을 이해해보자.


MMORPG의 플레이어 클래스를 설계하려고 한다. 간단하게 구현하기 위해 플레이어는 이름, HP, MP만을 멤버로 가진다. 먼저 플레이어가 가져야할 기능(인터페이스)을 Player.h에 선언한다.


class Player

{

    // 포인터를 만들기 위해 클래스 전방 선언을 한다.

    class Impl;

 

    // 과거에는 포인터를 사용했으나

    // C++11 이후부터는 std::unique_ptr을 사용한다.

    std::unique_ptr<Impl> impl;

public:

    Player(const std::string& name, int64_t hp, int64_t mp);

    ~Player();

 

    std::string     GetName() const noexcept;

    int64_t         GetHP() const noexcept;

    int64_t         GetMP() const noexcept;

 

    void            TakeDamage(int64_t amount);

    void            UseSkill(int64_t amount);

};

Player.h


Player 클래스는 오직 구현체에 대한 포인터만 가지고, 메소드 선언만 되어 있는 것을 볼 수 있다. 구현체는 Player.cpp에 작성하며 아래와 같다.


// 구현체 클래스

class Player::Impl

{

    // 기존에 Player 클래스에 있어야 할 멤버 변수가 구현체에 들어 있다.

    std::string     name = "";

    int64_t     hp = 100LL;

    int64_t     mp = 100LL;

public:

    Impl(const std::string& name, int64_t hp, int64_t mp)

        : name{ name }, hp{ hp }, mp{ mp }

    {

 

    }

 

    string      GetName() const noexcept { return name; }

    int64_t     GetHP() const noexcept { return hp; }

    int64_t     GetMP() const noexcept { return mp; }

 

    void        TakeDamage(int64_t amount) { hp -= amount; }

    void        UseSkill(int64_t amount) { mp -= amount; }

};

 

// 실제 Player 인터페이스의 구현부

Player::Player(const std::string& name, int64_t hp, int64_t mp)

    : impl{ make_unique<Impl>(name, hp, mp) }

{

}

 

Player::~Player() noexcept = default;

 

std::string Player::GetName() const noexcept

{

    // 구현체의 함수를 이용하는 것을 볼 수 있다.

    return impl->GetName();

}

 

int64_t Player::GetHP() const noexcept

{

    return impl->GetHP();

}

 

int64_t Player::GetMP() const noexcept

{

    return impl->GetMP();

}

 

void Player::TakeDamage(int64_t amount)

{

    impl->TakeDamage(amount);

}

 

void Player::UseSkill(int64_t amount)

{

    impl->UseSkill(amount);

}

Player.cpp


Player 메소드의 정의부를 보면 구현체의 메소드를 그대로 이용하는 것을 볼 수 있다.


오버헤드

Pimpl은 여러 오버헤드를 가지고 있다. 포인터를 사용하기 때문에 접근공간에 대한 오버헤드가 생기며, 동적할당을 이용하기 때문에 생명주기 관리도 이전보다는 조금 더 까다롭다. 또, 보면 알겠지만 그냥 클래스를 작성할 때보다 코드 작성이 복잡하다. 다시 말해 유지보수가 쉽지 않다. 그럼 이 관용구는 왜 쓰게 된 걸까?


장점

Pimpl 관용구를 사용하면 사용자에게 불필요한 정보들을 노출하지 않아, 바이너리 호환성을 보증하기 쉬워진다. 또, 멤버 선언에 꼭 필요한 헤더들만 포함 할 수 있다. 이 때문에 컴파일 시간이 매우 감소된다.

3. 정리하며

어댑터 패턴과의 차이

브릿지 패턴은 어댑터 패턴과 굉장히 유사하다. 두 패턴 모두 구현부를 감추기 때문이다. 하지만 목적의 차이가 분명하다. 어댑터는 어떤 코드에 맞게끔 기존의 코드를 재사용하기 위해 사용하고, 브릿지는 확장성을 고려하여 미리 예상해 설계 단계부터 고려한다는 차이점이 있다.

4. 참고자료