Ayden's journal

팩토리 패턴

팩토리 패턴(Factory Pattern)은 객체 생성 로직을 클라이언트 코드로부터 분리하여, 객체 생성 과정을 캡슐화한다. 이 패턴의 핵심 아이디어는 "어떤 객체를 생성할지"에 대한 결정을 별도의 팩토리 클래스로 위임함으로써, 클라이언트는 객체의 구체적인 생성 과정이나 클래스 이름을 알 필요 없이 필요한 객체를 얻을 수 있다는 점이다. 이를 통해 코드의 결합도를 낮추고, 새로운 객체 유형이 추가되더라도 기존 코드를 수정하지 않고 확장할 수 있는 유연성을 제공한다. 팩토리 패턴은 설계 원칙 중 하나인 개방-폐쇄 원칙(OCP)을 효과적으로 지원하며, 단순 팩토리, 팩토리 메서드, 추상 팩토리 등 다양한 형태로 활용될 수 있다.

 

Simple Factory Pattern

단순 팩토리 패턴은 객체 생성 로직을 별도의 팩토리 클래스에 위임하여, 클라이언트가 객체 생성 방식을 몰라도 필요한 인스턴스를 얻을 수 있도록 하는 패턴이다. 이 패턴에서는 주로 하나의 팩토리 클래스에 create 메서드를 정의하고, 전달받은 인자에 따라 적절한 객체를 반환한다. 예를 들어, 피자 가게 시스템에서는 PizzaFactory 클래스가 createPizza 메서드를 통해 치즈 피자, 페퍼로니 피자 등 다양한 피자 객체를 생성한다.

// 🍕 Concrete Products: 구체적인 피자 클래스들
class CheesePizza extends Pizza {
  constructor() {
    super("Cheese Pizza");
  }
}

class PepperoniPizza extends Pizza {
  constructor() {
    super("Pepperoni Pizza");
  }
}

class VeggiePizza extends Pizza {
  constructor() {
    super("Veggie Pizza");
  }
}

// 🏭 Simple Factory: PizzaFactory 클래스
class PizzaFactory {
  static createPizza(taste: Taste) {
    switch (taste: Taste) {
      case "cheese":
        return new CheesePizza();
      case "pepperoni":
        return new PepperoniPizza();
      case "veggie":
        return new VeggiePizza();
      default:
        throw new Error(`Unknown pizza type: ${type}`);
    }
  }
}

// 🍽️ Client: PizzaStore 클래스
class PizzaStore {
  orderPizza(taste: taste) {
    const pizza = PizzaFactory.createPizza(taste);
    pizza.prepare();
    pizza.bake();
    pizza.cut();
    pizza.box();
    return pizza;
  }
}

// ✅ 사용 예시
const store = new PizzaStore();
store.orderPizza("cheese");
store.orderPizza("pepperoni");

 

멀티턴 패턴의 아이디어를 활용하면 길어지는 switch를 효과적으로 제거할 수 있지 않을까 싶다.

type Class<T> = new () => T;
type Taste = "cheese" | "pepperoni" | "veggie";

class PizzaFactory {
  private static tastes: Map<Taste, Class<Pizza>> = new Map([
    ["cheese", CheesePizza],
    ["pepperoni", PepperoniPizza],
    ["veggie", VeggiePizza],
  ]);

  static create(taste: Taste): Pizza {
    const PizzaClass = this.tastes.get(taste);
    
    if (!PizzaClass) throw new NotFoundError("Unknown pizza type 😕")
    
    return new PizzaClass(); // 클래스의 인스턴스 생성
  }
}

 

Factory Method Pattern

팩토리 메서드 패턴은 객체 생성을 서브클래스에 위임하여, 클라이언트 코드가 구체적인 클래스에 의존하지 않도록 만드는 패턴이다. 이 패턴에서는 추상 클래스나 인터페이스에 factoryMethod를 정의하고, 실제 객체 생성은 이를 구현한 서브클래스에서 담당하게 된다.

예를 들어, 피자 가게를 운영하는 시스템에서 PizzaStore는 create()라는 메서드를 사용해 피자를 만들기 위해 특정 피자 공장을 호출한다. 각 피자 공장은 자신이 생성할 피자 유형을 알고 있으며, create() 메서드를 통해 해당 피자 객체를 생성한다. 이렇게 피자 가게는 PizzaFactory의 구체적인 구현을 알지 못한 채 create() 메서드만 호출하여 피자를 생성하고, 이로 인해 클라이언트 코드에서는 피자 객체를 생성하는 방법에 대한 정보 없이 사용자가 원하는 피자 타입을 요청만 하면 된다.

// 🍕 Concrete Factories: 구체적인 피자 공장 클래스들
interface IPizzaFactory {
  create(): void;
}

class CheesePizzaFactory implements IPizzaFactory {
  create() {
    return new CheesePizza();
  }
}

class PepperoniPizzaFactory implements IPizzaFactory {
  create() {
    return new PepperoniPizza();
  }
}

class VeggiePizzaFactory implements IPizzaFactory {
  create() {
    return new VeggiePizza();
  }
}

// 🏭 Simple Factory: PizzaFactory 클래스
class PizzaFactory {
  private static tastes: Map<Taste, Class<Pizza>> = new Map([
    ["cheese", CheesePizzaFactory],
    ["pepperoni", PepperoniPizzaFactory],
    ["veggie", PepperoniPizzaFactory],
  ]);

  static create(taste: Taste): IPizzaFactory {
    const PizzaClass = this.tastes.get(Taste);
    
    if (!PizzaClass) throw new NotFoundError("Unknown pizza type 😕")
    
    return new PizzaClass(); // 클래스의 인스턴스 생성
  }
}

// 🍽️ Client: PizzaStore 클래스
class PizzaStore {
  orderPizza(taste: Taste) {
    const Pizza = PizzaFactory.createPizza(taste);
    const pizza = Pizza.create();
    pizza.prepare();
    pizza.bake();
    pizza.cut();
    pizza.box();
    return pizza;
  }
}

// ✅ 사용 예시
const store = new PizzaStore();
store.orderPizza("cheese");
store.orderPizza("pepperoni");

 

피자 각각에 대한 팩토리 클래스를 생성했지만, 정작 각각의 피자 팩토리를 호출하는 로직은 단순 팩토리 패턴과 크게 다르지 않다. 게다가 각각의 피자 팩토리 클래스는 코드 한 줄에 불과하기 때문에 위의 예시는 과도한 설계라 볼 수 있다. 이미 코드가 충분히 단순하다면 굳이 분리할 필요가 없다.

하지만 각 객체의 생성 논리가 매우 복잡한 경우 단순 팩토리 패턴에서는 모든 생성 작업이 동일한 팩토리 클래스에 배치되고, 자연히 팩토리 클래스의 복잡도가 증가한다. 따라서 이 경우에는 팩토리 메서드 패턴을 사용하여 복잡한 생성 작업을 여러 팩토리 클래스로 분할하면 각각 팩토리 클래스가 훨씬 단순하게 구성된다고 보장할 수 있다.

 

Abstract Factory Pattern

<디자인 패턴의 아름다움>의 저자 왕정의 말에 따르면, 추상 팩토리 패턴은 원칙적으로 더 복잡하기 때문에 실제 프로젝트 개발에 잘 사용되지 않는다고 한다. 내가 이해한 바에 따르면 추상 팩토리 패턴은 팩토리 클래스가 생성하는 팩토리 클래스의 일관성을 유지하면서도, 구체적인 클래스에 의존하는 것이 아니라 추상화에 의존한다.

이전까지의 예시에서는 피자 팩토리가 오직 맛에 의존하여 구체적 클래스를 생성해냈다면, 아래의 예시는 맛 뿐만 아니라 뉴욕, 시카고, 디트로이트, 세인트루이스 스타일에 따라 클래스가 분류된다.

type Taste = "cheese" | "pepperoni" | "veggie";
type Style = "new york" | "detroit";
type Class<T> = new () => T;

class NewYorkPizzaFactory {
  private static tastes: Map<Taste, Class<Pizza>> = new Map([
    ["cheese", NewYorkCheesePizza],
    ["pepperoni", NewYorkPepperoniPizza],
    ["veggie", NewYorkVeggiePizza]
  ])

  static create(taste: Taste) {
    const PizzaClass = this.tastes.get(taste);
    
    if (!PizzaClass) throw new NotFoundError("Unknown pizza type 😕")
    
    return new PizzaClass(); // 클래스의 인스턴스 생성
  }
}

class DetroitPizzaFactory {
  private static tastes: Map<Taste, Class<Pizza>> = new Map([
    ["cheese", DetroitCheesePizza],
    ["pepperoni", DetroitPepperoniPizza],
    ["veggie", DetroitVeggiePizza]
  ])

  static create(taste: Taste) {
    const PizzaClass = this.tastes.get(taste);
    
    if (!PizzaClass) throw new NotFoundError("Unknown pizza type 😕")
    
    return new PizzaClass(); // 클래스의 인스턴스 생성
  }
}

class PizzaFactory {
  private static styles = new Map<Style, typeof NewYorkPizzaFactory | typeof DetroitPizzaFactory>([
    ["new york", NewYorkPizzaFactory],
    ["detroit", DetroitPizzaFactory],
  ]);

  static create(style: Style, taste: Taste): Pizza {
    const factory = PizzaFactory.styles.get(style);
    
    if (!factory) throw new NotFoundError("Unknown pizza style 😕");
    
    return factory.create(taste);
  }
}

 

PizzaStore는 특정 피자 공장 구현에 의존하지 않고, 추상화된 IPizzaFactory 인터페이스에 의존하기 때문에, 다양한 피자 공장 구현을 사용할 수 있게 된다. 이를 통해 피자 매장(PizzaStore)은 서로 다른 지역 스타일의 피자 공장을 유연하게 교체하거나 확장할 수 있다.

// 🍽️ Client: PizzaStore 클래스
class PizzaStore {
  constructor(private pizzaFactory: IPizzaFactory) {}

  orderPizza(taste: Taste) {
    const pizza = this.pizzaFactory.create(taste);
    pizza.prepare();
    pizza.bake();
    pizza.cut();
    pizza.box();
    return pizza;
  }
}

// ✅ 사용 예시
const newYorkPizzaStore = new PizzaStore(PizzaFactory.create("new york"));
newYorkPizzaStore.orderPizza("cheese");
newYorkPizzaStore.orderPizza("pepperoni");

 

블로그의 정보

Ayden's journal

Beard Weard Ayden

활동하기