[모던 C++ 디자인 패턴] 책을 바탕으로 공부하는 내용을 정리한 내용이다.
Bridge Pattern
최근 C++ 컴파일러들은 컴파일 속도가 굉장히 빨라졌다. 번역 단위(translation unit) 전체를 생성하는 대신 변경된 정의 부분만 새로 만들고 나머지는 재활용하는 방식으로 진화했다.
과거 C++ 개발자들은 유사하게 컴파일 시간을 단축시키기 위해 독특한 관례를 사용했다. Pimpl 관례가 그것이다.
Pimpl 관례
Pimpl(Pointer to Implementation)은 구현부를 포인터로 참조하는 관례를 말한다. 기술적 측면에서 어떤 역할을 수행하는지 살펴보자.
일반적으로 개인 정보를 담는 Person 클래스를 구현한다 해보자. 사람 이름을 저장하고, 인사말을 출력하는 메서드를 갖는다고 할 때 아래와 같은 형태로 정의될 수 있다.
struct Person {
Person();
~Person();
void greet();
std::string m_name;
class PersonImpl;
PersonImpl *impl;
};
위 코드는 단순한 클래스임에도 난해한 정의들이 들어간다. 굳이 불필요한 생성자와 소멸자가 있으며, PersonImpl은 무엇인지 직관적이지 않다. 이러한 어색함은 클래스 구현부를 다른 클래스(PersonImpl)에 숨기고자 하는 의도에서 발생한다.
클래스 구현부는 Person과 같은 .cpp 소스코드에 정의하게 된다.
PersonImpl은 아래와 같이 단순하다.
struct Person::PersonImpl {
void greet(Person *p);
};
원본 Person 클래스는 PErsonImpl을 전방 선언(forward-declare)하고 포인터로 관리한다. Person 생성자/소멸자는 PersonImpl 포인터를 초기화/삭제하는 작업을 수행한다. 따라서, 스마트 포인터를 사용해도 무관하다.
Person 클래스의 greet() 메서드는 어떻게 구현되어 있을까?
Person::Person() : impl(new PersonImpl) {
}
Person::~Person() {
delete impl;
}
void Person::greet() {
impl->greet(this);
}
void Person::PersonImpl::greet(Person *p) {
printf("hello %s", p->m_name.c_str());
}
예상하다시피 impl의 greet()에 모든것을 위임한다.
여기까지가 Pimpl 관례의 기본적인 개념이다.
Pimpl 관례의 장점은 3가지가 있다.
- 클래스 구현부의 상당 부분을 감출 수 있다.
- Person 클래스의 private/protected 멤버는 헤더로 노출이 된다. 실제로 사용은 불가능하겠지만 불필요한 노출을 막을 수 있다.
- 바이너리 호환성 보증이 쉽다.
- 숨겨진 구현 클래스에 대한 수정은 바이너리 호환성에 영향을 주지 않는다.
- 헤더 파일이 멤버 선언에 필요한 헤더만 인클루드할 수 있다.
- 헤더 파일에 구현에 필요한 다른 헤더를 포함하지 않아도 된다.
이러한 장점들로 인해 Pimpl을 적용하면 안정적인 헤더를 유지할 수 있다. 컴파일 시간도 줄어든다. 디자인 패턴 개념에서 Pimpl은 브릿지 패턴의 좋은 예다.
위 코드에서는 pimpl이 불투명 포인터로써, 포인터의 상세 사항을 숨길 수 있다. 공개용 인터페이스와 숨겨야할 세부 구현 내용 사이에 다리를 놓는 역할을 하고 있다.
Pimpl 관례는 브릿지 디자인 패턴의 특별한 하나의 예다. 일반적 관점에서 디자인 패턴을 살펴봐야 한다.
일반적 브릿지 패턴
두 종류의 클래스가 있다고 해보자. 하나는 기하 도형 클래스고 다른 하나는 화면에 그림을 그리는 랜더 클래스다. 랜더 클래스는 벡터로 그릴 수도 있고, 래스터(타자기처럼 한 줄씩)로 그릴 수도 있다. 그리고 기하 도형은 원으로 제한한다.
랜더러 베이스 클래스와 백터와 래스터에 대한 구현은 아래와 같이 구현할 수 있다. (그리기 대신 출력으로 대체)
struct Renderer {
virtual void renderCircle(float x, float y, float radius) = 0;
};
struct VectorRenderer : public Renderer {
void renderCircle(float x, float y, float radius) override {
std::cout << "Rasterizing circle of radius " << radius << std::endl;
}
};
struct RasterRenderer : public Renderer {
void renderCircle(float x, float y, float radius) override {
std::cout << "Drawing a vector dcircle of radius " << radius << std::endl;
}
};
기하 도형의 베이스 클래스 Shape는 위의 렌더러에 대한 참조를 가진다. 또한, draw() 메서드를 이용해 자기 자신을 그릴 수 있다. 추가적으로 크기 변경 가능한 resize() 메서드도 갖는다.
struct Shape {
protected:
Shape(Renderer &renderer) : m_renderer { renderer } {
}
public:
virtual void draw() = 0;
virtual void resize(float factor) = 0;
protected:
Renderer &m_renderer;
};
멤버 변수 m_renderer는 Renderer 클래스에 대한 참조로, 브릿지 패턴을 잘 나타내고 있다. Shape 클래스의 구현을 원으로만 제한했기 때문에, Circle 클래스를 살펴보도록 하자.
struct Circle : public Shape {
Circle(Renderer &renderer, float x, float y, float radius) : Shape(renderer), m_x(x), m_y(y), m_radius(radius) {
}
void draw() override {
m_renderer.renderCircle(m_x, m_y, m_radius);
}
void resize(float factor) override {
m_radius *= factor;
}
float m_x, m_y, m_radius;
};
브릿지(m_renderer)를 이용해 Circle 클래스를 렌더링 절차에 연결하는 draw() 안에 구현되어 있다.
렌더러는 자신이 다루는 것이 Circle인지 전혀 모르고, 자신이 참조 변수로 접근되고 있다는 것도 알지 못한다.
요약
브릿지는 두 객체를 연결하는 단순한 개념이다.
추상화(인터페이스)를 하면 어떤 컴포넌트가 다른 컴포넌트와 그 상세 구현을 전혀 알지 못하는 상태에서도 서로 연동될 수 있다.
브릿지 패턴에 연결되는 것들은 서로의 존재를 알아야만 한다. Circle은 Renderer의 참조를 가져야 하고, 반대로 Renderer는 원을 어떻게 그릴지 알고 있어야(renderCircle() 메서드에 해당)한다. 이 부분은 매개자(중개자; Meditator) 패턴과 대비되는 부분이다. Mediatator 패턴은 서로를 직접적으로 알지 못하더라도 서로 연동할 수 있다.
대표적으로 브릿지 패턴을 설명할 때, "구현부와 추상층를 분리하여 독립적으로 각자 변형할 수 있는 패턴"이라고 한다. 구현부와 추상층에 대한 이해를 위해 아래 블로그를 추가로 참고했다.
브릿지 패턴 구조에 따르자면, Abstraction은 Shape 클래스, Refined Abstraction은 Circle 클래스, Implementor는 Renderer 클래스, 그리고 ConcreteImplementor는 VectorRenderer와 RasterRenderer가 된다.
'study > design pattern' 카테고리의 다른 글
[디자인패턴][구조패턴] 데코레이터 Decorator - C++ (0) | 2022.08.14 |
---|---|
[디자인패턴][구조패턴] 컴포지트 Composite - C++ (0) | 2022.08.14 |
[디자인패턴][구조패턴] 어댑터 Adapter - C++ (0) | 2022.05.11 |
[디자인패턴][생성패턴] 싱글턴 Singleton - C++ (0) | 2022.04.30 |
[디자인패턴][생성패턴] 프로토타입 Prototype - C++ (0) | 2022.04.28 |