Typescript

빌더 패턴

bkdragon 2024. 11. 10. 23:09

빌더 패턴은 복잡한 객체를 생성하는 클래스와 표현하는 클래스를 분리하여, 동일한 절차에서도 서로 다른 표현을 생성하는 방법을 제공한다.

쉽게 말해 객체를 여러 단계에 걸처 만들 수 있다.

구성 요소

  • Director(디렉터): Builder를 사용하여 객체를 생성하는 클래스. 추상 팩토리 패턴의 Client와 비슷한 역할을 한다.
  • Builder(빌더): 객체의 각 부분을 만드는 방법을 정의하는 인터페이스
  • ConcreteBuilder(구체적인 빌더): Builder 인터페이스를 구현하여 실제 객체를 만드는 클래스
  • Product(제품): Builder를 통해 만들어지는 최종 객체

예시 코드

피자를 예로 들건데, 현실에서도 피자를 주문할 떄 크기를 선택하거나 토핑을 추가하거나 도우를 선택하는 등의 절차가 있을 수 있다.

// 제품
class Pizza {
    private toppings: string[] = [];
    private size: string = "";
    private crustType: string = "";
    public setSize(size: string): void {
        this.size = size;
    }
    public setCrustType(type: string): void {
        this.crustType = type;
    }
    public addTopping(topping: string): void {
        this.toppings.push(topping);
    }
    public describe(): string {
        return `${this.size} 사이즈 ${this.crustType} 피자 (토핑: ${this.toppings.join(", ")})`;
    }
}
// Builder: 피자를 만드는 인터페이스
abstract class PizzaBuilder<TPizza extends Pizza> {
    reset(): void {}
    setSize(size: string): void {}
    setCrustType(type: string): void {}
    addTopping(topping: string): void {}
    abstract getPizza(): TPizza;
}
// ConcreteBuilder: 실제 피자를 만드는 클래스
class CustomPizzaBuilder implements PizzaBuilder<Pizza> {
    private pizza: Pizza;
    constructor() {
        this.pizza = new Pizza();
    }
    public reset(): void {
        this.pizza = new Pizza();
    }
    public setSize(size: string): void {
        this.pizza.setSize(size);
    }
    public setCrustType(type: string): void {
        this.pizza.setCrustType(type);
    }
    public addTopping(topping: string): void {
        this.pizza.addTopping(topping);
    }
    public getPizza(): Pizza {
        const result = this.pizza;
        this.reset();
        return result;
    }
}

// 디렉터: 빌더를 사용하여 객체를 생성하는 클래스
class Director {
    preparePizza<TPizza extends Pizza>(
        builder: PizzaBuilder<TPizza>,
        option: {
            size: string;
            crustType: string;
            toppings?: string[];
        }
    ): TPizza {
        builder.reset();
        builder.setSize(option.size);
        builder.setCrustType(option.crustType);
        if (option.toppings) {
            option.toppings.forEach((topping) => {
                builder.addTopping(topping);
            });
        }
        return builder.getPizza();
    }
}

// 사용 예시
const builder = new CustomPizzaBuilder();
const director = new Director();
// 하와이안 피자 만들기
const hawaiianPizza = director.preparePizza(builder, {
    size: "라지",
    crustType: "씬",
    toppings: ["파인애플", "햄", "모짜렐라 치즈"],
});
console.log(hawaiianPizza.describe());
// 페퍼로니 피자 만들기
const pepperoniPizza = director.preparePizza(builder, {
    size: "미디움",
    crustType: "치즈크러스트",
    toppings: ["페퍼로니", "모짜렐라 치즈"],
});
console.log(pepperoniPizza.describe());

// 토핑 없는 피자 만들기

CustomPizzaBuilder 는 기본적인 피자를 만드는 빌더이다.

만약 토핑이 없는 피자를 만들고 싶다면, 새로운 빌더를 만들면 된다.

// 토핑 없는 피자 빌더 : 토핑 없는 피자를 만드는 구체적인 빌더
class NoToppingPizzaBuilder extends PizzaBuilder<Pizza> {
    private pizza: Pizza;
    reset(): void {
        this.pizza = new Pizza();
    }

    setSize(size: string): void {
        this.pizza.setSize(size);
    }

    setCrustType(type: string): void {
        this.pizza.setCrustType(type);
    }
    // addTopping 메서드는 없음

    getPizza(): Pizza {
        return this.pizza;
    }
}

const noToppingPizza = director.preparePizza(new NoToppingPizzaBuilder(), {
    size: "라지",
    crustType: "씬",
});
console.log(noToppingPizza.describe());

NoToppingPizzaBuilder 에서 addTopping 메서드를 구현하지 않았기 때문에 토핑 없는 피자만 만들 수 있다.

정리

빌더 패턴은 객체의 생성 과정을 여러 단계로 나누어 구성한다. 이를 통해 동일한 생성 과정을 가진 객체를 다양한 방법으로 생성할 수 있다. 특성 속성이 사용되지 않는 경우에도 유연하게 대처할 수 있다.