Ch.4 팩토리 패턴

객체지향 빵 굽기: 느슨한 결합으로 객체지향 디자인을 만들어 봅시다

Problem

const duck: Duck = new MallardDuck()
  • 인터페이스를 써서 코드를 유연하게 만들고자 한다.

  • 그럼에도 구상 클래스의 인스턴스를 만들어야 한다.

게다가 일련의 구상 클래스가 있다면 이런 식의 코드를 만들어야 한다.

let duck: Duck
if (picnic) {
  duck = new MallardDuck()
} else if (hunting) {
  duck = new DecoyDuck()
} else if (inBathTub) {
  duck = new RubberDuck()
}

new는 문제가 없지만, '변화'하는 무언가 때문에 new를 조심해서 사용해야 한다.

About

객체마을의 최첨단 피자 가게를 운영하는 예제가 나온다. 팩토리 패턴을 이용해 개선해본다.

The Code

팩토리 패턴은 총 2개가 나온다.

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

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

Extraction

function orderPizza(type: string): Pizza {
  let pizza: Pizza

  if (type === 'cheese') {
    pizza = new CheesePizza()
  } else if (type === 'pepperoni') {
    pizza = new GreekPizza()
  } else if (type === 'pepperoni') {
    pizza = new PepperoniPizza()
  }

  pizza.prepare()
  pizza.bake()
  pizza.cut()
  pizza.box()
  return pizza
}
  • 피자를 추가할 때마다 코드를 변경해야 한다.

  • 객체 생성 코드를 빼내자. 이걸 팩토리라고 부른다.

class SimplePizzaFactory {
  createPizza(type: string): Pizza {
    let pizza: Pizza = null

    if (type === 'cheese') {
      pizza = new CheesePizza()
    } else if (type === 'pepperoni') {
      pizza = new GreekPizza()
    } else if (type === 'pepperoni') {
      pizza = new PepperoniPizza()
    }
    return pizza
  }
}
class PizzaStore {
  factory: SimplePizzaFactory

  constructor(factory: SimplePizzaFactory) {
    this.factory = factory
  }

  orderPizza(type: string): Pizza {
    let pizza = factory.createPizza(type)

    pizza.prepare()
    pizza.bake()
    pizza.cut()
    pizza.box()
    return pizza
  }
}

Simple Factory

  • 간단한 팩토리(Simple Factory)는 디자인 패턴이라기 보다는 관용구에 가깝다.

  • 이걸 팩토리 패턴이라고 부르는 사람도 있다. 하지만 정확히는 패턴은 아니다.

  • 일종의 워밍업이다. 진짜 팩토리 패턴 2가지가 더 있다.

Factory Method Pattern

  • 객체마을의 최첨단 피자 가게가 SNS에서 큰 인기를 끌게 되어 다른 마을에서도 최첨단 피자 가게가 있으면 좋겠다는 수요가 생겼다.

  • 각 지점마다 다양한 스타일의 피자(뉴욕 스타일, 시카고 스타일, 캘리포니아 스타일)을 만들어야 한다.

  • PizzaStore에서 파생된 NYPizzaFactory, ChicagoPizzaFactory 등이 있어야 한다.

abstract class PizzaStore {
  orderPizza(type: string): Pizza {
    let pizza = this.createPizza(type) // factory 대신 PizzaStore의 함수를 사용

    pizza.prepare()
    pizza.bake()
    pizza.cut()
    pizza.box()
    return pizza
  }

  protected abstract createPizza(type: string): Pizza // 추가
}
  • 이제 createPizza는 서브클래스가 구현한 createPizza 팩토리 메소드로 구현 작업이 위임된다.

  • orderPizza는 어떤 스타일의 피자가 만들어졌는지 알지 못하고 알 필요도 없다. 하지만 피자인 것은 알고 있으므로 피자를 준비하고, 굽고, 자르고, 포장하는 작업을 완료한다.

위와 같이 코드를 수정하고, PIzzaStore 추상 클래스를 구현하는 지점을 만들자.

피자의 종류는 어떤 서브클래스를 선택했느냐에 따라 결정된다.

class NYPizzaStore extends PizzaStore {
  protected createPizza(type: string): Pizza {
    let pizza: Pizza = null

    if (type === 'cheese') {
      pizza = new NYStyleCheesePizza()
    } else if (type === 'clam') {
      pizza = new NYStyleClamPizza()
    } else if (type === 'pepperoni') {
      pizza = new NYStylePepperoniPizza()
    }
    return pizza
  }
}

피자 스타일 서브클래스를 만들고, createPizza를 오버라이드하기만 하면 된다.

We've Just Used Factory Method Pattern

위에서 만든 것은 팩토리 메서드 패턴이다.

팩토리 메소드 패턴에서는 객체를 생성할 때 필요한 인터페이스를 만든다.

  • 어떤 클래스의 인스턴스를 만들지는 서브클래스에서 결정한다.

  • 팩토리 메소드 패턴을 사용하면 클래스 인스턴스 만드는 일을 서브클래스에게 맡기게 된다.

  • CreatorPizzaStore, ProductPizza라고 볼 수 있다.

  • 사용하는 서브클래스에 따라 생산되는 객체 인스턴스가 결정된다.

  • Simple Factory와 달리 여러 번 재사용이 가능한 프레임워크를 만들 수 있다.

팩토리 메소드와 생산자 클래스를 꼭 추상으로 선언할 필요는 없다.

매개변수에 type으로 string 타입을 썼는데, 매개변수 형식을 나타내는 객체를 만들거나, 정적 상수를 쓰거나, enum을 사용할 수 있다.

팩토리의 장점은 여러 개지만,

  • 객체 생성 코드를 한 객체 또는 메소드에 넣으면서 중복되는 내용을 제거하고 관리할 때도 한 군데에만 신경을 쓸 수 있다.

  • 객체 인스턴스를 만들 때 인터페이스만 있으면 된다.

  • 인터페이스를 바탕으로 프로그래밍할 수 있어 유연하고 확장성이 뛰어난 코드를 만들 수 있다.

팩토리 메소드 패턴은 상속으로 객체를 만든다.

DIP; Dependency Inversion Principle

PizzaStore와 각종 Pizza 구현체 사이에 Pizza라는 추상 클래스(인터페이스)를 둠으로써 구현에 의존하지 않게 되는, 의존성 역전 원칙(DIP)을 준수했다. (고수준과 저수준 모듈이 하나의 추상 클래스에 의존한다.)

  • 팩토리 메소드 패턴만이 이 원칙을 준수할 수 있는 것은 아니다.

의존성 역전 원칙에 위배되는 객체지향 디자인을 피하는 가이드라인이 있다.

  • 변수에 구현 클래스의 레퍼런스를 저장하지 말자.

  • 구현 클래스에서 유도된 클래스를 만들지 말자.

  • 베이스 클래스에 이미 구현되어 있는 메소드를 오버라이드하지 말자.

Abstract Factory Pattern

최근 정보에 따르면, 새로운 프레임워크가 도입된 이후로 지점에서 우리가 정한 절차는 잘 따르는데, 몇몇 지점에서 자잘한 재료를 더 싼 재료로 바꿔서 마진을 높이고 있다고 한다. 이런 일이 계속되면 브랜드에 타격이 올 수 있으니 조치를 취해야 한다. -> 원재료 품질을 관리하자.

  • 원재료군으로 묶는다. (시카고, 뉴욕, 캘리포니아) -> 원재료 팩토리를 만든다.

  • 지역별로 팩토리를 만든다.

  • 새로 만든 원재료 팩토리를 PizzaStore 코드에서 사용하도록 모든 것을 하나로 묶는다.

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

추상 팩토리 메소드는 상속으로 객체를 만들지만, 추상 팩토리 패턴은 객체 합성(composition)으로 객체를 만든다.

  • 클라이언트에서 추상 인터페이스로 일련의 제품을 공급받을 수 있다. 이때, 실제로 어떤 제품이 생산되는지는 전혀 알 필요가 없으므로, 클라이언트와 팩토리에서 생산되는 제품을 분리할 수 있다.

  • 추상 팩토리 패턴 뒤에 팩토리 메소드 패턴이 구현되는 경우도 있다.

Summary

공통점

  • 객체를 만드는 역할을 한다.

  • 클라이언트와 구상 형식을 분리하는 역할을 한다.

  • 객체 생성을 캡슐화해서 애플리케이션의 결합을 느슨하게 만들고, 특정 구현에 덜 의존하도록 만든다.

팩토리 메소드 패턴

  • 상속으로 객체를 만든다.

  • 클래스를 확장하고 팩토리 메소드를 오버라이드하면 된다.

  • 한 가지 제품만 생성한다. 메소드도 하나면 된다.

  • 단순하다. 어떤 구상 클래스가 필요할지 미리 알수 없을 때 유용하다.

추상 팩토리 패턴

  • 합성으로 객체를 만든다.

  • 제품군을 만드는 추상 형식을 제공한다. 제품이 생산되는 방법은 이 형식의 서브클래스에서 정의한다. 팩토리를 사용하고 싶으면 인스턴스를 만든 다음 추상 형식을 써서 만든 코드에 전달하면 된다.

  • 새로운 제품을 추가하려면 인터페이스를 바꿔야 한다. 모든 서브클래스의 인터페이스도 바뀌게 된다.

  • 다양한 제품군을 생성하기에 인터페이스가 크고 복잡한 인터페이스가 필요하다.

  • 서로 연관된 일련의 제품, 즉 제품군을 만들 때 사용한다.

My Thoughts

  • Simple Factory 외에 패턴이 2개나 나와서 꽤 긴 챕터였다.

  • 둘 다 탄생한 이유는 같지만 용도는 조금씩 다르다. 특정 테마가 있는 제품군을 만들 땐 추상 팩토리 패턴, 그 외에는 팩토리 메소드 패턴을 사용해도 될 것 같다.

Last updated