Skip to content

Design Pattern - Factory

Published: at 오전 10:42

프로젝트의 복잡성이 올라감에 따라, 구조적인 문제를 해결하기 위해 디자인 패턴 공부를 공부하고, 제 생각을 정리합니다.

목차

펼치기

문제 상황

function main(type: Type) {
  let obj: Object;

  if (type === 'A') {
    obj = new ObjectA();
  } else if (type === 'B') {
    obj = new ObjectB();
  } else if (type === 'C') {
    obj = new ObjectC();
  }
  obj.method1();
  obj.method2();
  return obj;
}

여기서 새로운 타입이 추가되면, main 함수를 수정해야 한다. 점점 main문의 가독성은 떨어지고, ObjectA, ObjectB, ObjectC가 무슨일이 있을때마다, 신경을 써야하고 종국에는 유지보수가 힘든 코드베이스가 될것이다. 그렇기때문에 코드의 관심사에 따라 코드를 분리를 해야한다.

조건문 부분은 객체를 생성하는 코드로, 그리고 아래쪽 메소드를 사용하는 코드는 객체를 사용하는 코드로 분리할 수 있을 것 같다.

단순 팩토리

function objFactory(type: Type) {
  let obj: Object;

  if (type === 'A') {
    obj = new ObjectA();
  } else if (type === 'B') {
    obj = new ObjectB();
  } else if (type === 'C') {
    obj = new ObjectC();
  }

  return obj;
}

function otherUseCase(type: Type) {
  const obj: Object = objFactory(type); // <--
  obj.method3();
  ...
}

function main(type: Type) {
  const obj: Object = objFactory(type); // <--
  obj.method1();
  obj.method2();
  return obj;
}

이제 생성관련 변경은 objFactory에서만 변경하면되고, 해당 함수를 가져다가 여러곳에서 쓸 수 있을것 같다. 하지만, 소프트웨어는 항상 변하기 마련이다. 이번에는 만약 생성하는 객체의 스타일이 복잡하고 다양해 지면 어떻게 될까? 단순한 팩토리였지만 이제 팩토리도 복잡해질 것이다. 그때마다 기존 objFactory에 계속해서 수정해야 할까?


function objFactory(type: Type) {
  let obj: Object;

  if (type === 'A') {
    obj = new ObjectA();
  } else if (type === 'B') {
    obj = new ObjectB();
  } else if (type === 'C') {
    obj = new ObjectC();
  } else if (type === 'D') {
    ...
  } else if (type === 'FancyA') {
    ...
  } else if (type === 'FancyB') {
    ...
  }... // 300 줄 코드

  return obj;
}

팩토리 메소드

interface Object {}
interface SimpleObject extends Object {}
interface FancyObject extends OBject {}

abstract class Factory {
  abstract createObj(type: Type): Object; // <--

  useObj(type: Type) {
    const obj: Object = this.createObj(type); // <-- 생성은 서브클래스에 맡긴다.
    obj.method1();
    obj.method2();
  }

  otherUseCase(type: Type) {
    const obj: Object = this.createObj(type);  // <-- 생성은 서브클래스에 맡긴다.
    obj.method3();
  }
}

class SimpleFactory {
  public createObj(type: Type): SimpleObject {
    let obj: SimpleObject;

    if (type === 'A') {
      obj = new ObjectA();
    } else if (type === 'B') {
      ...
    }
    ...
    return obj;
  }
}

class FancyFactory {
  public createObj(type: Type): FancyObject {
    let obj: FancyObject;

    if (type === 'A') {
      obj = new FancyObjectA();
    } else if (type === 'B') {
      ...
    }
    ...
    return obj;
  }
}

function otherUseCase(type: Type) {
  const fac: Factory = new FancyFactory(); // or new SimpleFactory();
  fac.otherUseCase(type);
  ...
}

function main(type: Type) {
  const fac: Factory = new SimpleFactory(); // or new FancyFactory();
  fac.useObj(type);
  ...
}

이제 새로운 Object 타입군이 추가된다 해도 Factory를 상속하는 서브클래스를 추가만 하면 쉽게 확장할 수 있다. useCase의 로직 안에 createObj메소드가 사용된다는 점에서 템플릿 메서드와 비슷하다고 생각할 수도 있겠지만, 팩토리 메소드는 보다 특수한 경우라고 볼 수 있다.

추상 팩토리

여기서 다 나아가서 Object뿐 아니라 Abject, Bbject 같은 새로운 객체의 생성이 필요한 경우라면 어떻게 될까? 이제 팩토리에 createObj 말고 새로운 create<something> 메소드들을 추가하는 식으로 관련 생성 로직을 추상화 할 수 있다. 이렇게 하면 하나의 팩토리에서 관련있는 여러 클래스 인스턴스들을 생성할 수 있게된다.

abstract class Factory {
  abstract createObj(): Object;
  abstract createBbj(): BObject;
  abstract createAbj(): AObject;
  abstract createCbj(): CObject;
  ...
}

function main(type: Type) {
  const fac: Factory = new SimpleFactory();

  const obj = fac.createObj();
  const abj = fac.createAbj();
  ...
  ...
}

생각

프론트엔드 개발자 입장에서 생각했을때, 메소드 팩토리는 각 서브클래스는 각각의 컴포넌트들이고 추상 클래스(Factory)는 커스텀 훅의 역할을 하는 것 같다는 생각이 들게 한다. 상속이라는 것도 사실 context나 커스텀훅을 사용하는 것과 비슷하다는 생각을 했다. 오히려 상속보다 구성방식에 가까워서 훨씬 유연해 보이기도 한다.

평소에 프론트엔드 개발자의 입장에서 클래스를 어떻게 활용 할 수 있을까 고민을 했었는데, 클래스가 아닌 방법으로 이미 객체지향의 원칙을 지키며 개발하고 있었을 것 같다는 생각을 하게됐다. 그리고 view(dom)가 tree 구조로 되어 있다는 점에서, 클래스의 상속구조와 비슷하다는 생각도 들게한다. 때문에 view(dom)와 연관된 로직보단 순수한 로직쪽에서 클래스의 활용도가 높을 것 같다는 생각이 들었다. 그래서 canvas를 사용할때 많이 쓰는건가.

디자인 패턴을 공부하면서, 패턴 자체를 공부한다기보다, 어떤 케이스에서 코드를 어떻게 나누는게 좋을지, 어떤 관심사로 나눌 수 있을지에 대한 공부에 집중하면 좋을 것 같다는 생각을 하게 됐다.