Coding Memo

Factory 패턴 (팩토리 패턴) 본문

Language/C++

Factory 패턴 (팩토리 패턴)

minttea25 2024. 11. 28. 16:29

먼저 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++ 디자인 패턴 (드미트리 네스터룩, 권오인 옮김, 길벗)