프로그래밍 지식/디자인 패턴
디자인 패턴 정리 - 01. 생성 패턴
nextcoder
2024. 6. 3. 22:50
생성 패턴
- Simple Factory 패턴
: 객체를 생성하는 팩토리를 따로 두는 것
, 객체 생성 로직을 클라이언트 코드에서 분리
- 장점 : 복잡한 오브젝트의 생성 과정을 클라이언트가 직접 다룰 필요 없음
- 어떤 상황에서는 Object를 생성하는 과정이 복잡할 수 있다.
- 오브젝트 생성 과정은 모두 팩토리 안에 숨겨놓고 클라이언트는 "테슬라 모델3 만들어주세요" "테슬라 모델Y 만들어주세요"를 요청하면 팩토리가 필요한 오브젝트를 생성하여 return
#include <iostream>
#include <string>
#include <memory>
class Car {
public:
virtual ~Car() = default;
virtual void brake() = 0;
};
class Model3 : public Car {
public:
void brake() override {
std::cout << "Modle3 brake" << std::endl;
}
};
class ModelY : public Car {
public:
void brake() override {
std::cout << "ModelY brake" << std::endl;
}
};
// Factory 함수에서 객체를 생성
class TeslaFactory {
public:
std::unique_ptr<Car> createCar(const std::string& type) const {
if (type == "Model3") {
return std::make_unique<Model3>();
}
else if(type == "ModelY") {
return std::make_unique<ModelY>();
}
else
{
return nullptr;
}
}
};
// Client 함수
int main() {
TeslaFactory teslaFactory;
auto car1 = teslaFactory.createCar("Model3");
if (car1) {
car1->brake();
}
auto car2 = teslaFactory.createCar("ModelY");
if (car2) {
car2->brake();
}
}
- Factory Method Pattern
- 객체 생성 메서드를 하위 클래스에 위임하여 객체 생성을 캡슐화하는 패턴
- 객체 생성의 확장을 용이하게 함
- 단일 객체 생성
#include <iostream>
#include <string>
#include <memory>
class Car {
public:
virtual ~Car() = default;
virtual void brake() = 0;
};
class Model3 : public Car {
public:
void brake() override {
std::cout << "Modle3 brake" << std::endl;
}
};
class ModelY : public Car {
public:
void brake() override {
std::cout << "ModelY brake" << std::endl;
}
};
class TeslaFactory {
public:
virtual ~TeslaFactory() {}
// 객체 생성을 위한 메서드 (단일)
virtual std::unique_ptr<Car> createCar() const = 0;
void brake() const {
auto car = createCar();
car->brake();
}
};
class Model3Factory : public TeslaFactory {
public:
std::unique_ptr<Car> createCar() const override {
return std::make_unique<Model3>();
}
//costom 가능
};
class ModelYFactory : public TeslaFactory {
public:
std::unique_ptr<Car> createCar() const override {
return std::make_unique<ModelY>();
}
//costom 가능
};
// Client 함수
int main() {
Model3Factory model3Factory;
auto car1 = model3Factory.createCar();
if (car1) {
car1->brake();
}
ModelYFactory modelYFactory;
auto car2 = modelYFactory.createCar();
if (car2) {
car2->brake();
}
}
- 심플 팩토리 패턴 vs 팩토리 메서드 패턴
- 객체 생성 책임
- 심플 팩토리 패턴 : 클라이언트가 팩토리 클래스를 사용하여 객체 생성
- 팩토리 메서드 패턴 : 객체 생성 메서드가 추상 클래스나 인터페이스에 정의되고 하위 클래스에서 구현. 클라이언트가 하위 클래스를 사용하여 객체 생성
- 확장성
- 심플 팩토리 패턴 : 새로운 타입의 객체를 생성하려면 팩토리 클래스 코드 수정 필요
- 팩토리 메서드 패턴 : 새로운 타입의 객체를 생성하려면 새로운 하위 클래스를 추가. 기존 클래스 수정 필요 X
- 사용 시기
- 심플 팩토리 패턴 : 객체 생성 로직이 자주 변경되지 않거나 비교적 단순할 때
- 팩토리 메서드 패턴 : 객체 생성 로직이 자주 변경되거나 확장될 가능성이 있을 때
- 객체 생성 책임
- Abstract Factory Pattern
- 관련된 객체의 집합을 생성하는 인터페이스를 제공
- 다수 객체 생성
#include <iostream>
#include <string>
#include <memory>
class Brake {
public:
virtual ~Brake() = default;
virtual void use() = 0;
};
class Window {
public:
virtual ~Window() = default;
virtual void use() = 0;
};
class Model3Brake : public Brake {
public:
void use() override {
std::cout << "Modle3 brake" << std::endl;
}
};
class ModelYBrake : public Brake {
public:
void use() override {
std::cout << "ModelY brake" << std::endl;
}
};
class Model3Window : public Window {
public:
void use() override {
std::cout << "Modle3 window" << std::endl;
}
};
class ModelYWindow : public Window {
public:
void use() override {
std::cout << "ModelY window" << std::endl;
}
};
class TeslaFactory {
public:
virtual ~TeslaFactory() {}
virtual std::unique_ptr<Brake> createBrkae() const = 0;
virtual std::unique_ptr<Window> createWindow() const = 0;
};
class Model3Factory : public TeslaFactory {
public:
// 복수의 관련 객체들 집합 생성
virtual std::unique_ptr<Brake> createBrkae() const override {
return std::make_unique<Model3Brake>();
}
virtual std::unique_ptr<Window> createWindow() const override {
return std::make_unique<Model3Window>();
}
//costom 가능
};
class ModelYFactory : public TeslaFactory {
public:
virtual std::unique_ptr<Brake> createBrkae() const override {
return std::make_unique<ModelYBrake>();
}
virtual std::unique_ptr<Window> createWindow() const override {
return std::make_unique<ModelYWindow>();
}
//costom 가능
};
// Client 함수
int main() {
Model3Factory model3Factory;
auto brake1 = model3Factory.createBrkae();
auto window1 = model3Factory.createWindow();
brake1->use();
window1->use();
ModelYFactory modelYFactory;
auto brake2 = modelYFactory.createBrkae();
auto window2 = modelYFactory.createWindow();
brake2->use();
window2->use();
}
팩토리 메서드 패턴 vs 추상 팩토리 패턴
- 목적 :
- 팩토리 메서드 패턴 : 단일 객체의 생성
- 추상 팩토리 패턴 : 관련된 객체들의 집합을 생성
- 구조 :
- 팩토리 메서드 패턴 : 객체 생성을 위한 하나의 메서드가 있으며, 구체적인 서브 클래스가 이 메서드를 구현하여 객체 생성
- 추상 팩토리 패턴 : 여러 종류의 객체를 생성하는 여러 팩토리 메서드를 가진 인터페이스가 있으며, 구체적인 팩토리 클래스가 이 인터페이스 관련된 객체들을 생성
- 확장성 :
- 팩토리 메서드 패턴 : 새로운 객체 유형을 추가할 때 새로운 서브클래스 추가하여 팩토리 메서드 오버라이드
- 추상 팩토리 패턴 : 새로운 제품군을 추가할 때 새로운 팩토리 클래스를 추가하여 관련된 모든 제품을 생성하는 메서드 구현
ex. 제품군 객체들을 모두 만들어야할 때
- 팩토리 메서드 일 때 Client : 모델3 브레이크 객체 만들어주세요, 모델3 윈도우 객체 만들어주세요, ...
- 추상 팩토리 일 때 Client : 모델3와 관련된 객체들 만들어주세요 (팩토리 메서드를 한번 더 추상화)
팩토리 관련 패턴들의 차이점을 이해하기 어려웠다. (모두 추상화를 사용하고 코드 상 비슷해보였기 때문이다.)
구현의 차이점을 명확히 아는 것 보다는 각각의 패턴을 사용하는 컨셉이 중요한 것 같다. 내가 이해한 바로는 하기와 같다.
- 심플 팩토리 패턴 : 객체 생성하는 과정은 안 보여줄게. 클라이언트는 그냥 뭐 사용하고싶은지만 말해
- 팩토리 메서드 팩턴 : 객체 생성을 상위 클래스와 하위 클래스로 나눠서 나중에 다른 종류의 하위 클래스가 생길 확장성도 고려할래
- 추상 팩토리 패턴 : 클라이언트가 뭐 사용하고 싶다고 말하면 관련된 제품군들 객체를 싹 다 만들어줄래
Builder Pattern
#include <iostream>
#include <string>
class Car {
public:
void setWindow(const std::string &window) {
this->window = window;
}
void setWheels(const std::string &wheels) {
this->wheels = wheels;
}
void setBrake(const std::string &brake) {
this->brake = brake;
}
void setSeats(const int seats) {
this->seats = seats;
}
void show() const {
std::cout << "Car with window: " << window
<< ", wheels: " << wheels
<< ", seats: " << seats
<< ", brake: " << brake << std::endl;
}
private:
std::string window;
std::string wheels;
std::string brake;
int seats;
};
class CarBuilder {
public:
virtual ~CarBuilder() = default;
Car* getCar() {
return car;
}
void createNewCar() {
car = new Car();
}
virtual void buildWindow() = 0;
virtual void buildWheels() = 0;
virtual void buildSeats() = 0;
virtual void buildBrake() = 0;
protected:
Car *car;
};
class Car1Builder : public CarBuilder {
public:
void buildWindow() override {
car->setWindow("2024 Window");
}
void buildWheels() override {
car->setWheels("18 inch Alloy Wheels");
}
void buildSeats() override {
car->setSeats(2);
}
void buildBrake() override {
car->setBrake("2011 Brake");
}
};
class Car2Builder : public CarBuilder {
public:
void buildWindow() override {
car->setWindow("2021 Window");
}
void buildWheels() override {
car->setWheels("14 inch Alloy Wheels");
}
void buildSeats() override {
car->setSeats(2);
}
void buildBrake() override {
car->setBrake("2020 Brake");
}
};
class CarDirector {
public:
void setCarBuilder(CarBuilder *builder) {
this->builder = builder;
}
Car* getCar() {
return builder->getCar();
}
// 객체 생성 단계 명시
// 여러 인자를 나열하는 방식에서 벗어나 메서도 체이닝이 가능하다.
void constructCar() {
builder->createNewCar();
builder->buildWindow();
builder->buildWheels();
builder->buildSeats();
builder->buildBrake();
}
private:
CarBuilder *builder;
};
int main() {
CarDirector director;
Car1Builder sportsCarBuilder;
director.setCarBuilder(&sportsCarBuilder);
director.constructCar();
Car *Car1 = director.getCar();
Car1->show();
Car2Builder familyCarBuilder;
director.setCarBuilder(&familyCarBuilder);
director.constructCar();
Car *Car2 = director.getCar();
Car2->show();
delete Car1;
delete Car2;
return 0;
}
장점
- 객체 생성의 단계적 구성:
- 객체의 생성 과정을 단계별로 나눌 수 있어, 각 단계에서 필요한 설정을 수행할 수 있습니다.
- 클라이언트 코드가 어떤 단계에서든 객체의 특정 부분을 생성할 수 있습니다.
- 유연한 객체 생성:
- 다양한 구성의 객체를 동일한 생성 과정으로 만들 수 있어 코드의 재사용성을 높입니다.
- 동일한 빌더 인터페이스를 사용하여 여러 가지 변형된 객체를 생성할 수 있습니다.
- 복잡한 객체 생성의 단순화:
- 복잡한 객체의 생성 과정을 단순화하고, 생성 로직을 한 곳에 집중시켜 관리할 수 있습니다.
- 빌더 패턴을 사용하면 객체 생성 코드가 객체의 내부 표현과 분리되므로, 생성 로직을 더 잘 관리하고 유지보수할 수 있습니다.
- 불변 객체 생성에 유용:
- 빌더 패턴을 사용하면 불변 객체를 쉽게 생성할 수 있으며, 이는 멀티스레딩 환경에서 안전성을 제공합니다.
- 가독성 향상:
- 객체 생성 과정이 명확하게 드러나므로, 코드의 가독성과 이해도가 높아집니다.
- 메서드 체이닝을 통해 직관적이고 읽기 쉬운 코드 작성을 도와줍니다.
단점
- 복잡성 증가:
- 간단한 객체 생성에 대해서도 불필요하게 복잡한 구조를 도입할 수 있습니다.
- 빌더 클래스와 디렉터 클래스 등 여러 클래스를 추가로 작성해야 하므로, 코드가 길어지고 복잡해질 수 있습니다.
- 메모리 오버헤드:
- 빌더 객체와 최종 객체를 별도로 유지해야 하므로, 메모리 사용량이 증가할 수 있습니다.
- 객체 생성 시간이 증가할 수 있음:
- 단계별로 객체를 생성하고 설정하기 때문에, 객체 생성에 시간이 더 걸릴 수 있습니다.
- 일관성 유지가 어려울 수 있음:
- 모든 필수 필드를 설정하지 않고 객체를 생성하게 되면, 일관성 문제가 발생할 수 있습니다. 이를 방지하기 위해서는 적절한 검증 로직을 추가해야 합니다.
- 복잡한 객체를 위한 추가 코드 필요:
- 복잡한 객체를 생성할 때 필요한 설정 메서드를 많이 작성해야 하므로, 빌더 클래스의 구현이 복잡해질 수 있습니다.
빌더 패턴은 객체 생성의 복잡성을 줄이고, 생성 과정의 유연성을 높이는 데 매우 유용합니다. 그러나 간단한 객체에는 불필요하게 복잡성을 증가시킬 수 있으므로, 사용 시에는 필요성과 상황을 잘 고려해야 합니다.
참고 : chatGPT