1. Factory Method 패턴이란

1.1 Factory Method 패턴의 정의

객체를 생성하기 위한 인터페이스를 정의하고, 어떤 클래스의 인스턴스를 만들지는 서브클래스에서 결정하게 하는 패턴


정의만읽어서는 이게 무슨 내용인지 알기가 어렵다. UML과 함께 보도록 하자.


1.2 UML



Product - 생성될 객체의 인터페이스

ConcreteProduct - Product 인터페이스를 구현한 구상 클래스.

Creator - 객체를 생성하는 팩토리 메소드를 가진 인터페이스

ConcreteCreator - Creator 인터페이스를 구현한 구상 클래스.


여기서 ConcreteProduct와 ConcreteCreator 여러개의 구상 클래스들을 하나로 묶은것으로 봐도 된다.

즉, ConcreteProduct1,2,3... 이런식으로 여러 종류의 클래스가 Product 인터페이스를 구현 할 수 있고, ConcreteCreatort에 대해서도 마찬가지다.


Creator 인터페이스는 factoryMethod()를 통해 Product 인터페이스 객체를 생성한다. 하지만 Creator클래스는 Product 인터페이스의 구상 클래스들 중 어떤 클래스를 생성할 지 알 수 없다.

어떤 구상 클래스를 생성할지는 ConcreteCreator 클래스에서 factoryMethod()를 오버라이드하여 결정한다.


* Product 인터페이스 객체를 생성한다는 말이 조금 헷갈릴 수도 있을 것 같은데, 추상 클래스나 인터페이스는 원래 객체화가 불가능하다. 

객체화를 할 때는 당연히 Product 인터페이스를 구현한 구상 클래스를 객체화해야하고, 이 구상 클래스를 객체화 한다는 의미로 Product 인터페이스 객체를 생성한다는 말을 사용한 것이다.



2. 적용 예제


Head First Desgin Pattern에 나오는 예제이다.

이전에 Simple Factory 글을 쓸 때 나왔던 예제의 확장판이다.


객체마을 피자가게에서는 클래스를 이용하여 판매하는 피자 종류를 관리하고, 피자를 주문받는다.

피자가게 관리를 위해서 다음의 클래스 구조를 이용하고 있었다.





우선 피자 인터페이와 구상 클래스 중 하나인 CheesePizza 클래스는 다음과 같이 정의된다.

<Pizza.h>

#pragma once #include <string> using namespace std; class Pizza { public: virtual void prepare() = 0; //피자의 재료를 준비한다. virtual void bake() = 0; //피자를 굽는다 virtual void cut() = 0; //피자를 자른다. virtual void box() = 0; //피자를 박스에 담는다. virtual string toString() = 0; //피자의 종류를 스트링으로 리턴한다. };

<CheesePizza.h>

#include <iostream> #include "Pizza.h" using namespace std; class CheesePizza : public Pizza { public: CheesePizza(); void prepare() override; void bake() override; void cut() override; void box() override; string toString() override; };


객체마을 피자가게를 나타내는 PizzaStore 클래스에서 피자를 주문하는 orderPizza 메소드는다음과 같이 구현된다.


Pizza* PizzaStore::orderPizza(string type){

Pizza pizza;


//피자 객체 생성

if(type.compare("cheese") == 0)

pizza = new CheesePizza();

else if(type.compare("pepperoni") == 0)

pizza = new PepperoniPizza();

else if(type.compare("clam") == 0)

pizza = new ClamPizza();

else if(type.compare("veggi") == 0)

pizza = new VeggiPizza();



//피자 재료를 준비하고, 굽고, 자르고, 박스에 담아서 피자를 준비.

pizza.perpare();

pizza.bake();

pizza.cut();

pizza.box();

return pizza;

}


그런데 객체마을 피자나라의 가게 관리 프로그램이 유명해지면서, 여러 도시의 피자가게들이 이 프로그램을 이용 할 수 있게 해달라고 요청했다.


우리는 이 프로그램의 클래스 구조를 수정하여 여러 도시의 피자가게들이 이 프로그램을 이용 할 수있게 새로 만들어야한다.


문제점은, 각 지역마다 피자를 만드는 방식에 차이가 있다는 것이다.


어떤 도시에서는 피자를 만들때 빵은 얇고, 치즈는 조금 적게 넣는 편이고, 또 다른 도시에서는 빵을 두껍게하고 소스를 듬뿍 뿌려 맛을 낸다.


이를 반영하기 위해 피자 클래스는 다음과 같이 지역별로 나뉘어야 한다.


그런데 변경된 피자 클래스 구조를 반영하여, orderPizza() 메소드를 수정하면 다음과 같이 구현 될 것이다.


Pizza* PizzaStore::orderPizza(string type){

Pizza pizza;


//피자 객체 생성

if(type.compare("ChicagoCheese") == 0)

pizza = new ChicagoCheesePizza();

else if(type.compare("ChicagoPepperoni") == 0)

pizza = new ChicagoPepperoniPizza();

else if(type.compare("ChicagoClam") == 0)

pizza = new ChicagoClamPizza();

else if(type.compare("ChicagoVeggi") == 0)

pizza = new ChicagoVeggiPizza();

else if(type.compare("NYCheese")==0)

pizza = new NYCheesePizz();

else if(type.compare("NYVeggi")==0)

pizza = new NYVeggiPizza();

...



//피자 재료를 준비하고, 굽고, 자르고, 박스에 담아서 피자를 준비.

pizza.perpare();

pizza.bake();

pizza.cut();

pizza.box();

return pizza;

}



이렇게 되면 이 프로그램을 이용하는 피자가게가 늘어 날 때마다 if-else 문이 점점 늘어나게 된다.


이런 문제를 해결하기 위해서는 PizzaStore 클래스가 각 지역의 가게들 별로 나누어질 필요가 있다.

그러기 위해서 이제 팩토리 메소드 패턴을 적용하여, 피자가게와 피자 클래스를 다음과 같은 구조로 만든다.



이 다이어그램과 1.2의 UML과 비교하면, 각 요소는 다음과 같이 맵핑된다.


PizzaStore 인터페이스 - Creator 인터페이스

NYPizzaStore, ChicagoPizzaStore - ConcreteCreator

Pizza 인터페이스 - Product 인터페이스

NYCheesePizza,NYVegiPizza,...ChacigoClamPizza,ChacigoPepperoniPizza - ConcreteProduct



그리고 PizzaStore, NYPizzaStore, ChicagoPizzaStore는 각각 다음과 같이 정의/구현된다.

<PizzaStore.h>

#pragma once #include <string> #include "../Pizza/Pizza.h" using namespace std; //피자 가게 추상 클래스 class PizzaStore { public:

    //피자를 주문. Pizza* orderPizza(const string& type);  protected:

// 피자를 생성. 없는 타입이면 NULL 리턴. virtual Pizza* createPizza(const string& type) = 0; };

<PizzaStore.cpp>

#include "PizzaStore.h" PizzaStore::PizzaStore() { } Pizza* PizzaStore::orderPizza(const string& type) { Pizza * pizza; pizza = createPizza(type); pizza->prepare(); pizza->bake(); pizza->cut(); pizza->box(); return pizza; }

<NYPizzaStore.cpp>

#include "NYPizzaStore.h" NYPizzaStore::NYPizzaStore() { } Pizza * NYPizzaStore::createPizza(const string& type) { if (type.compare("cheese")) return new NYCheesePizza(); else if (type.compare("veggi")) return new NYVeggiPizza(); else if (type.compare("clam")) return new NYClamPizza(); else if (type.compare("pepperoni")) return new NYPepperoniPizza(); return NULL; }

<ChicagoPizzaStore.cpp>

#include "ChicagoPizzaStore.h" ChicagoPizzaStore::ChicagoPizzaStore() { } Pizza * ChicagoPizzaStore::createPizza(const string& type) { if (type.compare("cheese")) return new ChicagoCheesePizza(); else if (type.compare("veggi")) return new ChicagoVeggiPizza(); else if (type.compare("clam")) return new ChicagoClamPizza(); else if (type.compare("pepperoni")) return new ChicagoPepperoniPizza(); return NULL; }

구현된 프로젝트는 다음의 깃허브에서 확인 할 수 있다.

(https://github.com/InvincibleTyphoon/FactoryMethodPattern)

책에서는 Java를 사용하지만, 개인적으로 C++을 더 선호하는 관계로, 구현은 C++로 했다.


3. Factory Method 패턴의 장점

- 의존성 뒤집기 원칙이 지켜진다.

- 객체의 생성을 서브클래스가 담당한다

- 구상 클래스가 아닌 추상 클래스/인터페이스에 맞춰 구현 할 수 있음


Factory Method 패턴에서는 객체생성시 저수준 구성요소인 ConcreteProduct 대신 고수준 구성요소인 Product에 의존 하게된다.

피자가게 예제에서, PizzaStore 인터페이스는 피자 객체를 생성할 때, 저수준 구성요소인 NYCheesePizza, ChicagoClamPizza 등에 의존하지 않고, 고수준 구성요소인 Pizza에 의존한다.


객체의 생성을 서브클래스가 담당하기 때문에 생성할 객체 타입을 예측 할 수 없을 때 유용하고, 변화에 더 유연하게 대처 할 수 있다.

가령 위의 피자가게 예제에서, 캘리포니아(Califonia)의 피자가게에서도 이 프로그램을 이용 할 수 있게 해달라고 한다면 CalifoniaCheesePizza 등의 피자 클래스와 CalifoniaPizzaStore 클래스를 추가 해주면 되고, 뉴욕 피자가게에서 더이상 ClamPizza를 판매하지 않는다고 하면 NYClamPizza 클래스를 삭제하고 NYPizzaStore 클래스의 createPizza() 메소드에서 ClamPizza를 주문하는 부분을 삭제하면 된다.


그리고 구상 클래스가 아닌 추상 클래스/인터페이스에 맞춰 구현 할 수 있다는 점도 프로그램의 유연성을 늘려준다.





4. Factory Method 패턴 이용 시 유의 할 점(단점)

- 상속을 이용하기 때문에 서브클래싱이 안되는 경우를 고려해야한다.


C#를 예로 들면 C#에서는 인터페이스를 제외한 추상 클래스, 일반 클래스를 다중 상속 할 수 없다. 그래서 Creator가 모든 메소드를 가상 메소드로 선언한 인터페이스이거나, ConcreteCreator의 유일한 부모 클래스가 되어야 한다.

그리고 그 외의 대부분의 언어에서도 다중 상속을 사용하면 모호성이 발생할 수 있기 때문에 다중상속은 이용하지 않는 것을 권장한다.



5. 레퍼런스

- Head First Design Pattern(O'REILLY media)

- GoF의 디자인 패턴(Pearson Addison Wesley)

- 위키피디아 : Factory Method Pattern

(https://en.wikipedia.org/wiki/Factory_method_pattern)

- 윤성우저 열혈C++프로그래밍(Orange 

블로그 이미지

서기리보이

,