[ Programming > Design Pattern ]
[디자인 패턴] 팩토리 패턴
[디자인 패턴] 팩토리 패턴
팩토리 패턴은 생성 패턴의 한 종류다. 생성 패턴은 객체의 생성 방식을 결정하는 패턴으로 객체 생성을 추상화하는 패턴이다.
팩토리 패턴은 크게 2가지로 나뉘어진다. 팩토리 메서드 패턴과 추상 팩토리 패턴이다.
팩토리 메서드 패턴은 객체 생성을 구상 클래스의 메서드로 처리하는 것으로 객체 생성을 하는 인터페이스를 만들고 구현하는 패턴이다. 이 때 만들어지는 객체의 인터페이스를 제품이라고 표현하고 객체를 만드는 인터페이스를 생산자라고 표현한다.
추상 팩토리 패턴은 연관 및 의존하는 객체로 이루어진 하나의 객체를 생성하기 위한 인터페이스를 제공하는 패턴이다.
위 두 패턴은 모두 SOLID 원칙의 D(의존성 역전 원칙)과 깊은 관련이 있다. 왜냐하면 제품을 구상하는 제품 구상 클래스와 제품을 만드는 생산자 구상 클래스가 모두 구상 클래스를 의존하지 않고 제품 인터페이스를 통해 연결되기 때문이다.
1. 간단한 팩토리 패턴
간단한 팩토리 패턴을 설명하기 위해 요구사항을 만들고 이를 구현하겠다.
피자가게 요구사항
피자 주문을 받으면 피자 종류에 따라 type을 받는다.
type에 맞는 피자를 만들고 가공한다.
피자 가게마다 만드는 똑같은 피자도 만드는 방식이 다르거나, 만드는 피자 종류가 다를 수 있다.
위 요구 사항 그대로 생각 없이 구현하면 아래와 같이 나올 거다.
interface PizzaStore { Pizza orderPizza(String type);}interface Pizza { void prepare(); void bake(); void cut(); ...}public class StoreACheesePizza implements Pizza { ...}public class StoreBCheesePizza implements Pizza { ...}public class PizzaStoreA implements PizzaStore { @Override Public Pizza orderPizza(String type) { Pizza pizza = switch(type) { case "Cheese" -> new StoreACheesePizza(); case "Chocolate" -> new StoreAChocolatePizza(); default -> throw new IllegalArgumentException("Unknown pizza type: " + type); } return pizza; }}public class PizzaStoreB implements PizzaStore { @Override Public Pizza orderPizza(String type) { Pizza pizza = switch(type) { case "Cheese" -> new StoreBCheesePizza(); case "Chocolate" -> new StoreBChocolatePizza(); case "Special" -> new StoreBSpecialPizza(); default -> throw new IllegalArgumentException("Unknown pizza type: " + type); } pizza.prepare(); pizza.bake(); pizza.cut(); return pizza; }}
위 코드를 보면 Pizza 객체 생성은 따로 빼고 싶은 욕구가 생길 것이다. 왜냐하면 피자 생성에 많은 책임을 가지고 있어서 어떤 객체가 생기는지 까지 다 알고 있기 때문이다. 그리고 추상화와 캡슐화하기에도 좋다. 그래서 간단한 Factory 패턴을 적용해보자.
interface PizzaFactory { Pizza createPizza(String type);}public class StoreAPizzaFactory { public Pizza createPizza(String type) { return switch (type) { case "Cheese" -> new StoreACheesePizza(); case "Chocolate" -> new StoreAChocolatePizza(); default -> throw new IllegalArgumentException("Unknown pizza type: " + type); }; }}public abstract class PizzaStore { protected final PizzaFactory factory; public PizzaStore(PizzaFactory factory) { this.factory = factory; } public Pizza orderPizza(String type) { Pizza pizza = factory.createPizza(type); pizza.prepare(); pizza.bake(); pizza.cut(); return pizza; }}public class PizzaStoreA implements PizzaStore { private final PizzaFactory factory; public PizzaStoreA() { super(new StoreAPizzaFactory()); } private void addSticker() {...};}public class Main { public static void main(String[] args) { PizzaStore storeA = new PizzaStoreA(); Pizza pizza = storeA.orderPizza("Cheese"); }}
위 방식이 간단한 팩토리 패턴을 적용한 방식이다. 객체 생성 부분을 별도의 클래스로 캡슐화하여 책임을 분리했다. 그리고 PizzaStore의 orderPizza는 이제 Pizza 인터페이스에만 의존하도록 변경되었다.(concrete class에 의존 X)
아래는 패턴 적용한 클래스 다이어그램이다.

2. 팩토리 메서드 패턴
간단한 팩토리 메서드에서의 요구사항대로 팩토리 메서드 팬턴을 적용해보겠다.
팩토리 메서드 패턴은 말 그대로 메서드를 팩토리로 사용하는 것이다. 따라서 어떤 클래스의 인스턴스가 만들어질지는 서브 클래스에서 구현하는 메서드에따라 결정된다. 이 패턴의 장점은 PizzaStore의 구현체에서 추상 메소드를 구현하는 방식으로 메서드를 만들기 때문에 메뉴같은 것 추가 시 PizzaStore 인터페이스의 서브 클래스 구현 코드만 변경하면 된다.
public abstract class PizzaStore { public Pizza orderPizza(String type) { Pizza pizza = createPizza(type); pizza.prepare(); pizza.bake(); pizza.cut(); return pizza; } protected abstract Pizza createPizza(String type);}public class PizzaStoreA extends PizzaStore { @Override protected Pizza createPizza(String type) { return switch (type) { case "cheese" -> new StoreACheesePizza(); case "chocolate" -> new StoreAChocolatePizza(); default -> throw new IllegalArgumentException("Unknown pizza type: " + type); }; }}
위와 같이 추상 PizzaStore 클래서에서 createPizza를 추상 메서드로 만들었다. 그래서 SubClass에서 구현해야하는 팩토리 메서드가 되었고, 그 구현에 따라 무슨 피자가 만들어질 지 결정된다.
간단한 팩토리와 비교하면 간단한 팩토리A와 상점 A는 서로 공존해야만 하는데 굳이 분리해서 팩토리 A의 인터페이스가 바뀌면 그에 맞게 상점 A의 코드도 수정해야 한다.(물론 보통 interface로 안빼고 그냥 store에서만 사용하는 용도로 만들 것 같다.) 하지만 팩토리 메서드 패턴에서는 subclass의 메서드만 개별적으로 원하는데로 수정하면 된다.
아래는 팩토리 매서드 패턴을 적용한 클래스 다이어그램이다.

3. 추상 팩토리 패턴
여기서도 1. 간단한 팩토리 메서드에서 말한 요구사항 대로 추상 팩토리 패턴으로 구현해보겠다.
단, 피자 지점마다 다른 재료를 사용하여 피자를 만든다는 요구사항을 추가
예를 들면 피자 도우를 A 지점은 쌀로 만들고 B지점은 밀가루로 만든다. 또한 치즈는 A 지점은 모짜렐라를 B 지점은 고르곤졸라를 사용한다고 해보자. 그러면 A 지점의 모든 피자는 쌀 도우와 모짜렐라를 사용하고, B 지점은 밀가루 도우와 고르곤졸라를 사용한다.
우선 무지성 구현으로 한다면 어떤식으로 할까?? 아래와 같이 될 것 같다.
public abstract class Pizza { Dough dough; // 피자도우 Cheese cheese; // 치즈 ...}interface Dough{};interface Cheese{};public class RiceDough implements Dough{};public class FlourDough implements Dough{};public class MozzarellaCheese implements Cheese{};public class GorgonzolaCheese implements Cheese{};public class StoreACheesePizza extends Pizza { void prepare() { dough = new RiceDough(); cheese = new MozzarellaCheese(); }}public class StoreBCheesePizza extends Pizza { void prepare() { dough = new FlourDough(); cheese = new GorgonzolaCheese(); }}public class PizzaStoreA extends PizzaStore { @Override protected Pizza createPizza(String type) { return switch (type) { case "cheese" -> new StoreACheesePizza(); // 아니면 여기서 아래처럼 재료를 넣어줄 수도..? case "cheese" -> new StoreACheesePizza(new RiceDough(), new MozzarellaCheese()); case "chocolate" -> new StoreAChocolatePizza(); default -> throw new IllegalArgumentException("Unknown pizza type: " + type); }; }}
무지성으로 구현하면 위와 같이 Pizza는 많은 구현 재료(객체)와 의존성이 생기게 된다. 그리고 A사의 재료가 변경될 때 마다 모든 A사의 피자에 대해서 코드를 바꿔줘야할 것이다. (잘 만들면 아닐 수도 있겠지만..)
그러면 여기에 추상 팩토리 패턴을 적용해서 위 Pizza가 가진 여러 재료와의 의존성을 하나의 인터페이스에 의존하도록 변경해보자.
// 대부분 위에 선언한 클래스, 인터페이스를 그대로 쓴다. 다만 Ingredient 인터페이스와 그 구상 클래스가 생긴다.interface PizzaIngredientFactory { Dough createDough(); Cheese createCheese();}public abstract class Pizza { Dough dough; Cheese cheese; public Pizza(PizzaIngredientFactory ingredientFactory){ this.ingredientFactory = ingredientFactory; } abstract void prepare();}public class StoreAPizzaIngredientFactory implements PizzaIngredientFactory { @Override public Dough createDough() { return new RiceDough(); } @Override public Cheese createCheese() { return new MozzarellaCheese(); }}public class StoreACheesePizza extends Pizza { public StoreACheesePizza(PizzaIngredientFactory ingredientFactory){ super(ingredientFactory); } void prepare() { dough = ingredientFactory.createDough(); cheese = ingredientFactory.createCheese(); }}public class PizzaStoreA extends PizzaStore { private PizzaIngredientFactory ingredientFactory; public PizzaStoreA() { this.ingredientFactory = new StoreAPizzaIngredientFactory(); } @Override protected Pizza createPizza(String type) { return switch (type) { case "cheese" -> new StoreACheesePizza(ingredientFactory); case "chocolate" -> new StoreAChocolatePizza(ingredientFactory); default -> throw new IllegalArgumentException("Unknown pizza type: " + type); }; }}
위 코드가 추상 팩토리 패턴을 적용한 코드 예시다. Pizza의 재료는 ingredientFactory 인터페이스 함수를 통해서 나온다. 즉, A스토어에서 쓰이는 재료들을 별도로 ingredientFactory에 담아서 이 인터페이스를 구현한 구상 클래스의 구현에 따라 재료가 들어가는 것이다.
Pizza는 재료 개별에 대한 의존성이 사라지고, IngredientFactory 인터페이스에만 의존하게 된다.
아래는 구현했을 떄 클래스 다이어그램이다.

마무리
이번에는 디자인 패턴의 생성 패턴에 해당하는 팩토리 패턴을 공부해봤다. 팩토리 패턴은 단순한 팩토리 패턴, 메서드 팩토리 패턴, 추상 팩토리 패턴 이렇게 3가지로 나누어진다. 이 3개의 공통점은 모두 DIP(의존성 역전, 물론 단순한 팩토리는 DIP 적용 안할수도 있음)이라고 생각한다. 그리고 디자인 패턴을 공부하면서 느끼는 것은, 객체간 의존성은 개별 필드에 대한 의존성이 아닌 인터페이스(메서드)에 대한 의존성으로 만드는 것이 바람직한 설계일 확률이 높다는 것을 느꼈다. 왜냐하면 그렇게 해야만 확장성이 좋아지고, 결합도가 낮아지기 때문이다. 데이터 개별 필드에 대한 의존성 보다야 인터페이스 함수에 대한 의존성이 훨씬 유연한 것이 당연하지 않을까??
그리고 리팩터링, 클린 코드, 디자인 패턴 다 공부하면서 느끼는 것은 모두 같은 말을 하고 있다는 것이다. 다만 어떤 방향으로 설명하는지가 약간씩 다를 뿐이다... 오만한 생각일 수도 있겠지만 아무튼 그렇게 느꼈다.