C++의 프로토타입 디자인 패턴 이해하기

이상문
14 min readMay 6, 2023

--

C++와 같은 객체 지향 프로그래밍 언어로 작업할 때 객체 복사는 몇 가지 문제를 일으킬 수 있다. 다행히도 프로토타입 디자인 패턴을 이용하면 이러한 문제를 극복하는 데 도움이 될 수 있다. 이 글에서는 프로토타입 디자인 패턴의 구현, 모범 사례 및 예제를 다루며 C++의 프로토타입 디자인 패턴을 심층적으로 살펴보고자 한다.

프로토타입 디자인 패턴의 작동 방식

C++에서 객체 복사는 특히 복잡한 객체를 다룰 때 까다로울 수 있다. 프로토타입 디자인 패턴으로 매번 전체 초기화를 수행하지 않고도 객체의 복사본을 생성할 수 있다. 이를 위해 이 패턴은 심층 복사(deep copy)라는 개념을 사용한다. C++에서 심층 복사는 동적으로 할당된 데이터 멤버를 포함하여 객체의 모든 데이터 멤버를 새 객체에 복사하는 것을 의미한다.

프로토타입 디자인 패턴을 사용하여 객체 복사본을 만들려면 먼저 새 복사본을 만들기 위한 템플릿으로 사용할 프로토타입 객체를 만들어야 한다. 이 프로토타입 객체는 새 객체를 만들기 위한 청사진으로 사용한다.

class Prototype {
public:
virtual ~Prototype() {}
virtual Prototype* clone() = 0;
};

class ConcretePrototype : public Prototype {
public:
ConcretePrototype(int value) : value_(value) {}

// Copy constructor
ConcretePrototype(const ConcretePrototype& other) : value_(other.value_) {}

// Copy assignment operator
ConcretePrototype& operator=(const ConcretePrototype& other) {
if (this != &other) {
value_ = other.value_;
}
return *this;
}

Prototype* clone() {
return new ConcretePrototype(*this);
}

int getValue() const {
return value_;
}

private:
int value_;
};

복제를 위한 직렬화 및 역직렬화

앞선 방법 외에도 프로토타입 디자인 패턴을 사용하여 객체 복사본을 생성하는 또 다른 방법은 직렬화(serialize) 및 역직렬화(deserialize)를 사용하는 것이다. 이 메커니즘은 객체를 바이트 스트림으로 변환한 다음 객체의 새 복사본을 만드는 데 사용할 수 있다.

C++에서 직렬화를 위해 널리 사용되는 라이브러리 중 하나는 Boost 직렬화 라이브러리이다. 이 라이브러리는 간단한 구문을 사용하여 객체를 직렬화 및 역직렬화하는 방법을 제공한다. Boost 직렬화 라이브러리를 사용하려면 먼저 모든 데이터 멤버를 포함하여 직렬화하려는 객체를 정의해야 한다. 그런 다음 각 데이터 멤버를 직렬화 및 역직렬화하는 방법을 지정해야 한다. 마지막으로 라이브러리의 serialize() 메서드를 사용하여 객체를 직렬화 및 역직렬화할 수 있다.

직렬화 및 역직렬화와 함께 프로토타입 디자인 패턴을 사용하려면 복사할 오브젝트를 바이트 스트림으로 직렬화한 다음 새 오브젝트로 역직렬화해야 한다. 그러면 이 새 객체는 원본과 독립적이며 별도로 사용할 수 있다. 이 접근 방식은 직렬화를 통해 객체의 복사본을 자동으로 생성할 수 있으며, 데이터 저장 및 네트워크 통신과 같은 다른 용도로도 사용할 수 있다는 장점을 가진다.

#include <boost/archive/binary_iarchive.hpp>
#include <boost/archive/binary_oarchive.hpp>
#include <boost/serialization/base_object.hpp>
#include <boost/serialization/serialization.hpp>
#include <iostream>
#include <memory>

class Animal {
public:
virtual std::unique_ptr<Animal> clone() const = 0;
virtual void sound() const = 0;

template <class Archive>
void serialize(Archive &ar, const unsigned int version) {}
};

class Cat : public Animal {
public:
std::unique_ptr<Animal> clone() const override {
std::stringstream ss;
boost::archive::binary_oarchive oa(ss);
oa << *this;

boost::archive::binary_iarchive ia(ss);
std::unique_ptr<Animal> cloned_cat;
ia >> cloned_cat;

return cloned_cat;
}

void sound() const override { std::cout << "Meow!" << std::endl; }

template <class Archive>
void serialize(Archive &ar, const unsigned int version) {
ar &boost::serialization::base_object<Animal>(*this);
}
};

int main() {
std::unique_ptr<Animal> cat = std::make_unique<Cat>();
std::unique_ptr<Animal> cloned_cat = cat->clone();
cloned_cat->sound(); // "Meow!"

return 0;
}

앞의 예제에서는 virtual clone() 메서드와 sound() 메서드가 있는 추상 Animal 클래스가 있다. 또한 이러한 메서드를 구현하는 구체적인 Cat 클래스도 있다. clone() 메서드는 먼저 boost::archive::binary_oarchive를 사용하여 Cat 객체를 바이트 스트림으로 직렬화 한 다음 boost::archive::binary_iarchive를 사용하여 새 Animal 객체로 역직렬화한다. clone() 메서드는 원본 Cat 객체의 딥 카피인 새 Animal 객체에 unique_ptr을 반환한다.

C++로 프로토타입 디자인 패턴 구현하기

#include <iostream>
#include <string>

class Prototype {
public:
virtual Prototype* clone() = 0;
virtual void set_data(std::string data) = 0;
virtual void print_data() = 0;
};

class ConcretePrototype : public Prototype {
public:
ConcretePrototype* clone() override {
return new ConcretePrototype(*this);
}
void set_data(std::string data) override {
this->data = data;
}
void print_data() override {
std::cout << "Data: " << data << std::endl;
}
private:
std::string data;
};

class Client {
public:
void set_prototype(Prototype* prototype) {
this->prototype = prototype;
}
Prototype* get_prototype() {
return prototype->clone();
}
private:
Prototype* prototype;
};

int main() {
ConcretePrototype* concrete_prototype = new ConcretePrototype();
concrete_prototype->set_data("Hello, World!");

Client* client = new Client();
client->set_prototype(concrete_prototype);

Prototype* cloned_prototype = client->get_prototype();
cloned_prototype->print_data();

delete concrete_prototype;
delete client;
delete cloned_prototype;

return 0;
}

프로토타입 디자인 패턴을 사용하여 매번 생성자를 호출할 필요 없이 객체 복사본을 생성할 수 있다. clone() 함수를 사용하여 기존 객체의 복사본을 생성하면 시간과 리소스를 절약할 수 있다.

프로토타입 패턴이 유용한 상황

프로토타입 패턴은 객체를 생성하는 데 필요한 시간과 비용을 줄일 수 있으며, 객체 생성 과정에서 발생할 수 있는 오류를 방지하는 데 유용하다. 따라서 객체의 생성이 빈번하게 발생하거나 생성 비용이 높은 경우에 적합하다.

객체를 동적으로 구성하거나 구성 요소가 많은 복잡한 객체를 생성하는 경우에도 유용하다. 예를 들어 게임 개발에서는 많은 수의 캐릭터, 아이템, 무기 등의 객체를 생성해야 하며, 이들 객체는 각각의 고유한 속성을 가지고 있다. 이러한 경우, 프로토타입 패턴을 사용하여 기본적인 객체를 생성한 후 필요에 따라 수정하여 새로운 객체를 만들 수 있다.

객체를 생성하는 과정이 복잡하거나 다른 객체와 밀접하게 연관되어 있는 경우에도 프로토타입 패턴이 사용할 지점이다. 예를 들어, 객체를 생성하기 위해 외부 API와 통신해야 하는 경우에는 프로토타입 패턴을 사용하여 API와의 연동을 최소화하고 객체 생성 과정을 간소화할 수 있다.

#include <iostream>
#include <string>
#include <unordered_map>

class WeatherData
{
public:
virtual WeatherData* clone() = 0;
virtual void setTemperature(float temp) = 0;
virtual void setHumidity(float hum) = 0;
virtual void setPressure(float press) = 0;
virtual void display() = 0;
};

class WeatherDataAPI : public WeatherData
{
public:
WeatherDataAPI() {}
WeatherDataAPI(std::string location) : m_location(location) {}

void setTemperature(float temp) override { m_temperature = temp; }
void setHumidity(float hum) override { m_humidity = hum; }
void setPressure(float press) override { m_pressure = press; }
void display() override {
std::cout << "Current weather conditions in " << m_location << std::endl;
std::cout << "Temperature: " << m_temperature << " degrees Celsius" << std::endl;
std::cout << "Humidity: " << m_humidity << "%" << std::endl;
std::cout << "Pressure: " << m_pressure << " hPa" << std::endl;
}
WeatherDataAPI* clone() override { return new WeatherDataAPI(*this); }
private:
std::string m_location;
float m_temperature;
float m_humidity;
float m_pressure;
};

class WeatherDataCache
{
public:
static void init() {
// Initialize with default values for a few cities
m_weatherCache["London"] = new WeatherDataAPI("London");
m_weatherCache["New York"] = new WeatherDataAPI("New York");
m_weatherCache["Tokyo"] = new WeatherDataAPI("Tokyo");
}
static WeatherData* getWeatherData(std::string location) {
auto iter = m_weatherCache.find(location);
if (iter != m_weatherCache.end()) {
return iter->second->clone();
}
std::cout << "Fetching weather data for " << location << " from external API..." << std::endl;
// Call external API to fetch weather data for location
auto newData = new WeatherDataAPI(location);
newData->setTemperature(25.0f);
newData->setHumidity(70.0f);
newData->setPressure(1013.0f);
m_weatherCache[location] = newData;
return newData->clone();
}
private:
static std::unordered_map<std::string, WeatherDataAPI*> m_weatherCache;
};

std::unordered_map<std::string, WeatherDataAPI*> WeatherDataCache::m_weatherCache;

int main()
{
WeatherDataCache::init();

auto londonData1 = WeatherDataCache::getWeatherData("London");
auto londonData2 = WeatherDataCache::getWeatherData("London");
londonData1->display();
londonData2->display();

auto newYorkData = WeatherDataCache::getWeatherData("New York");
newYorkData->display();

auto tokyoData = WeatherDataCache::getWeatherData("Tokyo");
tokyoData->display();

return 0;
}

원격 날씨 API에서 데이터를 검색하여 여러 도시의 일기 예보를 표시하는 애플리케이션을 구축하는 상황을 생각해보자. 이 애플리케이션은 온도, 습도, 풍속 등의 정보를 포함하는 WeatherData 객체의 인스턴스를 여러 개 만들어야 한다.

프로토타입 디자인 패턴을 사용하지 않으면 만들고자 하는 WeatherData 인스턴스마다 별도의 API 호출을 수행해야 하므로 네트워크 오버헤드가 크게 발생하고 애플리케이션 속도가 느려질 수 있다.

프로토타입 패턴을 사용하여 이후의 모든 객체에 대한 템플릿 역할을 하는 단일 WeatherData 인스턴스를 만들 수 있다. 한 번의 API 호출로 필요한 데이터를 검색한 다음 이 템플릿을 복사하고 각 도시에 대한 관련 필드만 업데이트하여 여러 WeatherData 인스턴스를 만들 수 있다.

마무리

프로토타입 디자인 패턴은 C++에서 객체의 심층 복사본을 만드는 데 유용한 패턴이다. 이 패턴을 사용해서 객체 복사의 함정을 피하는 동시에 성능을 개선하고 코드 중복을 줄일 수 있다. 복사 생성자, 복사 할당 연산자, 직렬화/역직렬화와 같은 기술을 사용하여, 변화하는 요구 사항에 따라 잘 확장되는 효율적이고 유연하며 유지 관리가 가능한 코드를 만들 수 있다.

--

--

이상문
이상문

Written by 이상문

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

No responses yet