본 포스트는 헤드퍼스트 디자인패턴을 읽고 정리한 글입니다. |
Ch4 팩토리 패턴
본 포스트는 아래 포스트에 이어지는 글입니다.
Question: 원재료 공급
피자 가게의 모든 지점이 좋은 재료를 사용하도록 관리하는 시스템이 필요하다.
모든 지점에서 제품에 들어가는 재료군은 동일하지만, 지역마다 재료의 구체적인 종류는 조금씩 다르다.
- 재료군: 반죽, 소스, 치즈, 야채, 고기
- 구체적인 종류: ex. 모짜렐라 치즈, 레지아노 치즈
Problem
지역마다 각기 다른 재료를 보낼 수 있어야 한다.
=
특정 재료로 구성된 군을 각 지역마다 구현해야 한다.
Problem Solve: 원재료 추상 팩토리
팩토리용 인터페이스
원재료를 생산하는 팩토리용 인터페이스 정의
public interface PizzaIngredientFactory {
public Dough createDough();
public Sauce createSauce();
public Cheese createCheese();
public Veggie[] createVeggies();
public Pepperoni createPepperoni();
public Clam createClam();
}
이때 반환 타입으로 지정된 재료 역시 인터페이스로 정의되며,
구체적인 종류에 따라 구상 클래스가 존재한다.
ex. 인터페이스: Sauce 구상 클래스: TomatoSauce, MarinaraSauce, … |
구상 팩토리
지역별로 사용하는 재료의 구체적인 종류에 맞게 지역 별 구상 팩토리에서 재료를 공급한다.
public class ChicagoIngredientFactory implements PizzaIngredientFactory {
@Override
public Dough createDough() {
return new ThinCrustDough();
}
@Override
public Sauce createSauce() {
return new TomatoSauce();
}
@Override
public Cheese createCheese() {
return new MozzarellaCheese();
}
@Override
public Veggies[] createVeggies() {
Veggies[] veggies = { new Garlic(), new Onion() };
return veggies;
}
@Override
public Pepperoni createPepperoni() {
return new SlicedPepperoni();
}
@Override
public Clams createClam() {
return new FrozenClams();
}
}
Pizza 클래스 수정
prepare 메서드를 추상 메서드로 선언한다.
prepare 메서드는 피자를 만드는 데 필요한 재료를 가져오는 역할을 한다.
이때, 필요한 재료는 원재료 팩토리에서 가져온다.
public abstract class Pizza {
String name;
Dough dough;
Sauce sauce;
Veggies veggies[];
Pepperoni pepperoni;
Clams clam;
/**
* prepare 메서드를 추상 메서드로 수정
* -> 피자를 만드는 데 필요한 재료를 "원재료 팩토리"로부터 가져옴
* */
abstract void prepare();
public void bake() { System.out.println("175도에서 25분 간 굽기"); }
public void cut() { System.out.println("피자를 사선으로 자르기"); }
public void box() { System.out.println("상자에 피자 담기"); }
public String getName() {
return this.name;
}
}
팩토리 메서드 패턴의 뉴욕, 시카고 치즈 피자 비교
// 뉴욕
public class NYStyleCheesePizza extends Pizza {
public NYStyleCheesePizza() {
name = "뉴욕 스타일 소스와 치즈 피자";
dough = "씬 크러스트 도우";
sauce = "마리나라 소스";
toppings.add("잘게 썬 레지아노 치즈");
}
}
// 시카고
public class ChicagoStyleCheesePizza extends Pizza {
public ChicagoStyleCheesePizza() {
name = "시카고 스타일 딥 디쉬 치즈 피자";
dough = "두꺼운 크러스트 도우";
sauce = "플럼토마토 소스";
toppings.add("잘게 조각낸 모짜렐라 치즈");
}
}
팩토리 메서드 패턴을 적용한 피자 가게에서 뉴욕과 시카고의 치즈 피자를 비교해 보면
구성은 동일하지만, 다른 재료를 사용한다는 것을 알 수 있다.
즉, 피자의 준비 단계는 동일하다.
따라서, 지역별 피자 클래스를 따로 만들 필요가 없다.
지역별로 다른 점(원재료)은 위에서 만든 원재료 팩토리에서 처리할 수 있다.
Example - CheesePizza
지점과 무관하게 치즈를 준비하는 과정은 동일하다.
단, 지역별로 사용하는 구체적인 재료가 다를 뿐이다.
다시,
이는 원재료 팩토리에서 처리할 수 있다.
피자를 준비하는 과정에서 재료가 필요하면 원재료 팩토리의 메서드를 호출하여 만든다.
public class CheesPizza extends Pizza {
PizzaIngredientFactory ingredientFactory;
public CheesPizza(PizzaIngredientFactory ingredientFactory) {
this.ingredientFactory = ingredientFactory;
}
@Override
void prepare() {
System.out.println("준비 중: " + name);
dough = ingredientFactory.createDough();
sauce = ingredientFactory.createSauce();
cheese = ingredientFactory.createCheese();
}
}
느슨한 결합
피자 클래스는 어떤 재료가 사용되는지 전혀 모른다.
피자와 지역별 재료가 완벽하게 분리된다.
이렇게 하면,
모든 지역에서 어떤 팩토리를 사용하든 피자 클래스는 그대로 재사용할 수 있다.
PizzaStore 구상 클래스 수정
public class NYPizzaStore extends PizzaStore {
@Override
Pizza createPizza(String item) {
Pizza pizza = null;
**PizzaIngredientFactory ingredientFactory = new NYIngredientFactory();**
if (item.equals("cheese")) {
**pizza = new CheesePizza(ingredientFactory);**
pizza.setName("뉴욕 치즈 피자");
} else if (item.equals("veggie")) {
pizza = new VeggiePizza(ingredientFactory);
pizza.setName("뉴욕 야채 피자");
} else if (item.equals("pepperoni")) {
pizza = new PepperoniPizza(ingredientFactory);
pizza.setName("뉴욕 페퍼로니 피자");
}
return pizza;
}
}
장점
클라이언트의 코드 변화 없이 지역별로 다른 원재료를 사용하여 피자를 구현할 수 있다.
프로그램 흐름
뉴욕 스타일의 피자 주문
1. 뉴욕 피자 가게 생성
- PizzaStore nyPizzaStore = new NYPizzaStore();
2. 뉴욕 치즈 피자 주문
- nyPizzaStore.orderPizza(”cheese”);
3. orderPizza()가 createPizza() 호출
- Pizza pizza = createPizza(”cheese”);
여기까지는 팩토리 메서드 패턴과 동일
4. createPizza 내에서 원재료 팩토리 가동
- pizza = new CheesePizza(nyIngredientFactory);
5. orderPizza에서 prepare 메서드 호출 → 원재료 주문
@Override
public void prepare() {
System.out.println("준비 중: " + name);
dough = ingredientFactory.createDough();
sauce = ingredientFactory.createSauce();
cheese = ingredientFactory.createCheese();
}
6. orderPizza가 나머지 작업(bake, cut, box) 수행
추상 팩토리 패턴 (Abstract Factory Pattern)
구상 클래스에 의존하지 않고도 서로 연관되거나 의존적인 객체로 이루어진 제품군을 생산하는 인터페이스를 제공한다.
구상 클래스는 서브 클래스에서 만든다.
클라이언트에서 실제로 어떤 제품이 생산되는지 모르면서 추상 인터페이스로 일련의 제품을 공급받을 수 있다.
따라서, 클라이언트와 제품이 완벽하게 분리된다.
추상 팩토리 패턴의 구조
Abstract Factory - 추상 팩토리
모든 구상 팩토리에서 구현해야 한다.
제품을 생산할 때의 메서드가 정의되어 있다.
Abstract Product - 제품군
각 구상 팩토리에서 필요한 제품을 모두 만들 수 있다.
Concrete Factory - 구상 팩토리
서로 다른 제품군을 구현한다.
클라이언트에서 제품이 필요하면, 구상 팩토리 중 하나를 골라 사용한다.
→ 객체 인스턴스를 직접 만들 필요가 없다.
Concrete Product - 구상 제품
추상 제품군을 구현한다.
구상 팩토리에서 필요한 구상 제품을 가져가 사용한다.
Client
추상 팩토리를 바탕으로 만든다. 실제 팩토리는 실행 시에 결정된다.
구조도
피자 가게의 추상 팩토리 구조도
추상 팩토리 패턴을 이해하기 위해서는 NYPizzaStore 아래 부분만 이해해도 좋다.
헤드퍼스트 디자인패턴의 피자 가게에서는 Client 또한 추상 클래스와 구상 클래스로 이루어져 있으며,
각 클라이언트는 Pizza(역시 추상/구상 클래스 존재)라는 제품을 생산하고,
또, 각 Pizza에 들어갈 원재료를 구성하는 Factory가 PizzaIngredientFactory인 것이다.
PizzaIngredientFactory
제품군을 만드는 방법을 정의하는 추상 인터페이스
NY/ChicagoPizzaIngredientFactory
피자 원재료를 만드는 구상 클래스
- 자기 지역에 맞는 재료를 만드는 방법을 앎
- 서로 다른 제품군을 구현
재료군 인터페이스, 구상 클래스
원재료에 대한 추상 인터페이스와, 구체적인 종류에 따른 구상 클래스
NYPizzaStore
추상 팩토리의 클라이언트: PizzaStore의 인스턴스인 NYPizzaStore과 ChicagoPizzaStore
Another Example - 노트북 제조사
위에서 언급했듯이, 헤드퍼스트 디자인패턴에서 제공한 예시는 Client 단에서도 복잡하게 이루어져 있다.
추상 팩토리 패턴을 보다 직관적으로 이해할 수 있는 예를 들어보자.
Factory
노트북을 만드는 회사가 있다. 이 회사는 전 세계 각 나라에 공장을 두고자 한다.
우선, 미국과 한국에만 공장을 두도록 하자.
Product
하나의 회사가 만드는 제품이기 때문에 노트북의 구성은 동일할 것이다.
화면과 키보드만 있다고 가정하자.
공장과 노트북의 구성 요소(화면, 키보드)를 모두 인터페이스로 두고,
나라 별로 구상 클래스를 만들면 추상화에 집중할 수 있고 DIP를 준수할 수 있다.
DIP(Dependency Inversion Principle)
: 추상화된 것에 의존하게 만들고, 구상 클래스에 의존하지 않게 만든다.
Client
이 공장을 이용하여 노트북을 생산하는 주체
구조도
팩토리 메서드 패턴 vs 추상 팩토리 패턴
팩토리 메서드 패턴
- 상속으로 객체를 만든다.
- 한 가지 제품만 생산한다.
- 어떤 구상 클래스가 필요할지 미리 알 수 없을 때 유용하다.
- 서브 클래스를 만들고 팩토리 메서드를 구현하기만 하면 된다.
추상 팩토리 패턴
- 객체 구성을 만든다.
- 연관된 제품을 하나로 묶을 수 있다.
- 제품군에 제품이 추가되면 인터페이스를 수정해야 한다.
- 제품군을 만드는 추상 형식을 제공한다.
- 생산되는 방법은 서브 클래스에서 정의한다.
- 인스턴스를 만든 후, 추상 형식을 사용하여 코드에 전달한다.
공통점
클라이언트와 구상 형식을 분리하는 역할을 한다.
- 의존성이 줄어들어 유연하다.
- 느슨한 결합
Summary
팩토리 메서드 패턴 | 추상 팩토리 패턴 | |
팩토리 생성 | 상속 | 객체 구성 |
상속 | 인터페이스/추상 클래스 | 인터페이스 |
객체 생성 개수 | 1개 | n개 |