[디자인 패턴] 팩토리 패턴

이정규·2022년 6월 17일
1

팩토리 패턴

팩토리(factory)란 무엇인가.

팩토리를 말 그대로 직역하면 공장이다. 공장에서는 무슨 일을 하는가?

공장은 어떠한 물건을 만들어 내는 을 진행한다. 이를 객체지향에 빗대어 보자.

그렇다. 팩토리라는 것은 어떠한 객체를 생성하는 일을 진행해주는 것이다.

그런데 굳이 나누는 이유가 뭐야?

피자가게를 예시로 들어보자.

피자 가게라는 객체가 존재하고 이를 구현한 뉴욕 피자가게, 워싱턴 피자 가게, 이탈리안 피자 가게등이 있다고 해보자.

이들 피자 가게객체가 하는 일을 적어보자.

  1. 피자를 만든다.
  2. 피자를 굽는다.
  3. 피자를 포장한다.

여기서 바뀌는 부분과 바뀌지 않는 부분을 알아보자. 객체지향 설계에서 가장 중요한 핵심이다.

바뀌지 않는 부분

  1. 피자를 굽는다.
  2. 피자를 포장한다.

이 부분은 어떤 피자가 오든 동일한 일을 수행할 것이다.

바뀌는 부분

피자를 만드는 부분이다.

새로운 피자가 나오거나, 피자가 메뉴에서 없어진다면 이러한 부분들은 계속해서 변화가 이루어진다.

그렇다면 이 부분을 따로 빼보자. 라는 부분이 바로 팩토리 패턴이다.

팩토리 패턴이란

3가지로 나뉘어져 있다.

간단한 팩토리, 팩토리 메소드 패턴, 추상 팩토리 패턴 위 3가지가 존재한다.

간단한 팩토리

말 그대로 간단한 팩토리이다.

다음과 같이 구상한다. 그저 피자 객체를 생성하는 부분을 따로 나눈 것밖에 없다.

만약 코드로 표현하면 이렇다.

PizzaStore

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

    // 기타 메소드
}

Pizza

public interface Pizza {
    void prepare();
    void bake();
    void cut();
    void box();
}

CheesePizza, PepperoniPizza, ClamPizza, VeggiePizza와 같은 구현체들은 적지 않겠다.

SimplePizzaFactory

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

뉴욕피자 팩토리, 시카고 피자 팩토리등 여러가지 팩토리들을 만들어서 사용하도록 하면된다.

문제점

이렇게 만들면 객체 생성관리를 PizzaStore에서 하지 않으니 편해보인다.

하지만 PizzaStore에서 객체 생성에 전혀 관여하지 않다보니 객체들이 자기 멋대로 나오는 경우가 생길 수 있다.

이를 PizzaStore에서 Pizza를 제작할 수 있도록 만들어보자.

팩토리 메소드 패턴

정의

객체를 생성할 때 필요한 인터페이스를 만듭니다. 어떤 클래스의 인스턴스를 만들지는 서브클래스에서 결정합니다. 팩토리 메소드 패턴을 사용하면 클래스 인스턴스를 만드는 일을 서브 클래스에게 맡기게 됩니다.

예시

위에 나온 정의처럼 PizzaStore에서 Pizza를 제작하도록 해보자. 그러면서 유연성은 떨어지지 않도록 만들어보자.

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

여기서 주목할 점은 abstract Pizza createPizza(String type); 함수이다.

PizzaStore객체는 createPizza(type) 을 호출하여 피자를 생성한다. 하지만 어떤 피자가 올지는 모른다.

어떤 피자를 만들기 위한 구현은 서브 클래스에 위임하는 형태인 것이다.

즉, 팩토리 메소드를 추상 메소드로 선언해서 서브클래스가 객체 생성을 책임지도록 위임하는 형태인 것이다. 클라이언트(PizzaStore에서 orderPizza함수와 같은 코드)는 실제로 생성되는 객체가 무엇인지 알 수 없는 형태로 만드는 것이다.

NYPizzaStore

public class NYPizzaStore extends PizzaStore {

    @Override
    Pizza createPizza(String type) {
        Pizza pizza = null;
        switch (type) {
            case "cheese":
                pizza = new NYStyleCheesePizza();
                break;
            case "pepperoni":
                pizza = new NYStylePepperoniPizza();
                break;
            case "clam":
                pizza = new NYStyleClamPizza();
                break;
            case "veggie":
                pizza = new NYStyleVeggiePizza();
                break;
            default:
                System.out.println("다시 입력해주세요.");
                break;
        }
        return pizza;
    }
}

Pizza

public interface Pizza {
    void prepare();
    void bake();
    void cut();
    void box();
}

피자를 구현한 객체들은 적지 않겠다.

Test

public class Main {
    public static void main(String[] args) {
        PizzaStore pizzaStore = new NYStylePizzaStore();
        Pizza pizza = pizzaStore.orderPizza("cheese");

        System.out.println(pizza.toString());
    }
}

다음과 같이 테스트해보면 뉴욕스타일의 치즈 피자가 나오게 된다.

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

그렇게 함으로써 제품을 생산하는 부분과 제품을 사용하는 부분으로 분리할 수 있다. 느슨하게 결합을 유지시켜주는 것이다.

추상 팩토리 패턴

지금까지 피자 인터페이스에 재료들이 무엇이 있는지 명시하지 않았지만 있다고 생각하고 글을 적어보겠다.

피자 가게에서 원재료들을 어디에서 받는지 명시하지 않다보니 각 지점에서 알아서 원재료를 받아오게 되었다.

그런데 원재료들을 잘 가져오면 모르겠지만 값 싼 재료들을 받아오거나 자기들 마음대로 원재료들을 받아오는 경우가 생기게 되었다.

그래서 우리가 각 지점별로 원재료를 받아오는 팩토리를 지정해주도록 하려고 한다. 어떻게 하면 될까?

정의

구상 클래스에 의존하지 않고도 서로 연관되거나 의존적인 객체로 이루어진 제품군을 생산하는 인터페이스를 제공합니다. 구상 클래스는 서브 클래스에서 만든다.

예시

위의 정의를 빗대어보자. 여러 지점들의 피자가게가 존재한다. 각 지점마다 원재료를 받아오는 팩토리가 존재해야할 텐데 이 때 같은 공장에서 받아오는 지점들도 존재할 것이다.

이를 만들어보자.

PizzaIngredientFactory

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

NYPizzaIngredientFactory

public class NYPizzaIngredientFactory implements PizzaIngredientFactory {

    @Override
    public Dough createDough() {
        return new Dough();
    }

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

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

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

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

    @Override
    public Clams createClam() {
        return new Clams();
    }
}

이렇게 원재료를 만드는 추상 팩토리를 각 지점별로 구현한다.

Pizza

public abstract class Pizza {
    String name;

    public Dough dough;
    public Sauce sauce;
    public Veggies Veggies[];
    public Cheese cheese;
    public Pepperoni pepperoni;
    public Clams clam;

    abstract void prepare();
    void bake() {
        System.out.println("피자 굽기");
    }
    void cut() {
        System.out.println("피자 자르기");
    }
    void box() {
        System.out.println("상자에 피자 담기");
    }
}

CheesePizza

public class CheesePizza extends Pizza {
    PizzaIngredientFactory ingredientFactory;

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

    @Override
    public void prepare() {
        dough = ingredientFactory.createDough();
        sauce = ingredientFactory.createSauce();
        cheese = ingredientFactory.createCheese();
    }

    @Override
    public void bake() {

    }

    @Override
    public void cut() {

    }

    @Override
    public void box() {

    }
}

각 지점별로 원재료를 갖고 오는 부분을 만들었으니 이제 각 지점별 피자가 아닌 CheesePizza로 묶어둔 뒤, 원재료를 만들어주는 추상 팩토리를 인자로 받아 만든다.

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

NYPizzaStore

public class NYStylePizzaStore extends PizzaStore {

    @Override
    Pizza createPizza(String type) {
        Pizza pizza = null;
        PizzaIngredientFactory ingredientFactory = new NYPizzaIngredientFactory();
        switch (type) {
            case "cheese":
                pizza = new CheesePizza(ingredientFactory);
                break;
            case "pepperoni":
                pizza = new PepperoniPizza(ingredientFactory);
                break;
            case "clam":
                pizza = new ClamPizza(ingredientFactory);
                break;
            case "veggie":
                pizza = new VeggiePizza(ingredientFactory);
                break;
            default:
                System.out.println("다시 입력해주세요.");
                break;
        }
        return pizza;
    }
}

지점별로는 원재료 팩토리를 달리 갖고 있게 한다.

Test

public class Main {
    public static void main(String[] args) {
        PizzaStore pizzaStore = new NYStylePizzaStore();
        Pizza pizza = pizzaStore.orderPizza("cheese");

        System.out.println(pizza.toString());
    }
}

다음과 같이 진행하면 뉴욕 치즈 피자가 만들어진다.

좀 복잡하니깐 한 번 정리해보자.

정리

  1. NyStylePizzaStore라는 각 지점별의 PizzaStore객체를 생성한다.
  2. PizzaStore 추상클래스는 orderPizza() 를 이용해 피자를 주문받는다.
    1. createPizza() 라는 메소드를 호출하여 서브클래스(NYPizzaStore) 에서 피자를 만들도록 한다.
    2. 서브 클래스에서는 자신이 갖고 있는 PizzaIngredientFactory를 이용하여 재료를 받는다.
    3. PizzaIngredientFactory라는 인터페이스를 상속받아 구현한 NYPizzaIngredientFactory를 이용해 재료를 생성한다.
    4. 생성한 재료를 가지고 피자를 만든다.
    5. 생성한 피자를 PizzaStore에게 준다.

그림으로 표현하면 다음과 같다.

팩토리 메소드와 추상 팩토리의 차이

둘 다 객체를 생성하는 부분을 분리하기 위해 사용된다.

그렇다면 무슨 차이가 존재할까?

팩토리 메소드

  1. 클래스를 사용하여 제품을 만든다. 즉, 상속으로 이루어져 있다.
  2. 서브 클래스에서 인스턴스를 만들어준다.
  3. 어떤 구상 클래스를 필요로 하게 될지 알 수 없는 경우에도 유용하다.

추상 팩토리

  1. 객체를 사용하여 제품을 만든다. 즉, 구성으로 이루어져 있다.

  2. 일련의 연관된 제품들을 하나로 묶을 수 있다.

  3. 변경이 이루어질 때 인터페이스를 바꿔야 하기 때문에 서브 클래스들도 변경이 일어나야 한다.

  4. 구상 팩토리를 팩토리 메소드를 이용해 구현하는 경우도 있다.

    만약 도우를 만드는 createDough()함수를 팩토리 메소드로 구현하는 것이다. 어떤 도우를 쓸 지는 서브클래스에서 만들어주는 것이다.

  5. 구상 클래스에 직접 의존하지 않고 만들 수 있다.

참고
http://wiki.gurubee.net/pages/viewpage.action?pageId=1507401
헤드퍼스트 디자인 패턴

profile
강한 백엔드 개발자가 되기 위한 여정

0개의 댓글