팩토리 패턴
팩토리 패턴은 클라이언트와 구체 클래스의 직접적인 의존성을 끊기 위해 사용합니다. 따라서 팩토리 메서드 패턴과 추상 팩토리 패턴도 큰 범주에서는 같은 역할을 합니다. 아래의 코드를 보면 클라이언트(orderPizza)가 구체 클래스(CheesePizza, PepperoniPizza)를 직접적으로 알고 있습니다.
만약 여기서 파인애플 피자가 추가된다면 어떨까요? orderPizza의 코드를 변경해야 합니다. 변경에는 닫혀 있고, 확장에는 열려있어야 하는 OCP(Open Closed Principle) 법칙도 위반하게 되죠.
function orderPizza(type) {
let pizza = null;
if (type === "치즈") {
pizza = new CheesePizza();
} else if ("페퍼로니") {
pizze = new PepperoniPizza();
}
pizza.prepare();
pizza.bake();
}
팩토리 패턴을 사용해 위의 코드를 개선해보겠습니다. 클라이언트(orderPizza)는 더 이상 어떤 피자를 만들지 알 수가 없습니다. 즉, 구체 클래스에 대한 의존성이 제거가 된 것이죠. 파인애플 피자가 추가된다고 하더라도 클라이언트 코드를 변경할 필요가 없어집니다. 팩토리 함수(createPizza)만 변경해주면 되기 때문에 유지 보수하기 용이한 코드라고 볼 수 있을 것 같습니다.
function orderPizza(type) {
const pizza = createPizza(type);
pizza.prepare();
pizza.bake();
}
function createPizza(type) {
if (type === "치즈") {
return new CheesePizza();
} else if ("페퍼로니") {
return new PepperoniPizza();
} else throw new Error("알 수 없는 피자 종류");
}
팩토리 메서드 패턴
그럼 이제 팩토리 메서드 패턴에 대해 살펴보겠습니다. 피자를 생성하는 과정도 있지만 그 피자를 준비하고 굽는 과정을 하나의 객체로 제공해주고 싶다고 했을때 아래와 같이 추상 클래스를 사용할 수 있습니다. 실제로 피자를 만드는 책임은 이 추상 클래스를 상속하고 있는 서브 클래스에 있습니다.
abstract class PizzaStore {
public orderPizza(type) {
const pizza = this.createPizza(type);
pizza.prepare();
pizza.bake();
return pizza;
}
protected abstract createPizza(type): Pizza;
}
치즈 피자와 페페로니 피자를 만들고 싶으면 PizzaStore을 상속하는 CheesePizzaStore과 PepperoniPizzaStore을 정의해야 합니다.
class CheesePizzaStore extends PizzaStore {
createPizza(type) {
if (type === "체다치즈") {
return new CheddarCheesePizza();
} else if (type === "모짜렐라치즈") {
return new MozzarellaCheesePizza();
} else {
throw new Error("Unknown pizza type");
}
}
}
class PepperoniPizza extends PizzaStore {
createPizza(type) {
if (type === "페퍼로니") {
return new PepperoniPizza();
} else if (type === "어썸한 페퍼로니피자") {
return new AssumePepperoniPizza();
} else {
throw new Error("Unknown pizza type");
}
}
}
즉, 팩토리 메서드 패턴은 생성할 객체를 서브 클래스에서 결정하게 됩니다.
추상 팩토리 패턴
추상 팩토리 패턴은 생성해야할 객체를 그룹화할때 주로 사용합니다. 피자에는 여러가지 재료가 있죠. 각 재료를 생성하기 위한 인터페이스를 정의해보겠습니다.
피자에는 도우와 소스가 있죠. 이를 인터페이스로 나타낸 코드입니다.
interface PizzaIngredientFactory {
createDough(): Dough;
createSauce(): Sauce;
}
하지만 피자의 종류별로 도우와 소스가 다르기 때문에 피자의 종류별로 팩토리 함수를 만들어 주겠습니다.
class CheesePizzaIngredientFactory implements PizzaIngredientFactory {
createDough(): Dough {
return new CheeseDough();
}
createSauce(): Sauce {
return new CheeseSauce();
}
}
class PepperoniPizzaIngredientFactory implements PizzaIngredientFactory {
createDough(): Dough {
return new PepperoniDough();
}
createSauce(): Sauce {
return new PepperoniSauce();
}
}
실제로 이 팩토리 함수들이 어떻게 사용되는 확인해보죠. 아래의 코드를 살펴보면 각각의 피자들은 어떤 pizzaIngredientFactory를 가지는지 구체적으로 알고 있지 않습니다. 인터페이스만 알고 있기 때문이죠. 앞서 언급한 바와 같이 이렇게 코드를 작성하게 되면 구체 클래스에 대한 의존성을 제거할 수 있는 장점이 있습니다. CheesePizza에서 다른 팩토리를 사용하더라도 변경의 여파가 CheesePizza까지 퍼지지 않게 되는 것이죠.
class CheesePizza {
constructor(private pizzaIngredientFactory: PizzaIngredientFactory) {}
createPizza() {
const dough = this.pizzaIngredientFactory.createDough();
const sauce = this.pizzaIngredientFactory.createSauce();
// ...
}
}
class PepperoniPizza {
constructor(private pizzaIngredientFactory: PizzaIngredientFactory) {}
createPizza() {
const dough = this.pizzaIngredientFactory.createDough();
const sauce = this.pizzaIngredientFactory.createSauce();
// ...
}
}
팩토리 메서드 패턴과 추상 팩토리 패턴 차이
팩토리 메서드 패턴은 상속을 사용하여 서브 클래스에게 객체의 생성 역할을 위임합니다.
추상 팩토리 패턴은 합성을 사용합니다. (ex. CheesePizza -> PizzaIngredientFactory) 다만, 추상 팩토리 패턴도 실제 객체를 생성할 때는 팩토리 메서드를 사용합니다. (ex. createDough, createSauce)
마치며
이번 포스팅에서는 팩토리 패턴, 팩토리 메서드 패턴, 추상 팩토리 패턴에 대해 살펴보았습니다. 클라이언트로부터 구체 클래스의 의존성을 끊고 변경의 여파를 인터페이스로 경계 짓는다로 정리를 해보고 싶네요.
실제로 Nest.js service 레이어에서 클라이언트에서 보낸 데이터를 토대로 도메인 객체를 생성하는 것을 이 팩토리 패턴을 사용해서 구현했었습니다. 실무에서도 적용할만한 케이스들이 꽤 있는 것 같아서 다음에 기회가 된다면 실제 프로젝트에서 적용해본 사례를 공유해보겠습니다.
'Design pattern' 카테고리의 다른 글
[Design Pattern with TypeScript] #1 Factory Method (0) | 2022.08.26 |
---|