Coding Memo
Factory 패턴 (팩토리 패턴) 본문
먼저 Factory 패턴에 대해...
Factory Pattern은 객체 생성 로직을 캡슐화 하여, 객체 생성 방법과 사용을 분리시키고 생성방법을 특정 로직으로 강제할 수 있는 생성 디자인 패턴이다.
라이브러리에서 `~~Factory.~~~()`을 사용해보았거나 보았던 기억이 있을 것이다.
Factory를 통해서 어떤 객체를 생성하는 개념은 라이브러리에서 자주 찾아볼 수 있다. (Builder와는 조금 다른 개념이라는 것에 유의하자.)
이 Factory이 패턴이 필요한 시나리오와 특징에 대해서 알아보자.
Scenario
(해당 단락은 책을 참고하여 작성했다.)
Shape라는 구조체에 원과 정사각형의 정보를 저장하고 싶다고 가정하자. 그렇다면 다음과 같이 코드를 작성할 수 있을 것이다. (물론 딱 봐서는 circle과 square를 별도의 구조체로 분리하여 자식으로 만드는 방법을 떠올릴 수 있겠지만 지금은 넘어가자.)
struct Shape
{
Shape(const float radius) : site(radius * radius * 3.14f) {}; // circle
Shape(const float r) : site(r * r) {} // square
float site;
};
위 코드에서는 생성자에서부터 문제점이 있다. 생성자의 파라미터가 const float으로 같다. 오버로딩은 보통 프로그래밍 언어에서 타입으로 구분하기 때문에, 이름으로 구별되는 파라미터에 대해서는 오버로딩을 구현할 수 없다.
그러면 단순히 enum으로 Shape에 enum을 추가하여 Shape 생성 시 추가 정보를 넘겨 주도록 해보자.
만약 상황에 따라 (e.g. 사용자의 입력에 따라) Shape에 다른 모양을 저장하고 싶다면 어떻게 할까?
단순히 아래와 같은 코드를 생각할 수 있을 것이다.
enum class ShapeType
{
circle,
square
};
struct Shape
{
Shape(const float r, const ShapeType type = ShapeType::circle)
{
if (type == ShapeType::circle) site = r * r * 3.14f;
else site = r * r;
}
float site;
};
위와 같은 코드는 내가 실제로 예전에 많이 작성했던 코드이다. (...)
추가 정보를 넘겨주어 site를 각각 의도하는 모양에 맞게 계산한다.
여기서 한가지 문제아닌 문제는, 파라미터 r을 원의 반지름으로 할지, 정사각형의 한 변의 길이로 해야할지에 대한 명확성이 떨어진다는 것이다. 이런 부분이 다소 아쉽게 느껴진다.
(C++에서는 C# 처럼 파라미터의 이름을 직접 지정하는 기능이 없다!)
그렇다면 생성자를 사용하는 것 대신, 메서드 호출을 통해 생성을 맡기는 방식으로 바꿔보자.
struct Shape
{
private:
Shape(const float site) : site(site) {}
public:
static Shape NewCircle(const float radius) { return { radius * radius * 3.14f }; }
static Shape NewSquare(const float r) { return { r * r }; }
float site;
};
위와 같이 바꾸면 메인 코드에서도 이 Shape를 사용할 때 정말 명확해져 가독성이 좋아진다.
눈여겨 볼 점은 Shape에 대한 생성자를 private로 선언했다는 점이다. 이는 생성 자체를 Shape 클래스에 맡긴다는 의미이다.
Shape circle = Shape::NewCircle(5.0f);
Shape square = Shape::NewSquare(5.0f);
위와 같이 생성을 대신 맡아 해주는 메서드들을 모아둔 클래스를 만들 수 있는데, 이를 Factory 클래스라고 한다.
struct Shape
{
public:
float get_site() const { return site; }
private:
Shape(const float site) : site(site) {}
float site;
friend class ShapeFactory;
};
struct ShapeFactory
{
public:
static Shape NewCircle(const float radius) { return Shape { radius * radius * 3.14f }; }
static Shape NewSquare(const float r) { return Shape { r * r }; }
};
다시 한번 Shape의 생성자를 private로 하여 외부에서의 생성을 막았다는 점을 생각하자. 이는 더 나아가서, Shape에 대한 생성을 ShapeFactory를 통해 생성하도록 강제하고 있는 것이다.
위와 같이 Factory 패턴을 사용하면 코드 편집기에서도 편해진다. ShapeFactory 클래스에 있는 생성 메서드들을 찾기가 편해지기 때문이다.
Note: friend 기능은 C++에만 존재한다. 따라서 C#과 같은 다른 언어에서 이용할 때는, nested class 기능을 이용하면 된다.
물론 nested class는 C++에서도 지원한다.
struct Shape
{
public:
float get_site() const { return site; }
private:
Shape(const float site) : site(site) {}
struct ShapeFactory
{
public:
static Shape NewCircle(const float radius) { return Shape { radius * radius * 3.14f }; }
static Shape NewSquare(const float r) { return Shape { r * r }; }
};
float site;
public:
static ShapeFactory Factory;
};
Sample Code
Factory 패턴을 이용하여 아메리카노와 카페라떼를 파는 카페를 만들어보자.
카페는 CoffeeFactory가 될 것이고, Coffee 클래스를 상속하는 Americano와 Cafelatte가 있다고 하자.
사람들은 단순히 Coffee라는 객체를 들고 있을 것이다.
한 가지 특이한 점은 언제든 커피 제조 레시피를 미리 정해두어야 한다는 것이다.
struct Coffee;
struct Cafe // CoffeeFactory
{
public:
void SetRecipe(std::string menu, const std::function<std::unique_ptr<Coffee>()>& recipe)
{
_recipes[menu] = recipe;
}
std::unique_ptr<Coffee> Order(const std::string& menu)
{
if (_recipes.contains(menu) == false) return nullptr;
else return _recipes[menu]();
}
private:
std::map<std::string, std::function<std::unique_ptr<Coffee>()>> _recipes;
};
struct Coffee
{
public:
Coffee(const float volume) : _volume(volume) {}
virtual void drink(const float volume) = 0;
protected:
float _volume;
};
struct Americano : public Coffee
{
public:
Americano(const float volume) : Coffee(volume) {}
void drink(const float volume) override { _volume -= volume; std::cout << "Drink Americano." << std::endl; }
};
struct CafeLatte : public Coffee
{
public:
CafeLatte(const float volume) : Coffee(volume) {}
public:
void drink(const float volume) override { _volume -= volume; std::cout << "Drink CafeLatte." << std::endl; }
};
int main()
{
auto americano_recipe = []() -> std::unique_ptr<Coffee>
{
return std::make_unique<Americano>(500);
};
auto cafelatte_recipe = []() -> std::unique_ptr<Coffee>
{
return std::make_unique<CafeLatte>(300);
};
Cafe cafe;
cafe.SetRecipe("americano", americano_recipe);
cafe.SetRecipe("cafelatte", cafelatte_recipe);
// Order
auto americano = cafe.Order("americano");
auto cafelatte = cafe.Order("cafelatte");
americano->drink(100);
return 0;
}
위 코드에서 Coffee를 상속하는 Americano와 CafeLatte에 대해 객체 생성 방법을 직접 메인 코드에서 설정할 수 있도록 설계한 모습을 볼 수 있다. Americano와 CafeLatte에 대한 용량(volume)을 직접 설정하도록 해준 것이다. 좀 더 이어서 보면, 팩토리를 통해 Order 메서드를 호출하는데, 이 때, 생성할 음료의 문자열을 넘겨준다. 이 부분은 문자열이든, enum이든 사용하면 될 것이다. map을 통해, 주문 받은 음료에 맞는 팩터리를 찾아 해당 함수를 호출하여 객체를 생성한다. 이 과정이 바로 커피 주문 과정이 될 것이다.
사실 사용자가 직접 팩토리에서 필요한 객체를 생성하는 방법은 팩토리 패턴과 조금 거리가 있을 수도 있지만, 위 방법은 특정 상황에서 유용할 수 있다. 만약 라이브러리에서 어떤 객체를 생성할 때, 기본적으로 포함된 그 객체의 팩터리를 사용할 수 있지만, 이 과정의 일부분을 사용자에게 위임할 때 사용할 수 있다.
Factory Pattern 특징
1. 명확성
팩토리를 통해 객체를 얻을 때, 자신이 필요한 객체가 구제적으로 무엇인지 함수명이나 추가적인 인자를 통해 명확하게 얻을 수 있다.
2. 객체 생성 권한
어떤 객체를 얻거나 생성하려고 할 때, 생성자를 통해 일반적으로 만들 수가 없다. 즉, 반드시 팩터리를 통해 객체들이 만들어지는데, 만들어지는 과정에서 예외를 발생시키거나, nullptr를 반환하면서 객체 생성이나 획득에 실패했다는 의미를 부여할 수 있다.
3. 다형성
Cafe 코드 처럼, 자식 클래스로 객체 인스턴스를 만들고 부모 클래스에 대한 참조나 포인터로 반환할 수 있다. 또한 부모 클래스를 베이스로 하여 단일 팩토리 클래스로 다양한 객체를 생성할 수 있도록 할 수 있다.
Note: Factory Pattern은 메모리 풀링 및 오브젝트 풀링에서도 적용이 가능하다. 정해진 방법에 따라 객체를 미리 만들어 놓을 수 있고, Pool 기능을 팩터리가 대체하거나, Pool에서 팩터리를 가지고 있을 수 있다.
Note2: Factory Pattern은 Builder Pattern(빌더 패턴)과는 차이가 있다. 팩터리는 객체를 만들어준다. 그러나, 빌더는 객체에 대한 구성요소의 정보를 제공하며 여러 단계에 걸쳐 객체를 구체화해 나간다.
Reference
모던 C++ 디자인 패턴 (드미트리 네스터룩, 권오인 옮김, 길벗)
'Language > C++' 카테고리의 다른 글
브릿지 패턴 (Bridge Pattern) + Pimpl (0) | 2024.11.26 |
---|---|
[LNK2005] already defined in ~~.obj / inline 함수 (0) | 2024.10.28 |
템플릿 특수화, constexpr if 그리고 concept (3) | 2024.10.16 |
[C++20] concepts - requires (0) | 2024.10.02 |
Singleton 패턴 (0) | 2024.09.26 |