4장. 헤드퍼스트 디자인 패턴 - 팩토리 패턴
안녕하세요 😀
유로띠 입니다 😉
헤드퍼스트 디자인 패턴
TIL (Today I Learned)
3줄 요약
✏️ 간단한 팩토리 / 팩토리 메소드 / 추상 팩토리 메소드가 있다.
✏️ 의존성 뒤집기! - 추상화된 것에 의존하게 만들고 구상 클래스에 의존하지 않게 만든다.
✏️ 어떤 서브클래스를 선택했느냐에 따라 구현이 달라진다.
DAY 3
오늘 읽은 범위: 4장. 객체지향 빵 굽기 - 팩토리 패턴( 간단한 팩토리, 팩토리 메서드 패턴)
😉 책에서 기억하고 싶은 내용을 써보세요.
최첨단 피자 코드 만들기
피자 종류를 고르고 그에 맞게 피자를 만드는 코드도 추가해야 합니다.
Pizza orderPizza(String type) {
Pizza pizza;
if (type.equals("cheese")) {
pizza = new CheesePizza();
} elseif(type.equals("greek")) {
pizza = new GreekPizza();
} else if (type.equals("pepperoni") {
pizza = new PepperoniPizza();
}
pizza.prepare(); //피자 준비
pizza.bake(); // 피자 굽기
pizza.cut(); //피자 자르기
pizza.box(); // 피자 상자에 담기
return pizza;
}
조개피자(Clam Pizza)와 야채피자(Veggie Pizza) 2가지 피자를 메뉴에 추가하기로 했습니다.
이참에 별로 안 팔리는 그리스 스타일 피자(Greek Pizza)는 메뉴에서 제외하기로 했습니다.
Pizza orderPizza(String type) {
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();
}
pizza.prepare(); //피자 준비
pizza.bake(); // 피자 굽기
pizza.cut(); //피자 자르기
pizza.box(); // 피자 상자에 담기
return pizza;
}
피자 종류가 바뀔 때마다 코드를 계속 고쳐야 합니다.
타입에 따른 피자를 생성하는 코드는 변경에 닫혀 있지 않습니다. 피자 가게에서 메뉴를 변경하려면 이 코드를 직접 고쳐야 합니다.
orderPizza() 메소드에서 가장 문제가 되는 부분은 인스턴스를 만드는 구상 클래스를 선택 하는 부분입니다. 이 부분 때문에 상황이 변하면 코드를 변경해야 합니다. 이제 어떤 부분이 바뀌고 어떤 부분이 바뀌지 않는지를 파악했으니 캡슐화를 할 차례군요. (p.147)
객체 생성 부분 캡슐화하기
객체 생성을 처리하는 클래스를 팩토리(Factory)라고 부릅니다.
객체 생성 코드를 orderPizza 메소드에서 빼냅니다.
이 코드는 피자를 만드는 일만 처리하는 객체에 넣습니다.
다른 객체에서 피자를 만들어야 할 일이 있으면 이 객체로 와서 부탁하면 되죠.
이제 더 이상 orderPizza() 메소드에서 어떤 피자를 만들지 고민하지 않아도 됩니다.
orderPizza() 메소드는 Pizza 인터페이 스를 구현하는 피자를 받아서 그 인터페이스에서 정의했던 prepare(), bake(), cut(), box() 메소드를 호출하기만 하면 되죠. (p.148)
객체 생성 팩토리 만들기
새로 만들 SimplePizzaFactory 클래스. 이 클래스에서 하는 일은 단 하나입니다.
클라이언트가 받을 피자만 만듭니다.
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;
}
}
클라이언트 코드 수정하기
pizzaStore의 생성자에 팩토리 객체가 전달됩니다.
orderPizza() 메소드는 팩토리로 피자 객체를 만듭니다. 주문받은 형식을 그냥 이쪽으로 전달하기만 하면 되죠.
new 연산자 대신 팩토리 객체가 있는 create 메소드를 사용하였습니다.
public class PizzaStore {
SimplePizzaFactory factory;
public PizzaStore(SimplePizzaFactory factory) {
this.factory = factory;
}
public Pizza orderPizza(String type) {
Pizza pizza = factory.createPizza(type);
pizza.prepare();
pizza.bake();
pizza.cut();
pizza.box();
return pizza;
}
}
간단한 팩토리의 정의
간단한 팩토리(Simple Factory)는 디자인 패턴이라기보다는 프로그래밍에서 자주 쓰이는 관용구에 가깝습니다.
정확하게는 팩토리 패턴은 아닙니다. 🙅♂️
팩토리 메서드 패턴
다양한 팩토리 만들기
피자 프랜차이즈 사업을 시작해 봅시다.
각 지점마다 그 지역의 특성과 입맛을 반영한 다양한 스타일의 피자(뉴욕 스타일, 시카고 스타일, 캘리포니아 스타일 등)를 만들어야 합니다.
SimplePizzaFactory를 삭제하고, 3가지 서로 다른 팩토리 (NYPizzaFactory, ChicagoPizza Factory, CaliforniaPizzaFactory)를 만든 다음, PizzaStore에서 적당한 팩토리를 사용하도록 하는 방법입니다.
NYPizzaFactory nyFactory = new NYPizzaFactory(); // 뉴욕 스타일 피자 팩토리
PizzaStore nyStore = new PizzaStore(nyFactory);
nyStore.orderPizza("Veggie"); // 뉴욕 스타일 야채 피자
ChicagoPizzaFactory chicagoFactory = new ChicagoPizzaFactory(); // 시카고 스타일 피자 팩토리
PizzaStore chicagoStore = new PizzaStore(chicagoFactory);
chicagoStore.orderPizza("Veggie"); // 시카고 스타일 야채 피자
PizzaStore와 피자 제작 코드 전체를 하나로 묶어주는 프레임워크를 만들어야 합니다.
물론 그렇게 만들면서도 유연성을 잃어버리면 안 되겠죠.
SimplePizzaFactory를 만들기 전에 썼던 코드에는 피자를 만드는 코드가 PizzaStore와 직접 연결되어 있긴 했지만, 유연성이 전혀 없었습니다.
어떻게 해야 피자 가게와 피자 만드는 과정을 하나로 묶을 수 있을까요? 🧐
피자 가게 프레임워크 만들기
프레임워크는 '미리 구현된 코드가 나중에 짜는 코드를 실행하도록 한다.' 라고 보시면 됩니다.
피자를 만드는 일 자체는 전부 PizzaStore 클래스에 진행하면서도 지점의 스타일을 살릴 수 있는 방법이 있습니다.
이제 createPizza() 메소드를 PizzaStore에 다시 넣겠습니다.
하지만 이번에는 그 메소드를 추상(abstract) 메소드로 선언하고, 지역별 스타일에 맞게 PizzaStore의 서브클래스를 만들겠습니다.
package FactoryPattern.Store;
import FactoryPattern.Pizza.Pizza;
public abstract class PizzaStore {
public Pizza orderPizza(String type) {
Pizza pizza = createPizza(type); //서브클래스에서 결정하도록 함.
pizza.prepare();
pizza.bake();
pizza.cut();
pizza.box();
return pizza;
}
abstract Pizza createPizza(String type); //팩토리 객체 대신 이 메소드를 사용합니다.
}
그리고 각 지점에 맞는 서브클래스를 만들어야 합니다.
(NYPizzaStore, ChicagoPizzaStore, CaliforniaPizzaStore)
PizzaStore의 orderPizza() 메소드에 이미 주문 시스템이 잘 갖춰져 있습니다.
모든 지점에서 이 주문 시스템을 따라 주문이 진행되어야 합니다. 각 지점마다 달라질 수 있는 것은 피자 스타일 뿐입니다.
PizzaStore 프레임워크에 충실하면서도 각각의 스타일을 제대로 구현할 수 있는 orderPizza() 메소드를 PizzaStore 서브클래스에 구비할 수 있습니다.
createPizza()는 추상 메소드로 선언되어 있기에, 서브클래스를 만들 때 이 메소드를 반드시 구현해야만 합니다.
PizzaStore의 orderPizza() 메소드를 한번 살펴볼까요? 그 메소드는 추상 클래스인 PizzaStore 클래스에 정의되어 있습니다.
그 클래스의 서브클래스를 만들기 전까지는 구상 클래스가 만들어지지 않죠.
조금 더 생각해 보면, orderPizza() 메소드에서 Pizza 객체를 가지고 여러 가지 작업 (피자를 준비하고, 굽고, 자르고, 포장하는 작업)을 하지만, Pizza는 추상 클래스라서 orderPizza()는 실제로 어떤 구상 클래스에서 작업이 처리되고 있는지 전혀 알 수 없습니다.
바꿔 말하면, PizzaStore와 Pizza는 서로 완전히 분리되어 있습니다.
orderPizza()에서 createPizza()를 호출하면 Pizza의 서브클래스가 그 호출을 받아서 피자를 만듭니다.
어떤 종류의 피자가 만들어질까요? 그건 피자를 주문하는 피자가게에 따라 다릅니다.
NYStylePizzaStore에서 주문을 하면 뉴욕 스타일 피자가 만들어질 테고
ChicagoStylePizzaStore에서 주문을 하면 시카고 스타일 피자가 만들어지겠죠.
피자의 종류는 어떤 서브클래스를 선택했느냐에 따라 결정됩니다. (p.156)
NYPizzaStore.java
PizzaStore를 확장하기에 orderPizza() 메소드도 자동으로 상속받습니다.
package FactoryPattern.Store;
import FactoryPattern.Pizza.NYStyleCheesePizza;
import FactoryPattern.Pizza.NYStyleClamPizza;
import FactoryPattern.Pizza.NYStylePepperoniPizza;
import FactoryPattern.Pizza.NYStyleVeggiePizza;
import FactoryPattern.Pizza.Pizza;
public class NYPizzaStore extends PizzaStore {
@Override
Pizza createPizza(String type) {
if (type.equals("cheese")) {
return new NYStyleCheesePizza();
} else if (type.equals("pepperoni")) {
return new NYStylePepperoniPizza();
} else if(type.equals("clam")) {
return new NYStyleClamPizza();
} else if(type.equals("veggie")) {
return new NYStyleVeggiePizza();
} else {
return null;
}
}
}
NYStyleCheesePizza.java
package FactoryPattern.Pizza;
public class NYStyleCheesePizza extends Pizza {
public NYStyleCheesePizza() {
name = "뉴욕 스타일 소스와 치즈 피자";
dough = "씬 크러스트 도우";
sauce = "마리나라 소스";
}
}
고객에 입장에서 살펴보겠습니다.
기존에 피자가게와 피자 만드는 과정이 나뉘어 있었던 부분을 하나로 합쳐 NYPizzaStore()를 구현하여 NYPizzaStore에서 주문만 하면 뉴욕 스타일 피자가 만들어지게 됩니다.
public class Consumer {
NYPizzaFactory nyFactory = new NYPizzaFactory();
PizzaStore nyStore = new PizzaStore(nyFactory);
nyStore.orderPizza("Cheese");
}
public class Consumer {
public Pizza orderPizza() {
var pizzaStore = new NYPizzaStore(); // 소비자가 공장을 선택할 필요가 없습니다.
return pizzaStore.orderPizza("Cheese");
}
}
팩토리 메소드 선언하기
pizzaStore는 구상 클래스 인스턴스 만드는 일을 하나의 객체가 전부 처리하는 방식에서 일련의 서브클래스가 처리하는 방식으로 바뀌었습니다.
피자 객체 인스턴스를 만드는 일은 pizzaStore의 서브클래스(NYStylePizzaStore)에 있는 createPizza() 메소드에서 처리합니다.
package FactoryPattern.Store;
import FactoryPattern.Pizza.Pizza;
public abstract class PizzaStore {
public Pizza orderPizza(String type) {
Pizza pizza = createPizza(type);
pizza.prepare();
pizza.bake();
pizza.cut();
pizza.box();
return pizza;
}
abstract Pizza createPizza(String type);
}
최첨단 피자 만들기
orderPizza() 메소드는 어떤 스타일의 피자가 만들어졌는지 전혀 알지 못합니다. 하지만 피자라는 건 알고 있어서 그피자를 준비, 굽기, 자르기, 포장 하는 작업을 완료합니다.
package FactoryPattern;
import FactoryPattern.Store.NYPizzaStore;
public class PizzaStation {
public static void main(String[] args) {
NYPizzaStore store = new NYPizzaStore();
store.orderPizza("cheese");
}
}
// console.log
준비 중 : 뉴욕 스타일 소스와 치즈 피자
도우 : 씬 크러스트 도우
소스 : 마리나라 소스
175도에서 25분간 굽기
8조각으로 자르기
상자에 피자 담기
팩토리 메소드 패턴 살펴보기
모든 팩토리 패턴은 객체 생성을 캡슐화합니다. 팩토리 메소드 패턴은 서브클래스에서 어떤 클래스를 만들지 결정함으로써 객체 생성을 캡슐화합니다.
팩토리 메소드 패턴의 정의
팩토리 메소드 패턴(Factory Method Pattern)에서는 객체를 생성할 때 필요한 인터페이스를 만듭니다.
어떤 클래스의 인스턴스를 만들지는 서브클래스에서 결정합니다.
팩토리 메소드 패 턴을 사용하면 클래스 인스턴스 만드는 일을 서브클래스에게 맡기게 됩니다.
병렬 클래스 계층구조 알아보기
둘 다 추상 클래스로 시작하고, 그 클래스를 확장하는 구상 클래스들을 가지고 있습니다. 그리고 뉴욕 지점과 시카고 지점의 구체적인 구현은 구상 클래스들이 책임지고 있습니다.
각각의 pizzaStore에는 각자의 스타일 피자를 만드는 모든 방법이 캡슐화되어 있습니다.
객체 의존성 살펴보기
모든 피자 객체를 직접 생성해야 하므로, 이 pizzaStore는 모든 피자 객체에 직접 의존하게 됩니다.
피자 클래스 구현이 변경되면 pizzaStore까지 고쳐야 할 수도 있습니다.
피자 종류를 새로 추가하면 pizzaStore는 더 많은 피자 객체에 의존하게 됩니다.
의존성 뒤집기 원칙
: Dependeny Inversion Principle
PizzaStore는 고수준 구성 요소라고 할 수 있고, 피자 클래스는 저수준 구성 요소라고 할 수 있습니다.
PizzaStore 클래스는 구상 피자 클래스에 의존하고 있다는 사실을 확실하게 알 수 있습니다.
디자인 원칙
추상화된 것에 의존하게 만들고
구상 클래스에 의존하지 않게 만든다.
이 원칙에는 고수준 구성 요소가 저수준 구성 요소에 의존하면 안 되며, 항상 추상화에 의존하게 만들어야 한다는 뜻이 담겨 있습니다.
팩토리메소드패턴을 적용하면 고수준 구성 요소(PizzaStore)와 저수준 구성 요소(피자 객체) 모두가 추상 클래스인 Pizza에 의존한다는 사실을 알 수 있습니다.
추상 팩토리 메서드 패턴은 다음에...
🧐 오늘 익은 소감은? 떠오르는 생각을 가볍게 적어보세요
스터디를 통해 배운 것을 작성해 봅니다.
✅ 라이브러리와 프레임워크의 차이 - 코드 실행의 주체에 대한 구분이 있다.
✅ 팩토리 메소드는 하위 클래스에 실제 코드를 구현함으로써 변경이 유연해진다. 즉, 역전제어를 통해 항상 추상화에 의존하게 끔 한다.
✅ 추상 클래스를 통해 나중에 구현할 코드인 서브 클래스에서 구현을 강제하도록 한다.
🙋♂️ 궁금한 내용이 있거나, 잘 이해되지 않는 내용이 있다면 적어보세요.
⭐️ 간단한 팩토리와 팩토리 메소드 패턴의 차이를 잘 모르겠어요. (p.169 / A6)
간단한 팩토리 : 스스로 결정한다 -> "Cheese" -> 같은 타입의 인스턴스가 나온다.
팩토리 메소드 패턴: 서브클래스가 결정한다 -> 서로 다른 클래스 -> "Cheese" -> 서로 다른 인스턴스가 나온다.
⭐️ 뉴욕과 시카고 지점을 만들 때는 간단한 팩토리를 사용한 것 같은데요? (p.169 / A2)
createPizza() 메서드에서 어떤 일을 할지는 각 지점에서 결정한다.
간단한 팩토리를 사용할 때는 팩토리가 PizzaStore 안에 포함되는 별개의 객체이다.