본문 바로가기

ComputerScience/DesignPattern

[DesignPattern] Factory Pattern과 Factory Method, Abstract Factory 차이 (+피자가게 예시)

Factory Pattern

새로운 객체가 필요한 곳마다 new를 사용하여 구상 클래스를 코딩하면, 나중에 코드를 수정해야 할 가능성이 커지고 유연성이 떨어집니다.

 

디자인 패턴을 적용하지 않은 코드 예시를 보겠습니다.

public class DependentPizzaStore {

    public Pizza createPizza(String style, String type) {
        Pizza pizza = null;
        if (style.equals("NY")) {
            if (type.equals("cheese")) {
                pizza = new NYStyleCheesePizza();
            } else if (type.equals("veggie")) {
                pizza = new NYStyleVeggiePizza();
            } else if (type.equals("clam")) {
                pizza = new NYStyleClamPizza();

다음과 같이 if 문으로 판별하여 각 종류 별로 객체를 생성해야 하며 새로운 종류의 객체가 생긴다면 createPizza 메소드에서 새로운 조건과 그 객체를 추가해야 하는 번거로움이 생깁니다. 이는 OCP 원칙을 지키지 못합니다.

Simple Factory

간단한 팩토리는 어떻게 보면 팩토리 패턴이 아니라, 전략 패턴과 유사합니다.
인스턴스를 만드는 부분을 알고리즘 군으로 생각하고, 이를 캡슐화하여 상호 교체 가능하게 만듭니다.


Simple Factory Example

아래 코드는 인스턴스 생성 부분을 팩토리가 책임지도록 만들었습니다.

 

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

 

Factory

public class SimplePizzaFactory {

    public Pizza createPizza(String type) {
        Pizza pizza;

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

        return pizza;
    }
}

이렇게 간단한 팩토리를 만들면 구현을 변경할 때 여기저기 변경할 필요 없이 팩토리 클래스 하나만 고치면 됩니다.

하지만 팩토리가 클래스로 이루어져있어 확장이 어렵습니다. (OCP 위배)

팩토리 메소드 패턴 (Factory Method Pattern)

이미지 출처: https://en.wikipedia.org/wiki/Factory_method_pattern

"객체를 생성하는 인터페이스를 정의하지만, 어떤 클래스를 인스턴스화(객체를 생성)할지는 하위 클래스에게 맡긴다. 팩토리 메서드는 클래스가 인스턴스화를 하위 클래스에게 위임할 수 있게 한다." (Gang Of Four)

 

위의 UML 클래스 다이어그램에서는 Product 객체가 필요한 Creator 클래스가 직접 Product1 클래스를 인스턴스화하지 않습니다.

대신, Creator는 별도의 factoryMethod()를 참조하여 Product 객체를 생성하며, 이로써 Creator는 어떤 구체적인(Concrete) 클래스가 인스턴스화되는지에 독립적이 됩니다.

Creator의 하위 클래스들은 어떤 클래스를 인스턴스화할지를 재정의할 수 있습니다.

이 예시에서 Creator1 하위 클래스는 abstract factoryMethod()를 구현하여 Product1 클래스를 인스턴스화합니다.

 

아래 예제를 봅시다.

팩토리의 역할을 PizzaStore()의 서브클래스인 NYPizzaStore가 해주며, createPizza()를 오버라이드 및 구현해서 객체를 생성하는  수행합니다.

 

PizzaStore


PizzaStore는 Pizza 객체를 가지고 여러 가지 작업을 하지만, 실제로 어떤 구상 클래스에서 작업이 처리되고 있는지는 전혀 알 수 없다. 즉, 느슨한 결합(Loose Coupling)이 완성되었습니다.

public abstract class PizzaStore {
 
	abstract Pizza createPizza(String item);
 
	public Pizza orderPizza(String type) {
		//중요! 팩토리 메서드를 통해 객체 생성
		Pizza pizza = createPizza(type);

		System.out.println("--- Making a " + pizza.getName() + " ---");
		pizza.prepare();
		pizza.bake();
		pizza.cut();
		pizza.box();
		return pizza;
	}
}

public class NYPizzaStore extends PizzaStore {

	Pizza createPizza(String item) {
		if (item.equals("cheese")) {
			return new NYStyleCheesePizza();
		} else if (item.equals("veggie")) {
			return new NYStyleVeggiePizza();
		} else if (item.equals("clam")) {
			return new NYStyleClamPizza();
		} else if (item.equals("pepperoni")) {
			return new NYStylePepperoniPizza();
		} else return null;
	}
}

 

Pizza와 Pizza 상속 구현체

public abstract class Pizza {
	String name;
	String dough;
	String sauce;
	ArrayList<String> toppings = new ArrayList<String>();
 
	void prepare() {
		System.out.println("Prepare " + name);
		System.out.println("Tossing dough...");
		System.out.println("Adding sauce...");
		System.out.println("Adding toppings: ");
		for (String topping : toppings) {
			System.out.println("   " + topping);
		}
	}
  
	void bake() {
		System.out.println("Bake for 25 minutes at 350");
	}
 
	void cut() {
		System.out.println("Cut the pizza into diagonal slices");
	}
  
	void box() {
		System.out.println("Place pizza in official PizzaStore box");
	}
 
	public String getName() {
		return name;
	}

	public String toString() {
		StringBuffer display = new StringBuffer();
		display.append("---- " + name + " ----\n");
		display.append(dough + "\n");
		display.append(sauce + "\n");
		for (String topping : toppings) {
			display.append(topping + "\n");
		}
		return display.toString();
	}
}

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

 

main 

public class PizzaTestDrive {
 
	public static void main(String[] args) {
		PizzaStore nyStore = new NYPizzaStore();
		PizzaStore chicagoStore = new ChicagoPizzaStore();
 
		Pizza pizza = nyStore.orderPizza("cheese");
		System.out.println("Ethan ordered a " + pizza.getName() + "\n");
 
		pizza = chicagoStore.orderPizza("cheese");
		System.out.println("Joel ordered a " + pizza.getName() + "\n");
	}
}

 

팩토리 메소드의 장점과 단점

장점

  • Factory Method 패턴의 가장 큰 장점은 지금까지 본 것처럼 수정에 닫혀있고 확장에는 열려있는 OCP 원칙을 지킬 수 있다.
  • 다른 객체 없이 상속을 통해서 구현하므로 비교적 메모리를 아낄 수 있다.

단점

  • 간단한 기능을 사용할 때보다 많은 클래스를 정의해야 하기 때문에 코드량이 증가한다.

추상 팩토리 패턴 (Abstract Factory Pattern)

이미지 출처: https://en.wikipedia.org/wiki/Abstract_factory_pattern

"디자인 패턴" 책에서는 추상 팩토리 패턴을 "관련이 있거나 의존적인 객체들의 패밀리를 생성하기 위한 인터페이스로, 구체적인 클래스를 명시하지 않고 객체를 생성하는 패턴"이라고 설명하고 있습니다.

위의 UML Class Diagram에서는 ProductA와 ProductB 객체가 필요한 Client 클래스가 ProductA1과 ProductB1 클래스를 직접적으로 인스턴스화하지 않습니다. 대신, Client는 객체를 생성하기 위해 AbstractFactory 인터페이스를 참조하며, 이로써 Client는 객체가 어떻게 생성되는지(어떤 구체적인 클래스가 인스턴스화되는지)에 독립적이 됩니다. Factory1 클래스는 AbstractFactory 인터페이스를 구현하여 ProductA1과 ProductB1 클래스를 인스턴스화합니다.

UML Sequence Diagram은 런타임 상호 작용을 보여줍니다. Client 객체는 Factory1 객체에서 createProductA()를 호출하며, 이는 ProductA1 객체를 생성하고 반환합니다. 그 후에 Client는 Factory1에서 createProductB()를 호출하며, 이는 ProductB1 객체를 생성하고 반환합니다.

 

잘 와닿지 않으니 예를 한번 봅시다.

 

우리는 가게마다 각 피자의 재료가 다르다고 가정해 봅시다. 

팩토리 메소드 패턴에서는

재료가 추가, 수정될 때마다 Pizza를 상속받은 객체를 생성해주어야 하거나,

팩토리 메소드를 여러 개를 만들어서 매번 Pizza 객체에 전달해주어야 합니다. 

결국 코드의 변경을 야기하게 됩니다.

 

따라서 재료 별로 객체를 만들고 재료 팩토리를 하나 더 생성하여 가게 별로 재료를 관리하도록 합니다.

 

재료 클래스

public interface Dough {
	public String toString();
}

public class ThinCrustDough implements Dough {
	public String toString() {
		return "Thin Crust Dough";
	}
}

 

Abstract Factory

팩토리 메서드는 하나의 팩터리 메서드만 정의하고, 이를 반환하는 것과는 달리,
추상 팩토리 패턴은 제품군을 묶어 모든 메서드를 정의하고, 여러 객체를 반환합니다.

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

 

NYPizzaStore

위에서 생성한 추상 팩토리를 생성하고 Concrete Pizza 객체에 전달하여 객체를 생성해 줍니다.

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("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 PepperoniPizza(ingredientFactory);
			pizza.setName("New York Style Pepperoni Pizza");
 
		} 
		return 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 Pizza createPizza(String style, String type) {
        Pizza pizza = null;
        if (style.equals("NY")) {
            if (type.equals("cheese")) {
                pizza = new NYStyleCheesePizza();
            } else if (type.equals("veggie")) {
                pizza = new NYStyleVeggiePizza();
            } else if (type.equals("clam")) {
                pizza = new NYStyleClamPizza();
            } else if (type.equals("pepperoni")) {
                pizza = new NYStylePepperoniPizza();
            }
        } else if (style.equals("Chicago")) {
            if (type.equals("cheese")) {
                pizza = new ChicagoStyleCheesePizza();
            } else if (type.equals("veggie")) {
                pizza = new ChicagoStyleVeggiePizza();
            } else if (type.equals("clam")) {
                pizza = new ChicagoStyleClamPizza();
            } else if (type.equals("pepperoni")) {
                pizza = new ChicagoStylePepperoniPizza();
            }
        } else if (style.equals("Seoul")) { // New Pizza Store
            if (type.equals("cheese")) {
                pizza = new SeoulStyleCheesePizza();
            } else if (type.equals("veggie")) {
                pizza = new SeoulStyleVeggiePizza();
            } else if (type.equals("clam")) {
                pizza = new SeoulStyleClamPizza();
            } else if (type.equals("pepperoni")) {
                pizza = new SeoulStylePepperoniPizza();
            }
        }

 

Factory Method를 적용한 경우

단순히 아래 클래스들을 추가적으로 생성하고 구현할 뿐, 다른 클래스들의 코드 수정은 필요 없습니다.

 

 

Abstract Factory를 적용한 경우

서울 스타일 추상 팩토리와 객체 생성을 담당하는 피자 스토어를 상속한 서울피자스토어를 구현해 주면 됩니다.

 

참고 자료

https://github.com/IT-Book-Organization/HeadFirst-DesignPattern/blob/main/Chapter_04/README.md

https://en.wikipedia.org/wiki/Abstract_factory_pattern

 

Abstract factory pattern - Wikipedia

From Wikipedia, the free encyclopedia Software design pattern UML class diagram The abstract factory pattern in software engineering is a design pattern that provides a way to create families of related objects without imposing their concrete classes, by e

en.wikipedia.org

https://en.wikipedia.org/wiki/Factory_method_pattern