BackEnd/Design Patterens

[Design Patterns] Factory Pattern : 팩토리 패턴

샤아이인 2022. 1. 13.

Head First Design Patterns 책을 읽으며 정리한 내용입니다. 문제가 될 시 글을 내리도록 하겠습니다!

Factory Pattern 이란?

1) 팩토리 메소드 패턴

객체를 생성하기 위한 인터페이스 정의하는데, 어떤 클래스의 인스턴스를 만드는지는 서브클래스에서 결정하게 만듭니다.

이 패턴을 사용하면 클래스의 인스턴스를 만드는 일을 서브클래스에서 책임지는 것입니다.

출처 -  https://johngrib.github.io/wiki/factory-method-pattern/
 

위의 다이어그램을 보면 Creator에는 제품을 갖고 원하는 일을 하기 위한 메소드 들이 구현되어 있습니다.

하지만 제품을 만들어주는 FactoryMethod()는 추상 메소드로 정의되어 있을 뿐 구현되어 있지는 않습니다.

따라서 모든 서브클래스 에서 FactoryMethod()를 구현해 주어야 합니다.

서브 클래스인 ConcreteCreator에서 실제 제품을 생산하는 FactoryMethod()를 구현해 주고 있습니다. 즉 인스턴스를 만들어 내는 일은 ConcreteCreator가 책임집니다.

 

2) 추상 팩토리 패턴

인터페이스를 이용하여 서로 연관된, 또는 의존하는 객체를 구상 클래스를 지정하지 않고도 생성할 수 있습니다.

이 기법을 사용하여 제품군을 생성하기 위한 인터페이스를 제공할 수 있었습니다. 이 인터페이스를 이용하는 코드를 만들면 코드를 제품을 생산하는 실제 팩토리와 분리시킬 수 있습니다.

 

 

이 말이 무슨 의미인지 다음 단락부터 예시를 통하여 설명해 보겠습니다!

 

패턴 소개

피자를 주문하는 클래스가 있다고 해봅시다. 피자의 종류는 여러 가지가 있을 것이며, 피자를 고르고 만들기 위한 코드가 필요합니다.

Pizza orderPizza (String type) {
    Pizza pizza;

    if(type.equals("cheese"))
        pizza = new CheesePizza();
    else if (type.equals("pepperoni"))
        pizza = new PepperoniPizza();

    pizza.prepare();
    pizza.bake();
    pizza.cut();
    pizza.box();
    return 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("veggie")) // 신메뉴 추가
        pizza = new veggiePizza();

    pizza.prepare();
    pizza.bake();
    pizza.cut();
    pizza.box();
    return pizza;
}
 

위의 orderPizza()에서 가장 문제 되는 부분은 피자 인스턴스를 만들 구상 클래스를 선택하는 과정입니다.

이 부분은 코드가 지속적으로 바뀔 수 있는 부분이니 캡슐화를 해주는 것이 유용할 것 같군요~

 

따로 피자를 만드는 일만 처리하는 객체를 만들어 그 객체 내부에 피자 인스턴스를 만드는 코드를 집어넣을 것입니다.

이렇게 객체 생성을 처리하는 클래스를 팩토리라고 부릅니다.

orderPizza() 는 새로 만든 객체를 호출하기만 하면 됩니다. 더 이상 orderPizza() 에서 어떤 피자를 만들어야 할지 고민하지 않아도 되는 것입니다! 구상클래스 들로부터 해방되는 것 이죠!

 

따라서 팩토리로 따로 분리하면 다음과 같이 코드가 변경될 것 입니다.

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("veggie"))
            pizza = new veggiePizza();

        return pizza;
    }
}

public class PizzaStore {
    SimplePizzaFactory factory;

    public PizzaStore (SimplePizzaFactory factory){ // pizzaStore 의 생성자에 팩토리 객체를 전달
        this.factory = factory;
    }

    Pizza Pizza orderPizza (String type) {
        Pizza pizza;

        pizza = factory.createPizza(type);  // factory를 사용하여 피자 객체를 생성

        pizza.prepare();
        pizza.bake();
        pizza.cut();
        pizza.box();
        return pizza;
    }
}
 

여기 까지 나온내용이 팩토리 디자인 패턴은 아닙니다!. 그냥 간단한 팩토리 정도라 불릴 수 있습니다.

그림으로 보면 다음과 같습니다.

 

 

그림에서 볼 수 있듯, 피자를 생성하는 팩토리가 피자 가게와 분리되어 있죠.

 

여기서 각 점포 별로 조금씩 다른 스타일의 피자를 만들려면 각 점포별 PizzaFactory()를 만들어 사용해야 할 것 입니다.

SeoulPizzaFactory fac = new SeoulPizzaFactory(); // 점포별 피자 팩토리
PizzaStore seStore = new PizzaStore(fac);
seStore.order("spicy");
 

이러한 간단한 팩토리는 유연성이 떨어지고, 생성하는 제품 변경이 어렵습니다.


시간이 지나점포 별로 점점 다양해지는 피자 제조 과정에 당황한 본사 에서는 피자 가게  피자 제작 과정을 하나로 묶어주는 프레임워크를 만들어야 되겠다는 결론에 봉착합니다.

 

피자를 만드는 활동 자체는 pizzaStore 클래스에 국한 시키면서도, 점포별 고유 스타일을 살릴 수 있도록 하는 방법이 있습니다!!

 

바로! createPizza를 추상 메소드로 선언하고, 각 점포 별 특징에 맞도록 PizzaStore의 서브클래스를 만드는 것 입니다!

public abstract class PizzaStore {
    abstract Pizza createPizza(String type); // 팩토리 객체 가 아닌! 팩토리 메소드

    Pizza Pizza orderPizza (String type) {
        Pizza pizza;

        pizza = createPizza(type);  // factory를 사용하여 피자 객체를 생성

        pizza.prepare();
        pizza.bake();
        pizza.cut();
        pizza.box();
        return pizza;
    }
}
 

PizzaStore의 orderPizza() 메소드에는 이미 준비된 주문 시스템이 잘 작동하고 있습니다. 따라서 모든 점포에 이 방식을 사용하고 있죠! 하지만 피자를 만드는 과정은 점포마다 조금씩 다른 상황입니다. 캡슐화 해야할것 같군요!

 

이렇게 달라지는 부분을 createPizza() 메소드에 집어넣고, 그 메소드 에서 해당 점포의 피자를 만드는 책임을 지도록 하는 것 이죠! 또한 createPizza()는 추상메소드 이기 때문에 서브 클래스에서 메소드를 구현하면서 자신의 스타일 대로 구현하면 되는 것 입니다.

 

 

그럼 결론적으로 서브클래스 에서 자신의 스타일을 결정한다는 의미인데...? 이게 무슨 말이지?

 

orderPizza() 를 호출하게 되면 서브클래스의 팩토리 메소드 createPizza()를 호출하게 되고, 이로부터 인스턴스를 받게 됩니다

어떤 스타일의 피자 인스턴스를 받게 되는 것 일까요? orderPizza() 입장에서는 전혀 알수 없습니다. 클라이언트 코드와 서브클래스에 있는 객체 생성 코드가 분리 된 것 입니다.

 

그럼 누가 결정하는 것 일까요?

 

스타일은 주문하는 점포에 따라 달라집니다.

 

Seoul 서브 클래스에서 주문하면 서울 스타일의 피자가 만들어 지고, Busan 서브 클래스에서 주문하면 부산 스타일의 피자가 만들어 질 것 입니다. 다음은 Seoul 서브 클래스 입니다.

public class SeoulPizzaStore extends PizzaStore {
    Pizza createPizza(String item){
        if(item.equals("cheese")){
            return new SeoulStyleCheesePizza();
        }else if(item.equals("pepperoni")){
            return new SeoulStylePepperoniPizza();
        }
    }
}
 

서브 클래스에서 실제로 "결정" 하는 것이 아니라, 우리가 선택한 pizzaStore의 서브클래스 종류에 따라 결정되니 이는 피자의 종류를 해당 서브클래스 에서 결정한다 할수 있는 것 입니다.

public class PizzaTestDrive {
    public static void main(String[] args){
        PizzaStore nyStore = new NYPizzaStore();
        PizzaStore seStore = new SeoulPizzaStore();

        Pizza pizza = nyStore.orderPizza("cheese");
        System.out.println("Ethan ordered a " + pizza.getName() + "\n");

        pizza = seStore.orderPizza("cheese");
        System.out.println("JiWoo ordered a " + pizza.getName() + "\n");
    }
}
 

위의 코드에서와 같이 2개의 피자 점포를 생성한 후, 각 점포에서 구현한 orderPizza를 호출하게 됩니다.

결과로는 각 점포의 특성에 맞는 피자가 생성될 것 입니다.

 

이를 그림으로 확인 봅시다!

 
 

여기서 잠깐!!

의존성 뒤집기 원칙 (Dependency Inversion Princople)

이 원칙은 "특정 구현이 아닌 인터페이스에 맞춰서 프로그래밍 하자"는 원칙과 유사하지만,

고수준 구성요소가 저수준 구성요소에 의존하면 안 된다는 것이 내포되어 있습니다.

 

여기서 "고수준" 구성 요소 "저수준" 구성 요소에 의해 정의 되는 행동이 들어있는 구성요소를 말합니다.

여지까지 설명해온 pizza 예시에서 PizzaStore가 고수준 구성 요소 이고, 피자 객체들이 저수준 구성요소 라 할 수 있습니다.

 

추상화된 것에 의존하도록 만들자.

구상클래스에 의존하지 않도록 만들자.

 

의존성 뒤집기 원칙에 따르면, 구상 클래스와 같이 구체적인 것이 아니라 추상 클래스나 인터페이스 같은 추상적인 것에 의존하는 코드를 구현해야 합니다.

 

다음 예시를 확인해 봅시다!

 
 

위의 그림에서는 PizzaStore(고수준) 에서 모든 객체들을 직접 생성해야 해서 pizza 객체들에(저수준) 직접적으로 의존하고 있는 상황입니다. 이는 위에서 빨간 글씨로 언급했던 내용입니다.

 

예를 들어 pizza1 클래스의 구현이 변경(즉 구상 클래스가 변경되면)되면 PizzaStore 까지 코드를 수정해야 할 수도 있습니다.

 

여기서 생각하는 순서를 뒤집어봅시다!

 

맨 위의 PizzaStore를 생각하는 대신 Pizza에 대해 먼저 생각해보면, 어떤 피자든 동일한 인터페이스를 공유하도록 pizza 인터페이스를 만들어 PizzaStore가 여러가지 구상 클래스가 아닌! pizza 인터페이스 에만 의존하도록 만들어야 합니다.

 

그렇게 하려면 PizzaStore에서 구상 클래스들을 없애기 위해 팩토리를 사용해야 됩니다.

팩토리를 사용하면 다양한 구상 피자 들이 추상화된 Pizza 인터페이스 에 의존하게 되고, 마찬가지로 PizzaStore도 추상회 된pizza 인터페이스에 의존하게 됩니다. 이제는 의존성이 뒤집히게 된 것 입니다!

 

의존성 뒤집기 원칙을 지키기 위한 가이드 라인

▶ 어떤 변수에도 구상 클래스에 대한 레퍼런스를 저장하지 맙시다.

▶ 구상 클래스에서 유도된 클래스를 만들지 맙시다.

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


지금 부터는 추상 팩토리 패턴에 대하여 알아봅시다!

 

이번에는 조금 다른 문제에 봉착하였습니다!

몇몇 점포에서 재료 원가를 줄이기 위해 더 싼 재료로 바꿔서 이득을 본다는 소문이 돌기 시작했습니다...

 

이를 해결하기 위해서 원재료를 생산하는 팩토리를 만들고 점포마다 재료를 배달하기로 하였습니다.

 

먼저 원재료를 생성할 팩토리를 위한 인터페이스부터 정의하겠습니다. 일종의 제품군을 형성하는 것 이죠!

public interface PizzaIngredientFactory {
    public Dough createDough();
    public Sauce createSauce();
    public Cheese createCheese();
    public Veggies[] createVeggies();
    public Pepperoni createPepperoni();
    public Clams createClam();
}
 

이제 지역별로 팩토리를 만듭니다. 예를 들어 뉴욕 원재료 공장은 다음과 같습니다.

public class NYPizzaIngredientFactory implements PizzaIngredientFactory {

    public Dough createDough() {
        return new ThinCrustDough();
    }

    public Sauce createSauce() {
        return new MarinaraSauce();
    }

    public Cheese createCheese() {
        return new ReggianoCheese();
    }

    public Veggies[] createVeggies() {
        Veggies veggies[] = { new Garlic(), new Onion(), new Mushroom(), new RedPepper() };
        return veggies;
    }

    public Pepperoni createPepperoni() {
        return new SlicedPepperoni();
    }

    public Clams createClam() {
        return new FreshClams();
    }
}
 

재료군에 들어있는 각 재료의 뉴욕 버전을 만듭니다.

 

팩토리 준비가 끝났으니 Pizza 클래스에서 팩토리에서 생성한 원재료만 사용하도록 코드를 수정해주면 됩니다.

public abstract class Pizza{
    String name;
    Dough dough;
    Sauce sauce;
    Veggies[] veggies;
    Cheese cheese;
    Pepperoni pepperoni;
    Clams clam;

    abstract void prepare(); // 추상 메소드

    void bake(){
        System.out.println("Bake for 25 minutes at 350");
    }

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

    void bos(){
        System.out.println("Place pizza in official PizzaStore box");
    }

    void setName(String name){ this.name = name; }

    String getName(){ return name; }
}
 

Pizza의 추상클래스는 완료되었습니다.

사실 서로 다른 점포마다 피자에 사용하는 재료가 조금 씩 다를 뿐 만드는 방법은 모든 점포에서 동일합니다.

같은 페퍼로니피자라면 모든 점포에서 페퍼로니가 피자에 들어갑니다!

짠 페퍼로니든, 단 페퍼로니든, 쓴 페퍼로니든 어떤 페퍼로니든 넣어야 한다 이말이죠!

 

따라서 피자마다 클래스를 지역별로 만들 필요가 없게된 것 입니다.

위에서 팩토리메소드를 공부할때는 pizza를 구현할때 뉴요치즈피자, 시카고치즈피자 등을 나누어 구현해야 했습니다만...

더이상 그럴 필요강 없어졌습니다! 만드는 과정을 똑같고 페퍼로니 종류만 달라지기 때문입니다.

즉 재료군은 똑같다 이말입니다.

 

다음과 같은 치즈 피자 코드를 살펴봅시다.

public class CheesePizza extends Pizza {
    PizzaIngredientFactory ingredientFactory;

    public CheesePizza(PizzaIngredientFactory ingredientFactory){
        this.ingredientFactory = ingredientFactory;
    }

    void prepare() {
        System.out.println("Preparing " + name);
        dough = ingredientFactory.createDough();
        sauce = ingredientFactory.createSauce();
        cheese = ingredientFactory.createCheese();
    }
}
 

이제 다 끝났습니다. 정상적으로 생산 하는지만 확인하면 됩니다!

public class NYPizzaStore extends PizzaStore {
    Pizza createPizza(String item){
        Pizza pizza = null;
        PizzaIngredientFactory ingredientFactory = new NYPizzaIngredientFactory(); // 뉴욕지점에는 뉴욕재료를

        if(item.equals("cheese")){
            pizza = new CheesePizza(ingredientFactory);
            pizza.setName("New York Style Cheese Pizza");
        }else if(item.equals("Veggie")){
            pizza = new VeggiePizza(ingredientFactory);
            pizza.setName("New York Style Veggie Pizza");
        }else if(item.equals("clam")){
            pizza = new ClamPizza(ingredientFactory);
            pizza.setName("New York Style Clam Pizza");
        }else if(item.equals("pepperoni")){
            pizza = new PepproniPizza(ingredientFactory);
            pizza.setName("New York Style Clam Pizza");
        }

        return pizza;
    }
}
 

orderPizza() 메소드를 호출하면 createPizza() 메소드가 호출되게 됩니다. 인자로 cheese 를 넘겨준다 해봅시다.

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

 pizza = new CheesePizza(nyIngredientFactory);
 

prepare() 메소드 호출되어 팩토리에서 만들어진 원 재료들이 피자에 들어가게 됩니다.

드디어 피자가반환되어 완성되었습니다!

 

 
출처 -  https://spiralmoon.tistory.com/entry /디자인-패턴-추상-팩토리-패턴-Abstract-factory-pattern

AbstractFactory는 모든 구상 팩토리에서 구현해야 하는 인터페이스 입니다.

ConcreteFactory 들은 구상 팩토리 인데, 서로 다른 재료군들을 생산합니다. 클라이언트는 이중 자신에게 적합한 팩토리를 선택하여 사용하기만 하면 되기 때문에 제품 객체의 인스턴스를 직접 만들필요가 없습니다!

댓글