본문 바로가기

Backend/디자인 패턴

Factory Pattern (팩토리 패턴)

팩토리 패턴 예시부터 보겠습니다.

 

const createImage = (name) => {
  if (name.match(/\.jpeg$/)) {
    return new Jpeg(name);
  } else if (name.match(/\.gif$/)) {
    return new Gif(name);
  } else if (name.match(/\.png$/)) {
    return new Png(name);
  } else {
    throw new Error('Not supported format');
  }
};

 

누군가가 보기엔 이건 팩토리 패턴이 아니라 그냥 전략 패턴이 아니냐 라고 할 수 있긴 합니다만..

팩토리 패턴은 이름 그대로 객체를 생성하는 공장으로 받아들이면 심플하며, 객체를 생성하는 책임을 다른 무언가로 분리하는 것입니다.

분리함으로써 더 많은 유연성과 제어력을 제공합니다.

 

변경될 가능성이 높은 생성자를 그대로 두기보단 객체 생성 담당을 분리함으로써 객체간의 결합도를 낮추고, 또한 생성된 객체의 생성자를 노출시키지 않아 객체 확장 또는 수정을 못하도록 합니다. (객체지향 디자인 패턴 기본 원칙이 수정엔 닫혀있어도 확장엔 열려있어야 한다고 하긴 합니다만..)

 

이제부터 볼 특성들은 딱히 다른 언어들에는 해당 사항이 없는, JS 에서의 장점일 수 있습니다.

 

- 캡슐화

 

클로저가 있기 때문에 팩토리는 캡슐화 메커니즘으로도 사용할 수 있습니다.

 

const createPizza = (size) => {
  const properties = {};

  const pizza = {
    setSize: size => {
      if (!size) throw new Error('must select size');
      properties.size = size;
    },
    getSize: () => {
      return properties.size;
    },
  };

  pizza.setSize(size);
  return pizza;
};

const pizza = createPizza('Large');
console.info(pizza.getSize()); // 'Large'
console.info(pizza.size); // undefined

 

사실 대부분 TS 를 쓰기 때문에 이럴 일은 잘 없지만.. 접근 제어자가 없는 JS 에서는 이런식으로 클로저를 활용해 size 프로퍼티에 바로 접근할 수 없도록 할 수 있습니다. (ES2019 에선 # 접근 제어자가 JS 에서도 생겼습니다)

뭐 이건 딱히 팩토리의 장점이라고 보긴 애매..하네요.

 

- 팩토리를 통한 모킹 객체 반환

 

class Debugger {
  start() {
    //
  }
  end() {
    //
  }
}

module.exports = () => {
  switch (process.env.NODE_ENV) {
    case 'development':
      return new Debugger();
    case 'production':
      return {
        start: () => {},
        end: () => {},
      };
    default:
      throw new Error('set ENV');
  }
};

 

dev 환경에서만 디버깅을 수행하는 Debugger 객체가 있다고 가정해보겠습니다.

위처럼 팩토리를 만들어 dev 환경일 땐 Debugger 객체를, prod 환경일 땐 동일한 인터페이스를 가진 모킹 객체를 반환하도록 만들 수 있습니다. 이것은 JS 라서 한쪽에선 new 연산자를 통한 객체를, 다른 한쪽에선 객체 리터럴을 반환할 수 있는 장점이 결합된 예시입니다.

(이건 덕 타이핑이라고 합니다)

 

- 추상 팩토리 패턴

 

기왕 팩토리 패턴을 본 김에 자주 보이는 추상 팩토리 패턴에 대해 알아보겠습니다. (실제론 그닥 쓸일은 없었습니다만..)

팩토리 패턴은 객체 생성을 팩토리에서 담당하도록 분리했다면, 추상 팩토리 패턴은 관련있는 객체들을 묶어 팩토리 클래스로 만들고 이 팩토리를 조건에 따라 만들도록 하는 팩토리를 만드는 패턴입니다.

 

interface Motor {
  //
}

class GenesisMotor implements Motor {
  //
}

class KiaMotor implements Motor {
  //
}

class MotorFactory {
  public createMotor(type: string): Motor | null {
    switch (type) {
      case 'Genesis':
        return new GenesisMotor();
      case 'Kia':
        return new KiaMotor();
      default:
        return null;
    }
  }
}

interface CarBody {
  //
}

class GenesisBody implements CarBody {
  //
}

class KiaBody implements CarBody {
  //
}

class CarBodyFactory {
  public createCarBody(type: string): CarBody | null {
    switch (type) {
      case 'Genesis':
        return new GenesisBody();
      case 'Kia':
        return new KiaBody();
      default:
        return null;
    }
  }
}

class CarFactory {
  public createCar(type: string): void {
    const motorFactory = new MotorFactory();
    const bodyFactory = new CarBodyFactory();

    motorFactory.createMotor(type);
    bodyFactory.createCarBody(type);
    ...
  }
}

const createCar = () => {
  const carFactory = new CarFactory();
  carFactory.createCar('Genesis');
};

 

제 편의를 위해 갑자기 TS 로 왔습니다. 위는 기존 팩토리 패턴을 사용해 차를 생성하는 부분을 코드화 해본 것입니다.

제네시스, 기아 모터를 생성하는 모터 팩토리와 제네시스, 기아 차 바디를 생성하는 바디 팩토리에 이를 또 생성하는 차 팩토리까지 생성하면서 파츠가 어차피 전부 제네시스 혹은 기아일텐데 좀 불필요한 부분이 많아 보입니다.(실제론 여러 제조사를 쓸 수 있지만 예시입니다..)

파츠가 늘어날수록 객체 생성 부분 또한 상당히 늘어나겠죠. 이런 부분을 동일한 제조사로 묶어 같은 제조사 파츠로 구성되도록 해보죠.

 

interface Motor {
  //
}

class GenesisMotor implements Motor {
  //
}

class KiaMotor implements Motor {
  //
}

interface CarBody {
  //
}

class GenesisBody implements CarBody {
  //
}

class KiaBody implements CarBody {
  //
}

interface CarFactory {
  createMotor(): void;
  createCarBody(): void;
}

class GenesisFactory implements CarFactory {
  createMotor(): GenesisMotor {
    return new GenesisMotor();
  }
  createCarBody(): GenesisBody {
    return new GenesisBody();
  }
}

class KiaFactory implements CarFactory {
  createMotor(): KiaMotor {
    return new KiaMotor();
  }
  createCarBody(): KiaBody {
    return new KiaBody();
  }
}

class FactoryOfCarFactory {
  public createCar(type: string): void {
    let factory: CarFactory;
    switch (type) {
      case 'Genesis':
        factory = new GenesisFactory();
        break;
      case 'Kia':
        factory = new KiaFactory();
        break;
      default:
        throw new Error('set Type');
    }

    factory.createCarBody();
    factory.createMotor();
  }
}

const createCar = () => {
  const factoryOfCarFactory = new FactoryOfCarFactory();
  factoryOfCarFactory.createCar('Genesis');
};

 

중간의 MotorFactory, CarBodyFactory 와 같은 무의미한 팩토리가 사라지고 각 제조사별 팩토리를 만들어 제네시스면 제네시스 모터에 제네시스 바디를 만들게 되었습니다. 어떻게 보면 뭐가 좋아진건가 싶기도 하지만 (실제론 차라는게 이렇게 만들어지진 않으니 예시를 잘못 든 것 같기도 하네요) 추상 팩토리 패턴은 관련 있는 객체들을 구체적인 클래스에 의존하지 않고 만들 수 있도록 초점이 기존 팩토리 패턴과 다르다는 점을 주의깊게 보시면 될 것 같습니다.

이런 케이스에 대해선 추상화를 지향할 수 있지만, 결국 뭔가를 추가하려면 일이 커지는 것은 똑같거나 혹은 일이 더 커질 수 있다는 단점이 있습니다.