C++에서 브리지 패턴 살펴보기

이상문
6 min readMay 11, 2023

소프트웨어 개발 프로젝트의 복잡성이 증가함에 따라 코드 품질, 유지보수성 및 유연성을 보장하기 위해 효과적인 디자인 패턴을 사용하는 것이 중요성이 커지고 있다. 이 글에서는 C++에서 브리지 패턴을 살펴본다.

Pimpl Idiom

piml idiom(“Pointer to implementation”의 줄임말)는 클래스의 공용 인터페이스와 구현 세부 사항을 분리하는 디자인 패턴이다. 이렇게 분리하면 캡슐화가 향상되고 코드 유지 관리성이 향상된다. pimpl의 기본 개념은 클래스 자체만 접근할 수 있는 별도의 구현 클래스를 사용하여 클래스 사용자로부터 구현 세부 정보를 숨기는 것이다.

// Person.h
#include <memory>

class PersonImpl;

class Person {
public:
Person(std::string name, int age);
std::string getName() const;
void setName(std::string name);
int getAge() const;
void setAge(int age);
private:
std::unique_ptr<PersonImpl> pImpl;
};
// Person.cpp
#include "Person.h"

class PersonImpl {
public:
std::string name;
int age;
};

Person::Person(std::string name, int age)
: pImpl(std::make_unique<PersonImpl>()) {
pImpl->name = name;
pImpl->age = age;
}

std::string Person::getName() const {
return pImpl->name;
}

void Person::setName(std::string name) {
pImpl->name = name;
}

int Person::getAge() const {
return pImpl->age;
}

void Person::setAge(int age) {
pImpl->age = age;
}

앞의 예제에서 Person 클래스의 공용 인터페이스는 name과 age를 가져오고 설정하는 메서드만 노출한다. PersonImpl 클래스의 구현 세부 사항은 Person 클래스의 사용자에게는 숨겨져 있다. 이 패턴을 사용하면 공용 인터페이스에 영향을 주지 않고 Person 클래스의 구현 세부 사항을 변경할 수 있으므로 코드 유지 관리가 더 쉬워진다.

pimpl idiom을 사용했을 때 장점은 바이너리 호환성이 향상된다는 것입니다. 구현 세부 사항이 사용자에게 숨겨져 있기 때문에 숨겨진 구현 클래스의 데이터 멤버를 수정해도 바이너리 호환성에 영향을 미치지 않는다. 하지만 구현 파일에 필요한 모든 헤더를 포함해야 하기 때문에 컴파일 시간이 느려진다는 단점이 있다.

브리지 패턴

브리지 패턴은 추상화(인터페이스)와 그 구현을 연결하는 디자인 패턴이다. 추상화와 구현을 독립적으로 변경해야 할 때 유용하다. 브리지 패턴은 상속이 아닌 구성이 우선인 원칙을 기반으로 하므로 유연성이 뛰어나고 추상화와 구현 간의 결합을 줄일 수 있다.

브리지 패턴의 예를 살펴보자. Circle, Square 및 기타 모양을 포함하는 Shape 클래스 계층 구조가 있다. 또한 각각 모양을 그릴 수 있는 VectorRenderer와 RasterRenderer를 포함하는 Renderer 클래스 계층 구조로 구성한다.

브리지 패턴을 사용하려면 먼저 셰이프를 렌더링하는 메서드를 사용하여 Renderer 인터페이스를 정의한다.

class Renderer {
public:
virtual void render() = 0;
};

다음으로 Renderer 인터페이스를 구현하는 실제 렌더러를 정의한다.

class VectorRenderer : public Renderer {
public:
void render() override {
std::cout << "Rendering vector shape\n";
}
};

class RasterRenderer : public Renderer {
public:
void render() override {
std::cout << "Rendering raster shape\n";
}
};

다음, Renderer 인터페이스를 사용할 Shape 추상 클래스를 정의한다.

class Shape {
protected:
Renderer& renderer_;
public:
Shape(Renderer& renderer) : renderer_{ renderer } {}
virtual void draw() = 0;
virtual ~Shape() {}
};

마지막으로 Shape및 Renderer 인터페이스를 사용하는 실제 Shape를 구현한다.

class Circle : public Shape {
public:
Circle(Renderer& renderer) : Shape{ renderer } {}
void draw() override {
std::cout << "Drawing a circle\n";
renderer_.render();
}
};

main 함수에서는 특정 렌더러를 사용해서 Circle를 그릴 수 있다.

int main() {
RasterRenderer rasterRenderer;
Circle circle(rasterRenderer);
circle.draw();
return 0;
}

앞의 예제에서 Circle 클래스는 Renderer 인터페이스에 의해 구현과 분리되었다. Circle 클래스 자체를 변경하지 않고도 다른 렌더러를 생성하여 Circle 클래스와 함께 사용할 수 있다. 이렇게 하면 코드가 더 유연해지고 유지 관리가 쉬워진다.

마무리

브리지 패턴은 어댑터 및 데코레이터와 같은 다른 디자인 패턴과 유사하다는 점에 주목할 필요가 있다. 어댑터 패턴은 한 인터페이스를 다른 인터페이스에 적용하는 데 사용되는 반면 데코레이터 패턴은 기존 객체에 기능을 추가하는 데 사용된다는 점에서 차이가 있다. 브리지 패턴은 추상화와 구현을 분리하는 데 사용된다.

--

--

이상문

software developer working mainly in field of streaming, using C++, javascript