느슨한 결합으로 객체지향 디자인을 만들어 봅시다
new 연산자를 사용한다고 해서 새로운 객체가 만들어지지는 않습니다. 4장에서는 객체의 인스턴스를 만드는 작업이 항상 공개되어야 하는 것은 아니며, 오히려 모든 것을 공개했다가는 결합 문제가 생길 수 있다는 사실을 배웁니다. 설마 그런 골치 아픈 문제를 원하는 사람은 없겠죠? 팩토리 패턴으로 불필요한 의존성을 없애서 결합 문제를 해결하는 방법을 알아봅시다.
이전에 특정 구현을 바탕으로 프로그래밍하지 않아야 한다는 원칙을 배웠다. 그러나 new
를 사용하게 되면 구상 클래스의 인스턴스가 만들어지므로 결국 특정 구현을 사용하게 되는 셈이다.
new
는 자바의 뼈대를 이루는 연산자이므로 아예 사용하지 않을 수는 없다. 이 때 인터페이스에 맞춰서 코딩하면 시스템에서 일어날 수 있는 여러 변화에 대응할 수 있다. 인터페이스를 바탕으로 만들어진 코드는 어떤 클래스든 특정 인터페이스만 구현하면 사용할 수 있기 때문이다. 이는 모두 다형성
덕분이다.
반대로 구상 클래스를 많이 사용하면 새로운 구상 클래스가 추가될 때마다 코드를 고쳐야 하므로 변경에 닫혀 있는 코드가 되는 셈이다. 새로운 구상 형식을 써서 확장해야 할 때는 어떻게 해서든 다시 열 수 있게 만들어야 한다.
어떻게 하면 애플리케이션에서 구상 클래스의 인스턴스 생성 부분을 전부 찾아내서 애플리케이션의 나머지 부분으로부터 분리(캡슐화)할 수 있을까?
피자 가게에서 피자를 주문하는 메소드(orderPizza()
)를 만들어보자.
특정 메뉴에 대한 주문이 들어오면 해당 피자 종류를 바탕으로 구상 클래스의 인스턴스를 만들고 인스턴스 변수에 해당 인스턴스를 대입하는 식으로 만들 수 있다. 그런데 만약 판매하는 피자 종류가 바뀐다면 매번 코드를 변경해줘야 한다.
그렇다면 인스턴스를 만드는 구상 클래스를 선택하는 부분을 캡슐화하면 되지 않을까?
객체 생성 부분을 orderPizza()
메소드에서 뽑아내야 한다. 우선 객체 생성 코드만 따로 빼서 피자 객체를 만드는 일만 전담하는 객체에 넣어보자.
이렇게 새로 만들 객체는 팩토리
가 된다. 객체 생성을 처리하는 클래스를 팩토리라고 하기 때문이다.
simplePizzaFactory
를 만들고 나면 orderPizza()
메소드는 새로 만든 이 객체의 클라이언트가 된다. 즉, 새로 만든 객체를 호출하는 것이다. 피자가 필요할 때마다 피자 공장에 피자를 하나 만들어 달라고 부탁하는 셈이다.
이제 orderPizza()
메소드에서는 Pizza 인터페이스를 구현하는 피자를 받아서 그 인터페이스에서 정의했던 준비, 굽기, 자르기, 박싱하기 등의 메소드만 호출하면 된다.
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;
}
}
이렇게 캡슐화하면 무슨 장점이 있을까? 원래 코드에 있던 일부를 그냥 다른 객체로 넘긴 게 전부 아닌가?
앞서 본 예시에서는 orderPizza()
메소드만 있었지만, 이렇게 새로 만들어낸 겍체를 사용하는 클라이언트가 매우 많을 수도 있다. 그런 상황에서 피자 객체 생성 작업을 팩토리 클래스로 캡슐화해 놓으면 구현을 변경할 대 여기저기 고칠 필요 없이 팩토리 클래스 하나만 고치면 된다.
팩토리를 새로 만들어주었다면 이전 orderPizza()
메소드에서 객체를 생성하는 부분에 썼던 코드 대신 들어갈 코드가 필요하다. 클라이언트 코드는 어떻게 고치면 될까?
public class PizzaStore {
SimplePizzaFactory factory; // PizzaStore에 SimplePizzaFactory의 레퍼런스를 저장한다.
public PizzaStore(SimplePizzaFactory factory) { // PizzaStore의 생성자에 팩토리 객체가 전달된다.
this.factory = factory;
}
public Pizza orderPizza(String type) { // orderPizza() 메소드는 팩토리로 피자 객체를 만든다.
Pizza pizza;
pizza = factory.createPizza(type); // new 연산자 대신 팩토리 객체의 메소드를 쓴다.
pizza.prepare();
pizza.bake();
pizza.cut();
pizza.box();
return pizza;
}
}
위 코드를 보면 new
연산자 대신 팩토리 객체의 메소드를 쓴 것을 볼 수 있다. 이제 더 이상 구상 클래스의 인스턴스를 만들 필요가 없는 것이다!
Simple Facotry
는 디자인 패턴이라기 보다는 프로그래밍에서 자주 쓰이는 관용구에 가깝다. 이 간단한 팩토리를 '팩토리 패턴' 이라고 부르는 사람들도 있다. 하지만 엄밀히 말하면 패턴은 아니다!
간단한 팩토리는 왜 쓰이는 것일까?
PizzaStore
: 팩토리를 사용하는 클라이언트
이제 PizzaStore는 SimplePizzaFactory로부터 피자 인스턴스를 받게 된다.
SimplePizzaFactory
: 피자 객체를 생성하는 팩토리
이 애플리케이션에서 유일하게 구상 Pizza 클래스를 직접 참조하는 부분이다.
Pizza
: 팩토리에서 만드는 피자
메소드를 오버라이드해서 쓸 수 있도록 추상 클래스로 정의했다.
CheesePizza
, VeggiePizza
, ClamPizza
, PepperomniPizza
: 팩토리에서 생산하는 제품에 해당하는 구상 클래스
각 피자는 Pizza 인터페이스를 구현해야 하며, 구상 클래스여야 한다.
이 두 조건을 만족하면 팩토리에서 피자를 만들고 클라이언트로 넘길 수 있다.
다양한 스타일의 피자를 만들 수 있도록 여러 팩토리를 만들어보자.
앞서 배운 방식대로 하면 SimplePizzaFactory
를 삭제하고, 피자 종류별 여러 팩토리를 만든 다음 PizzaStore
에서 적당한 팩토리를 사용하도록 하면 된다.
하지만 이렇게 하게 되면 한 가지 문제점이 생긴다. 지점별로 회사에서 만든 팩토리로 피자를 만들긴 하는데, 굽는 방식이 달라진다거나 종종 피자를 자르는 것을 까먹는 일이 생길 수도 있다.
이 문제를 해결하려면 PizzaStore
와 피자 제작 코드 전체를 하나로 묶어주는 프레임워크를 만들어야 한다. 물론 유연성도 잃어서는 안된다. 그러나 SimplePizzaFactory
를 만들기 전에 썼던 코드에서는 피자를 만드는 코드가 PizzaStore
와 직접 연결되어 있긴 했지만 유연성이 전혀 없었다.
어떻게 해야 피자 가게와 피자 만드는 과정을 하나로 묶을 수 있을까?
피자를 만드는 일 자체는 전부 PizzaStore
클래스에서 진행하면서도 지점의 스타일을 살릴 수 있는 방법이 있다.
createPizza()
메소드를 다시 PizzaStore
에 넣고 이번에는 이 메소드를 추상 메소드로 선언한 뒤, 지역 스타일에 맞게 PizzaStore
의 서브 클래스를 만들어보도록 하자.
public abstract class PizzaStore {
public Pizza createPizza(String type){
Pizza pizza;
Pizza pizza = createPizza(type); // 팩토리 객체가 아닌 PizzaStore에 있는 createPizza를 호출한다.
pizza.prepare();
pizza.bake();
pizza.cut();
pizza.box();
return pizza;
}
abstract Pizza createPizza(Stirng type); // 팩토리 객체 대신 이 메소드를 사용한다. 즉, 팩토리 메소드가 PizzaStore의 추상 메소드로 바뀌었다.
}
PizzaStore
의 orderPizza()
메소드에 이미 주문 시스템이 잘 갖춰져 있다. 이제 각 지점마다 달라질 수 있는 것은 피자 스타일 뿐이다. 달라지는 점은 createPizza()
메소드에 넣고 그 메소드에서 해당 스타일의 피자를 만들도록 하면 된다. 따라서 PizzaStore
의 서브클래스에서 createPizza()
메소드를 구현한다.
PizzaStore
의 orderPizza()
메소드는 추상 클래스인 PizzaStore
클래스에 정의되어 있다. 그 클래스의 서브클래스를 만들기 전까지는 구상 클래스가 만들어지지 않는다.
orderPizza()
메소드에서 Pizza 객체를 가지고 여러 가지 작업을 하지만, Pizza는 추상 클래스라서 orderPizza()
는 실제로 어떤 구상 클래스에서 작업이 처리되고 있는지 전혀 알 수 없다. 즉, PizzaStore와 Pizza는 서로 완전히 분리되어 있다.
orderPizza()
에서 createPizza()
를 호출하면 Pizza의 서브 클래스가 그 호출을 받아서 피자를 만든다. 어떤 종류의 피자가 만들어지는지는 피자를 주문하는 피자 가게에 따라 달라진다.
각 지점에서는 PizzaStore의 서브클래스를 만들고 지역별 특성에 맞게 createPizza()
메소드만 구현하면 PizzaStore의 기능을 그대로 받아서 쓸 수 있다.
예를 들어 뉴욕 스타일의 피자는 아래의 코드를 사용해 만들 수 있다.
public class NYPizzaStore extends PizzaStore {
Pizza createPizza(String item) {
if (item.equals("cheese")) {
return new NYStyleCheesePizza();
} else if (item.equals("veggie")) {
return new NYStyleVeggiePizza();
} else if (item.equals("clam")) {
return new NYStyleClamPizza();
} else if (item.equals("pepperoni")) {
return new NYStylePepperoniPizza();
} else return null;
}
}
PizzaStore
를 수정한 결과, 구상 클래스 인스턴스 만드는 일을 하나의 객체가 전부 처리하는 방식에서 일련의 서브클래스가 처리하는 방식으로 바뀌었다.
public abstract class PizzaStore {
public Pizza createPizza(String type){
Pizza pizza;
Pizza pizza = createPizza(type);
pizza.prepare();
pizza.bake();
pizza.cut();
pizza.box();
return pizza;
}
protected abstract Pizza createPizza(Stirng type);
}
팩토리 메소드는 객체 생성을 서브클래스에 캡슐화할 수 있다. 그러면 슈퍼클래스에 있는 클라이언트 코드와 서브클래스에 있는 객체 생성 코드를 분리할 수 있다.
abstract Product factoryMethod(String type)
위와 같은 코드를 자세히 분석해보자.
PizzaStore nyPizzaStore = new NYPizzaStore();
NYPizzaStore
인스턴스를 생성한다.
nyPizzaStore.orderPizza("cheese");
nyPizzaStore
인스턴스의 orderPizza()
메소드가 호출된다. 그러면 PizzaStore
에 정의된 메소드가 호출된다.
orderPizza()
메소드에서 createPizza()
메소드를 호출한다.Pizza pizza = createPizza("cheese");
팩토리 메소드인 orderPizza()
메소드는 서브클래스에서 구현했다. 이 경우에는 뉴욕 스타일 치즈 피자가 리턴된다.
pizza.prepare();
pizza.bake();
pizza.cut();
pizza.box();
이 메소드들은 모두 orderPizza()
팩토리 메소드에서 리턴한 특정 피자 객체 내에 정의되어 있다. (orderPizza()
메소드는 NYPizzaStore
에 정의되어 있다.)
또한orderPizza()
메소드에서 피자 객체를 받았지만 그 피자 객체가 어느 구상 클래스의 객체인지는 전혀 알지 못한다.
public abstract class Pizza { // 우선 Pizza 추상 클래스를 만든 다음 클래스를 확장해서 구상 클래스를 만든다.
// 피자마다 이름, 반족, 소스, 토핑이 필요하다.
String name;
String dough;
String sauce;
List<String> toppings = new ArrayList<String>();
// 피자 준비 과정에서 몇 가지 정해진 단계를 따라야 한다.
void prepare() {
System.out.println("Prepare " + name);
System.out.println("Tossing dough...");
System.out.println("Adding sauce...");
System.out.println("Adding toppings: ");
for (String topping : toppings) {
System.out.println(" " + topping);
}
}
// 추상 클래스에서 피자를 굽고, 자르고, 상자에 담는 일에 몇 가지 기본 값을 제공한다.
void bake() {
System.out.println("Bake for 25 minutes at 350");
}
void cut() {
System.out.println("Cut the pizza into diagonal slices");
}
void box() {
System.out.println("Place pizza in official PizzaStore box");
}
public String getName() {
return name;
}
}
이제 구상 서브클래스를 만들어야 한다. 예시로 뉴욕 스타일 치즈 피자를 만들어보면 아래와 같다.
public class NYStyleCheesePizza extends Pizza {
public NYStyleCheesePizza() {
name = "NY Style Sauce and Cheese Pizza";
dough = "Thin Crust Dough";
sauce = "Marinara Sauce";
toppings.add("Grated Reggiano Cheese");
}
}
모든 팩토리 패턴은 객체 생성을 캡슐화한다. 팩토리 메소드 패턴은 서브 클래스에서 어떤 클래스를 만들지 결정함으로써 객체 생성을 캡슐화한다.
createPizza()
메소드가 팩토리 메소드가 되어 이 메소드에서 제품(객체)를 생산함PizzaStore
의 서브클래스가 따로 있으므로 createPizza()
메소드 구현을 활용해서 그 가게 고유의 피자를 마음대로 만들 수 있음팩토리 메소드 패턴에서는 객체를 생성할 때 필요한 인터페이스를 만든다.
어떤 클래스의 인스턴스를 만들지는 서브클래스에서 결정한다. 즉, 팩토리 메소드 패턴을 사용하면 클래스 인스턴스 만드는 일을 서브클래스에게 맡기게 된다.
여기서 '결정한다' 는 표현을 쓰는 이유는 실행 중에 서브클래스에서 어떤 클래스의 인스턴스를 만들지를 결정해서가 아니라, 생산자 클래스가 실제 생산될 제품을 전혀 모르는 상태로 만들어지기 때문이다.
따라서 더 정확히 말하면, 사용하는 서브클래스에 따라 생산되는 객체 인스턴스가 결정된다.
객체 인스턴스를 직접 만들면 구상 클래스에 의존해야 한다.
아래 심하게 의존적인 PizzaStore를 한번 살펴보자.
이 코드에서는 모든 피자 객체를 팩토리에 맡겨서 만들지 않고 PizzaStore 클래스 내에서 직접 만들었다. 이 때의 단점은 아래와 같다.
구상 클래스 의존성을 줄이면 좋다는 사실은 이제 알 것이다. 이를 정리해놓은 디자인 원칙이 있다. 바로 의존성 뒤집기 원칙(Dependency Inversion Principle) 이다.
의존성 뒤집기 원칙(Dependency Inversion Principle) : 추상화된 것에 의존하게 만들고 구상 클래스에 의존하지 않게 만든다.
이전에 봤던 "구현보다는 인터페이스에 맞춰서 프로그래밍한다." 라는 디자인 원칙과 비슷해 보인다. 물론 비슷하긴 하지만 의존성 뒤집기 원칙에서는 추상화를 더 많이 강조한다.
이 원칙에서는 고수준 구성 요소가 저수준 구성 요소에 의존하면 안되며, 항상 추상화에 의존하게 만들어야 한다는 뜻이 담겨있다. (PizzaStore는 고수준 구성 요소, 피자 클래스는 저수준 구성 요소라고 할 수 있다.)
앞서 살펴본 심하게 의존적인 PizzaStore의 가장 큰 문제점은 PizzaStore가 orderPizza()
메소드에서 구상 형식의 인스턴스를 직접 만들기 때문에 모든 종류의 피자에 의존하게 된다는 점이다.
사실 Pizza라는 추상 클래스를 만들기는 했지만, 이 코드에서 구상 피자 객체를 생성하는 것은 아니기에 추상화로 얻는 것이 별로 없다.
여기에 팩토리 메소드 패턴을 적용하면 인스턴스 만드는 부분을 orderPizza()
에서 뽑아낼 수 있다.
팩토리 메소드 패턴을 적용하면 고수준 구성 요소인 PizzaStore와 저수준 구성 요소인 피자 객체 모두가 추상 클래스인 Pizza에 의존한다는 사실을 알 수 있다.
팩토리 메소드 패턴이 의존성 뒤집기 원칙을 준수하는 유일한 방법은 아니지만 적합한 방법 중 하나라고 할 수 있다.
new
연산자를 사용하면 구상 클래스의 레퍼런스를 사용하게 된다. 따라서 팩토리를 써서 구상 클래스의 레퍼런스를 변수에 저장하는 일을 미리 방지하자.
구상 클래스에서 유도된 클래스를 만들면 특정 구상 클래스에 의존하게 된다. 인터페이스나 추상 클래스처럼 추상화된 것으로부터 클래스를 만들어야 한다.
이미 구현되어 있는 메소드를 오버라이드한다면 베이스 클래스가 제대로 추상화되지 않는다. 베이스 클래스에서 메소드를 정의할 때는 모든 서브클래스에서 공유할 수 있는 것만 정의해야 한다.
그런데 잘 보면, 이 가이드라인을 다 지키다 보면 영원히 프로그램을 다 못 만들 것 같다.
맞다. 다른 원칙들과 마찬가지로, 이 가이드라인은 항상 지켜야 하는 규칙이 아니라 우리가 지향해야 하는 바를 알려주는 것이다.
지금까지 피자 가게의 코드를 팩토리 메소드를 이용해 수정해보았다.
그런데 여기서 원재료를 생산하는 공장까지 관리하도록 원재료군을 처리하는 방법까지 고려해본다면 어떻게 코드를 수정할 수 있을까?
이 팩토리에서는 원재료군에 들어있는 각각의 원재로를 생산한다.
우선 모든 원재료를 생산하는 팩토리용 인터페이스를 정의하는 것부터 해보도록 하자.
public interface PizzaIngredientFactory {
public Dough createDough();
public Sauce createSauce();
public Cheese createCheese();
public Veggies[] createVeggies();
public Pepperoni createPepperoni();
public Clams createClam();
}
인터페이스에 각 재료별 생성 메소드를 정의하였다. 여러 가지 새로운 클래스가 도입되었으므로 재료마다 하나씩 클래스를 만들어야 한다.
public class NYPizzaIngredientFactory implements PizzaIngredientFactory {
public Dough createDough() {
return new ThinCrustDough();
}
public Sauce createSauce() {
return new MarinaraSauce();
}
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();
}
}
모든 재료 공장에서 구현해야 하는 인터페이스를 뉴욕 원재료 팩토리에서도 구현한다. 재료군에 들어있는 재료를 뉴욕 지점에 알맞게 만든다.
공장을 다 만들었다면 재료를 생산할 준비가 끝난 것이다. Pizza 클래스가 팩토리에서 생산한 원재료를 사용할 수 있도록 코드를 다시 고쳐보자.
우선 Pizza 추상 클래스부터 시작해보자.
public abstract class Pizza {
String name;
Dough dough;
Sauce sauce;
Veggies veggies[];
Cheese cheese;
Pepperoni pepperoni;
Clams clam;
// prepare() 메소드를 추사아 메소드로 만듦
abstract void prepare();
void bake() {
System.out.println("Bake for 25 minutes at 350");
}
void cut() {
System.out.println("Cutting the pizza into diagonal slices");
}
void box() {
System.out.println("Place pizza in official PizzaStore box");
}
void setName(String name) {
this.name = name;
}
String getName() {
return name;
}
public String toString() {
// 피자 이름을 출력하는 부분
}
}
이제 prepare()
메소드를 추상 메소드로 만들었다. 이 부분에서 피자를 만드는 데 필요한 재료들을 가져오게 된다. 물론 모든 원재료는 원재료 팩토리에서 가져온다.
추상 클래스 준비도 끝났으니 각각 피자를 만들어보자. 기존 코드에서 달라진 점은 원재료를 팩토리에서 바로 가져온다는 점 말고는 없다.
치즈 피자 코드를 예시로 보면 아래와 같다.
public class CheesePizza extends Pizza {
PizzaIngredientFactory ingredientFactory;
// 피자의 원재료를 제공하는 팩토리가 필요하다.
public CheesePizza(PizzaIngredientFactory ingredientFactory) {
this.ingredientFactory = ingredientFactory;
}
// prepare() 메소드에서 치즈 피자를 만드는 각 단계를 처리한다.
void prepare() {
System.out.println("Preparing " + name);
dough = ingredientFactory.createDough();
sauce = ingredientFactory.createSauce();
cheese = ingredientFactory.createCheese();
}
}
sause = ingredientFactory.createSause();
핵심이 되는 부분인 위 코드를 자세히 살펴보자.
ingredientFactory
는 우리가 사용하는 원재료 팩토리다. Pizza 클래스는 원재료 팩토리가 맞기만 하면 어떤 팩토리를 쓰든 상관하지 않는다.createSause()
메소드에서는 해당 지역에서 사용하는 소스를 리턴한다.각 지점을 돌면서 제대로 된 피자를 만드는지 확인해보고 지역별 재료 공장의 레퍼런스를 전달해주면 된다.
public class NYPizzaStore extends PizzaStore {
// 뉴욕 지점에는 뉴욕 피자 원재료 팩토리를 전달해줘야 한다. 뉴욕 스타일 피자를 만들 때 필요한 재료는 이 팩토리에서 공급한다.
protected Pizza createPizza(String item) {
Pizza pizza = null;
PizzaIngredientFactory ingredientFactory =
new NYPizzaIngredientFactory();
// 피자 형식마다 새로운 Pizza 인스턴스를 만들고 원재료를 공급받는데 필요한 팩토리를 지정해준다.
if (item.equals("cheese")) {
// 피자에 맞는 재료를 만드는 팩토리를 피자 객체에 전달해준다.
pizza = new CheesePizza(ingredientFactory);
pizza.setName("New York Style Cheese Pizza");
} else if (item.equals("veggie")) {
pizza = new VeggiePizza(ingredientFactory);
pizza.setName("New York Style Veggie Pizza");
} else if (item.equals("clam")) {
pizza = new ClamPizza(ingredientFactory);
pizza.setName("New York Style Clam Pizza");
} else if (item.equals("pepperoni")) {
pizza = new PepperoniPizza(ingredientFactory);
pizza.setName("New York Style Pepperoni Pizza");
}
return pizza;
}
}
그럼 이 코드를 바탕으로 새롭게 피자를 주문하는 과정을 알아보자.
PizzaStore nyPizzaStore = new NYPizzaStore();
NYPizzasStore 인스턴스를 생성한다.
nyPizzaStore.orderPizza("cheese");
nyPizzaStore 인스턴스의 orderPizza() 메소드가 호출된다.
Pizza pizza = createPizza("cheese");
원재료 팩토리를 사용하므로 여기서부터 달라진다.
Pizza pizza = new CheesePizza(nyIngredientFactory);
뉴욕 원재료 팩토리를 사용하는 Pizza 인스턴스를 만든다.
PizzaStore에서 원재료 팩토리를 선택하고 그 인스턴스를 만든다. 원재료 팩토리는 각 피자의 생성자에 전달된다.
void prepare() {
dough = factory.crateDough();
sauce = factory.crateSause();
chees = factory.crateCheese();
}
이제 팩토리 패턴을 하나 더 배웠다. 앞서 본 것 처럼 제품군을 만들 때 쓸 수 있는 패턴이다.
추상 팩토리 패턴(Abstract Factory Pattern) : 구상 클래스에 의존하지 않고도 서로 연관되거나 의존적인 객체로 이루어진 제품군을 생산하는 인터페이스를 제공한다. 구상 클래스는 서브 클래스에서 만든다.
추상 팩토리 패턴을 사용하면 클라이언트에서 추상 인터페이스로 일련의 제품을 공급받을 수 있다.
이때, 실제로 어떤 제품이 생산되는지는 전혀 알 필요가 없다. 따라서 클라이언트와 팩토리에서 생산되는 제품을 분리할 수 있다.
클래스 다이어그램은 아래와 같다.
PizzaStore의 시선으로 전체 구조를 보면 아래와 같다.
본 포스팅에 쓰인 이미지와 내용의 모든 출처는 책 '헤드 퍼스트 디자인 패턴' 에 있습니다.