1. Abstract Factory 패턴이란

1.1 Abstract Factory 패턴의 정의

인터페이스를 이용하여 서로 연관된, 또는 의존하는 객체를 구상 클래스를 지정하지 않고도 생성할 수 있게 하는 패턴


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


1.2 UML




AbstractFactoty - 각각의 Product 객체를 생산하는 메소드를 지닌 추상 팩토리 인터페이스

ConcreteFactory - AbstractFactory 인터페이스를 구현한 구상 팩토리 클래스

Client - 팩토리에게 객체의 생성을 요청하는 클래스

AbstractProductA,B - 팩토리에 의해 생성되는 클래스들에 대한 인터페이스

ProductA_1,A_2,B_1,B_2 - 실제 생성될 Product 구상 클래스





2. 적용 예제


Head First Desgin Pattern에 나오는 예제를 수정한 것이다.

앞부분 내용은 Factory Method 패턴 때 썼던 글과 똑같으니 대충 읽어봐도 괜찮을 듯 하다.


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

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





우선 피자 인터페이와 구상 클래스 중 하나인 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 문이 점점 늘어나게 된다.


이런 문제를 해결하기 위해서 추상 팩토리 패턴을 적용하여 피자 객체 생성의 역할을 팩토리에게 넘기도록 다음의 구조로 수정한다.

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


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


PizzaStore - Client

PizzaFactory - AbstractFactory

NYPizzaFactory, ChicagoFactory - ConcreteFactory1,2

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

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


그리고 PizzaFactory, NYPizzaFactory는 다음과 같이 정의/구현된다.

<PizzaFactory.h>

#pragma once #include <string> #include "../Pizza/Pizza.h" class PizzaFactory { public: //type을 기준으로 피자를 리턴 //만약 없는 타입이라면 NULL 리턴 virtual Pizza* createPizza(const string& type) = 0; };


<NYPizzaFactory.h>

class NYPizzaFactory : public PizzaFactory { public: NYPizzaFactory(); Pizza* createPizza(const string& type) override; };

<NYPizzaFactory.cpp>

NYPizzaFactory::NYPizzaFactory() { } Pizza* NYPizzaFactory::NYPizzaFactory::createPizza(const string& type) { Pizza * pizza = NULL; if (type.compare("cheese") == 0) pizza = new NYCheesePizza(); else if (type.compare("pepperoni") == 0) pizza = new NYPepperoniPizza(); else if (type.compare("clam") == 0) pizza = new NYClamPizza(); else if (type.compare("veggi") == 0) pizza = new NYVeggiPizza(); return pizza; }


이렇게 구현하고 나면, 사용 할 때는 PizzaStore를 생성 할 때는 다음과 같이 각 분점에 맞는 PizzaFactory를 넘겨주면 된다.


//뉴욕 피자 팩토리를 넘겨줘서 뉴욕식 피자를 만드는 PizzaStore 생성

PizzaStore nyPizzaStore(&nyPizzaFactory);

//시카고 피자 팩토리를 넘겨줘서 시카고 피자를 만드는 PizzaStore 생성 PizzaStore chicagoPizzaStore(&chicagoPizzaFactory);


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

(https://github.com/InvincibleTyphoon/AbstractFactoryPattern/tree/master)

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


3. Abstract Factory 패턴의 장점

- 객체 생성을 캡슐화 할 수 있다.

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

- 관련 있는 클래스들을 묶어서 하나의 제품군으로 만들 수 있다.

- 싱글톤 패턴을 적용하기 적합하다.


객체를 생성하는 부분이 팩토리에 캡슐화되므로 코드 중복을 방지 할 수 있다.


구상 클래스가 아닌 추상 클래스/인터페이스에 맞춰 구현 할 수 있기 때문에 특정 구현에 덜 의존적이고, 프로그램을 더 유연하게 만들어 준다.


구상 팩토리가 생산해내는 제품별로 클래스를 분류 할 수 있다. 이 덕분에 제품군을 대체하기 쉽고, 서로 연관된 여러 제품들을 만들어야 할 때 유용하다.


한 제품군을 생산하는 팩토리는 프로그램 내에서 한개로 충분하다. 그래서 Abstract Factory 패턴을 적용할 때는 싱글톤 패턴으로 구현하는 것도 좋은 방법이 될 수 있다.


4. Abstract Factory 사용시 유의사항(단점)

- 새로운 종류의 제품을 제공하기 어렵다.


Abstract Factory 인터페이스가 생성 할 수 없는 새로운 종류의 제품이 생긴다면 그 제품을 반영하기 위해서는 Abstract Factory 와 모든 구상 팩토리들을 수정해야한다.


가령 위의 피자가게 예제에서 피클을 생산하는 기능을 추가하려면, PizzaFactory 인터페이스에 createPickle() 메소드가 추가되어야하고, NYPizzaFactory, ChicagoPizzaFactory 클래스에서 createPickle() 메소드를 구현해야한다.


5. 레퍼런스

- Head First Design Pattern(O'REILLY media)

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

블로그 이미지

서기리보이

,