패턴에 대해 공부하다 보면 팩토리 패턴에 대해 자주 나옵니다. 그만큼 유용하고, 많이 쓰이는 패턴이죠. 팩토리 패턴의 기본 개념은 아래와 같습니다.
enum CLASS_TYPE { CLASS_FOO, CLASS_BAR }; IBaseObject* CFactory::CreateInstance(CLASS_TYPE classType) { switch (classType) { case CLASS_FOO: return new CFoo(); case CLASS_BAR: return new CBar(); } return nullptr; }
팩토리 패턴을 사용하게 되면 아래와 같은 장점이 있습니다.
- 객체의 추상화 단계가 높아진다
- 객체 생성이 팩토리 메소드에 캡슐화가 이루어진다.
- 사용자는 생성하는 객체에 대해 알필요가 없어진다.
- 객체 추가 삭제가 용이해진다
다만, 위와 같은 형태의 팩토리 메소드에는 몇 가지 단점이 있습니다. 일단, 객체가 추가 되거나 삭제 될때마다 코드를 변경 해주어야합니다. enum 값을 추가하고, 팩토리 메소드 안에 새로운 new 코드를 추가해야 하죠. 또 한가지 치명적인 문제가 해당 소스 파일에서 생성하려고 하는 객체의 선언을 알고 있어야 합니다. 팩토리 메소드에서 생성할수 있는 객체의 헤더를 모두 include 해놔야 하죠.
이런 문제점을 해결한 팩토리 클래스를 만들어보겠습니다.
/////////////////////////////////// Factory.h class IBaseObject; class CFactory { public: // 팩토리 클래스는 싱글톤으로 제작 static CFactory* Instance(); // 객체 생성 함수(팩토리 메소드) IBaseObject* CreateInstance(std::string name); // 객체 생성 람다 등록 함수 void RegisterFactoryFunction(std::string name, std::function<IBaseObject*(void)> classFactoryFunction); private: // 객체 생성 람다가 저장될 컨테이너 타입 using FactoryRegistry = std::unordered_map<std::string, std::function<IBaseObject*(void)>>; FactoryRegistry mFactoryRegistry; }; // 객체 생성 람다를 팩토리에 등록시켜주는 템플릿 클래스 template<class T> class CRegistrar { public: CRegistrar(std::string className) { CFactory::Instance()->RegisterFactoryFunction(className, [](void) -> IBaseObject* { return new T(); }); } }; // 객체를 팩토리에 등록 시켜주는 매크로 #define REGISTER_CLASS(NAME, TYPE) \ CRegistrar<TYPE> registrar_##TYPE(NAME); /////////////////////////////////// Factory.cpp CFactory* CFactory::Instance() { static CFactory factory; return &factory; } IBaseObject* CFactory::CreateInstance(std::string name) { auto it = mFactoryRegistry.find(name); if (it != mFactoryRegistry.end()) return it->second(); return nullptr; } void CFactory::RegisterFactoryFunction(std::string name, std::function<IBaseObject*(void)> factoryFunc) { mFactoryRegistry[name] = factoryFunc; }
유연한 객체 등록을 위해 enum아 아닌 문자열을 사용합니다. 그리고 unodered_map 컨테이너를 이용해 객체를 생성 해주는 람다를 저장합니다. CreateInstance 함수를 통해 객체의 이름을 검색하여, 해당 객체 생성 람다를 호출하게 됩니다.
실제 사용 예제는 아래와 같습니다.
/////////////////////////////////// Object.h class CFoo : public IBaseObject { public: virtual void Print() override; }; class CBar : public IBaseObject { public: virtual void Print() override; }; /////////////////////////////////// Object.cpp void CFoo::Print() { std::cout << "This is Foo" << std::endl; } REGISTER_CLASS("foo", CFoo); void CBar::Print() { std::cout << "This is Bar" << std::endl; } REGISTER_CLASS("bar", CBar); /////////////////////////////////// main.cpp int _tmain(int argc, _TCHAR* argv[]) { // 해당 객체 헤더를 include 할 필요 없다. auto one = CFactory::Instance()->CreateInstance("foo"); auto two = CFactory::Instance()->CreateInstance("bar"); one->Print(); two->Print(); return 0; }
새로운 객체를 만들면서 팩토리 클래스는 전혀 수정할 필요 없습니다. 팩토리 클래스에 Object.h 헤더를 추가 할 필요도 없습니다. 등록 매크로를 통해 자동으로 객체의 이름과 타입이 팩토리 클래스에 등록이 되고, 객체를 생성하고, 사용 하는 곳에서도 객체의 형태를 전혀 모르는 상태에서 객체의 이름만 알면 생성 및 사용이 가능해졌습니다.