북극곰의 개발일기

팩토리 메소드 패턴(Factory Method Pattern)





posted by purplebeen on Tue Jan 30 2018 23:37:35 GMT+0900 (KST) in JAVA


블로그에 포스팅하는게 점점 더 귀찮아져간다 ㅋㅋㅋㅋ

각설하고 이번에는 팩토리 메소드 패턴을 다룰려고 한다.

팩토리 메소드 패턴

팩토리 메소드 패턴 : 객체를 생성하기 위한 인터페이스를 만들고 어떤 클래스의 인스턴스를 만들지는 서브클래스에서 결정하도록 한다. 팩토리 메소드를 이용하면 인스턴스를 만드는 일을 서브클래스로 미룰 수 있다.

객체지향 디자인 원칙

  1. 바뀌는 부분을 캡슐화한다.
  2. 상속보다는 객체 구성을 활용한다.
  3. 구현이 아닌 인터페이스에 맞춰서 프로그래밍한다.
  4. 서로 상호작용을 하는 객체 사이에서는 가능하면 느슨하게 결합하는 디자인을 활용해야 한다.
  5. 클래스는 확장에 대해서는 열려있지만 변경에 대해서는 닫혀 있어야 한다.(OCP)
  6. 추상화된 것에 의존하라. 구상 클래스에 의존하지 않도록 한다.

new 키워드의 문제점

new 키워드는 구상 클래스의 인스턴스 즉 구상 객체를 만든다.

그런데 구상 클래스를 바탕으로 코딩을 하게 되면 나중에 "변화"가 생겼을때 수정해야할 가능성이 높아지고, 유연성이 떨어지게 된다.

피자 가게를 운영한다고 치자. 주문 받는 부분을 다음과 같이 짤 수 있을 것이다.

Pizza orderPizza(String type) { 
	Pizza pizza; 
	if(type.equals("cheese")) { 
		pizza = new CheesePizza(); 
	} else if(type.equals("greek")) {
		pizza = new GreekPizza(); 
	} else if(type.equals("pepporoni")) { 
		pizza = new PepperoniPizza(); 
	} 
	pizza.prepare(); 
	pizza.bake(); 
	pizza.cut(); 
	pizza.box(); 
	return pizza; 
	} 
}

피자 의 종류가 한두개가 아니므로 종류가 추가될때마다 조건문을 써줘서 비교를 해야할 것이다.

즉 이 부분이 변화가 생기는 부분이 된다. 그러면 이 부분을 캡슐화하여 간단한 팩토리를 만들어보자.

package FactoryPattern;

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;
	}

}

자 이제 피자를 만들기 위한 객체 생성 부분을 전담할 클래스를 정의하여 간단한 팩토리를 만들었다. 이제 Pizza Store 클래스를 이에 맞추어 수정해보자

package FactoryPattern;

public class PizzaStore {
	SimplePizzaFactory factory;
	public PizzaStore(SimplePizzaFactory 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;
	}
}

SimplePizzaFactory 객체를 생성자에서 인자값으로 받아와서 PizzaStore 안에 있는 factory에 넘겨주었다.

또한 orderPizza(String type)라는 메소드에서 String형태 변수 type를 받아와 SimplePizzaFactory의 createPizza 메소드를 실행할 때 인자값으로 넘겨줌으로서 그에 맞는 객체를 생성할 수 있다.

여기까지는 상관이 없다. 피자의 종류가 한정되어 있으니까.

그런데 만약 여기서 피자 가게가 서로 다른 지역에 맞춰서 피자의 재료가 바꿔서 팩토리를 다시 만들어야한다면 어떻게 될까?

지점이 생길때마다 계속해서 팩토리를 수정해야할 것이다. 그런데 이렇게 되면 확장에는 열려있지만 변화에는 닫혀있어야 한다는 OCP 원칙에 위배 된다.

이 원칙을 지키면서 이러한 변경에 유연하도록 하기 위해서는

이러한 객체의 생성 과정을 서브클래스에서 결정시키도록 하면 될 것이다.

Pizza스토어에서 변화가 있는 부분(Create Pizza 메소드)를 추상화해서 Pizza Store의 서브 클래스에서 결정하도록 하면 된다.

이에 맞게 수정해보자.

package FactoryPattern;

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);
}

createPizza() 메소드를 추상(abstract) 메소드로 선언 했다. 이제 각 지점에 맞게 하위 클래스를 구현해보자

package FactoryPattern;

public class NYPizzaStore extends PizzaStore {
	@Override
	Pizza createPizza(String type) {
		if(type.equals("cheese")) {
			return new NYStyleCheesePizza();
		} else {
			return null;
		}
	}
	

}
package FactoryPattern;

public class ChicagoPizzaStore extends PizzaStore {
	@Override
	Pizza createPizza(String type) {
		// TODO Auto-generated method stub
		if(type.equals("cheese")) return new ChicagoStyleCheesePizza();
		else return null;
	}
	
}

PizzaStore의 하위클래스인 NYPizzaStore와 ChicagoPizzaStore에서 createPizza()메소드를 오버라이딩 하여 객체 생성하는 부분을 구현했다.

이렇게 하게되면 새로운 분점이 추가되더라도 새로운 하위클래스를 추가하기만 하며 될 것이다.

자 이제 그러면 우리가 최종적으로 만들어야할 제품인 Pizza 클래스를 만들어 보자.

package FactoryPattern;

import java.util.ArrayList;

public abstract class Pizza {
	String name;
	String dough;
	String sauce;
	ArrayList toppings = new ArrayList();
	
	void prepare() {
		System.out.println("Preparing " + name);
		System.out.println("Tossing dough....");
		System.out.println("Adding Sauce....");
		System.out.println("Adding toppings...");
		for(int i = 0; i < toppings.size(); i++) {
			System.out.println("   " + toppings.get(i));
		}
	}
	
	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");
	}
	
	public String getName() {
		return name;
	}
}

String형으로 이름과 도우, 소스를 받아오고 토핑은 어레이리스트로 받아주고 있다.

bake() cut() box()에서 각 과정을 거치고 있고. getName()메소드를 통해서 이름을 리턴하고 있다.

이제 그러면 Pizza의 하위 클래스인 뉴욕풍 치즈 피자와 시카고풍 치즈피자 클래스를 만들어 보자.

package FactoryPattern;

public class NYStyleCheesePizza extends Pizza {
	public NYStyleCheesePizza() {
		name = "Ny Style Sauce and Chese Pizza";
		dough = "Thin Crust Dough";
		sauce = "Marinara Reggiano Chese";
		
		toppings.add("grated Reggiano Cheese");
	}
}
package FactoryPattern;

public class ChicagoStyleCheesePizza extends Pizza{

	public ChicagoStyleCheesePizza() {
		// TODO Auto-generated constructor stub
		name = "Chicago style Deep Dish Cheese Pizza";
		dough = "Extra Thick Crust dough";
		sauce = "Plum Tomato Sauce";
		
		toppings.add("Shredded Mozzarella Cheese");
	}

	void cut() {
		System.out.println("Cutting the pizza into square slices");
	}
	

}

NYStyleCheesePizza에서는 Pizza를 상속하여 이름. 도우. 소스를 설정하고 토핑을 추가하였다.

ChicagoStyleCheesePizza에서는 이름,도우,소스를 설정하고 토핑을 추가할 뿐만 아니라 cut()메소드를 오버라이딩하여 ChicagoStyleCheesePizza만의 자르는 특징을 추가했다.

자 이제 그러면 이렇게 만든 피자들을 테스트해보자.


package FactoryPattern;

public class PizzaTestdrive {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		PizzaStore nyStore = new NYPizzaStore();
		PizzaStore chicagoStore = new ChicagoPizzaStore();
		
		Pizza pizza = nyStore.orderPizza("cheese");
		System.out.println("Ethan ordered a " + pizza.getName() + "\n");
		
		pizza = chicagoStore.orderPizza("cheese");
		System.out.println("Joel ordered a " + pizza.getName() + "\n");
	}

}

NYPizzaStore와 ChicagoPizzaStore 객체를 만들어 orderPizza메소드를 통해 피자를 생성하였다. 이를 실행시켜보면,

Preparing Ny Style Sauce and Chese Pizza
Tossing dough....
Adding Sauce....
Adding toppings...
   grated Reggiano Cheese
Bake for 25 minutes at 350
Cutting the pizza into diagonal slices
Place pizza in official PizzaStore box
Ethan ordered a Ny Style Sauce and Chese Pizza

Preparing Chicago style Deep Dish Cheese Pizza
Tossing dough....
Adding Sauce....
Adding toppings...
   Shredded Mozzarella Cheese
Bake for 25 minutes at 350
Cutting the pizza into square slices
Place pizza in official PizzaStore box
Joel ordered a Chicago style Deep Dish Cheese Pizza

NYCheesePizza와 ChicagoCheesePizza객체가 생성되어 변경된 부분이 제대로 실행됨을 알 수 있다.