본문 바로가기
JAVA/DesignPattern

디자인패턴 - 팩토리 패턴 (factory pattern)

by 램쥐뱅 2016. 5. 12.

팩토리 패턴 (factory pattern)


팩토리 메소드 패턴 : 객체를 생성하기 위한 인터페이스를 정의하는데, 어떤 클래스의 인스턴스를

                                 만들지는 서브클래스에서 결정하게 만든다. 즉 팩토리 메소드 패턴을 이용하면

                                 클래스의 인스턴스를 만드는 일을 서브클래스에게 맡기는 것.


추상 팩토리 패턴 : 인터페이스를 이용하여 서로 연관된, 또는 의존하는 객체를 구상 클래스를 지정하지 않고도 생성.


new를 사용하는 것은 구상 클래스의 인스턴스를 만드는 것이다.

당연히! 인터페이스가 아닌 특정 구현을 사용하게 되어버리는 것.

일련의 구상 클래스들이 있을때는 어쩔수 없이 다음과 같은 코드를 만들어야 하는 경우가 있음.


 Duck duck;

 if ( type == picnic ) duck = new MallardDuck();

 else if ( type == hunting ) duck = new DecoyDuck();

 else if ( type == inBathTub) duck = new RubberDuck();


이런 코드가 있다는 것은, 뭔가 변경하거나 확장해야 할 때 코드를 다시 확인하고 추가 또는 제거해야 한다는 것을 의미함.


인터페이스에 맞춰서 코딩을 하면 시스템에서 일어날 수 있는 여러 변화를 이겨낼 수 있다. 왜냐하면..

다형성 덕분에 어떤 클래스든 특정 인터페이스만 구현하면 사용할수 있기 때문.

반대로. 구상 클래스를 많이 사용하면 새로운 구상 클래스가 추가될 때마다 코드를 고쳐야 하기때문에 많은 문제가 생길수 있다.

즉 변화에 대해 닫혀 있는 코드가 되어버리는 것.


디자인 원칙으로 봤을때 구상 클래스를 바탕으로 코딩을 하면 나중에 코드를 수정해야 할 가능성이 높아지고, 유연성이 떨어진다는걸 다시한번 확인했는데 그렇다면 회피할수 있는 방법은??

- 바뀔 수 있는 부분을 찾아내서 바뀌지 않는 부분하고 분리시켜야 한다는 원칙.




피자 가게를 운영하고 있고 피자가게 클래스를 만들어야 된다고 가정할때.



 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("pepperoni")) pizza = new PepperoniPizza();       


       pizza.prepare();

       pizza.bake();

       pizza.cut();

       pizza.box();

       return pizza;

 } 


굵게 표시된 부분이 매번 바뀌는 부분.. 글엄 객체 생성 부분을 캡슐화 해보자.

그리고 새로 만들 객체에는 팩토리(Factory) 라는 이름을 붙이기로 한다.


간단한 피자 팩토리를 만든다.


 public class SimplePizzaFactory {

public Pizza createPizza(String type){ //이런 경우에는 static메소드로 선언하는 경우가 종종 있음.

Pizza pizza = null;

if(pizza.equals("cheese")) pizza = new CheesePizza();

if(pizza.equals("pepper")) pizza = new PepperoniPizza();

if(pizza.equals("clam")) pizza = new ClamPizza();

if(pizza.equals("veggie")) pizza = new VeggiePizza();

return pizza;

}

 }



워밍업. 간단한 팩토리 (Simple Factory)

 

 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;

}

 }


간단한 팩토리는 디자인 패턴이라고 할 수는 없다.

프로그래밍을 하는데 있어서 자주 쓰이는 관용구에 가깝다고 할 수 있다.





피자 가게가 사업이 확장되어 여러 지역별로 각각의 다른 스타일의 피자를 만들어 내야하는 문제가 생겼다.

모든 프랜차이즈 분점에서 PizzaStore 코드를 사용하여 진행을 한다.


간단한 팩토리를 사용한다면 ???

SimplePizzaFactory 를 빼고 세가지 서로 다른 팩토리 (NYPizzaFactory, ChicagoPizzaFactory, CalifornizPizzaFactory)를 만들어서 적용한다면?




 PizzaStore nyStore = new PizzaStore(new NYPizzaFactory());

 nyStore.orderPizza("cheese");

 

 PizzaStore chicagoStore = new PizzaStore(new ChicagoPizzafactory());

 chicagoStore.orderPizza("cheese");


이런식으로 구현을 해보니.. 각 팩토리를 가진 피자가게 체인점들이 서로의 구현방식이 달라지는 일이 발생할수도 있게 됬음. (PizzaStore이 각각 있다보니 굽는 방식이 달라진다거나 피자를 자르는 단계를 빼먹거나 하는..)




1. 팩토리 메소드 패턴


피자가게와 피자 제작 과정 전체를 하나로 묶어주는 프레임워크를 만들어야 된다는 결론!!

파자를 만드는 활동 자체는 전부 PizzaStore 클래스에 국한시키면서도 분점마다 고유의 스타일을 살리수 있는 방법은 ??



 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); //Pizza 인스턴스를 만드는 일은 팩토리 역할을 하는 메소드에서 맡아 처리

 }


이제 각 분점을 위한 지역별로 서브클래스를 만들어줘야 한다. 피자의 스타일은 각 서브클래스에서 결정.



이제 ChicagoPizzaStore, NYPizzaStore 에는구상 피자클래스를 분기해주는 각각의 createPizza 메소드가 있음.



 public class NYPizzaStore extends PizzaStore{

@Override

public Pizza createPizza(String type){

Pizza pizza = null;

if(type.equals("cheese")) pizza = new NYStyleCheesePizza();

if(type.equals("peper")) pizza = new NYStylePepperoniPizza();

if(type.equals("clam")) pizza = new NYStyleClamPizza();

if(type.equals("veggie")) pizza = new NYStyleVeggiePizza();

return pizza;

}

 } 



 public class ChicagoPizzaStore extends PizzaStore{

@Override

public Pizza createPizza(String type){

Pizza pizza = null;

if(type.equals("cheese")) pizza = new ChicagoStyleCheesePizza();

if(type.equals("peper")) pizza = new ChicagoStylePepperoniPizza();

if(type.equals("clam")) pizza = new ChicagoStyleClamPizza();

if(type.equals("veggie")) pizza = new ChicagoStyleVeggiePizza();

return pizza;

}

 } 




 public abstract class Pizza{
String name;
String dough;
String sauce;
ArrayList<String> toppings = new ArrayList<>();
public void prepare(){
System.out.println("Preparing : "+name);
System.out.println("Tossing dough...");
System.out.println("Adding source");
System.out.println("Adding toppings");
for (String topping : toppings) {
System.out.println("\ttopping : "+topping);
}
}
public void bake(){
System.out.println("Bake for 25 minutes at 350");
}
public void cut(){
System.out.println("Cutting the pizza into diagonal slices");
}
public void box(){
System.out.println("Place pizza in official PizzaStore box");
}
public String getname(){
return this.name;
}
 }


 public class NYStyleCheesePizza extends Pizza{

public NYStyleCheesePizza() {

this.name = "NY Style CheesePizza";

this.dough = "Thin Crust Dough";

this.sauce = "Marinara Sauce";

this.toppings.add("Grated Reggiano Cheese");

}

 }



 public class ChicagoStyleCheesePizza extends Pizza{

public ChicagoStyleCheesePizza() {

this.name = "Chicago Style CheesePizza";

this.dough = "Extra Thick Crust Dough";

this.sauce = "Plum Tomato Sauce";

this.toppings.add("Shredded mozzarella Cheese");

}

@Override

public void cut() {

System.out.println("Cutting the pizza into square slices");

}

 }

 


 public class PizzaTestDrive {

public static void main(String[] args) {

PizzaStore nyStore = new NYPizzaStore();

PizzaStore chicagoStore = new ChicagoPizzaStore();

Pizza nySytpePizza = nyStore.orderPizza("cheese");

System.out.println(nySytpePizza.getname());

System.out.println();

Pizza chicagoStypePizza = chicagoStore.orderPizza("cheese");

System.out.println(chicagoStypePizza.getname());

}

 } 



모든 팩토리 패턴에서는 객체 생성을 캡슐화 한다.

팩토리 메소드 패턴에서는 서브 클래스에서 어떤 클래스를 만들지를 결정하게 함으로써 객체 생성을 캡슐화 한다.



객체를 생산하는 생산자 클래스




제품을 생산하는 제품 클래스



위의 클래스 아이어 그램을 보면 생산을 담당하는 PizzaStore 추상 클래스에서 객체를 만들기 위한 메소드, 즉 팩토리 메소드를 위한 인터페이스를 제공한다는 것을 알수있다. PizzaStore에 구현되어 있는 다른 메소드 orderPizza 에서는 팩토리 메소드에 의해 생산된 제품을 가지고 필요한 작업을 처리한다. 하지만 실제 팩토리 메소드를 구현하고 제품(객체 인스턴스)을 만들어 내는 일은 서브클래스에서만 할수 있다.




이로써 구상 클래스에 대한 의존성을 줄이는 것이 좋다는 것은 이제 확실해졌다.

이런 내을 정리해  놓은 객체지향 디자인 원칙이 바로 의존성 뒤집기 원칙 (Dependency Inversion Principle) 이다.


디자인 원칙


추상화된 것에 의존하도록 만들어라. 구상 클래스에 의존하도록 만들지 않도록 한다.


왜 의존성 뒤집기 원칙이냐면...


PizzaStore -> NYStyleCheesePizza

PizzaStore -> ChicagoStypeCheesePizza

PizzaStore -> NYStyleVeggiePizza


이런식으로 의존이 되던 좋지않은 디자인이


PizzaStore -> Pizza

Pizza <- NYStyleCheesePizza

Pizza <- ChicagoStyleCheesePizza

Pizza <- NYStyleVeggiePizza


이런식으로 의존관계게 뒤집어지는 형상.


팩토리 메소드 패턴을 적용하고 나면 고수준 구성요소(PizzaStore)와 저수준 구성요소(NYStyleCheesePizza, ChicagoStylePizza, ..) 들이 모두 추상 클래스인 Pizza에 의존하게됨. (고수준 모듈과 저수준 모듈이 둘다 하나의 추상 클래스에 의존)

팩토리 메소드 패턴이 의존성 뒤집기 원칙을 준수하기 위해 쓸 수 있는 유일한 기법은 아니지만 가장 적합한 벙법 가운데 하나. 



의존성 뒤집기 원칙에 위배되는 객체지향 디자인을 피하는데 도움이 되는 가이드.


 1. 어떤 변수에도 구상 클래스에 대한 레퍼런스를 지정하지 않는다.

     - new 연산자를 사용하면 레퍼런스를 사용하게 되는 것.


 2. 구상 클래스에서 유도된 클래스를 만들지 않는다.

     - 구상클래스에서 유도된 클래스를 만들면 특정 구상 클래스에 의존하게됨, 추상화 된것을 사용해야 함.


 3. 베이스 클래스에 이미 구현되어 있던 메소드를 오버라이드 하지 않는다.

     - 이미 구현되어 있는 메소드를 오버라이드 한다는 것은 애초부터 베이스 클래스가 제대로 추상화 된것이 아니었다고

        볼수있다. 베이스 클래스에서 메소드를 정의할 때는 모든 서브 클래스에서 공유할 수 있는 것만 정의해야함.



위의 가이드 라인은 다른 원칙들과 마찬가지로 항상 지켜야 하는 규칙이 아니라. 지향해야하는 바를 밝히는 것.

String 인스턴스는 사실 별생각 없이 쓰는데 엄밀히 말하자면 이것도 원칙에 위배되는 것이지만 별문제가 되지않는다. 왜냐하면 String 클래스가 바뀌는 일의 거의 없을테니까. 하지만 자신이 만들고 있는 클래스가 바뀔 가능성이 있다면 팩토리 메소드 패턴 같은 기법을 써서 변경될 수 있는 부분을 캡슐화 하여야 한다.



이렇게 PizzaStore 디자인이 모양새를 갖췄다. 유연한 프레임워크도 만들어 졌고, 디자인 원칙도 충실하게 지켰다.

각각 체인점들이 미리 정해놓은 절차를 잘 따르고 있지만 몇몇 체인점들이 자잘한 재료를 더 싼 재료로 바꿔서 원가를 절감해 마진을 남기고 있다. 원재료의 품질까지 관리하는 방법이 있을까??

     - 원재료 군을 만들어 파악하자.

       제품에 들어가는 재료군(반죽, 소스, 치즈, 야채, 고기)은 같지만, 지역마다 재료의 구체적인 내용이 조금씩 다르다.



2. 추상 팩토리 패턴



원재료 공장을 만들어보자.


1. 지역별로 팩토리를 만들어 각 생성 메소드를 구현하는 PizzaingredientFactory 클래스를 만들어야 함.

2. ReggianoCheese, RedPeppers, ThickCrustDough와 같이 팩ㄴ토리에서 사용할 원재료 클래스들을 구현한다.

3. 만든 원재료 공장을 PizzaStore 코드에서 사용하도록 함으로써 모든 것을 하나로 묶어준다. 



 public interface PizzaIngredientFactory {

public Dough createDough();

public Sauce createSauce();

public Cheese createCheese();

public Veggies[] createVeggies();

public Pepperoni createPepperoni();

public Clams createClams();

 } 




 public class NYPizzaingredientFactory implements PizzaIngredientFactory{

@Override

public Dough createDough() {

return new ThinCrustdough();

}


@Override

public Sauce createSauce() {

return new MarinaraSauce();

}


@Override

public Cheese createCheese() {

return new ReggianoCheese();

}


@Override

public Veggies[] createVeggies() {

Veggies veggies[] = { new Farlic(), new Onion(), new Mushroom(), new RedPepper() };

return veggies;

}


@Override

public Pepperoni createPepperoni() {

return new SlicedPepperoni();

}


@Override

public Clams createClams() {

return new Freshclams();

}

 }



 public class ChicagoPizzaingredientFactory implements PizzaIngredientFactory{
@Override
public Dough createDough() {
return new ThickCrustDough();
}

@Override
public Sauce createSauce() {
return new PlumTomatoSauce();
}

@Override
public Cheese createCheese() {
return new MozzarellaCheese();
}

@Override
public Veggies[] createVeggies() {
Veggies veggies[] = { new BlackOlives(), new Spinach(), new EggPlant()};
return veggies;
}

@Override
public Pepperoni createPepperoni() {
return new Slicedpepperoni();
}

@Override
public Clams createClams() {
return new FrozenClam();
}
 }
 

 public abstract class Pizza{
String name;
Dough dough;
Sauce sauce;
Veggies veggies[];
Cheese cheese;
Pepperoni pepperoni;
Clams clams;
public abstract void prepare(); //추상 메소드로 변경됨.
public void bake(){
System.out.println("Bake for 25 minutes at 350");
}
public void cut(){
System.out.println("Cutting the pizza into diagonal slices");
}
public void box(){
System.out.println("Place pizza in official PizzaStore box");
}
public String getname(){
return this.name;
}
 } 



 


 public class CheesePizza extends Pizza{

PizzaIngredientFactory ingredientFactory;

public CheesePizza(PizzaIngredientFactory ingredientFactory) {

this.ingredientFactory = ingredientFactory;

}


@Override

public void prepare() {

this.dough = ingredientFactory.createDough();

this.sauce = ingredientFactory.createSauce();

this.cheese = ingredientFactory.createCheese();

}

 } 




 public class ClamPizza extends Pizza{

PizzaIngredientFactory ingredientFactory;

public ClamPizza(PizzaIngredientFactory ingredientFactory) {

this.ingredientFactory = ingredientFactory;

}


@Override

public void prepare() {

this.dough = ingredientFactory.createDough();

this.sauce = ingredientFactory.createSauce();

this.cheese = ingredientFactory.createCheese();

this.clams = ingredientFactory.createClams();

}

 }



 public class NYPizzaStore extends PizzaStore{

@Override

public Pizza createPizza(String type){

Pizza pizza = null;

PizzaIngredientFactory ingredientFactory = new NYPizzaingredientFactory();

if(type.equals("cheese")){

pizza = new CheesePizza(ingredientFactory);

pizza.setName(ingredientFactory.NY_STYLE+" Cheese Pizza");

}else if(type.equals("peper")){

pizza = new PepperoniPizza(ingredientFactory);

pizza.setName(ingredientFactory.NY_STYLE+" Pepperoni Pizza");

}else if(type.equals("clam")){

pizza = new ClamPizza(ingredientFactory);

pizza.setName(ingredientFactory.NY_STYLE+" Clam Pizza");

}else if(type.equals("veggie")){

pizza = new VeggiePizza(ingredientFactory);

pizza.setName(ingredientFactory.NY_STYLE+" Veggie Pizza");

}

return pizza;

}

 } 






이제 전체적인 흐름은.


1. 뉴욕 피자가게를 만든다.

     - PizzaStore nyPizzaStore = new NYPizzaStore();


2. 주문을 한다.

     - nyPizzaStore.orderPizza("cheese");


3. orderPizza 메소드에서는 우선 createPizza() 메소드를 호출한다

     - Pizza pizza = createPizza("cheese");


4. createPizza() 메소드가 호출되면 원재료 공장이 돌아가기 시작한다.

     - Pizza pizza = new CheesePizza(nyIngredientFactory);


5. 피자를 준비하는 prepare()메소드가 호출되면 팩토리에 원재료 주문이 들어간다.

     - void prepare(){

            dough = nyIngredientFactory.createDough();

            sauce = nyIngredientFactory.createSauce();

            cheese = nyIngredientFactory.createCheese();

       }


6. 준비단계가 끝나고 orderPizza() 메소드에서는 피자를 굽고, 자르고, 포장한다.





다시 한번 정리를 해보면..



 추상 팩토리 패턴 : 제품군을 생성하기 위한 인터페이스를 생성 그 인터페이스를 구성하여 사용할수 있게끔 하는것.


 추상 메소드 패턴 : 하나의 추상클래스에서 추상 메소드를 만들고 서브클래스들이

                          그 추상메소드를 구현하여 인스턴스를 만들게끔 하는것.













참고.


Head First Design patterns.

댓글