본문 바로가기

Fundamental/Design Pattern

[디자인 패턴] 4. 팩토리 패턴(Factory Pattern)

 

 

간단 팩토리(Simple Factory)
오직 객체 생성을 목적으로 하는 클래스를 만들어서 객체를 생성하는 부분을 캡슐화하는 기술이며 패턴이라고 불리진 않는다. 인터페이스로 상위 팩토리 객체를 만들고 이를 서브 클래스가 구현하는 방식이 일반적으로 사용된다.

팩토리 메소드 패턴(Factory Method Pattern)
객체를 생성하는 부분을 메소드로 캡슐화하고, 이 메소드를 서브 클래스에서 구현하도록 해서 객체 생성을 서브 클래스가 결정하도록 캡슐화하는 패턴이다. 간단 팩토리보다 코드 변경에 대한 유연성이 좋다.

추상 팩토리 패턴(Abstract Factory Pattern)
구상 클래스에 의존하지 않고도 서로 연관된 객체로 이루어진 제품군을 생성하는 인터페이스를 사용해서 객체 생성을 추상화 하는 패턴이다.

 


🍕 피자 가게를 운영해 봅시다

 

당신은 서울에 어느 한 피자 가게를 운영하고 있고, 피자 주문 시스템을 만들어 보기로 합니다.

 

 

 

현재 치즈 피자와 콤비네이션 피자를 팔고 있으므로, 해당 피자를 정의합니다.

public abstract class Pizza {
    private String name;
    private String dough;
    private String sauce;
    private List<String> toppings = new ArrayList<>();

    public void prepare() {}
    public void bake() {}
    public void cut() {}
    public void box() {}
}

class CheesePizza extends Pizza {}
class CombinationPizza extends Pizza {}

 

 

피자 가게 객체는 피자 종류에 따라 다른 피자를 만들어서 손님에게 제공합니다.

public abstract class PizzaStore {
    public Pizza orderPizza(String type) {
        Pizza pizza;
        
        if (type.equals("cheese") {
            pizza = new CheesePizza();
        } else if (type.equals("combination") {
            pizza = new CombinationPizza();
        } else {
            return null;
        }

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

 

 

치즈 피자를 주문하려면 아래처럼 주문할 수 있겠죠?

public static void main(String[] args) {
    PizzaStore pizzaStore = new PizzaStore();
    pizzaStore.orderPizza("cheese");
}

 

 

가게를 운영하다 보니 건너편의 피자 가게에서 새로운 메뉴를 출시했다는 소식이 들려오네요. 이에 맞춰서 당신도 메뉴를 늘리기로 결심합니다.

 

public class PizzaStore {
    public Pizza orderPizza(String type) {
        Pizza pizza;
        
        if (type.equals("cheese") {
            pizza = new CheesePizza();
        } else if (type.equals("combination") {
            pizza = new CombinationPizza();
        } else if (type.equals("vegetable")) { // 야채 피자가 추가됐네요!
            pizza = new VegetablePizza();
        } else {
            return null;
        }

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

 

 

생각해보니 앞으로 메뉴가 추가되거나 삭제되는 경우, 이 PizzaStore 코드를 계속 고쳐야 하므로, 변경에 닫혀있어야 하는 OCP 원칙에 어긋나게 됨을 깨닫게 됩니다. 피자를 준비하고, 굽는 등의 프로세스는 항상 같아야 하기 때문에 변경이 없으므로, 계속 변경되는 부분인 피자를 만드는 부분을 캡슐화하기로 결정합니다.

 

public class SimplePizzaFactory {
    public Pizza createPizza(String type) {
        Pizza pizza = null;
        
        if (type.equals("cheese") {
        	pizza = new CheesePizza();
        } else if (type.equals("combination") {
        	pizza = new CombinationPizza();
        } else if (type.equals("vegetable")) {
        	pizza = new VegetablePizza();
        } else {
        	return null;
        }
    }
}

public class PizzaStore {
    SimplePizzaFactory factory;
    
    public PizzaStore(SimplePizzaFactory factory) {
        this.factory = factory;
    }
    
    public Pizza orderPizza(String type) {
        Pizza pizza = factory.createPizza(type);

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

 

 

이제 피자를 만드는 부분이 캡슐화 되어서 메뉴가 추가되거나 삭제되어도 PizzaStore 코드를 고칠 일이 없겠네요!

이와 같이 단순히 객체를 만들어서 반환해주는 객체를 간단 팩토리(Simple Factory)라고 합니다. 정확히 말하면 패턴은 아니지만 자주 사용되는 기술입니다. (지금은 단순히 객체를 생성하는 코드를 다른 클래스한테 떠넘긴 것일 뿐이긴 합니다..)

 

어쨌든, 이렇게 가게를 운영하다보니 유명 유튜버가 가게를 다녀간 이후로 큰 인기를 끌게 되었습니다!

SNS에서 부산에서도 이 피자를 꼭 먹어보고 싶다는 글들이 꾸준히 올라오고 있네요.

결국 당신은 부산에 분점을 내기로 결심합니다.

 

하지만 서울에서 판매되는 피자는 부산에서 판매되는 피자의 모양, 굽는 방식 등이 달라서 그 지역 특성에 맞게 피자를 만들 필요가 있을 것 같네요(실제로도 그런가요? 🤔). 기존에 피자를 생성하는 팩토리 객체를 interface로 만들고 서브 클래스에서 구현하게 한다면 다형성을 활용해서 상황에 맞게 피자를 만들 수 있겠다는 아이디어가 떠오릅니다.

 

interface SimplePizzaFactory {
    Pizza createPizza(type String);
}

class SeoulPizzaFactory implements SimplePizzaFactory {
    Pizza createPizza(type String) {
        if (type.equals("cheese") {
            pizza = new SeoulStyleCheesePizza();
        } else if (type.equals("combination") {
            pizza = new SeoulStyleCombinationPizza();
        } else if (type.equals("vegetable")) {
            pizza = new SeoulStyleVegetablePizza();
        } else {
            return null;
        }
    }
}

class BusanPizzaFactory implements SimplePizzaFactory {
    Pizza createPizza(type String) {
        if (type.equals("cheese") {
            pizza = new BusanStyleCheesePizza();
        } else if (type.equals("combination") {
            pizza = new BusanStyleCombinationPizza();
        } else if (type.equals("vegetable")) {
            pizza = new BusanStyleVegetablePizza();
        } else {
            return null;
        }
    }
}

 

 

이제 서울 지점과 부산 지점을 만들고 피자를 주문해볼까요?

public static void main(String[] args) {
    SeoulPizzaFactory seoulFactory = new SeoulPizzaFactory();
    PizzaStore seoulPizzaStore = new PizzaStore(seoulFactory);
    seoulPizzaStore.orderPizza("cheese");
    
    BusanPizzaFactory busanFactory = new BusanPizzaFactory();
    PizzaStore busanPizzaStore = new PizzaStore(busanFactory);
    busanPizzaStore.orderPizza("cheese");
}

 

 

이제 피자 스타일이 추가될 때마다 그 지역의 피자 팩토리를 만들어서 PizzaStore에 넣어주면 되겠네요!

 

하지만 좀 더 생각해보니.. 지점마다 항상 팩토리 객체를 만들어줘야 하고, PizzaStore가 팩토리 객체에 의존하고 있기 때문에, 어느 한 지점에서 팩토리 객체를 수정하게 되면, 그 팩토리를 사용하는 다른 지점에도 영향이 가서 좀 걱정되기 시작합니다. 같은 스타일이어도 지역마다 또 조금씩 다를 수 있기 때문이죠.

 

그래서 피자를 만드는 방법을 각 지점이 독립적으로 직접 결정하도록 운영 정책을 바꾸게 됩니다. 단, 모든 지점에서의 일관적인 서비스 품질을 위해서 피자를 준비하고, 굽는 등의 프로세스는 모든 지점이 동일하게 작업하도록 강제해야 하겠죠.

 

public abstract class PizzaStore {
    // 모든 지점은 피자를 판매할 때 동일한 프로세스를 가져야 합니다!
    public final Pizza orderPizza(String type) {
        Pizza pizza = createPizza(type); // 이제 팩토리 객체 대신 서브 클래스의 메소드를 사용합니다.

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

    abstract Pizza createPizza(String type);
}

class SeoulPizzaStore extends PizzaStore {
    @Override
    Pizza createPizza(String type) {
        if (type.equals("cheese")) {
            return new SeoulStyleCheesePizza();
        } else if (type.equals("combination")) {
            return new SeoulStyleCombinationPizza();
        } else if (type.equals("vegetable")) {
            return new SeoulStyleVegetablePizza();
        } else {
            return null;
        }
    }
}

class BusanPizzaStore extends PizzaStore {
    @Override
    Pizza createPizza(String type) {
        if (type.equals("cheese")) {
            return new BusanStyleCheesePizza();
        } else if (type.equals("combination")) {
            return new BusanStyleCombinationPizza();
        } else if (type.equals("vegetable")) {
            return new BusanStyleVegetablePizza();
        } else {
            return null;
        }
    }
}

 

 

이제 피자를 주문해볼까요?

public static void main(String[] args) {
    PizzaStore seoulPizzaStore = new SeoulPizzaStore();
    seoulPizzaStore.orderPizza("cheese");
    
    PizzaStore seoulPizzaStore = new BusanPizzaStore();
    busanPizzaStore.orderPizza("cheese");
}

 

 

앞으로 새로운 지점이 생길 때마다 각자의 지역 스타일에 맞게 알아서 피자를 만들 수 있게 변경되었습니다!

각 지점은 동일한 프로세스로 피자를 판매하므로 모든 지점이 서비스 품질을 유지할 수 있고, 각 지점에서 피자 만드는 방법을 변경하더라도 다른 지점에 영향이 가지 않겠네요.

 

위와 같이 객체를 생성하는 부분을 추상 메소드로 만들고 서브 클래스에서 각자 구현하는 방식을 팩토리 메소드 패턴(Factory Method Pattern)이라고 합니다. 이렇게 팩토리 메소드 패턴을 사용하면 객체를 생성하는 부분을 캡슐화할 수 있습니다.


🧀 원재료 공장을 만들어봅시다 🥫

 

피자 가게가 점점 번창하면서 여러 지역에 지점이 많이 생겼습니다. 이렇게까지 성공하게 된 건 여러 이유가 있겠지만, 피자에 들어가는 신선한 원재료들도 한 몫 했을 것입니다. 그런데 마진을 많이 남기기 위해 피자에 맞지 않거나 저렴한 원재료를 사용하는 지점이 생겨나고 있다는 소문이 돌기 시작합니다.. 당신은 이 상황을 막기 위해 각 지점에 정해진 원재료를 공급할 수 있는 방법을 고민합니다.

 

피자는 그 지역의 스타일에 따라 다른 원재료로 만들어지며, 그 피자에 맞는 원재료만을 사용해 피자를 만들도록 원재료를 공급하고 강제하는 방법을 생각해냈습니다.

 

일단 원재료 객체를 생산하는 팩토리인 PizzaIngredientFactory를 만들고, 이 팩토리의 메소드를 구현하도록 강제합니다. 지역마다 원재료가 다르므로, SeoulPizzaIngredientFactory와 BusanPizzaIngredientFactory로 구상 클래스를 만들 수 있겠네요.

 

 

interface PizzaIngredientFactory {
    public Dough createDough();
    public Cheese createCheese();
}

// 서울에선 두꺼운 도우와 모짜렐라 치즈로 피자를 만드는 것이 국룰!
class SeoulPizzaIngredientFactory implements PizzaIngredientFactory {
    public Dough createDough() {
        return new ThickDough();
    }

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

// 부산에선 얇은 도우와 레자노 치즈로 피자를 만드는 것이 국룰!
class BusanPizzaIngredientFactory implements PizzaIngredientFactory {
    public Dough createDough() {
        return new ThinDough();
    }

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

interface Dough {}
class ThickDough implements Dough {}
class ThinDough implements Dough {}

interface Cheese {}
class MozzarellaCheese implements Cheese{}
class ReggianoCheese implements Cheese{}

 

 

이제 기존 Pizza 클래스에 원재료인 Dough와 Cheese를 인터페이스로 추상화하고, 이 Pizza 클래스를 상속받는 각 피자들은 추상 메소드인 prepare 메소드를 구현해서 그 피자에 맞는 원재료 객체들을 생성해주도록 합니다.

 

 

 

import java.util.ArrayList;
import java.util.List;

public abstract class Pizza {
    Dough dough;
    Cheese cheese;

    public abstract void prepare();
    public void bake() {}
    public void cut() {}
    public void box() {}
}

class SeoulStyleCheesePizza extends Pizza {
    PizzaIngredientFactory factory;

    public SeoulStyleCheesePizza(PizzaIngredientFactory factory) {
        this.factory = factory;
    }

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

class BusanStyleCheesePizza extends Pizza {
    PizzaIngredientFactory factory;

    public BusanStyleCheesePizza(PizzaIngredientFactory factory) {
        this.factory = factory;
    }

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

 

위와 같이 구상 피자 클래스(SeoulStyleCheesePizza, ...)는 인터페이스로 추상화되어 있는 팩토리 객체를 사용해서 도우와 치즈 객체를 생성하고 있으므로, 구상 원재료 클래스(ThinDough, MozzarellaCheese, ...)에 의존하지 않습니다. 이러한 패턴을 추상 팩토리 패턴이라고 합니다. 추상화를 통한 확장성이 장점이지만, 인터페이스에 메소드를 추가해야 하는 경우엔 모든 서브 클래스들을 수정해야 하는 단점도 있습니다.

 

위에서 살펴본 팩토리 메소드 패턴과 추상 팩토리 패턴은 언제 사용될까요?

  • 팩토리 메소드 패턴: 클래스 내 코드와 인스턴스를 만드는 부분을 분리시켜야 할 때 사용됩니다.
  • 추상 팩토리 패턴: 서로 연관된 일련의 제품군을 만들어야 할 때 사용됩니다.
  • 간단 팩토리: 위 패턴들과는 달리 단순히 객체를 생성해주는 객체입니다. 서브 클래스가 추가될 때마다 팩토리 객체를 계속 수정해야 합니다.

 

 

 


팩토리 패턴에서 배울 수 있는 객체지향 원칙

  • 바뀌는 부분은 캡슐화한다.
  • 상속보다는 구성을 활용한다.
  • 구현보다는 인터페이스에 맞춰서 프로그래밍한다.
  • 상호작용하는 객체 사이에서는 가능하면 느슨한 결합을 사용해야 한다.
  • 클래스는 확장에는 열려 있어야 하지만 변경에는 닫혀 있어야 한다(OCP).
  • 추상화된 것에 의존하게 만들고, 구상 클래스에 의존하지 않게 만든다.

 

간단 팩토리(Simple Factory)
오직 객체 생성을 목적으로 하는 클래스를 만들어서 객체를 생성하는 부분을 캡슐화하는 기술이며 패턴이라고 불리진 않는다. 인터페이스로 상위 팩토리 객체를 만들고 이를 서브 클래스가 구현하는 방식이 일반적으로 사용된다.

팩토리 메소드 패턴(Factory Method Pattern)
객체를 생성하는 부분을 메소드로 캡슐화하고, 이 메소드를 서브 클래스에서 구현하도록 해서 객체 생성을 서브 클래스가 결정하도록 캡슐화하는 패턴이다. 간단 팩토리보다 코드 변경에 대한 유연성이 좋다.

추상 팩토리 패턴(Abstract Factory Pattern)
구상 클래스에 의존하지 않고도 서로 연관된 객체로 이루어진 제품군을 생성하는 인터페이스를 사용해서 객체 생성을 추상화 하는 패턴이다.