Outdated/Column
[Design Pattern] 브릿지 패턴(bridge pattern)
해달
2020. 4. 11. 08:00
목차
브릿지 패턴
Pimpl 관용구
정리하며
참고자료
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. 참고자료