본문 바로가기
디자인 패턴

8. Factory Method Pattern, Abstract Factory Patter

by spaul 2023. 12. 2.

이번 포스팅에서는 팩토리 메서드 패턴과 추상 팩토리 패턴에 대해 함께 알아보겠습니다.

▣ What is Factory Method Pattern?

Define an interface for creating an object, but let subclasses decide which class to instantiate. Factory Method lets a class defer instantiation to subclasses.

 

 팩토리 메서드 패턴은 객체 생성용 인터페이스를 정의하지만, 서브클래스가 어떤 클래스를 인스턴스화 할 지 결정할 수 있도록 하는 패턴입니다.

▣ What is Abstract Factory Pattern?

Provides an interface for creating families of related or dependent objects without specifying their concrete classes.

 

 추상 팩토리 패턴은 구체적인 클래스를 명시하지 않고 서로 관련된 혹은 의존적인 객체들을 생성할 수 있는 인터페이스를 제공하는 패턴입니다.

 

 사실 정의만 봐서는 두 가지 패턴의 차이를 파악하기가 어렵습니다. 두 패턴의 차이의 핵심은, 팩토리 메서드는 하나의 제품(product)를 생성하는데 중점을 두는 반면, 추상 팩토리 패턴은 관련된 여러 제품들의 패밀리를 생성하는데 중점을 둔다는 것입니다. 뒤에서 좀 더 자세한 예시를 통해 알아보겠습니다.

 

 우리가 객체를 생성할 때 'new'라는 키워드를 사용해서 객체를 생성합니다. 여기서 new는 인터페이스 또는 추상클래스가 아닌 concrete class의 객체를 생성합니다. 그런데 우리가 new를 사용해서 객체를 생성할 때, 생성해야 될 객체가 너무 많아진다면 일일이 new를 이용해서 객체를 생성하는 것이 여간 힘든 일이 아닐 것입니다. 예를 들어 앞선 포스팅에서 'Duck Simulation'을 간단히 구현했던 것을 기억하신다면, 우리가 Mallard Duck을 100마리 정도 만들었는데, 이 100마리의 Mallard Duck을 전부 Rubber Duck으로 바꾸고 싶다고 합시다. 그러면 Mallard Duck 객체들을 전부 삭제하고 Rubber Duck 객체를 다시 생성하든지, 아니면 일일이 객체의 참조를 바꾸든지 해야되는데, 상상만해도 참으로 끔찍한 일이 아닐 수 없습니다. 

 

 결국 팩토리 메서드 패턴과 추상 팩토리 패턴 둘 다 위와 같은 문제를 해결하기 위해, 실제 구현되는 클래스의 객체를 생성할 때 객체의 종류가 달라져 클라이언트 코드를 수정해야 하는 일을 줄이기 위해 존재하는 패턴입니다.

 

 여기까지 대충은 이해하실 것 같은데 예시를 안들어 볼 수 없겠죠? 아래에서 예시와 함께 설명해보겠습니다.

 

■ 예제1) 피자 가게

예시로 우리가 피자 가게를 운영하고 있다고 가정해봅시다. 피자를 주문하는 방법은 아래와 같습니다.

void prepareToBoxing(Pizza pizza) {
    pizza.prepare();
    pizza.bake();
    pizza.cut();
    pizza.box();
}

Pizza orderPizza() {
    Pizza pizza = new Pizza();
    prepareToBoxing(pizza);
    return pizza;
}

 

피자를 주문하면 새로운 피자를 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("pepperoni") {
        pizza = new PepperoniPizza();
    } else if (type.equals("clam") {
        pizza = new ClamPizza();
    } else if (type.equals("veggie") {
        pizza = new VeggiePizza();
    }
    prepareToBoxing(pizza);
    return pizza;
}

 

 if-else문을 사용하여 일단 분리는 해놨는데, 딱 봐도 피자 종류가 늘어나거나 줄어든다면 코드를 수정해야 될 것 같습니다. 코드를 보아하니 prepareToBoxing(pizza); 부분은 바뀔 일이 없을 것 같네요. 바뀌는 부분과 바뀌지 않는 부분을 분리하기 위해서, 새로운 클래스를 생성하여 객체 생성 부분을 캡슐화 해보겠습니다.

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

 

그럼 이제 SimplePizzaFactory를 사용하여 피자를 생성할 수 있게됩니다.

public class PizzaStore {
    SimplePizzaFactory factory;
    public PizzaStore(SimplePizzaFactory factory) {
        this.factory = factory;
    }
    public Pizza orderPizza(String type) {
        Pizza pizza = null;
        pizza = factory.createPizza(type);
        prepareToBoxing(pizza);
        return pizza;
    }
    void prepareToBoxing(Pizza pizza) {
… // 기존 코드
    }
}

 

 기존에는 orderPizza() 메서드 안에 피자를 종류별로 분류하여 지저분 했었는데, SimplePizzaFactory를 사용하여 코드가 깔끔해진 것을 볼 수 있습니다. 또한 만약 피자 종류가 늘어나거나 줄어들어도 SimplePizzaFacotry에서만 변경하면 되게됩니다.

피자 가게 프로그램의 클래스 다이어그램

 

 위의 예시에서 본 것처럼, PizzaStore 클래스가 아니라 SimplePizzaFactory에서 객체를 생성하도록 했습니다. 하지만 직접적으로 팩토리 메서드 패턴이나 추상 팩토리 패턴을 사용했다고 말하기는 어려울 것 같습니다. SimplePizzaFactory에서 딱히 상속을 사용했다거나 하진 않았기 때문입니다.

 

■ 예제2) 피자 프랜차이즈 사업

 우리의 피자 가게가 잘되서 프랜차이즈 사업을 하려고 합니다. 각 지점마다 해당 지역의 특성과 취향을 고려한 다른 스타일의 피자를 각각 만들어보려고 합니다. 여기선 간단하게 뉴욕 스타일과 시카고 스타일 두 개로 나누어 살펴보겠습니다.

 

 PizzaStore와 피자를 만드는 과정을 일괄적으로 처리하기 위해서, 피자 가게와  피자 제작 과정 전체를 하나로 묶어주는 Framework를 만들어 주려고 합니다. 그러기 위해 PizzaStore 클래스를 abstract class로 만들고, createPizza()를 abstract method로 만들어 상속받는 각각의 클래스에서 스타일에 맞게 피자를 제작하도록 하겠습니다.

 

아래는 전체 코드입니다.

public abstract class PizzaStore {
    public void prepareToBoxing(Pizza pizza) {
        pizza.prepare();
        pizza.bake();
        pizza.cut();
        pizza.box();
    }

    public Pizza orderPizza(String type) {
        Pizza pizza = createPizza(type);
        prepareToBoxing(pizza);
        return pizza;
    }

    abstract Pizza createPizza(String type);
}
public class ChicagoPizzaStore extends PizzaStore {

    @Override
    Pizza createPizza(String type) {
        if (type.equals("cheese")) {
            Pizza pizza = new ChicagoStyleCheesePizza();
            return pizza;
        } else if(type.equals("pepperoni")) {
            Pizza pizza = new ChicagoStylePepperoniPizza();
            return pizza;
        }
        return null;
    }

}
public class NYPizzaStore extends PizzaStore {

    @Override
    Pizza createPizza(String type) {
        if (type.equals("cheese")) {
            Pizza pizza = new NYStyleCheesePizza();
            return pizza;
        } else if (type.equals("pepperoni")) {
            Pizza pizza = new NYStylePepperoniPizza();
            return pizza;
        }
        return null;
    }
}

 

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

}

 

  Pizza 클래스를 상속받는 다른 피자 관련 클래스에 대한 코드는 생략하겠습니다. 혹시 구현해보실 분들을 위해 힌트를 드리자면 Pizza를 상속하여 각각의 클래스의 constructor에서 name, dough, sauce, topping 등을 지정해주면 됩니다. 위 코드에서 볼 수 있듯이 PizzaStore의 서브 클래스인 ChicagoPizzaStore와 NYPizzaStore의 cratePizza()에서 pizza를 만들어 return하고 있는 것을 볼 수 있습니다. 이는 팩토리 메서드 패턴의 정의인 '슈퍼 클래스에서 객체 생성용 인터페이스를 정의하고, 서브클래스가 어떤 클래스를 인스턴스화 할 지 결정한다'라는 것의 예시로 볼 수 있을 것입니다. 여기까지 팩토리 메서드 패턴의 구현에 대해 살펴보았습니다.

 

■ 예제3) 피자 원재료 품질 관리

 마지막으로 추상 팩토리 패턴의 사례를 살펴보기 위해, 피자 원재료 품질 관리 예시를 보겠습니다. 우리가 운영하는 피자 프랜차이즈가 너무 잘되서 여러개의 분점들을 관리하게 되었습니다. 그런데 문제는, 각각의 지점들의 재료들이 일관되게 똑같지 않다는 것이었습니다. 뉴욕 지역의 지점들에서 사용하는 재료와 시카고 지역의 지점들에서 사용하는 재료들이 같은 것도 있지만 서로 다른 것도 있었던 것이죠. 결국 우리는 각 지역을 분리하여 지역별로 동일한 재료를 사용하여 피자를 만들도록 하고 싶습니다. 그러기 위해 각 지역의 재료를 공급하는 공장을 만들어 관리합니다. 

 

public class NYPizzaStore extends PizzaStore {
    protected 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("pepperoni")) {
            pizza = new CheesePizza(ingredientFactory);
            pizza.setName("New York Style Pepperoni Pizza");
        }
        return 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 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();
    }
}

 

 예제 2의 코드에서는 치즈피자가 NYCheesePizza와 ChicagoCheesePizza로 명시적으로 분리되어 있었는데, 이번에는 CheesePizza 하나의 클래스에서 처리하는 대신 NYPizzaIngredientFactory와 ChicagoPizzaIngredientFactory로 나뉘어 각각의 지역마다 다른 재료를 사용하여 처리할 수 있도록 하였습니다. 처음에 설명드릴 때 추상 팩토리 패턴은 구체적인 클래스를 명시하지 않고 서로 관련된 혹은 의존적인 객체들을 생성할 수 있는 인터페이스를 제공하는 패턴이라고 하였습니다. 현재 NYPizzaIngredientFactory에서 피자와 관련된, Dough, Sauce, Cheese 등의 객체를 생성하는 것을 볼 수 있는데, 이것이 바로 추상 클래스 패턴을 사용한 대표적인 예시라고 할 수 있습니다.

 

 글이 너무 길어질까봐 코드도 이해하시는데 꼭 필요한 것만 넣고, 최대한 쉽게 설명드리려고 했는데 이해가 잘 되셨을지 모르겠네요. 예시 자체를 이해하는 것도 중요하지만, 예시들을 통해 팩토리 메서드 패턴이 무엇인지, 추상 팩토리 패턴이 무엇인지 아는 것이 훨씬 더 중요하다고 생각합니다. 제 생각에 이 두 가지 패턴이 디자인 패턴들 중에서 꽤나 어려운 패턴 같다는 생각이 드네요. 혹시 이해가 가지 않는 것이나 틀린 내용이 있으면 댓글로 남겨주세요! 

 

References

[1] 조용주, 고급객체지향프로그래밍

[2] Eric Freeman, Head Frist Design Patterns

'디자인 패턴' 카테고리의 다른 글

9. Builder Pattern  (0) 2023.12.05
7. Decorator Pattern  (0) 2023.11.28
6. Iterator Pattern  (0) 2023.11.14
5. Singleton Pattern  (0) 2023.11.13
4. Observer Pattern  (0) 2023.11.07