Typescript로 다시 쓰는 GoF - Abstract Factory

아홉번째태양·2023년 8월 27일
0

Abstract Factory란?

앞서서 이미 Factory Method라는 디자인 패턴에 대해서 다루었었다. 인스턴스를 생성해내는 추상적인 객체를 만들어두고, 구체적으로 어떤 인스턴스를 어떻게 만들지는 하위에서 구체적인 객체를 작성하면서 결정하는 디자인 패턴이었다.

Abstract Factory도 유사하다. 하지만 다른 것이라면 Abstract Factory의 목적은 하나의 "인스턴스"를 어떻게 만들지가 아닌 "비슷한 유형"의 객체들을 어떻게 만들어낼지에 대한 디자인 패턴이다.

그래서 추상적인 클래스들의 인터페이스들을 이용해 어떤식으로 조립이되는지에 대한 과정을 그려놓고, 어떤 제품을, 즉 결과물을 얻을지는 나중에 정의한 구체적인 객체들과 공장에 전달되는 매개변수에 맡겨놓는다.



언제 쓸까?

Abstract Factory는 테마나 스킨처럼 기능과 속성이 유사한 제품군을 만들어야할 때 사용할 수 있다. 이때, Abstract Factory패턴을 적용함으로서 다음과 같은 이점을 가져갈 수 있다.

  • 유사한 제품군들 간에 일관된 기능과 속성을 공유할 수 있으며, 유사 제품을 쉽게 추가할 수 있다.
  • 각 객체를 구현하는 코드와 사용하는 코드를 분리할 수 있다.
  • pluggable한 기능이나 설정을 덧붙일 때 특정 제품군을 타게팅하기가 쉽다.

하지만, Abtract Factory는 제품의 조립과정이 이미 정의되어 있기 때문에 중간에 제품을 조립하기 위한 부품을 추가하거나 변경하는 것은 어려울 수 있다는 점에 주의하자.



Abstract Factory 구현

햄버거와 피자를 파는 가게에서 주문을 받아 주문서를 작성하고 주문내역을 출력하는 프로그램을 Abstract Factory 패턴을 이용해 만들어보자.

Abstract Factory에는 등장하는 객체가 꽤 다양하고, 또 다양한만큼 복잡하게 여겨지기도 한다. 하지만, Abstract Factory가 무엇을 하려하는 디자인 패턴인지를 인지하면 객체는 많을지언정 각각의 역할은 결국 크게 다음의 네가지로 좁혀질 수 있다.

  1. AbstractProduct 추상적인 제품
    AbstractFactory에 의해 만들어질 추상적인 제품 혹은 제품을 구성하는 부품의 인터페이스를 미리 정의

  2. AbstractFactory 추상적인 공장
    AbstractProduct의 인스턴스를 만들어주기 위한 일련의 비즈니스 로직을 담은 인터페이스를 결정

  3. ConcreteProduct 구체적인 제품
    AbstractProduct의 인터페이스를 구현

  4. ConcreteFactory 구체적인 공장
    AbstractFactory의 인터페이스를 구현

  5. Client 의뢰자
    AbstractFactory와 AbstractProduct에서 정의한 인터페이스를 사용하여 제품을 만들어내고 사용


구조 훑어보기

예제를 위해 비교적 많은 객체가 등장하기 때문에 그 이전에 앞서 살펴본 각 역할에 따라 필수객체만으로 간단하게 구조만 먼저 훑어보자.

interface AbstractFactory {
  createProductA(): AbstractProductA;
  createProductB(): AbstractProductB;
}

class ConcreteFactory implements AbstractFactory {
  createProductA(): AbstractProductA {
    return new ConcreteProductA();
  }
  createProductB(): AbstractProductB {
    return new ConcreteProductB();
  }
}

interface AbstractProductA {}
interface AbstractProductB {}

class ConcreteProductA implements AbstractProductA {}
class ConcreteProductB implements AbstractProductB {}

앞서 이야기한 것처럼 AbstractFactory 패턴의 목적은 "비슷한 유형"의 객체를 만들어내는데 있다. 따라서, ConcreteFactory에서 ConcreteProduct를 만들어주며, 이때 Template Method, Factory Method 같은 다른 디자인 패턴이 함께 접목되어 인스턴스를 어떻게 만들어줄지에 대해서도 인터페이스로 미리 정의해둘 수 있을 것이다.

이제 진짜 예제 코드를 작성해보자.


AbstractProduct

우선 각 메뉴들의 공통된 속성을 나타내는 Item이라는 객체와, 각 메인메뉴 Main과 음료 Drink, 그리고 주문서 Order의 인터페이스를 정의한다.

interface Item {
  price: number,
  info(): string;
}

abstract class Main implements Item {
  constructor(
    public price: number,
    protected options: string,
  ) {}

  abstract info(): string;
}

abstract class Drink implements Item {
  constructor(
    public price: number,
    protected size: string,
  ) {}

  abstract info(): string;
}

abstract class Order {
  protected items: Item[] = [];
  protected total: number = 0;

  add(item: Item) {
    this.items.push(item)
  }

  getTotal(): number {
    this.total = 0;
    this.items.forEach(item => {
      this.total += item.price;
    })
    return this.total
  }

  abstract print(): void;
}

처음부터 갑자기 네 가지 객체를 AbstractProduct으로서 만들었지만, 이들은 결국 Order라는 완성된 제품을 만들어내기 위한 부품들에 해당할 뿐이다.

Item에 해당하는 MainDrink가 있고, 이들은 Orderadd메소드를 이용해서 추가가 된다.


AbstractFactory

앞에서 만들어낸 Order를 찍어낼 수 있는 OrderFactory의 인터페이스를 만들자.

abstract class OrderFactory {
  static getFactory(menu: 'burger'|'pizza'): OrderFactory {
    switch (menu) {
      case 'burger':
        return new BurgerFactory();
      case 'pizza':
        return new PizzaFactory();
      default:
        throw new Error('Invalid menu');
    }
  }

  abstract createMain(): Main
  abstract createDrink(): Drink
  abstract createOrder(): Order
}

OrderFactory의 역할은 단순히 어떤 제품의 인스턴스를 만들어낼지만 결정하며, 그 밖의 해당 인스턴스로 수행할 수 있는 작업들이 함께 정의될 수 있다.

이 예제에서는 주문서에 메뉴를 추가할 때 Orderadd 메소드를 사용하도록 하면서 createMain과 같은 메소드들이 생성된 각 메뉴를 리턴하도록 했다. 하지만, 여기서 Builder 패턴도 접목시킬 수도 있을 것이다.


ConcreteProduct

이제 각 인터페이스들에 구체적인 코드를 구현할 차례다.

// Burger
class Burger extends Main {
  info(): string {
    return `Burger with ${this.options}`
  }
}

class SoftDrink extends Drink {
  info(): string {
    return `Soft drink with ${this.size}`
  }
}

class BurgerOrder extends Order {
  print(): void {
    console.log('Burger order:');
    this.items.forEach(item => {
      console.log(item.info());
    })
    console.log(`Total: ${this.getTotal()}`);
  }
}

// Pizza
class Pizza extends Main {
  info(): string {
    return `Pizza with ${this.options}`
  }
}

class Beer extends Drink {
  info(): string {
    return `Beer with ${this.size}`
  }
}

class PizzaOrder extends Order {
  print(): void {
    console.log('Pizza order:');
    this.items.forEach(item => {
      console.log(item.info());
    })
    console.log(`Total: ${this.getTotal()}`);
  }
}

ConcreteFactory

마지막으로 PizzaHamburger를 만들어내는 ConcreteFactory를 구현한다.

class BurgerFactory extends Factory {
  createMain() {
    return new Burger(100, 'cheese')
  }

  createDrink() {
    return new SoftDrink(50, 'large')
  }

  createOrder() {
    return new BurgerOrder();
  }
}

class PizzaFactory extends Factory {
  createMain() {
    return new Pizza(200, 'pepperoni')
  }

  createDrink() {
    return new Beer(100, 'small')
  }

  createOrder() {
    return new PizzaOrder();
  }
}

Client(구현한 코드의 사용)

완성된 코드를 실행해보자.

Abstract Factory 패턴에서는 실제 제품을 만들어내고 사용하는 단의 코드에서는 각 제품의 클래스를 직접 인스턴스화하거나 사용할 필요가 없어진다는 것에 주목하면서 보자.

const burgerFactory = Factory.getFactory('burger');
const burger = burgerFactory.createMain();
const drink = burgerFactory.createDrink();
const burgerOrder = burgerFactory.createOrder();
burgerOrder.add(burger);
burgerOrder.add(drink);
burgerOrder.print();

const pizzaFactory = Factory.getFactory('pizza');
const pizza = pizzaFactory.createMain();
const beer = pizzaFactory.createDrink();
const pizzaOrder = pizzaFactory.createOrder();
pizzaOrder.add(pizza);
pizzaOrder.add(beer);
pizzaOrder.print();



참고자료

Java언어로 배우는 디자인 패턴 입문 - 쉽게 배우는 Gof의 23가지 디자인패턴 (영진닷컴)
sbcode.net - Abstract Factory Design Pattern

0개의 댓글