타입스크립트 디자인 패턴 -팩토리 패턴-

May 14, 2024

공장에서 찍어나오는 간편한 인스턴스

타입스크립트 디자인 패턴 -팩토리 패턴-

팩토리 패턴이란?

팩토리 패턴은 인스턴스의 생성에 대한 인터페이스를 부모 클래스에서 제공하면서 직접적인 생성은 서브 클래스에서 이루어지도록 작동하는 생성 디자인 패턴이다. 즉 인스턴스 생성에 대한 직접적인 책임을 팩토리 클래스를 통해서 생성되게 함으로써 객체 생성에 대한 결정을 인스턴스 코드로부터 분리시킬 수 있게된다.

팩토리 메서드 패턴 만들기

만약 자동차 판매를 위한 프로그램을 작성한다고 가정하자. 우리가 프로그래밍 할 자동차 판매 프로그램의 경우 CarTruck 총 두가지의 자동차들을 판매한다. 모든 자동차들은 각기 다른 색상(color)이 도색되어 있고, 신차인지 중고차인지 차의 상태(state)를 가지고 있다. Car의 경우는 문의 개수(door)가 차마다 다르며, Truck의 경우 각각의 트럭이 다른 크기의 휠(wheelSize)를 가진다.

팩토리 메서드의 생성 대상을 만들기

1interface VehicleOptions { 2 state?: 'brand new' | 'used'; 3 color?: string; 4} 5 6abstract class Vehicle { 7 public state: VehicleOptions['state']; 8 9 public color: string; 10 11 public constructor({ state = 'brand new', color = 'silver' }: VehicleOptions) { 12 this.state = state; 13 this.color = color; 14 } 15} 16
CarTruck의 공통사항을 자세히 파악하여 Car 인스턴스와 Truck 인스턴스의 추상 클래스인 Vehicle 클래스를 생성하자. Vehicle은 기본 값으로 statecolor를 인자로 받아 필드에 설정한다. 추상 클래스를 사용함으로써 서브 클래스의 멤버의 접근자를 설정하고 객체의 공통사항을 추상화 시킬 수 있다. 하지만 상속된 클래스 간 결합도가 높아지니 추상 클래스를 사용할 때에는 클래스간 관계를 정확하게 판단해야한다.
1interface CarOptions extends VehicleOptions { 2 door?: number; 3} 4 5class Car extends Vehicle { 6 public door: number; 7 8 public state: Vehicle['state']; 9 10 public constructor({ door = 4, color, state }: CarOptions) { 11 super({ state, color }); 12 this.door = door; 13 } 14} 15
door 필드를 소유한 서브 클래스 Car를 생성해준다.
1interface TruckOptions extends VehicleOptions { 2 wheelSize?: 'small' | 'medium' | 'large'; 3} 4 5class Truck extends Vehicle { 6 public wheelSize: TruckOptions['wheelSize']; 7 8 public constructor({ wheelSize = 'large', color, state }: TruckOptions) { 9 super({ color, state }); 10 this.wheelSize = wheelSize; 11 } 12} 13
wheelSize 필드를 소유한 서브 클래스 Car를 생성해준다. 두 클래스 모두 Vehicle 추상 클래스를 상속하고 있으므로 견고한 타입을 통해 안정성을 추구할 수 있다.

팩토리 클래스 생성하기

1interface CarCreateOption extends CarOptions { 2 vehicleType: 'car'; 3} 4 5interface TruckCreateOption extends TruckOptions { 6 vehicleType: 'truck'; 7} 8 9type AllVehicleOptions = CarCreateOption | TruckCreateOption; 10 11class VehicleFactory { 12 public create(options: AllVehicleOptions): Vehicle { 13 const { vehicleType } = options; 14 15 switch (vehicleType) { 16 case 'car': 17 return new Car(options); 18 case 'truck': 19 return new Truck(options); 20 } 21 } 22} 23
이제 팩토리 클래스를 생성할 차례이다. VehicleFactory를 통해 create 메서드에 생성될 서브클래스의 옵션을 주입하여 서브클래스를 생성한다.

사용 예시

1const vehicleFactory = new VehicleFactory(); 2 3vehicleFactory.create({ 4 vehicleType: 'car', 5}); 6// Output: Car { state: 'brand new', color: 'silver', door: 4 } 7 8vehicleFactory.create({ 9 vehicleType: 'truck', 10 color: 'black', 11}); 12// Output: Truck { state: 'brand new', color: 'black', wheelSize: 'large' } 13

팩토리 패턴이 아닐 경우

1const ModuleA = { 2 createVehicle(options: AllVehicleOptions) { 3 const { vehicleType } = options; 4 5 switch (vehicleType) { 6 case 'car': 7 return new Car(options); 8 case 'truck': 9 return new Truck(options); 10 } 11 }, 12}; 13 14const ModuleB = { 15 createVehicle(options: AllVehicleOptions) { 16 const { vehicleType } = options; 17 18 switch (vehicleType) { 19 case 'car': 20 return new Car(options); 21 case 'truck': 22 return new Truck(options); 23 } 24 }, 25}; 26 27const ModuleC = { 28 createVehicle(options: AllVehicleOptions) { 29 const { vehicleType } = options; 30 31 switch (vehicleType) { 32 case 'car': 33 return new Car(options); 34 case 'truck': 35 return new Truck(options); 36 } 37 }, 38}; 39 40// ...약 30개의 생성 로직이 포함된 모듈들 41
만약 팩토리 메서드 패턴이 아니라 인스턴스가 생성되는 순간마다 new를 사용하여 생성한다고 가정하자. 그 경우 상기 코드처럼 감 함수마다 생성하는 로직이 직접 적혀있을 것이다. 따라서 생성하는 조건에 따른 분기처리도 생성되는 순간마다 직접 구현되어있어야하며 이에 따라 의미없는 중복이 발생된다. 만약에 여기서 다음과 같은 요구사항이 추가된다.
???: 앞으로는 CarTruck 말고도 SUV도 추가될거에요!
1const ModuleA = { 2 createVehicle(options: AllVehicleOptions) { 3 const { vehicleType } = options; 4 5 switch (vehicleType) { 6 case 'car': 7 return new Car(options); 8 case 'truck': 9 return new Truck(options); 10 case 'suv': 11 return new Truck(options); 12 } 13 }, 14}; 15 16const ModuleB = { 17 createVehicle(options: AllVehicleOptions) { 18 const { vehicleType } = options; 19 20 switch (vehicleType) { 21 case 'car': 22 return new Car(options); 23 case 'truck': 24 return new Truck(options); 25 case 'suv': 26 return new Truck(options); 27 } 28 }, 29}; 30 31const ModuleC = { 32 createVehicle(options: AllVehicleOptions) { 33 const { vehicleType } = options; 34 35 switch (vehicleType) { 36 case 'car': 37 return new Car(options); 38 case 'truck': 39 return new Truck(options); 40 case 'suv': 41 return new Truck(options); 42 } 43 }, 44}; 45 46// ... 47
이 경우 우리는 SUV라는 인스턴스를 생성하기 위해 약 30개의 메서드를 수정해야한다.

하지만 팩토리 패턴이라면

1class VehicleFactory { 2 public create(options: AllVehicleOptions): Vehicle { 3 const { vehicleType } = options; 4 5 switch (vehicleType) { 6 case 'car': 7 return new Car(options); 8 case 'truck': 9 return new Truck(options); 10 case 'suv': 11 return new Truck(options); 12 } 13 } 14} 15 16const vehicleFactory = new VehicleFactory(); 17 18const ModuleA = { 19 createVehicle(options: AllVehicleOptions) { 20 vehicleFactory.create(options); 21 }, 22}; 23 24const ModuleB = { 25 createVehicle(options: AllVehicleOptions) { 26 vehicleFactory.create(options); 27 }, 28}; 29 30const ModuleC = { 31 createVehicle(options: AllVehicleOptions) { 32 vehicleFactory.create(options); 33 }, 34}; 35
하지만 생성 로직이 팩토리 클래스로 이관된다면 다르다. 생성에 대한 로직을 온전히 팩토리 클래스가 담당하게 되면서 생성에 관련된 수정사항이 발생할 경우 팩토리 메서드만을 수정함으로써 수정에 대한 파편 범위를 매우 줄일 수 있다. 자바스크립트라면 new 키워드를 안쓰면서 콜백으로 활용할 가능성 또한 열리는 추가적인 이점이 있다. 실제 코드

References