FIF's 코딩팩토리

팩토리 패턴(Factory Pattern) 정리 본문

Back-End/Design Pattern(디자인 패턴)

팩토리 패턴(Factory Pattern) 정리

FIF 2019. 6. 4. 13:42
반응형

팩토리 패턴(Factory Pattern)

 

팩토리 메소드 패턴 : 객체를 생성하기 위해 인터페이스를 정의하는데, 어떤 클래스의 인스턴스를 만들지는 서브클래스에서 결정하도록 만든다.

즉, 팩토리 메소드 패턴을 이용하면 클래스의 인스턴스 만드는 일을 서브클래스에게 맡길 수 있다.

 

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

 

new를 사용한다는 것은 구상 클래스의 인스턴스를 만든다는 의미이다.

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

왜냐하면 다형성 때문에 어떤 클래스든 간 특정 인터페이스만 구현하면 되기 때문이다.

 

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

즉, 변화에 대해서는 닫혀 있는 코드가 된다.

 

디자인 원칙상 구상 클래스를 바탕으로 코딩을 하면 추후 코드를 수정해야 할 가능성이 높고, 유연성이 떨어진다.

이 문제를 해결하기 위해 바뀔 수 있는 부분을 찾아내서 바뀌지 않는 부분하고 분리시켜야 한다는 원칙이 팩토리 패턴이다.

 

피자가게가 있다. 이 피자가게에서는 치즈, 페페로니, 야채 피자를 판매한다.

피자가게가 흥하면서 뉴욕, 시카고, 캘리포니아에 분점도 생겼다.

그런데 분점마다 피자를 만드는 방식이 다르다는 문제가 생겼다.(굽는 방식이 다르다던지, 피자 자르는 단계가 없다던지…)

 

그래서 피자가게와 제작과정을 하나로 묶어주는 프레임워크를 만들다는 결론이 나왔다.

 

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;
      }
       //Pizza 인스턴스를 만드는 일은 팩토리 역할을 하는 메소드에서 맡아 처리한다.
       abstract Pizza createPizza(String type);
}

 

 

각각의 분점을 위해 지역별로 서브클래스를 만들어 줘야 한다.

피자 스타일은 각 서브클래스에서 결정한다.

 

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

public class NYPizzaStore extends PizzaStore {
      @Override
      Pizza createPizza(String type) {
            Pizza pizza = null;
            if(type.equals("cheese")) {
                  pizza = new NYStyleCheesePizza();
            }
            if(type.equals("pepper")) {
                  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
      Pizza createPizza(String type) {
            Pizza pizza = null;
            if(type.equals("cheese")) {
                  pizza = new ChicagoStyleCheesePizza();
            }
            if(type.equals("pepper")) {
                  pizza = new ChicagoStylePepperoniPizza();
            }
            if(type.equals("clam")) {
                  pizza = new ChicagoStyleClamPizza();
            }
            if(type.equals("veggie")) {
                  pizza = new ChicagoStyleVeggiePizza();
            }
            return pizza;
      }

 

import java.util.ArrayList;

//피자제조 과정은 똑같이 통일한다.(단, 스타일은 다를 수 있다.)
public abstract class Pizza {
      String name; // 파자 이름
      String dough; // 피자 도우
      String sauce; // 피자 소스
      ArrayList<String> 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 slice");
      }
      void box() {
            System.out.println("Place pizza in official PizzaStroe box");
      }
      public String getName() {
            return this.name;
       }

 

public class NYStyleCheesePizza extends Pizza {

      public NYStyleCheesePizza() {
            name = "NY Style Sauce and Cheese Pizza";
            dough = "Thin Crust Dough";
            sauce = "Marinara Sauce";
            toppings.add("Grated Reggiano Cheese");
      }

 

public class ChicagoStyleCheesePizza extends Pizza{
      public ChicagoStyleCheesePizza() {
            name = "Chicago Style Deep Dish Cheese Pizza";
            dough = "Extra Thick Crust Dough";
            sauce = "Plum Tomato Sauce";
            toppings.add("Shredded Mozzarella Cheese");
      }
      @Override
      void cut() {
            System.out.println("Cutting the pizza into sqaare slices");
      }

 

public class PizzaTestDrive {
      public static void main(String[] args) {
            PizzaStore nyStore = new NYPizzaStore();
            PizzaStore chicagoStore = new ChicagoPizzaStore();
            Pizza nyStylePizza = nyStore.orderPizza("cheese");
            System.out.println(nyStylePizza.getName());
            System.out.println("-------------------------------------------------");
            Pizza chicagoStylePizza = chicagoStore.orderPizza("cheese");
            System.out.println(chicagoStylePizza.getName());
      }
}

 

 

 

 

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

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

 

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

 

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

 

위의 클래스 다이어그램을 보면 생산을 담당하는 PizzaStore  추상 클래스에서 객체를 만들기 위한 메소드, 즉 팩토리 메소드를 위한 인터페이스를 제공한다는 것을 알 수 있다.

PizzaStore에 구현되어 있는 다른 메소드 orderPizza에서는 팩토리 메소드에 의해 생산된 제품을 가지고 필요한 작업을 처리한다.

하지만, 실제 팩토리 메소드를 구현하고 제품(객체 인스턴스)을 만들어 내는 일은 서브클래스에서만 할 수 있다.

 

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

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

 

디자인 원칙 : 추상된 것에 의존하도록 만들어라. 구상 클래스에 의존하지 않도록 만들어라.

 

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

PizzaStore -> NYStyleCheesePizza

PizzaStore -> ChicagoStyleCheesePizza

 

이런 식으로 의존관계가 뒤집어졌다.

PizzaStore -> Pizza

Pizza <- NYStyleCheesePizza

Pizza <- ChicagoStyleCheesePizza

 

팩토리 메소드 패턴을 적용하면 고수준 구성요소(PizzaStore)와

저수준 구성요소(NYStyleCheesePizza, ChicagoCheesePizza) 들이 모두 추상 클래스인 Pizza에 의존하게 된다.(고수준 모듈과 저수준 모듈이 하나의 추상 클래스에 의존 : 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 null;
      }
      @Override
      public Sauce createSauce() {
            return null;
      }
      @Override
      public Cheese createCheese() {
            return null;
      }
      @Override
      public Veggies[] createVeggies() {
            return null;
      }
      @Override
      public Pepperoni createPepperoni() {
            return null;
      }
      @Override
      public Clams createClams() {
            return null;
      }

 

 

 

 

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 PizzaIngredientFactory getIngredientFactory() {
            return ingredientFactory;
      }
      public void setIngredientFactory(PizzaIngredientFactory ingredientFactory) {
            this.ingredientFactory = ingredientFactory;
      }
 }

 

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() 메소드에서는 피자를 굽고, 자르고, 포장한다.

 

다시 한번 정리

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

 

추상 메소드 패턴 : 하나의 추상 클래스에서 추상 메소드를 만들고 서브클래스들이 그 추상 메소드를 구현하여 인스턴스를 만들게끔 하는 것.

반응형
Comments