본문 바로가기

c c++ mfc

디자인패턴 - 빌더 패턴(Builder Pattern)

반응형

빌더 패턴(Builder Pattern)은 복잡한 객체를 단계별로 생성할 수 있도록 도와주는 디자인 패턴입니다. 이 패턴은 객체 생성의 과정을 캡슐화하여, 동일한 생성 절차에서 서로 다른 표현을 만들 수 있게 해줍니다. 특히, 객체의 속성이 많거나 복잡한 경우에 유용합니다.

 

빌더패턴

주요 특징

단계별 객체 생성: 객체를 생성할 때 여러 단계로 나누어 생성할 수 있어, 각 단계를 통해 필요한 속성을 설정할 수 있습니다.
불변성: 생성된 객체는 불변(immutable)으로 만들 수 있어, 생성 후 상태 변경을 방지할 수 있습니다.
가독성 향상: 명확한 메서드 체이닝을 통해 객체 생성 과정을 쉽게 이해할 수 있습니다.


C++ 코드 예시

아래는 빌더 패턴을 사용하여 복잡한 Pizza 객체를 생성하는 예시입니다.

#include <iostream>
#include <string>
#include <vector>

// Product 클래스
class Pizza {
private:
    std::string size;
    std::vector<std::string> toppings;

public:
    Pizza(std::string size, std::vector<std::string> toppings)
        : size(size), toppings(toppings) {}

    void display() {
        std::cout << "Pizza Size: " << size << std::endl;
        std::cout << "Toppings: ";
        for (const auto& topping : toppings) {
            std::cout << topping << " ";
        }
        std::cout << std::endl;
    }
};

// Builder 클래스
class PizzaBuilder {
private:
    std::string size;
    std::vector<std::string> toppings;

public:
    PizzaBuilder& setSize(const std::string& size) {
        this->size = size;
        return *this;
    }

    PizzaBuilder& addTopping(const std::string& topping) {
        toppings.push_back(topping);
        return *this;
    }

    Pizza build() {
        return Pizza(size, toppings);
    }
};

// 클라이언트 코드
int main() {
    PizzaBuilder builder;
    Pizza pizza = builder.setSize("Large")
                         .addTopping("Cheese")
                         .addTopping("Pepperoni")
                         .addTopping("Olives")
                         .build();

    pizza.display();

    return 0;
}

 

설명

1. Product 클래스: Pizza 클래스는 생성할 객체를 정의합니다. 피자의 사이즈와 토핑을 속성으로 가집니다.
2. Builder 클래스: PizzaBuilder 클래스는 피자를 생성하기 위한 메서드를 제공합니다. 사용자는 사이즈를 설정하고, 원하는 토핑을 추가할 수 있습니다.
3. 메서드 체이닝: setSize와 addTopping 메서드는 PizzaBuilder의 참조를 반환하므로, 메서드 체이닝을 통해 연속적으로 호출할 수 있습니다.
4. 객체 생성: 클라이언트 코드에서 PizzaBuilder를 사용하여 피자의 사이즈와 토핑을 설정하고, build 메서드를 호출하여 최종적으로 Pizza 객체를 생성합니다.

 

다른 예제 하나 더 살펴보겠습니다.

#include <iostream>
#include <string>
using namespace std;

// Product
class Car {
    string engine;
    string wheels;
    string body;

public:
    void setEngine(const string& e) { engine = e; }
    void setWheels(const string& w) { wheels = w; }
    void setBody(const string& b) { body = b; }

    void showSpecifications() const {
        cout << "Engine: " << engine << "\n";
        cout << "Wheels: " << wheels << "\n";
        cout << "Body: " << body << "\n";
    }
};

// Builder
class CarBuilder {
protected:
    Car* car;

public:
    CarBuilder() { car = new Car(); }
    virtual ~CarBuilder() { delete car; }

    virtual void buildEngine() = 0;
    virtual void buildWheels() = 0;
    virtual void buildBody() = 0;

    Car* getCar() { return car; }
};

// Concrete Builder
class SportsCarBuilder : public CarBuilder {
public:
    void buildEngine() override { car->setEngine("V8 Engine"); }
    void buildWheels() override { car->setWheels("18 inch Alloy Wheels"); }
    void buildBody() override { car->setBody("Carbon Fiber Body"); }
};

class SUVCarBuilder : public CarBuilder {
public:
    void buildEngine() override { car->setEngine("V6 Engine"); }
    void buildWheels() override { car->setWheels("20 inch Steel Wheels"); }
    void buildBody() override { car->setBody("High-Strength Steel Body"); }
};

// Director
class CarDirector {
    CarBuilder* builder;

public:
    void setBuilder(CarBuilder* b) { builder = b; }

    Car* construct() {
        builder->buildEngine();
        builder->buildWheels();
        builder->buildBody();
        return builder->getCar();
    }
};

// Client Code
int main() {
    CarDirector director;

    // Building a Sports Car
    SportsCarBuilder sportsCarBuilder;
    director.setBuilder(&sportsCarBuilder);
    Car* sportsCar = director.construct();
    cout << "Sports Car Specifications:\n";
    sportsCar->showSpecifications();

    // Building an SUV Car
    SUVCarBuilder suvCarBuilder;
    director.setBuilder(&suvCarBuilder);
    Car* suvCar = director.construct();
    cout << "\nSUV Car Specifications:\n";
    suvCar->showSpecifications();

    return 0;
}

설명

1. Product 클래스 (Car) : Car는 빌더 패턴을 통해 생성될 복잡한 객체입니다. 이 클래스는 다양한 구성 요소를 포함하며, 각 구성 요소는 단계적으로 설정됩니다.

  • engine, wheels, body라는 세 가지 멤버 변수가 있으며, 각각 차량의 엔진, 바퀴, 차체를 나타냅니다.
  • 각각의 setEngine, setWheels, setBody 메서드를 통해 구성 요소를 설정합니다.
  • showSpecifications 메서드는 Car 객체의 현재 사양을 출력합니다.

2. 빌더 인터페이스 (CarBuilder) : CarBuilder는 객체 생성의 인터페이스를 제공합니다.

  • 추상 클래스로 설계되어 있어, 구체적인 빌더 클래스들이 이를 상속받고 실제 구현을 제공합니다.
  • buildEngine, buildWheels, buildBody라는 세 개의 순수 가상 메서드가 정의되어 있으며, 이는 객체 생성의 단계별 프로세스를 나타냅니다.
  • getCar 메서드는 생성 중인 Car 객체를 반환합니다.

3. 구체적인 빌더 클래스 : 구체적인 빌더는 CarBuilder를 상속받아 실제 객체 생성 논리를 구현합니다.

1) SportsCarBuilder:

  • 스포츠카 특성에 맞는 엔진, 바퀴, 차체를 설정합니다.
    • 엔진: "V8 Engine"
    • 바퀴: "18 inch Alloy Wheels"
    • 차체: "Carbon Fiber Body"

2) SUVCarBuilder:

  • SUV 차량 특성에 맞는 엔진, 바퀴, 차체를 설정합니다.
    • 엔진: "V6 Engine"
    • 바퀴: "20 inch Steel Wheels"
    • 차체: "High-Strength Steel Body"

4. 디렉터 클래스 (CarDirector) : CarDirector는 빌더의 흐름을 관리하고 객체 생성 과정을 조정합니다.

  • setBuilder 메서드를 통해 사용할 빌더를 설정합니다.
  • construct 메서드는 빌더의 메서드들을 호출해 객체를 단계적으로 생성합니다.
    • 엔진을 먼저 설정 (buildEngine).
    • 그다음 바퀴를 설정 (buildWheels).
    • 마지막으로 차체를 설정 (buildBody).
  • 최종적으로 생성된 Car 객체를 반환합니다.

5. 클라이언트 코드 : 클라이언트는 CarDirector를 사용하여 객체를 생성하며, 빌더의 세부적인 구현을 신경 쓰지 않아도 됩니다.

  • 스포츠카 생성: SportsCarBuilder를 CarDirector에 전달하여 스포츠카를 생성.
  • SUV 생성: SUVCarBuilder를 CarDirector에 전달하여 SUV를 생성.
  • 생성된 차량 사양을 showSpecifications로 출력.

6. 출력 결과

프로그램을 실행하면 아래와 같은 출력이 예상됩니다:

Sports Car Specifications:
Engine: V8 Engine
Wheels: 18 inch Alloy Wheels
Body: Carbon Fiber Body

SUV Car Specifications:
Engine: V6 Engine
Wheels: 20 inch Steel Wheels
Body: High-Strength Steel Body

 

빌더 패턴(Builder Pattern)은 객체 생성 과정에서 장점과 단점을 가지고 있습니다. 아래에서 각각을 자세히 살펴보겠습니다.

장점


1. 유연한 객체 생성: 빌더 패턴은 복잡한 객체를 단계별로 생성할 수 있게 해주므로, 고객이 원하는 다양한 조합의 객체를 쉽게 만들 수 있습니다. 각 속성을 개별적으로 설정할 수 있어 유연성이 높습니다.


2. 코드 가독성 향상: 메서드 체이닝을 사용하여 객체를 생성하는 과정이 명확해지므로, 코드가 읽기 쉬워집니다. 각 속성이 어떤 값을 가지는지 쉽게 파악할 수 있습니다.


3. 불변 객체 생성: 빌더 패턴을 사용하면 생성된 객체를 불변으로 만들 수 있어, 객체의 상태가 변경되지 않도록 보장할 수 있습니다. 이는 코드의 안정성을 높입니다.


4. 복잡한 객체 생성 간소화: 생성자가 너무 많은 매개변수를 받는 경우, 빌더 패턴을 사용하여 매개변수의 수를 줄이고 각 매개변수를 명확하게 설정할 수 있습니다.


5. 표준화된 생성 과정: 객체 생성 과정이 표준화되므로, 여러 개발자가 협업할 때 일관된 방식으로 객체를 생성할 수 있습니다. 이는 유지보수성을 높입니다.


단점


1. 복잡성 증가: 객체 생성 로직을 분리하여 빌더 클래스를 도입함으로써 코드의 복잡성이 증가할 수 있습니다. 간단한 객체의 경우에는 오히려 불필요한 복잡성을 초래할 수 있습니다.


2. 추가적인 클래스 필요: 빌더 패턴을 적용하면 추가적인 클래스(빌더 클래스)가 필요하게 되므로, 클래스의 수가 증가하고 관리해야 할 요소가 늘어납니다.


3. 객체 생성을 위한 초기화 비용: 빌더 패턴은 객체를 생성하는 데 여러 단계를 거치므로, 객체 생성 시 초기화 비용이 증가할 수 있습니다. 이는 성능에 영향을 미칠 수 있습니다.


4. 상태 관리: 빌더 패턴을 사용할 때, 객체의 상태를 관리해야 하므로, 빌더가 여러 번 사용될 때 상태를 잘 관리하지 않으면 의도치 않은 결과를 초래할 수 있습니다.


결론


빌더 패턴은 복잡한 객체를 보다 유연하고 가독성 있게 생성할 수 있는 강력한 도구입니다. 그러나 사용 시 코드의 복잡성이 증가할 수 있으므로, 간단한 객체를 생성할 때는 적합하지 않을 수 있습니다. 따라서, 객체의 복잡성과 요구 사항에 따라 빌더 패턴의 사용 여부를 신중하게 결정하는 것이 중요합니다.

반응형