"추상화된 것에 의존하게 만들고, 구상 클래스에 의존하지 않게 한다"
-> 가이드 일 뿐이지만, 지키라고 하는 이유를 다시 한번 생각해보자.
이전에 내가 올렸던 팩토리 패턴은 자바/스프링을 통해서 "간단한 팩토리"를 구현했다.
오늘은 좀 더 나아가서, 간단한 팩토리를 다시 한번 정리하고 좀 더 거대한 녀석으로 만들어 보자.
팩토리 패턴은, 객체의 생성 부분을 캡슐화해서 상위 추상이 아닌 구상 클래스 그 자체가 생성을 수행하는 패턴이다.
(이 때, 상위 추상은 어떤 구현체로 생성되는 지 "몰라"도 된다는 점이 포인트.)
피자 가게를 운영해봅시다!
public class OrderPizza {
Pizza orderPizza(){
Pizza pizza = new Pizza();
pizza.prepare();
pizza.bake();
pizza.cut();
pizza.box();
return pizza;
}
}
그런데, 피자 종류는 하나가 아니다! 따라서 좀 더 다양하게 만들어 보자.
if / else 문을 사용해서 분기를 타면 좋겠네!
public class OrderPizza {
Pizza orderPizza(String type){
Pizza pizza = null;
if (type.equals("cheese")){
pizza = new CheesePizza();
} else if(type.equals("peperoni")){
pizza = new PeperoniPizza();
} else {
throw new NullPointerException("그런 피자는 없어요!");
}
pizza.prepare();
pizza.bake();
pizza.cut();
pizza.box();
return pizza;
}
}
오! 이제 피자를 CheesePizza, PeperoniPizza 등 여러 타입으로 생성 가능하구나!
추상 클래스인 pizza, 그리고 그 구현체들인 CheesePizza, PeperoniPizza 를 각각 처리 할 수 있다!
맞은 편 가게에는 파인애플 피자도 있던데, 여긴 안파나요?
...(이전 코드들)
Pizza orderPizza(String type){
Pizza pizza = null;
if (type.equals("cheese")){
pizza = new CheesePizza();
} else if(type.equals("peperoni")){
pizza = new PeperoniPizza();
} else if (type.equals("pineapple")) { // 새로 추가됨!
pizza = new PineapplePizza();
} else {
throw new NullPointerException("그런 피자는 없어요!");
}
pizza.prepare();
pizza.bake();
pizza.cut();
pizza.box();
return pizza;
}
어? 그런데 이렇게하면 OrderPizza 클래스의 매서드에 매번 피자가 새로 생길때마다 코드의 변경이 생긴다!!!
오더피자의 코드는 변경하지 않고, 피자를 새로 만들 수 있는 방법은 없을까?
분기문을 타는 부분(if/else)이 아무래도 마음에 걸린다. "생성"의 역할을 떼서 다른 곳으로 빼버리면 좋겠다!
그럼, 피자를 선별해주는 공장 클래스를 만들어서 처리해보자!
public class SimplePizzaFactory {
public Pizza createPizza(String type){
Pizza pizza = switch (type) {
case "cheese" -> new CheesePizza();
case "peperoni" -> new PeperoniPizza();
case "pineapple" -> new PineapplePizza();
default -> throw new NoSuchElementException("그런 피자는 없어요!");
};
return pizza;
}
}
public class PizzaStore {
SimplePizzaFactory simplePizzaFactory;
public PizzaStore(SimplePizzaFactory simplePizzaFactory) {
this.simplePizzaFactory = simplePizzaFactory;
// 생성자에 아규먼트로 받아서 생성 매서드가 이루어진다.
}
public Pizza orderPizza(String type){
Pizza pizza;
pizza = simplePizzaFactory.createPizza(type);
pizza.prepare();
pizza.bake();
pizza.cut();
pizza.box();
return pizza;
}
}
위 다이어그램에서는 Pizza 의 구현체들만 화살표로 나와있지만, 참조 관계는
핵심은, 객체의 생성을 Factory 클래스에 위임한것.
위 작업이 바로 "간단한 팩토리" 라고 명명된 방식이다.
간단한 팩토리를 통해서 똑똑하게 피자를 생산한 결과 큰 돈을 벌어서, 프랜차이즈 사업을 시작하기로 했다.
뉴욕 피자 팩토리, 시카고 피자 팩토리, 캘리포니아 피자 팩토리를 나눠서 세 가지 스타일 피자를 판매해보자
PizzaStore 에서 적당한 팩토리를 사용하면 될 것 같다!
NYPizzaFactory nyFac = new NYPizzaFactory(); // 뉴욕피자 팩토리
PizzaStore nyStore = new PizzaStore(nyFac); // pizzaStore 생성을 위해 팩토리 주입
nyStore.orderPizza("vegie"); // 아규먼트로 type 전달
오! 이런식으로 계속 뭔가를 추가 할 수 있을 거 같은데요?
그런데, 지점에서 우리가 만든 팩토리로 피자를 굽긴 하는데, 완전히 지네 맘대로 하고 있다!(피자를 자르지 않거나, 포장하지 않거나, 굽지 않은 피자를 리턴하는 등)
그냥 PizzaStore 에서 피자를 생산하는 일은 다 하고, 지점의 스타일(세부사항)을 살릴 수 있는 방법은 없을까?
즉, 핵심 로직은 공유하면서 유연한 확장이 가능할 순 없을까?
public abstract class PizzaStore {
abstract Pizza createPizza(String type);
// 추상 매서드로 createPizza 선언! 이부분이 지점마다의 차이를 만들 것이다.
public Pizza orderPizza(String type){
Pizza pizza;
pizza = createPizza(type);
pizza.prepare();
pizza.bake();
pizza.cut();
pizza.box();
return pizza;
}
}
추상 매서드 createPizza()를 사용해서, 하위 팩토리들은 모두 createPizza() 매서드를 구현하도록 강제한다.
이를 통해서 하위 팩토리들은 자신만의 필드,매서드를 가지면서도, "Pizza"라는 객체를 리턴하는 매서드를 구현 할 수 있다. 즉, 유연하게 생성이 가능하다
public class NYPizzaStore extends PizzaStore{
@Override
Pizza createPizza(String type) {
Pizza pizza;
if (type.equals("NYCheese")){
pizza = new NYCheesePizza();
} else if (type.equals("NYPeperoni")){
pizza = new NYPeperoniPizza();
} else {
throw new NoSuchElementException("그런 건 없어요!");
}
return pizza;
}
}
public class ChicagoPizzaStore extends PizzaStore{
@Override
Pizza createPizza(String type) {
Pizza pizza;
if (type.equals("CHCheese")){
pizza = new NYCheesePizza();
} else if (type.equals("CHPeperoni")){
pizza = new NYPeperoniPizza();
} else {
throw new NoSuchElementException("그런 건 없어요!");
}
return pizza;
}
}
위와 같이 뉴욕, 시카고 피자 스토어를 선언한다.
각각 적절한 타입이 들어왔을 때 어떤 객체를 리턴 할 지 구성 가능하다.
public abstract class PizzaStore {
abstract Pizza createPizza(String type);
// 피자 인스턴스 만드는 일은 팩토리 매소드에서 알아서 처리함
public Pizza orderPizza(String type){
Pizza pizza;
pizza = createPizza(type);
// pizza 가 어떤 타입인진 몰라도 됨
// 이전 코드 : Pizza pizza = new NYStylePizza();
pizza.prepare();
pizza.bake();
pizza.cut();
pizza.box();
return pizza;
}
}
createPizza() 부분에 적절한 아규먼트만 넣어준다면(type), PizzaStore 입장에서는 "어떤 클래스를 사용해 만든 피자객체"인지 알 필요가 없다.
이제 피자 스토어는 구현체의 인스턴스를 만들기 위한 코드의 변경이 없이, 심지어 어떤 타입인지 알지 못하더라도 상관 없이 새 인스턴스를 반환해 줄 수 있다.
그럼, 같이 테스트 해보자
public abstract class Pizza {
String name;
String dough;
String sauce;
List<String> toppings = new ArrayList<>();
public void prepare(){
System.out.println("준비 중 : " + name);
System.out.println("도우를 돌리는 중...");
System.out.println("소스 뿌리는 중");
System.out.println("토핑을 올리는 중: ");
for (String topping : toppings) {
System.out.println("토핑 : " + topping);
}
}
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 NYCheesePizza extends Pizza{
public NYCheesePizza() {
name = "뉴욕 스타일 소스와 치즈 피자";
dough = "씬 크러스트 도우";
sauce = "마리나라 소스";
toppings.add("잘게 썬 파마산 치즈");
}
}
public class ChicagoCheesePizza extends Pizza{
public ChicagoCheesePizza() {
name = "시카고 딥 디쉬 피자";
dough = "아주 두꺼운 크러스트 도우";
sauce = "플럼 토마토 소스";
toppings.add("잘게 조각낸 모짜렐라 치즈");
}
@Override
public void cut() {
System.out.println();
}
}
위와 같이 필드에 대해서 각자 구현한 구상 클래스들이다.
추상의 핵심 매서드인 cut 매서드도 오버라이드해서 사용이 가능하다 -> 유연성!
어떤 클래스의 인스턴스를 만들지 서브클래스에서 결정하게 하는 것.
즉, 변경되는 부분을 분리해서 캡슐화 + 유연하게 확장 가능하게 한다
복잡성 : 추가적 클래스, 팩토리 매서드 패턴에서 제공하는 새 "방법"(=type 같은)들이 복잡성을 증가시킬 가능성이 있음.
++ 제품 객체의 갯수마다 공장 서브 클래스를 모두 구현해야되서 클래스 폭발이 일어날수 있다는 점
+_+이제 패턴 공부까지!! ㅎㅎ 조만간 요거 참고해서 공부 할게요 ㅎㅎ 정리 너무 잘했어영