참고: Head First Design Patterns
피자 가게 프로그램을 개발중이다. 피자를 만드는 메소드를 구현해보자.
Pizza orderPizza() {
Pizza pizza = new Pizza();
pizza.prepare();
pizza.bake();
pizza.cut();
pizza.box();
return pizza;
}
위와 같이 만들 수 있겠다. 그런데 보통 피자가 한 종류는 아닐테니, 피자 종류를 인자로 받아보자.
Pizza orderPizza(String type) {
Pizza pizza;
if(type.equals("cheese"))
pizza = new CheesePizza();
else if(type.equals("greek"))
pizza = new GreekPizza();
else if(type.equals("pepperoni"))
pizza = new PepperoniPizza();
else if ...
pizza.prepare();
pizza.bake();
pizza.cut();
pizza.box();
return pizza;
}
그래서, 조건문을 통해 Pizza 인터페이스의 적절한 구현체를 생성해주기로 했다.
언제나 그렇듯 나이브한 코드의 문제점은 기능의 추가, 삭제 등 유지보수 단계에서 발생한다. 코드가 잘 돌아가게 하는 것 만이 목표라면 위의 코드는 문제가 될 것이 없다.
그러나 우리 피자가게는 비인기 메뉴인 그릭 피자를 없애는 대신, 조개 피자와 야채 피자를 신메뉴로 추가하기로 결정한다.
if(type.equals("cheese"))
pizza = new CheesePizza();
else if(type.equals("pepperoni"))
pizza = new PepperoniPizza();
else if(type.equals("clam"))
pizza = new ClamPizza();
else if(type.equals("veggie"))
pizza = new VeggiePizza();
조건문에서 그릭 피자를 빼고 조개와 야채 피자를 추가하였다. 그런데, 매번 이렇게 orderPizza() 메소드를 수정하는 것이 과연 옳은가? 우리는 피자의 종류를 결정하는 코드 외에는 바뀌지 않기를 원한다. 반면, 피자 메뉴가 바뀔 때마다 피자를 결정하는 조건문은 수정되어야 한다.
피자를 생성해 줄 새로운 객체 하나를 만들어 보자. 이런 객체(혹은 메소드)를 팩토리(Factory)라고 부른다.
public class SimplePizzaFactory {
public Pizza createPizza(String type) {
Pizza pizza = null;
if(type.equals("cheese"))
pizza = new CheesePizza();
else if(type.equals("pepperoni"))
pizza = new PepperoniPizza();
else if(type.equals("clam"))
pizza = new ClamPizza();
else if(type.equals("veggie"))
pizza = new VeggiePizza();
return pizza;
}
}
위와 같이 피자 생성을 해줄 SimplePizzaFactory를 만들었다. 이제 피자 메뉴에 변동이 생기더라도 그와 무관한 orderPizza()는 전혀 건들지 않아도 된다!
이제 다시 orderPizza()로 돌아가보자
Public class PizzaStore {
SimplePizzaFactory factory;
public PizzaStore(SimplePizzaFactory factory) {
this.factory = factory;
}
Pizza orderPizza(String type) {
Pizza pizza = factory.createPizza(type)
pizza.prepare();
pizza.bake();
pizza.cut();
pizza.box();
return pizza;
}
}
위와 같은 단순한 구조의 팩토리는 디자인 패턴이라고 부르지는 않지만, 자주 사용된다. 뒤에서 더 자세히 다룰 팩토리 패턴과 구분하기 위해서 심플 팩토리라고 부른다.
Pizza pizza = createPizza(type)
사실 우리의 SimplePizzaFactory처럼 객체를 만드는 대신 정적인 메소드를 이용하는 것도 흔한 방법이다. 이를 정적 팩토리(Static Factory)라고 부른다. 구현하기 단순하고 간단하지만, create메소드의 행동에 변화를 주고 싶을 때 상속을 활용하지 못한다는 단점도 있다.
본격적으로 팩토리 패턴에 대해 알아보기 전에, 우리의 피자 가게가 성공하는 바람에 프랜차이즈 매장을 오픈하게 되었다! 뉴욕, 시카고, 그리고 캘리포니아에 분점을 오픈하기로 한다. 그런데, 각 지역 주민들이 선호하는 로컬 피자 스타일에 맞춰 우리도 지역 분점마다 다른 피자를 만들기로 하자. 예를 들면 뉴욕 피자는 얇은 크러스트, 시카고 피자는 두꺼운 크러스트 등.
우리의 SimplePizzaFactory를 이용해 NYPizzaFactory, ChicagoPizzaFactory, CaliforniaPizzaFactory를 만들면 되지 않을까?
PizzaStore nyStore = new PizzaStore(nyFactory);
PizzaStore chicagoStore = new PizzaStore(chicagoFactory);
PizzaStore californiaStore = new PizzaStore(californiaFactory);
그러고는 PizzaStore의 생성자에 적절한 팩토리를 넘겨주자! 그러나, 이렇게 구현하니 분점에 대한 관리가 안되기 시작했다. 뉴욕, 시카고, 캘리포니아 모두 제각각 PizzaStore 이기 때문에 이들 사이에 일관된 퀄리티 유지가 어려워 진 것이다.
PizzaStore들을 효과적으로 통제할 방법이 없을까?우리는 create 기능을 다시 PizzaStore에게 줄 것이다. 다만, 이번엔 추상 메소드의 형태로 구현 할 것이다.
그러기 위해서는 PizzaStore역시 추상 클래스여야 할 것이다.
public abstract class PizzaStore {
public Pizza orderPizza(String type) {
Pizza pizza;
pizza = createPizza(type);
pizza.prepare();
pizza.bake();
pizza.cut();
pizza.box();
return pizza;
}
protected abstract Pizza createPizza(String type);
...
}
이제PizzaStore의 서브클래스들이 가질 createPizza() 메소드를 직접 구현해보자.
public Pizza NYPizzaStore {
public Pizza createPizza(String type) {
if(type.equals("cheese"))
return new NYStyleCheesePizza();
else if(type.equals("pepperoni"))
return new NYStylePepperoniPizza();
else if ...
else
return null;
}
}
public Pizza ChicagoPizzaStore {
public Pizza createPizza(String type) {
if(type.equals("cheese"))
return new ChicagoStyleCheesePizza();
else if(type.equals("pepperoni"))
return new ChicagoStylePepperoniPizza();
else if ...
else
return null;
}
}
...
Pizza를 인스턴스화 하는 모든 책임이 팩토리로써 역할을 수행하는 하나의 메소드에게 넘어갔다. 이러한 메소드를 팩토리 메소드라고 부르며, 피자의 생성은 이 팩토리 메소드를 구현한 서브클래스들이 결정하도록 함으로써 캡슐화한다.

클래스 다이어그램으로 나타낸다면 위와 같다. PizzaStore처럼 팩토리 메소드가 정의된 추상 클래스를 크리에이터(Creator), Pizza처럼 크리에이터의 생성 대상을 프로덕트(Product)라고 한다.
이런 팩토리 메소드를 이용한 디자인 패턴을 우리는 다음과 같이 정의한다.
객체 생성을 위한 인터페이스이나, 인스턴스화 할 클래스에 대한 결정권은 서브클래스들이 갖도록 하는 디자인 패턴.
여기서 결정이라는 표현을 쓴 이유는, 크리에이터는 팩토리 메소드를 제공하나 어떤 프로덕트가 실제로 생성될지 전혀 알지 못하는 추상 메소드이기 때문에 프로덕트는 전적으로 크리에이터의 서브클래스의 선택에 의해 생성되기 때문이다. (단, 항상 추상 메소드로 구현되어야 하는 것은 아니다. 디폴트 팩토리 메소드를 정의할 수도 있다.)
이러한 디자인 패턴의 진가는 의존성에 관련하여 나온다.
Dependency Inversion Principle(DIP) :
경고한 구현체 대신 추상화에 대해 의존하라.
얼핏 들으면 "구현체 대신 인터페이스에 대해 프로그래밍하라"와 상당히 비슷해 보인다. 그러나, 이 '의존성 역전 원칙'은 좀 더 나아가
하이레벨(High-Level) 객체들은 로우레벨(Low-Level) 객체들에게 의존하면 안되며, 둘 모두 추상화에 의존해야 된다.
라는 의미를 담고있다. 여기서 하이레벨 객체는 로우레벨 객체에 대한 행동이 정의된 객체를 의미한다. 우리의 PizzaStore이 하이레벨, Pizza가 로우레벨 객체라고 볼 수 있다.

잠시 우리가 팩토리 메소드 패턴을 적용하기 이전, 그러니까 orderPizza() 메소드 안에서 조건문으로 피자 만들던 시절로 돌아가보자. 이 때의 PizzaStore 과 Pizza들의 의존성을 나타내면 위와 같다. Pizza의 변경은 곧 PizzaStore의 코드 수정으로 이어지므로, PizzaStore가 Pizza에 의존한다 라고 표현할 수 있다.

PizzaStore은 더이상 Pizza의 구현체들을 직접 생성하지 않는다. 따라서, 오직 Pizza에 의존한다. CheesePizza등은 Pizza의 구현체이기에 당연하게도 Pizza에 의존한다. 즉, 하이레벨 객체와 로우레벨 객체 모두 추상 클래스인 Pizza에게 의존한다! 정확히 위에서 살펴본 디자인 원칙에 부합한다고 할 수 있다.
위의 두 다이어그램을 비교해보면 의외로 쉽게 깨달을 수 있다. 하이레벨 객체들의 의존을 받던 로우레벨 객체들이, 자신들보다 상위레벨의 추상 클래스를 의존하도록 화살표의 방향이 역전되었기 때문이다. 다만, 모든 원칙들이 그렇듯 최대한 지키려고 노력하되, 모든 코드를 철저히 위 원칙대로만 짤 필요는 없다.
다시 우리의 피자가게로 돌아와보자. 이제 남은 문제는 피자에 들어갈 재료 관리이다. 뉴욕, 시카고, 캘리포니아의 피자가게마다 같은 피자여도 재료와 소스에 차이가 있다고 해보자.
예시)
이런 경우에는 어떻게 하는 것이 좋을까? 아! 재료를 위한 팩토리를 만들면 되겠구나!
public interface PizzaIngredientFactory {
public Dough createDouch();
public Sauce createSauce();
public Cheese createCheese();
public Veggies[] createVeggies();
public Pepperoni createPepperoni();
public Clams createClam();
}
위와 같이 재료를 위한 create 메소드들을 묶은 인터페이스를 만들었다. 이를 바탕으로 뉴욕 피자를 위한 팩토리 클래스를 만들어보자.
public class NYPizzaIngredientFactory implements PizzaIngredientFactory {
public Dough createDough() { return new ThinCrustDough(); }
public Sauce createSauce() { return new MarinaSauce(); }
public Cheese createCheese() { return new ReggianoCheese(); }
public Veggies[] createVeggies() {
Veggies[] veggies = {
new Garlic(), new Onion(), new Mushroom, new RedPepper()
};
return veggies;
}
public Pepperoni createPepperoni() { return new SlicedPepperoni(); }
public Clams createClam() { return new FreshClams(); }
}
이제 이 팩토리 클래스가 어떻게 사용되는지를 살펴보자.
public abstract class Pizza {
String name;
Dough dough;
Sauce sauce;
...
}
public class CheesePizza extends Pizza {
PizzaIngredientFactory ingredientFactory;
public CheesePizza(PizzaIngredientFactory ingredientFactory) {
this.ingredientFactory = ingredientFactory;
}
void prepare() {
dough = ingredientFactory.createDough();
sauce = ingredientFactory.createSauce();
cheese = ingredientFactory.createCheese();
}
}
위 코드와 같이 재료들의 생성은 팩토리 클래스가 도맡아서 한다. 도우, 소스 등의 Ingredients들은 오롯이 PizzaIngredientFactory에만 의존하며, Pizza는 이들에 대해 전혀 관심을 갖지 않는다.
마지막으로, PizzaStore의 createPizza() 메소드 구현을 마무리해보자.
public class NYPizzaStore extends PizzaStore {
public Pizza createPizza(String type) {
Pizza pizza = null;
PizzaIngredientFactory ingredientFactory
= new NYPizzaIngredientFactory();
if(type.equals("cheese"))
pizza = new CheesePizza(ingredientFactory)
else if(type.equals("pepperoni"))
pizza = new PepperoniPizza(ingredientFactory);
else if ...
return pizza;
}
}
NYStyleCheesePizza처럼 각 지역마다 Pizza를 구현했던것에 비해, Pizza에게 적절한 팩토리 클래스를 넘겨주면 알아서 지역에 맞는 재료들를 생성해준다.
이처럼 추상 팩토리 패턴을 이용하면 여러 프로덕트의 묶음을 생성하는데에 유리하다. 어떻게 보면, 팩토리 메소드들을 한 인터페이스에 묶어서 더 큰 팩토리를 만들었다고도 볼 수 있다. 따라서, 추상 팩토리 패턴은 아래와 같이 정의한다.
연관되거나 의존적인 객체들의 묶음을 생성해주는 인터페이스를 제공하는 디자인 패턴
팩토리 메소드 패턴
- 팩토리 메소드의 오버라이딩을 통해
create()메소드를 구현- 상속을 이용
추상 팩토리 패턴
- 인터페이스(혹은 추상클래스)의
create()메소드를 직접 구현- 구성(Composition)을 이용
비슷한 듯 다른 두 팩토리 패턴의 차이를 정리하면 위와 같다. 생성해야 하는 프로덕트들이 서로 연관된 여러 프로덕트들(우리의 ingredients들 처럼)인지, 그렇지 않은지(Pizza 처럼)를 잘 분석해서 적절한 패턴을 적용하면 된다.