팩토리 패턴(Factory Pattern)
- 팩토리 패턴은 생성 패턴 중에서도 가장 기본이 되는 패턴입니다.
C++
에서는 클래스의 인스턴스를 생성하기 위해서new
키워드를 사용합니다.C++11
이상의 모던 C++(Mordern C++
)에서는 객체의 생성으로new
키워드를 사용하는 것을 추천하지 않습니다.shared_ptr
이나unique_ptr
과 같은 스마트 포인터(Smart Pointer)를 사용하길 권고합니다.
- 일반적인 객체를 생성하는 방법은 다음과 같습니다.
class Sample
{
public:
void hello()
{
std::cout << "Hello world" << std::endl;
}
};
int main()
{
Sample sample = new Sample();
std::shared_ptr<Sample> sample = std::make_shared<Sample>(); // 스마트 포인터
return 0;
}
- 이렇게 객체를 생성하는 키워드
new
나 C++ STL 스마트 포인터(std::make_shared<T>
)를 사용하여 객체를 직접 생성하면 객체의 의존성이 발생하는 문제가 있습니다. (결합 관계가 발생하였다고도 합니다.) - 강력한 결합 관계가 되면 클래스 이름의 수정과 같이 변경사항이 발생할 경우 모든 코드를 찾아 수정해야 합니다.
- 팩토리 패턴은 이러한 결합 관계를 최소화하려는 목적이 있으며, 객체의 생성을 간접적으로 수행합니다.
팩토리 패턴의 의미
- 팩토리 패턴은 객체 생성을 별도의 클래스로 분리하여 생성을 위임합니다.
- 즉 객체를 생성하고 캡슐화하여 위임하는 것을 의미합니다.
팩토리 패턴: 생성할 객체의 인터페이스 클래스 선언
- C++에는 가상 함수(Virtual Function)를 사용하면 많은 이점이 있습니다.
- 팩토리 패턴에도 가상 함수를 사용합니다.
// 생성할 객체의 타입
enum class LanguageType
{
Korean = 0,
English
// ... 생성할 객체가 더 있다면 여기에 추가
} Language;
// 생성할 객체들의 인터페이스 클래스
class ILanguage
{
public:
virtual void text() = 0; // 가상 함수
};
- 팩토리로 생성할 객체들의 타입을 구분할
Language
가 필요합니다. - 객체의 타입을 구분하지 않더라도, 공통된 인터페이스를 갖는 인터페이스 클래스
ILanguage
가 필요합니다.
팩토리 패턴: 객체 생성 클래스 구현
- 객체 생성의 역할을 수행할 클래스를 하나 만듭니다.
class Factory final
{
public:
static std::shared_ptr<ILanguage> getInstance(const LanguageType& type)
{
// 객체의 타입을 알아야 하는 단점이 있다.
if (type == LanguageType::Korean)
{
return std::make_shared<Korean>();
}
else if (type == LanguageType::English)
{
return std::make_shared<English>();
}
}
};
- 객체를 생성할 팩토리는 반드시 생성할 객체의 타입을 알아야 합니다.
- 객체의 타입을 모른다면 어떤 객체를 만들어야 할지 모르기 때문입니다.
팩토리 패턴: 실제 임무를 수행할 객체 클래스 구현
- 인터페이스 클래스를 상속받아, 실제로 사용할 클래스를 구현합니다.
C++
문법적으로 상속(Inheritance)이지만 관용적 표현으로인터페이스를 구현한다.
라는 표현을 사용합니다.- 인터페이스 클래스는 추상화된 클래스로 구현체가 없기 때문입니다.
- 실제로 인터페이스 클래스를 상속하는 자식 클래스가 해당 인터페이스를 실제로 구현하기 때문입니다.
class English final :
public ILanguage
{
void text() override
{
std::cout << "Hello, English class" << std::endl;
}
};
class Korean final :
public ILanguage
{
void text() override
{
std::cout << "Hello, Korean class" << std::endl;
}
};
English
클래스와Korean
클래스는 모두ILanguage
인터페이스의text()
가상 함수를 오버로딩하여 구현하고 있습니다.
팩토리 패턴: 객체의 생성을 호출할 클래스 구현
- 팩토리를 사용하여 객체들을 생성할 클래스를 구현해봅니다.
class Hello final
{
public:
void greeting(const LanguageType& type)
{
// 인터페이스 클래스로 받으면 타입에 관계 없이 하나로 받을수 있다.
std::shared_ptr<ILanguage> language = Factory::getInstance(type);
language->text();
}
};
- 한 가지 중요한 점은 팩토리 클래스의
getInstance()
의 반환 값은std::make_shared<Korean>()
또는std::make_shared<English>()
입니다. - 하지만 이들의 추상체인
ILanguage
클래스로도Korean
와English
를 받을 수 있으며, 이들의 공통 메서드인text()
를 호출할 수 있습니다. - 타입이
ILanguage
여도 실제로 반환한Korean
와English
클래스의text()
메서드가 실행됩니다.
팩토리 패턴: 실행 코드
int main(const int argc, const char* argv[])
{
// 팩토리 패턴
std::shared_ptr<Hello> hello = std::make_shared<Hello>();
hello->greeting(LanguageType::Korean);
hello->greeting(LanguageType::English);
return 0;
}
이처럼 팩토리 패턴은 생성과 관련된 모든 처리를 별도의 클래스로 위임합니다.
팩토리 패턴의 장/단점은 다음과 같습니다.
장점
- 클래스의 유연성과 확장성이 개선된다.
- 어떤 객체를 생성할지 모르는 초반에 유용하다.
단점
- 생성을 전담할 클래스가 생기므로, 관리해야하는 클래스가 늘어난다.
- 팩토리가 실제로 생성할 객체들의 타입을 알아야 한다.
객체의 타입을 알아야 하는 팩토리 패턴의 단점은 객체의 타입까지도 추상화한 팩토리 메서드 패턴으로 발전하게 됩니다.
심플 팩토리(Simple Factory)
- 팩토리 패턴은 객체의 생성을 외부 클래스로 위임한다고 하였습니다.
- 심플 팩토리는 객체의 생성을 외부 클래스가 아닌 내부적으로 해결하는 방법입니다.
class SimpleFactory final
{
public:
void greeting()
{
std::shared_ptr<ILanguage> language = factory(LanguageType::Korean);
language->text();
}
// 객체의 메서드로 객체 생성을 담당한다.
static std::shared_ptr<ILanguage> factory(const LanguageType& type)
{
if (type == LanguageType::Korean)
{
return std::make_shared<Korean>();
}
else if (type == LanguageType::English)
{
return std::make_shared<English>();
}
}
};
- 이처럼 객체의 생성이 내부 메서드로 이동하였습니다.
- 하지만 실제로 생성할 객체의 타입을 알아야 한다는 단점은 변함 없습니다.