본문 바로가기
소프트웨어/디자인 패턴

[5] 팩토리 메소드 패턴

by Riverandeye 2020. 10. 29.

팩토리 메서드 패턴을 이용하여 불필요한 의존성을 없애는 방법에 대해 알아봅시다. 

 

디자인 원칙중 하나가, 구현이 아닌 인터페이스를 바탕으로 개발하라는 것이였는데요

new를 이용해서 객체를 생성하는 것을 돌아보면

결국 특정 구현을 바탕으로 프로그래밍 하는 것이 됩니다.

 

어떤 객체를 생성할 때 조건이 붙게 되는 경우가 있을 것입니다. 

if(picnic){
	duck = new MallardDuck();
}
else if(hunting){
	duck = new DecoyDuck();
}

위와 같이 특정 조건에 대해 서로 다른 구현체를 인터페이스에 대입하려면 저런 식의 조건문이 필요한데요

이런 코드가 있다는 건 결국 변경 혹은 확장할 때 동일한 코드를 다시 확인하고 추가/제거 해야한다는 뜻이 됩니다. 

 

코드에서 구상 클래스를 많이 사용하게 되면, 새로운 구상 클래스가 추가 될 때 마다

코드를 고쳐야 합니다. 그럼 어떻게 해야 수정에 대해 닫혀있는 코드를 만들 수 있을까요?

결국 그런 부분들은 변경되는 부분이니까, 그 부분을 분리시키면 될 것 같네요!

 

인스턴스를 만드는 부분을 전부 분리 / 캡슐화 시켜, 객체를 생성하는 공장 (팩토리) 을 정의하여

해당 팩토리에서 개별 인스턴스를 구성하는 로직을 담게 되면

특정 타입의 서브 클래스들을 생성하는 로직을 팩토리로 위임할 수 있게 됩니다. 

 

문제 상황을 보면서 어떤식으로 팩토리가 도움을 주는지에 대해 이해해 봅시다. 

 

문제 상황

피자 가게가 여러가지 피자를 만드는 상황입니다. 

하나의 가게에서 피자를 만들기 위해, 입력에 따라 리턴하는 피자를 분기하여 만드는데

그 분기하는 로직을 팩토리라는 클래스를 만들어서 역할을 위임할 수 있습니다. 

 

export default class PizzaFactory {
  public createPizza(type: PizzaType): Pizza {
    switch (type) {
      case PizzaType.pepperoni:
        return new PepperoniPizza();

      case PizzaType.potato:
        return new PotatoPizza();
    }
  }
}

간단한 팩토리 예시입니다. 

개별 객체를 생성하는 것을 "팩토리"라는 객체에 그 역할을 위임합니다.

아 그러면, 이제 이 팩토리를 Store에 주입해서 createPizza 메소드에서 Factory의 createPizza를 호출함으로써

그 역할을 대신할 수 있게 됩니다. 

 

import { PizzaType } from "../pizza";
import PizzaFactory from "../pizza/factory";

export default class PizzaStore {
  public pizzaFactory: PizzaFactory;

  public orderPizza(pizzaType: PizzaType) {
    return this.pizzaFactory.createPizza(pizzaType);
  }
}

이런 식으로요

그러면 PizzaFactory의 종류에 따라 Store는 서로 다른 방식으로 피자를 생산할 수 있게 됩니다. 

 

갑자기 피자가게가 성공해서 여러 피자 Store가 생겼습니다.

서로 다른 지역의 스타일을 반영하기 위해, 지역별로 서로 다른 팩토리가 생성되었습니다. 

그래서 SeoulPizzaFactory, BuasnPizzaFactory 가 생겼고

BusanPizzaFactory는 지역 특산물인 돼지국밥을 이용하여 PigMeatPizza를 생성합니다. 

 

문제는 여기서 발생합니다. 

지역별로 공통된 팩토리를 주입 받는 것 만으로는 부족하게 되었습니다.

실제로는 각 매장별로 색다른 피자들을 추가해야 되는 상황이 발생했습니다. 

 

그럴 땐 어떻게 해야 할까요? 개별 PizzaFactory 에서 해야 할 일들을 

각 PizzaStore의 서브클래스의 특정 메소드에서 수행하게 하면 될 것 같습니다.

 

그럼 그런 팩토리 메소드를 하위 클래스에서 구현하게끔 규정하는 abstract 메소드를 정하고

이를 base class의 메소드에서 가져다가 사용하는 방식으로 구성해봅시다. 

 

PizzaStore Class

import Pizza from "../pizza/abstract";

export default abstract class PizzaStore {
  public 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;
}

공통된 orderPizza 메소드는 그대로 사용하고

개별 하위 클래스에서 createPizza 메소드를 직접 구현하게끔 합니다. 

 

import Pizza from "../pizza/abstract";
import PizzaStore from "./abstract";
import SeoulChonnomPizza from "../pizza/seoul-chonnom-pizza";

export default class SeoulPizzaStore extends PizzaStore {
  public constructor() {
    super();
  }

  public createPizza(type: string): Pizza {
    switch (type) {
      case "SeoulChonnomPizza":
        return new SeoulChonnomPizza();
    }
  }
}

그리고 하위 클래스에선 해당 클래스에 적합한 createPizza 메소드를 구현해줍니다. 

이렇게 함으로써, 공통으로 사용되는 orderPizza 메소드에서 호출되는 createPizza를

개별 하위 클래스에서 구현하므로

해당 Store에 상황에 맞는 유연한 코드를 작성할 수 있게 됩니다. 

 

여기서 잘 살펴보면, 피자를 생산하는 클래스와, 실제 피자 클래스가 둘로 나뉘게 됩니다.

둘 다 추상 클래스로부터 시작해서 해당 클래스를 확장합니다.

 

팩토리 메서드 패턴

팩토리 메서드 패턴에서는

객체를 생성하기 위한 인터페이스를 정의하는데,

어떤 클래스의 인스턴스를 만들지에 대해서는 서브클래스에서 정의하게 됩니다.

클래스의 인스턴스를 만드는 일을 서브클래스에게 맡기게 됩니다.

factory Method Pattern (출처 : https://www.hanbit.co.kr/store/books/look.php?p_code=B9860513241)

위 클래스 다이어그램에서 실제로 객체를 동적으로 생성해주는 역할은

실제로 구현된 ConcreteCreater 의 factoryMethod가 그 역할을 합니다. 

 

Product와 Creator 간의 느슨한 결합을 만들어냈다고 생각하면 좋을 것 같습니다. 

구상 클래스에 대한 의존성을 줄이는 데 큰 도움이 되었습니다. 

이러한 원칙을 "Dependency Inversion Principle" 이라고 합니다. 

개별 구상 클래스가 아닌 상위의 추상화된 것에 의존할 수 있게 되었습니다. 

 

구상 클래스에 의존하지 않고 추상화된 것에 의존한다는 것은

PizzaStore가 BusanGukbabPizza 와 같은 실제 구현된 클래스에 의존하는게 아니라

Pizza 라는 abstract class 혹은 interface 와 같은 상위 개념에 의존한다는 것을 의미합니다.

 

그림으로 나타나면 이와 같습니다. 

 

구상 클래스에 의존적인 코드 (출처 : https://www.hanbit.co.kr/store/books/look.php?p_code=B9860513241)

PizzaStore가 개별 구상 클래스에 의존하는 상황을

 

인터페이스, 추상화에 의존하는 코드 (출처 : https://www.hanbit.co.kr/store/books/look.php?p_code=B9860513241)

다음과 같이 Pizza라는 하나의 인터페이스, 추상화된 클래스에 의존하는 경우로 바꾼 케이스 입니다. 

 

이와 같이 팩토리 메서드 패턴을 이용하면

고수준의 PizzaStore과 저수준의 Concrete Pizza 모두 추상 클래스인 Pizza에 의존하게 됩니다. 

 

다음 시간에는, 추상 팩토리 패턴에 대해서 알아보겠습니다. 

'소프트웨어 > 디자인 패턴' 카테고리의 다른 글

[6] 싱글턴 패턴  (0) 2020.11.05
[4] Decorator Pattern  (0) 2020.10.28
[3] Strategy Pattern  (2) 2020.10.25
[2] Observer Pattern  (0) 2020.10.22
[1] Reactor Pattern  (4) 2020.10.19

댓글