헤드퍼스트 디자인패턴
- 본 책을 읽고 책의 내용을 간략하게 정리한 글입니다.
Chapter 4. 객체지향 빵 굽기 - 팩토리 패턴
new
연산자가 보이면 “구상”을 떠올려라
new
연산자를 사용하면 구상 클래스의 인스턴스가 생성된다. (인터페이스가 아닌 특정 구현을 사용해서)- 앞서 구상 클래스를 바탕으로 코딩하면 나중에 코드를 수정해야 할 가능성이 커지고, 유연성이 떨어진다고 배웠다.
- 일련의 구상 클래스가 있다면 아래와 같은 코드를 만들어야 한다. ```java Duck duck;
// 컴파일하기 전에는 어떤 오리의 인스턴스를 만들어야 하는지 알 수 없다. // 이런 코드는 관리와 갱신이 어려워지고 오류가 생길수도 있다. if (picnic) { duck = new MallardDuck(); } else if (hunting) { duck = new DecoyDuck(); } else if (bathtub) { duck = RubberDuck(); }
#### `new` 연산자의 문제
* `new` 연산자 자체에 문제는 없다. 단 **변화**하는 무언가가 문제를 일으킨다.
* 인터페이스에 맞춰서 코딩하면 시스템에서 일어날 수 있는 여러 변화에 대응할 수 있다.
* **다형성**
* 인터페이스를 바탕으로 만든 코드는 어떤 클래스던 인터페이스만 구현하면 사용할 수 있다.
* **변경에 닫혀 있는** 코드
* 구상 클래스를 많이 사용하면 새로운 구상 클래스가 추가될 때마다 코드를 고쳐야 한다.
### 피자 주문 코드 만들기
```java
Pizza orderPizza() {
Pizza pizza = new Pizza();
pizza.prepare();
pizza.bake();
pizza.cut();
pizza.box();
return pizza;
}
- 위 예제 코드에서 피자 종류를 고르고 그에 맞게 피자를 고르는 코드를 추가한다.
Pizza orderPizza(String type) { Pizza pizza = new Pizza(); // 피자 종류를 바탕으로 구상 클래스의 인스턴스를 만든다. // 해당 부분이 바뀌는 부분! (신메뉴나 메뉴 변경이 될 때마다 바뀌는 부분이다.) if ("cheese".equals(type)) { pizza = new CheesePizza(); } else if ("greek".equals(type)) { pizza = new GreekPizza(); } else if ("pepperoni".equals(type)) { pizza = new PepperoniPizza(); } pizza.prepare(); pizza.bake(); pizza.cut(); pizza.box(); return pizza(); }
orderPizza()
메서드에서 문제가 되는 부분은 인스턴스를 만드는 구상 클래스를 선택하는 부분이다.- 상황이 변하면 코드를 변경해야 하는 부분
객체 생성 부분 캡슐화
- 우선 객체 생성 코드만 따로 빼서 피자 객체를 만드는 일만 전담하는 객체에 넣는다.
- 객체 생성을 처리하는 클래스를 팩토리(Factory)라고 부른다.
- 간단한 피자 팩토리 생성하기
public class PizzaFactory { public Pizza createPizza(String type) { Pizza pizza = null; if ("cheese".equals(type)) { pizza = new CheesePizza(); } else if ("greek".equals(type)) { pizza = new GreekPizza(); } else if ("pepperoni".equals(type)) { pizza = new PepperoniPizza(); } else if ("veggie".equals(type)) { pizza = new VeggiePizza(); } return pizza; } }
- 클라이언트 코드에서 팩토리로 피자를 생성하도록 고쳐보기
public class PizzaStore { PizzaFactory factory; public PizzaStore(PizzaFactory factory) { this.factory = factory; } public Pizza orderPizza(String type) { Pizza pizza; pizza = factory.createPizza(type); pizza.prepare(); pizza.bake(); pizza.cut(); pizza.box(); return pizza; } // 기타 메서드 }
주의: “인터페이스를 구현한다”
- 해당 책에서 “인터페이스를 구현한다”라는 표현이 나오는데, 항상 “클래스를 선언하는 부분에
implements
키워드로 어떤 자바 인터페이스를 구현하는 클래스를 만든다”라고 생각하지 말 것! - 일반적으로 상위 형식에 있는 구상 클래스는 그 상위 형식의 “인터페이스를 구현”하는 클래스라고 생각하면 된다.
다양한 팩토리 만들기
- 다양한 스타일의 피자(뉴욕, 시카고, 캘리포니아 스타일)를 만들어야 한다.
- 방법1. 3가지 서로 다른 팩토리를 만든 다음 PizzaStore에서 사용하도록 하는 방법 ```java // 뉴욕 스타일의 피자를 만드는 팩토리 사용 NYPizzaFactory nyFactory = new NYPizzaFactory(); PizzaStore nyStore = new PizzaStore(nyFactory); nyStore.orderPizza(“veggie”);
// 시카고 스타일의 피자를 만드는 팩토리 사용 ChicagoPizzaFactory chicagoFactory = new ChicagoPizzaFactory(); PizzaStore chicagoStore = new PizzaStore(chicagoFactory); chicagoStore.orderPizza(“veggie”);
* 지점들을 조금 더 제대로 관리할 순 없을까?
* 어떻게 피자 가게와 피자를 만드는 과정을 하나로 묶을 수 있을까?
### 피자 가게 프레임워크 만들기
* 팩토리의 `createPizza()` 메서드를 다시 PizzaStore 클래스에 다시 넣는다. 대신 해당 메서드를 추상 메서드로 선언하고 지역별로 서브클래스를 만든다.
```java
public abstract class PizzaStore {
public Pizza orderPizza(String type) {
Pizza pizza;
pizza = createPizza(type);
pizza.prepare();
pizza.bake();
pizza.cut();
pizza.box();
return pizza;
}
abstract Pizza createPizza(String type);
}
서브클래스에서 결정할 것
- PizzaStore 서브클래스들은 PizzaStore 프레임워크에 충실하면서도 각 지점마다 피자 스타일이 달라지도록 구현한다.
| PizzaStore |
| ------------- |
| createPizza() |
| orderPizza() |
- 각 서브클래스는
createPizza()
메서드를 오버라이드하지만,orderPizza()
는 PizzaStore에서 정의한 내용 그대로 사용한다. - 정의한 메서드를 고쳐 쓸 수 없게 하고 싶다면
orderPizza()
메서드를final
로 선언한다.| NYStylePizzaStore | | -------------------- | | createPizza() |
public Pizza createPizza(type) { if ("cheese".equals(type)) { pizza = new NYStyleCheesePizza(); } else if ("veggie".equals(type)) { pizza = nw NYStyleVeggiePizza(); } // 그 외 피자 타입들 ... }
| ChicagoStylePizzaStore | | ---------------------- | | createPizza() |
public Pizza createPizza(type) { if ("cheese".equals(type)) { pizza = new ChicagoStyleCheesePizza(); } else if ("veggie".equals(type)) { pizza = nw ChicagoStyleVeggiePizza(); } // 그 외 피자 타입들 ... }
- 피자의 종류는 어떤 서브클래스를 선택했느냐에 따라 결정된다.
피자 스타일 서브클래스 만들기
- 프렌차이즈 형태로 사업하기
- PizzaStore의 서브클래스를 만들고 지역별 특성에 맞게
createPizza()
메서드만 구현하면 된다.
- PizzaStore의 서브클래스를 만들고 지역별 특성에 맞게
- 뉴욕 스타일 피자 서브클래스
public class NYPizzaStore extends PizzaStore { // createPizza() 메서드는 PizzaStore에서 추상 메서드로 선언되었으므로 구상 클래스에서 반드시 구현해야 한다. Pizza createPizza(String type) { if ("cheese".equals(type)) { return new NYStyleCheesePizza(); } else if ("veggie".equals(type)) { return new NYStyleVeggiePizza(); } else return null; } }
팩토리 메서드 선언하기
- 위 예제 코드에서 PizzaStore 클래스에 추상 메서드로 선언한
createPizza()
가 팩토리 메서드 - 구상 클래스 인스턴스를 만드는 일을 일련의 서브클래스가 처리하는 방식
abstract Product factoryMethod(String type)
- 팩토리 메서드를 추상 메서드로 선언하여 서브클래스가 객체 생성을 책임지도록 한다.
- 팩토리 메서드는 특정 객체를 반환하며, 그 객체는 보통 슈퍼클래스가 정의한 메서드 내에서 쓰인다.
- 팩토리 메서드는 클라이언트에서 실제 생성되는 구상 객체가 무엇인지 알 수 없게 만드는 역할도 한다.
- 팩토리 메서드를 만들 때 매개변수로 만들 객체 종류를 선택할 수도 있다.
Pizza 클래스 만들기
// 이 클래스를 확장하여 구상 클래스로 만들기 위해 추상 클래스로 만든다.
public abstract class Pizza {
String name;
String dough;
String sauce;
List<String> toppings = new ArrayList<>();
void prepare() {
System.out.println("준비 중: " + name);
System.out.println("도우를 준비하는 중...");
System.out.println("소스를 뿌리는 중...");
System.out.println("토핑을 올리는 중...");
for (String topping : toppings) {
System.out.println(" " + topping);
}
}
void bake() {
System.out.println("175도에서 25분 간 굽기");
}
void cut() {
System.out.println("피자를 6조각으로 자르기");
}
void box() {
System.out.println("피자박스에 피자 담기");
}
public String getName() {
return name;
}
// ...
}
- Pizza 구상 서브클래스 만들기
public class NYStyleCheesePizza extends Pizza { public NYStyleCheesePizza() { name = "뉴욕 스타일 치즈 피자"; dough = "씬 크러스트 도우"; sauce = "마리나라 소스"; toppings.add("잘게 썬 레지아노 치즈"); } }
public class ChicagoStyleCheesePizza extends Pizza { public ChicagoStyleCheesePizza() { name = "시카고 스타일 딥 치즈 피자"; dough = "두꺼운 크러스트 도우"; sauce = "플럼토마토 소스"; toppings.add("잘게 조각낸 모짜렐라 치즈"); } void cut() { System.out.println("네모난 모양으로 피자 자르기"); } }
피자 만들기 코드 테스트
public class PizzaTest {
public static void main(String args[]) {
PizzaStore nyStore = new NYStylePizzaStore();
PizzaStore chicagoStore = new ChicagoStyleStore();
Pizza pizza = nyStore.orderPizza("cheese");
System.out.println(pizza.name + "\n");
pizza = chicagoStore.orderPizza("cheese");
System.out.println(pizza.name + "\n");
}
}
팩토리 메서드 패턴 살펴보기
- 모든 팩토리 패턴은 객체 생성을 캡슐화한다.
- 팩토리 메서드 패턴은 서브클래스에서 어떤 클래스를 만들지 결정함으로써 객체 생성을 캡슐화한다.
생산자(Creator) 클래스
- 추상 생산자 클래스
- 서브클래스에서 객체를 생성하려고 구현하는 팩토리 메서드(추상 메서드)를 정의한다.
- 생산자 자체는 어떤 구상 제품 클래스가 만들어질지 미리 알 수 없다.
| PizzaStore | | ------------- | | createPizza() | | orderPizza() |
- 제품을 생산하는 클래스: 구상 생산자 (concrete creator)
| NYPizzaStore | | ------------- | | createPizza() |
| ChicagoPizzaStore | | ----------------- | | createPizza() |
제품(Product) 클래스
- 팩토리에서 생산되는 제품
| Pizza | | ----- |
병렬 클래스 계층 구조
- 구상 생산자별로 수많은 제품을 만들 수 있다.
- 예로 뉴욕 피자 생산자는 여러 가지 뉴욕 스타일 피자를 만든다.
- 생상자 클래스와 그에 대응되는 제품 클래스는 병렬 계층구조로 볼 수 있다.
팩토리 메서드 패턴의 정의
- 팩토리 메서드 패턴에서는 객체를 생성할 때 필요한 인터페이스를 만든다.
- 어떤 클래스의 인스턴스를 만들지는 서브클래스에서 결정한다.
- 팩토리 메서드 패턴을 사용하면 클래스 인스턴스 만드는 일을 서브클래스에게 맡기게 된다.
- 생산자 클래스는 실제 생산될 제품을 전혀 모르는 상태로 만들어진다.
- 사용하는 서브클래스에 따라 생산되는 객체 인스턴스가 결정된다.
의존성 뒤집기 원칙
디자인 원칙 6
- 추상화된 것에 의존하게 만들고 구상 클래스에 의존하지 않게 만든다.
- “구현보다는 인터페이스에 맞춰서 프로그래밍한다”라는 원칙과 비슷하게 보일 수 있다.
- 해당 원칙에서는 추상화를 더 강조한다.
- 고수준 구성 요소가 저수준 구성 요소에 의존하면 안 되며, 항상 추상화에 의존하게 만들어야 한다는 뜻
- 고수준 구성 요소
- 다른 저수준 구성 요소에 의해 정의되는 행동이 들어있는 구성 요소
- PizzaStore의 행동은 Pizza에 의해 정의되므로 PizzaStore는 고수준 구성 요소라고 할 수 있다.
- PizzaStore에서 사용하는 Pizza 객체는 저수준 구성 요소
- 의존성 뒤집기 원칙에 따르면, 구상 클래스처럼 구체적인 것이 아닌 추상 클래스나 인터페이스와 같이 추상적인 것에 의존하는 코드를 만들어야 한다.
의존성 뒤집기 원칙을 지키는 방법
- 변수에 구상 클래스의 레퍼런스를 저장하지 않는다.
new
연산자를 사용하면 구상 클래스의 레퍼런스를 사용하게 된다.- 그러니 팩토리를 사용하여 구상 클래스의 레퍼런스를 변수에 저장하는 일을 미리 방지한다.
- 구상 클래스에서 유도된 클래스를 만들지 않는다.
- 구상 클래스에서 유도된 클래스를 만들면 특정 구상 클래스에 의존하게 된다.
- 인터페이스나 추상 클래스처럼 추상화된 것으로부터 클래스를 만들어야 한다.
- 베이스 클래스에 이미 구현되어 있는 메서드를 오버라이드하지 않는다.
- 이미 구현되어 있는 메서드를 오버라이드한다면 베이스 클래스가 제대로 추상화되지 않는다.
- 베이스 클래스에서 메서드를 정의할 때는 모든 서브클래스에서 공유할 수 있는 것만 정의해야 한다. * 다른 원칙들과 마찬가지로, 항상 지켜야 하는 규칙이 아니라 우리가 지향해야 할 바를 알려주는 것이다. * 이런 가이드라인을 습득한 상태에서 디자인한다면 원칙을 지키지 않은 부분을 명확하게 파악할 수 있으며, 합리적인 이유로 불가피한 상황에서만 예외를 둘 수 있을 것이다.
피자 원재료군으로 묶기
- 각 스타일의 피자마다 사용하는 재료는 서로 다르다.
- 서로 다른 원재료를 제공하려면 원재료군을 처리할 방법을 생각해야 한다.
원재료 팩토리 만들기
- 각 스타일마다 달라지는 부분을 처리하는 건 뒤에서 생각하고, 우선 원재료를 생산하는 팩토리를 만든다.
public interface PizzaIngerdientFactory { public Dough createDough(); public Sauce createSauce(); public Cheese createCheese(); public Veggies[] createVeggies(); public Pepperoni createPepperoni(); public Clams createClams(); }
뉴욕 스타일 원재료 팩토리 만들기
- 뉴욕 스타일 피자에는 마리나라 소스, 레지아노 치즈, 조개 등이 들어간다고 가정해본다.
public class NYPizzaIngredientFactory implements PizzaIngredientFactory { public Dough createDough() { return new ThinCrushDough(); } 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 SlicePepperoni(); } public Clams createClams() { return new FreshClams(); } }
Pizza 클래스 변경하기
- Pizza 클래스에서 팩토리로 원재료를 생성하여 사용하도록 수정해본다.
public abstract class Pizza { String name; Dough dough; Sauce sauce; Veggies veggies[]; Cheese cheese; Pepperoni pepperoni; Clams clams; // prepare() 메서드를 추상 메서드로 만듬 // 이 부분에서 필요한 재료들을 가져온다. 원재료는 원재료 팩토리에서 가져온다. abstract void prepare(); void bake() { System.out.println("175도에서 25분 간 굽기"); } void cut() { System.out.println("피자를 6조각으로 자르기"); } void box() { System.out.println("피자박스에 피자 담기"); } void setName(String name) { this.name = name; } public String getName() { return name; } public String toString() { // 피자 이름 출력 부분 } }
- 이제 지역 스타일 별 피자를 만들기 위한
NYCheesePizza
나ChicagoCheesePizza
클래스는 더 이상 사용하지 않아도 된다.- 두 지역 스타일 피자를 비교해보면 다른 재료를 사용한다는 것만 빼면 똑같은 형식이다.
- 즉 피자마다 지역별로 클래스를 따로 만들 필요가 없어졌다.
- 치즈 피자를 만드는 예시 코드
public class CheesePizza extends Pizza { PizzaIngredientFactory ingredientFactory; public CheesePizza(PizzaIngredientFactory ingredientFactory) { this.ingredientFactory = ingredientFactory; } void prepare() { System.out.println("준비 중: " + name); // 재료가 필요할 때마다 팩토리에 있는 메서드를 호출해 만든다. dough = ingredientFactory.createDough(); sauce = ingredientFactory.createSauce(); cheese = ingredientFactory.createCheese(); } }
- 조개 피자를 만드는 예시 코드
public class ClamPizza extends Pizza { PizzaIngredientFactory ingredientFactory; public ClamPizza(PizzaIngredientFactory ingredientFactory) { this.ingredientFactory = ingredientFactory; } void prepare() { System.out.println("준비 중: " + name); dough = ingredientFactory.createDough(); sauce = ingredientFactory.createSauce(); cheese = ingredientFactory.createCheese(); clam = ingredientFactory.createClam(); } }
새로운 팩토리 코드로 피자 주문하기
새롭게 피자가 만들어지는 과정
- 해당 예제에서는 뉴욕 스타일 피자를 만든다고 가정한다.
- 뉴욕 스타일 피자를 주문하기 위해선 우선 뉴욕 피자 가게가 필요하다.
Pizza nyPizzaStore = new NYPizzaStore();
- 피자 가게에 주문을 한다.
nyPizzaStore.orderPizza("cheese");
orderPizza()
메서드는 먼저createPizza()
메서드를 호출한다.Pizza pizza = createPizza("cheese");
createPizza()
메서드가 호출되면 원재료 팩토리가 돌아간다.Pizza pizza = new CheesePizza(nyIngredientFactory);
- 피자를 준비하기 위해
prepare()
메서드를 호출하면 팩토리에 원재료가 들어간다.void prepare() { dough = factory.createDough(); sauce = factory.createSauce(); cheese = factory.createCheese(); }
추상 팩토리 패턴의 정의
- 추상 팩토리 패턴: 구상 클래스에 의존하지 않고도 서로 연관되거나 의존적인 객체로 이루어진 제품군을 생산하는 인터페이스를 제공, 구상 클래스는 서브클래스에서 만든다.
- 추상 팩토리 패턴을 사용하면 클라이언트에서 추상 인터페이스로 일련의 제품을 공급받을 수 있다. 이 때, 실제로 어떤 제품이 생산되는지 전혀 알 필요가 없다.
- 클라이언트와 팩토리에서 생산되는 제품을 분리할 수 있다.
팩토리 메서드 패턴과 추상 팩토리 패턴의 차이
팩토리 메서드 패턴
- 객체 생성 방식이 상속을 사용하여 객체를 만든다.
- 서브클래스로 객체를 만든다.
- 한 가지 종류의 객체를 생성한다.
- 단일 메서드를 통해 객체를 생성한다.
추상 팩토리 메서드
- 객체 생성 방식이 객체 구성을 사용하여 객체를 만든다.
- 제품군을 만드는 추상 형식을 제공하여, 제품이 생산되는 방법은 해당 형식의 서브클래스에서 정의한다.
- 다량의 제품군을 생성할 때 사용한다.
- 여러 메서드를 통해 여러 종류의 객체를 생성한다.