Banner Made by Banner Maker

소프트웨어 디자인 패턴 중 팩토리 메서드 패턴(Factory Method Pattern)에 대해서 알아보겠습니다.

Factory?

먼저 이 패턴에 팩토리(Factory) 라는 이름이 붙은 이유는 공장에서 상품을 생산하듯이, 팩토리 메서드로 비슷한 객체를 찍어내는 동작을 하기 때문입니다.
image.png

Template Method Pattern & Factory Method Pattern

잠시 Java와 같은 객체 지향 언어 관점에서 살펴보겠습니다.

팩토리 패턴 (Factory Pattern)이 아닌 팩토리 메서드 패턴(Factory Method Pattern) 이라고 하는 이유는 이 패턴이 템플릿 메서드 패턴(Template Method Pattern)에서 파생한 것이기 때문입니다.

템플릿 메서드 패턴이란, 상속 관계에 있는 두 클래스에서 상위 클래스가 중요한 뼈대를 결정하고, 하위 클래스에서는 추상 메서드를 통해 구체적인 내용을 결정하도록 하는 디자인 패턴입니다.
image.png

이런 구조로 만들게 되면 상위 클래스에서 로직을 공통화할 수 있다는 장점이 있습니다. 이러한 템플릿 메소드 패턴을 인스턴스 생성에 응용한 것이 팩토리 메서드 패턴입니다.

하지만 JavaScript는 프로타입 기반 언어로, 원래는 Java나 C++ 에서 사용하는 클래스라는 개념이 없습니다. 대신 프로토타입을 이용해 상속을 구현하거나, 혹은 new 키워드와 함수를 이용해 클래스처럼 동작하도록 흉내낼 수 있습니다.
(c.f. ES6에서 추가된 class는 문법적 설탕일 뿐 내부적으로는 ES5 이하에서 prototype을 이용해 구현한 것과 같습니다.)

function Person(name) {
  this.name = name
  this.introduce = function() {
    return 'My name is' + this.name
  }
}

// name 프로퍼티와 introduce 메소드를 미리 초기화한 객체를 리턴합니다.
var user1 = new Person('godori')
var user2 = new Person('irodog')

user1.introduce() // My name is godori
user2.introduce() // My name is irodog

이와 같이 Person이라는 개별 객체를 new 키워드로 생성할 수 있습니다.

JavaScript Factory Function

저는 new 키워드로 객체를 생성하지 않고 ES6의 Object.assign()을 사용해서 팩토리 함수를 구현해 보겠습니다. 이 함수는 여러 객체의 속성을 복사해서 하나로 합칠 수 있습니다.

예를 들어, 다음 코드에서 target에 source의 속성을 복사한 returnedTarget은 target과 같은 속성들을 가집니다.

const target = { a: 1, b: 2 };
const source = { b: 4, c: 5 };
const returnedTarget = Object.assign(target, source);
console.log(target);         // { a: 1, b: 4, c: 5 }
console.log(returnedTarget); // { a: 1, b: 4, c: 5 }

이를 활용해 target 대신 빈 객체 {} 를 넣고 여러 개의 source 객체를 인자로 넣으면, 빈 객체에 다른 객체들이 합쳐진 새로운 객체를 얻을 수 있습니다.

이는 연결형 상속 이라고도 하는데, 객체 속성을 복사함으로서 상속을 구현할 수 있습니다.
다음의 엔지니어를 만드는 예시를 보면 더 이해하기 쉽습니다.

const jsSkill = {
  knowJS() {
    return true
  }
}

const javaSkill = {
  knowJava() {
    return true
  }
}

const isWorking = {
  isWorking() {
    return true
  }
}

const frontendEngineer = Object.assign({}, jsSkill, isWorking)
const backendEngineer = Object.assign({}, javaSkill, isWorking)
const fullStackEngineer = Object.assign({}, jsSkill, javaSkill, isWorking)

이제 이 방법을 팩토리 함수에서 사용해 봅시다.

Hello, Animal Factory! 🐰 🐯 🐻

먼저 Animal 이라는 팩토리 함수를 정의합니다. 이 함수는 내부에서 빈 객체를 만들고 name 프로퍼티와 walk라는 함수를 갖도록 초기화하여 반환합니다.

const Animal = function(name) {
  const animal = {}
  animal.name = name
  animal.walk = function() {
    console.log(this.name + ' walks');
  }
  return animal
}

const rabbit = Animal('rabbit');

이 Animal 팩토리로 만들어진 동물에게 특정한 기능을 주고 싶습니다. canKill 이라는 객체를 만들고 Object.assign으로 복사하면 됩니다.

const canKill = {
  kill() {
    console.log('I will kill you!')
  }
}

const tiger = Object.assign(Animal('tiger'), canKill)

Animal 팩토리로 만들어진 객체에게 통해 kill() 능력을 부여한 tiger 객체를 생성했습니다!

하지만 assign을 객체 생성시마다 일일이 적용해주는 건 불-편합니다. 따라서 먼저 만들었던 Animal 팩토리에서 베이스가 되는 객체를 생성하고, Object.assign을 통해 kill 능력을 가진 동물을 생산할 수 있는 KillingAnimal 이라는 팩토리 함수를 만들었습니다.

const KillingAnimal = function(name) {
  return Object.assign(Animal(name), canKill)
}

const bear = KillingAnimal('bear')

같은 방법으로 flyswim 등의 다양한 능력을 가진 동물 공장을 만들 수 있을 것입니다.

하는 김에 사람 공장도 만들어 보았습니다.

const HumanBorn = function(obj) {
  let isWalking = false
  return Object.assign({}, obj, {
    walk() {
      isWalking = true
    },
    isWalking() {
      return isWalking
    }
  })
}

const StarkIndustry = function(obj) {
  let isFlying = false
  return Object.assign({}, obj, {
    fly() {
      isFlying = true
      return this
    },
    isFlying() {
      return isFlying
    }
  })
}

const tony = HumanBorn()
const ironman = StarkIndustry(tony)

const jarvis = StarkIndustry()
const vision = HumanBorn(jarvis)

흠... 빨리 가망이 없는 최종편이 보고 싶어요.

* * *

팩토리 메서드 패턴을 공부하기 위해 java로 디자인 패턴을 설명한 책을 본 다음 JavaScript로 팩토리 메소드 패턴을 이해하려다 보니 많은 혼란이 왔습니다. 나름대로 여러 자료를 참고해서 정리해 보았는데, 혹시나 잘못된 내용은 피드백 해주시면 감사하겠습니다.

Ref