Dev/디자인패턴

[타입스크립트로 살펴보는 디자인패턴 4] 팩토리 패턴

싯벨트 2024. 12. 25. 06:53
728x90

🙋‍♂️ 디자인패턴 구현코드 깃헙

심플 팩토리 (Simple Factory)

무언가를 생산하는 곳을 공장, 즉 팩토리라고 한다. 코드에서도 마찬가지로 객체를 생성하는 과정을 캡슐화하여, 클라이언트가 직접 new 키워드를 사용하지 않고도 객체를 생성할 수 있는 클래스를 구비하는 것을 팩토리 패턴이라고 칭한다.

팩토리 패턴에는 팩토리 메서드 패턴추상 팩토리 패턴이 있으며, 객체를 생성하는 팩토리 자체를 구성하는 것은 심플 팩토리라고 한다. 팩토리 구비를 통해 객체 생성 관리를 수월하게 하여 코드의 재사용성과 유연성을 확보할 수 있다.

피자 매장 주문 시스템을 통해 심플 팩토리를 먼저 살펴보자.

피자 매장 주문 시스템

팩토리에서 피자를 만들어서 PizzaStore에 전달하는 구조이다. 피자 객체 생성 작업을 팩토리 클래스로 캡슐화 함으로써, 구현을 변경할 때 팩토리 클래스의 변경만으로 대응할 수 있다.

코드

pizza.ts

오버라이드를 하여 사용할 수 있도록 추상클래스로 정의했다.

export abstract class Pizza {
    abstract name: string;

    prepare(): void {
        console.log("피자 준비");
    }

    bake(): void {
        console.log("피자 굽기");
    }

    cut(): void {
        console.log("피자 커팅");
    }

    box(): void {
        console.log("피자 포장");
    }
}

pizza.menu.ts

Pizza 클래스를 구현한 구상 클래스들이다.

import { Pizza } from "./pizza";

export class CheesePizza extends Pizza {
    name = "치즈 피자";
}

export class PepperoniPizza extends Pizza {
    name = "페페로니 피자";
}

export class ClamPizza extends Pizza {
    name = "조개 피자";
}

export class VeggiePizza extends Pizza {
    name = "야채 피자";
}

simple.pizza.factory.ts

전달받은 피자 타입에 따라 부합하는 구상 클래스 인스턴스를 만든다.

import { Pizza } from "./pizza";
import { CheesePizza, ClamPizza, PepperoniPizza, VeggiePizza } from "./pizza.menu";

export class SimplePizzaFactory {
    createPizza(type: string): Pizza {
        let pizza: Pizza;

        if (type === "cheese") {
            pizza = new CheesePizza();
        } else if (type === "pepperoni") {
            pizza = new PepperoniPizza();
        } else if (type === "clam") {
            pizza = new ClamPizza();
        } else if (type === "veggie") {
            pizza = new VeggiePizza();
        } else {
            throw new Error("등록되지 않은 메뉴입니다.");
        }

        console.log(`주문한 피자: ${pizza.name}`);
        return pizza;
    }
}

pizza.store.ts

피자 매장은 타입과 함께 주문을 넣으면 타입에 맞는 피자 객체는 팩토리가 만들어준다.

import { Pizza } from "./pizza";
import { SimplePizzaFactory } from "./simple.pizza.factory";

export class PizzaStore {
    constructor(private factory: SimplePizzaFactory) {}

    orderPizza(type: string): Pizza {
        const pizza: Pizza = this.factory.createPizza(type);

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

        return pizza;
    }
}

지점 확장 케이스

피자 가게가 장사가 잘 돼서 다른 지역에 지점을 만들기로 했다. 다양한 피자를 만들어야 하니 지점별 팩토리를 구성해서 운영을 하기로 결정했다. 그런데 이렇게 되면 지점이 확장될 때마다 PizzaStore 클래스의 수정이 불가피해진다. factory 필드에 팩토리 타입과, 어떤 팩토리를 사용할지 결정하는 생성자 로직의 변경이 발생하기 때문이다.

export class PizzaStore {
    private factory: NewYorkPizzaFactory | ChicagoPizzaFactory;

    constructor(factoryType: string) {
        if (factoryType === "NewYork") {
            this.factory = new NewYorkPizzaFactory();
        } else if (factoryType === "Chicago") {
            this.factory = new ChicagoPizzaFactory();
        } else {
            throw new Error("Unknown factory type.");
        }
    }

    orderPizza(type: string): Pizza {
        const pizza = this.factory.createPizza(type);

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

        return pizza;
    }
}

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

팩토리 메서드 패턴에서는 객체를 생성할 때 필요한 인터페이스를 만든다. 어떤 클래스의 인스턴스를 만들지는 서브클래스에서 결정한다. 팩토리 메서드 패턴을 사용하면 클래스 인스턴스 만드는 일을 서브클래스에 맡기게 된다.

 

객체 생성을 서브클래스에 캡슐화하는 팩토리 메서드 패턴을 적용하면, PizzaStore가 주문 메서드를 호출할 때 피자 종류는 서브클래스인 각 지점이 결정함으로써 기존 코드를 수정할 필요 없이 새로운 지점들을 추가할 수 있다.

코드

pizza.store.ts

createPizza는 팩토리 메서드이며, 추상메서드로 선언하여 서브클래스가 객체 생성을 책임지도록 한다. 특정 객체를 리턴하고, 그 객체는 보통 슈퍼클래스가 정의한 메서드 내에서 쓰인다. 팩토리 메서드는 슈퍼클래스의 메서드(클라이언트)에서 생성되는 구상 객체가 무엇인지 알 수 없게 만드는 역할도 한다.

import { Pizza } from "./pizza";

export abstract class PizzaStore {
    orderPizza(type: string): Pizza {
        const pizza = this.createPizza(type);

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

        return pizza;
    }

    protected abstract createPizza(type: string): Pizza;
}

ny.pizza.branch.ts

피자의 타입을 받아서 뉴욕 지점의 피자 객체를 생성 및 리턴한다.

import { Pizza } from "../pizza";
import {
    NYStyleCheesePizza,
    NYStyleClamPizza,
    NYStylePepperoniPizza,
    NYStyleVeggiePizza,
} from "../menu/ny.pizza.menu";
import { PizzaStore } from "../pizza.store";

export class NYPizzaBranch extends PizzaStore {
    createPizza(type: string): Pizza {
        if (type === "NY cheese") {
            return new NYStyleCheesePizza();
        } else if (type === "NY pepperoni") {
            return new NYStylePepperoniPizza();
        } else if (type === "NY clam") {
            return new NYStyleClamPizza();
        } else if (type === "NY veggie") {
            return new NYStyleVeggiePizza();
        } else {
            throw new Error("등록되지 않은 메뉴입니다.");
        }
    }
}

ny.pizza.menu.ts

import { Pizza } from "../pizza";

export class NYStyleCheesePizza extends Pizza {
    name = "NY 치즈 피자";
    dough = "씬 크러스트 도우";
    sauce = "마리나라 소스";
    toppings = ["레지아노 치즈", "파마산 치즈"];
}

export class NYStylePepperoniPizza extends Pizza {
    name = "NY 페페로니 피자";
    dough = "씬 크러스트 도우";
    sauce = "마리나라 소스";
    toppings = ["레지아노 치즈", "수제 햄"];
}

export class NYStyleClamPizza extends Pizza {
    name = "NY 조개 피자";
    dough = "씬 크러스트 도우";
    sauce = "마리나라 소스";
    toppings = ["레지아노 치즈", "조개 관자"];
}

export class NYStyleVeggiePizza extends Pizza {
    name = "NY 야채 피자";
    dough = "씬 크러스트 도우";
    sauce = "마리나라 소스";
    toppings = ["레지아노 치즈", "각종 야채"];
}

pizza.ts

export abstract class Pizza {
    name: string;
    dough: string;
    sauce: string;
    toppings: string[];

    prepare(): void {
        console.log(`준비 중: ${this.name}`);
        console.log(`${this.dough}를 돌리는 중`);
        console.log(`${this.sauce}를 뿌리는 중`);
        console.log(`토핑을 올리는 중: ${this.toppings.join(", ")}`);
    }

    bake(): void {
        console.log("오븐에서 굽기");
    }

    cut(): void {
        console.log("8등분으로 커팅하기");
    }

    box(): void {
        console.log("상자에 담기");
    }

    getName() {
        return this.name;
    }
}

pizza.order.ts

두 사람이 뉴욕 지점 피자, 시카고 지점의 피자를 주문하는 코드이며, 출력은 아래와 같다.

import { ChicagoPizzaBranch } from "./branch/chicago.pizza.branch";
import { NYPizzaBranch } from "./branch/ny.pizza.branch";
import { Pizza } from "./pizza";
import { PizzaStore } from "./pizza.store";

export class PizzaOrder {
    private nyBranch: PizzaStore = new NYPizzaBranch();
    private chicagoBranch: PizzaStore = new ChicagoPizzaBranch();
    private pizza: Pizza;

    constructor() {
        this.pizza = this.nyBranch.orderPizza("cheese");
        console.log(`철수가 주문한 ${this.pizza.getName()}\n`);

        this.pizza = this.chicagoBranch.orderPizza("clam");
        console.log(`영희가 주문한 ${this.pizza.getName()}\n`);
    }
}

new PizzaOrder();

디자인 원칙

의존성 뒤집기 원칙(Dependency Inversion Principle)

추상화된 것에 의존하게 만들고, 구상 클래스에 의존하지 않게 만든다

이 원칙은 고수준 구성 요소가 저수준 구성 요소에 의존하면 안되며, 항상 추상화에 의존하게 만들어야 한다는 의미이다. 이때 고수준’ 구성 요소란 ‘저수준’ 구성 요소에 의해 정의되는 행동이 들어있는 구성 요소를 뜻한다. PizzaStore를 예로 들면, PizzaStore 내부에는 Pizza 객체를 준비하고, 굽고, 자르고, 포장하는 행동들이 정의되어 있다. 따라서, PizzaStore는 고수준 구성 요소이고, Pizza는 저수준 구성 요소이다.

의존성 뒤집기 원칙 적용하기

팩토리 패턴을 적용하지 않았던 PizzaStore 클래스의 경우, 피자의 모든 객체를 PizzaStore 클래스 내부에서 만들었다. 모든 피자 객체에 직접 의존하게 되고, 피자 구상 클래스들이 변경되면 PizzaStore 코드도 변경되어야 한다. 이 경우, “PizzaStore는 피자 클래스 구현에 의존한다”고 말한다. 

반면, 팩토리 메서드 패턴으로 적용한 경우를 살펴보면 PizzaStore가 Pizza 추상 클래스에 의존하고, 피자 구상 클래스들도 Pizza 추상 클래스에 의존하는 것을 볼 수 있다.

 

일반적으로 개념을 구성하면 위에서 아래로 가게 되는데, 의존성은 아래(저수준)에서 위(고수준)로 가야 한다. 의존성 뒤집기 원칙을 적용할 때 도움이 되는 가이드라인은 다음과 같다.

  1. 변수에 구상 클래스의 레퍼런스를 저장하지 않는다.
    • new 연산자를 사용하면 구상 클래스의 레퍼런스를 사용하게 된다. 팩토리를 사용하여 구상 클래스의 레퍼런스를 변수에 저장하는 일을 미리 방지한다
  2. 구상 클래스에서 유도된 클래스를 만들지 않는다
    • 구상 클래스에서 유도된 클래스를 만들면 특정 구상 클래스에 의존하게 된다. 인터페이스나 추상 클래스처럼 추상화된 것으로부터 클래스를 만들어야 한다.
  3. 베이스 클래스에 이미 구현되어 있는 메서드를 오버라이드 하지 않는다.
    • 이미 구현되어 있는 메서드를 오버라이드하면 베이스 클래스가 제대로 추상화되지 않는다. 베이스 클래스에서 메서드를 정의할 때는 모든 서브클래스에서 공유할 수 있는 것만 정의해야 한다.

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

추상 팩토리 패턴은 구상 클래스에 의존하지 않고도 서로 연관되거나 의존적인 객체로 이루어진 제품군을 생산하는 인터페이스를 제공한다. 구상 클래스는 서브클래스에서 만든다.

원재료 품질 관리 시스템 구축

지점별 원재료 관리를 위해 추상 팩토리 패턴을 활용했다. 어떤 종류들을 관리할지 정의한 팩터리 인터페이스와 지점별 원재료 팩터리를 적용하여 지점이 확장되었을 때, 새로 추가된 지점의 원재료 팩토리 추가만으로 대응할 수 있다.

코드

pizza.ingredient.factory.ts

원재료 팩터리 인터페이스를 정의한다. 각 지점별 팩토리는 이를 구현한다.

import { Cheese, Clams, Dough, Pepperoni, Sauce, Veggies } from "./pizza.ingredient.type";

export interface PizzaIngredientFactory {
    createDough(): Dough;
    createSauce(): Sauce;
    createCheese(): Cheese;
    createVeggies(): Veggies[];
    createPepperoni(): Pepperoni;
    createClams(): Clams;
}

ny.pizza.ingredient.factory.ts

뉴욕 지점에서 사용하는 원재료들을 구성했다.

import {
    FreshClams,
    Garlic,
    MarinaraSauce,
    Mushroom,
    Onion,
    RedPepper,
    ReggianoCheese,
    SlicedPepperoni,
    ThinCrustDough,
} from "./pizza.ingredient";
import { PizzaIngredientFactory } from "./pizza.ingredient.factory";
import { Cheese, Clams, Dough, Pepperoni, Sauce, Veggies } from "./pizza.ingredient.type";

export class NYPizzaIngredientFactory implements PizzaIngredientFactory {
    createDough(): Dough {
        return new ThinCrustDough();
    }

    createSauce(): Sauce {
        return new MarinaraSauce();
    }

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

    createVeggies(): Veggies[] {
        return [new Garlic(), new Onion(), new Mushroom(), new RedPepper()];
    }

    createPepperoni(): Pepperoni {
        return new SlicedPepperoni();
    }

    createClams(): Clams {
        return new FreshClams();
    }
}

pizza.menu.ts

기존에는 지점별로 메뉴에 대한 클래스를 마련했지만, 추상 팩토리 패턴을 적용하면 객체 구성을 활용하여 지점별 팩터리를 변수로 받아서 활용하기 때문에 피자 메뉴 클래스는 공통으로 사용할 수 있다.

import { Pizza } from "./pizza";
import { PizzaIngredientFactory } from "./ingredient/pizza.ingredient.factory";

export class CheesePizza extends Pizza {
    constructor(private ingredientFactory: PizzaIngredientFactory) {
        super();
    }

    prepare(): void {
        console.log(`준비 중: ${this.getName()}`);
        this.dough = this.ingredientFactory.createDough();
        this.sauce = this.ingredientFactory.createSauce();
        this.cheese = this.ingredientFactory.createCheese();
    }
}

export class PepperoniPizza extends Pizza {
    constructor(private ingredientFactory: PizzaIngredientFactory) {
        super();
    }

    prepare(): void {
        console.log(`준비 중: ${this.getName()}`);
        this.dough = this.ingredientFactory.createDough();
        this.sauce = this.ingredientFactory.createSauce();
        this.pepperroni = this.ingredientFactory.createPepperoni();
    }
}

export class ClamPizza extends Pizza {
    constructor(private ingredientFactory: PizzaIngredientFactory) {
        super();
    }

    prepare(): void {
        console.log(`준비 중: ${this.getName()}`);
        this.dough = this.ingredientFactory.createDough();
        this.sauce = this.ingredientFactory.createSauce();
        this.clams = this.ingredientFactory.createClams();
    }
}

export class VeggiePizza extends Pizza {
    constructor(private ingredientFactory: PizzaIngredientFactory) {
        super();
    }

    prepare(): void {
        console.log(`준비 중: ${this.getName()}`);
        this.dough = this.ingredientFactory.createDough();
        this.sauce = this.ingredientFactory.createSauce();
        this.veggies = this.ingredientFactory.createVeggies();
    }
}

패턴 적용 상황

  • 팩토리 메서드 패턴 - 클라이언트 코드와 인스턴스를 만들어야 할 구상 클래스를 분리시켜야 할 때
  • 추상 팩토리 패턴 - 서로 연관된 일련의 제품을 만들어야 할 때

참고문서

헤드퍼스트 디자인패턴, 한빛미디어