CS - 디자인패턴 (2) 팩토리 메서드

김영현·2024년 12월 3일
0

CS

목록 보기
32/32

물류 운송 프로그램

트럭을 이용한 간단한 물류배송 프로그램이 존재한다고 가정해보자.
운송수단은 트럭만 있으므로 Truck클래스 하나에 많은 기능이 들어가있다.

class Truck {
    ...
}

그러다 앱이 인기가 생겨서 선박회사에서도 기능을 추가해달라는 요청이 들어왔다.
이게 왠걸? Truck클래스에 많은 기능이 결합되어있어서 Ship클래스를 추가하려면, 코드베이스 전체를 변경해야한다.
만약 선박회사뿐만이 아니라 비행기 회사에서도 요청이 들어온다면, 또다시 큰 리소스를 지불하고 코드를 변경해야할 것이다.

어떻게 해결하면 좋을까?


팩토리 메서드 패턴

팩토리 메서드 패턴은 생성 패턴(인스턴스 생성)의 한 종류다.

객체를 직접 new연산자를 호출해 생성하는 것이 아니라, 팩토리 메서드에 대한 호출로 대체한다.

이러한 행위는 무의미해볼 수도 있다. 단순히 인스턴스 생성을 다른부분으로 옮겼으니! 그러나 이러한 변경덕분에 자식 클래스에서 팩토리 메서드를 오버라이딩하고 해당 메서드에 의해 생성되는 인스턴스들의 클래스를 변경할 수 있게 되었다.

참고로 팩토리 메서드를 사용하는 코드를 클라이언트 코드라 부르며, 클라이언트 코드는 자식 클래스에서 반환되는 인스턴스간 차이에 대해 알지못해도 된다.

type Vehicles = "Truck" | "Ship" | "Airplane";

interface Vehicle {
    type: Vehicles;
    drive(): void;
}

class Truck implements Vehicle {
    type:Vehicles = "Truck";
    drive(): void {
        console.log("트럭으로 배송갑니다!");
    }
}

class Ship implements Vehicle {
    type:Vehicles = "Ship";
    drive(): void {
        console.log("배타고 가는중~");
    }
}

class Airplane implements Vehicle {
    type:Vehicles = "Airplane";
    drive(): void {
        console.log("비행기타고 가는중!");
    }
}

//팩토리 함수
function createVehicle(type: Vehicles): Vehicle {
  const vehicleMap: { [key in Vehicles]: new () => Vehicle } = {
      Truck,
      Ship,
      Airplane
  };

  const VehicleClass = vehicleMap[type];
  
  if (!VehicleClass) {
      throw new Error(`Unknown vehicle type: ${type}`);
  }
  
  return new VehicleClass();
}

// 클라이언트 코드. 클라이언트 코드는 팩토리 메서드가 변경되든 말든, 운송수단을 가져와 배송메서드를 실행하기만 하면 된다.
export function deliver(type: Vehicles): void {
    const vehicle = createVehicle(type);
    console.log(`Delivering a ${vehicle.type}...`);
    vehicle.drive();
}

장황한 설명과 코드를 보면 어려울 수 도 있다. 그러나 핵심은 상속, 인터페이스에 의존하여 객체생성을 하위클래스에서 한다는 점이다.
다시말해 중복 코드를 제거하여 공통점을 부모 클래스에 모아놓고, 객체생성은 하위클래스에서 진행하는게 팩토리 메서드 패턴이라고도 볼수 있다.
이는 프로그래밍의 정수와 일치한다.

팩토리 메서드의 장단점

트레이드오프는 어느 기술에나 존재한다.

장점은 다음과 같다

  • 부모 클래스와 하위 클래스가 단단하게 결합되지 않는다.
  • 단일 책임 원칙을 지킬 수 있게 되었다. => 각 운송수단 클래스는 각 운송수단만 담당한다.
  • 개방/폐쇄 원칙을 지킬수 있게 되었다. => 클라이언트코드를 훼손하지 않고 팩토리 메서드 부분만 수정하면 된다!

단점은 패턴을 구현하기위해 많은 자식클래스가 생길 수 있다. 가장 좋은 방법은 부모 클래스들의 기존 계층구조에 패턴을 도입하는 것이라는데, 아직 와닿지는 않는다. 다음에 써먹을일이 생긴다면 써먹겠음!

번외) openapigenerator-cli

이전에 프로젝트를 진행할때 api client를 openApiGenerator-cli를 이용하여 자동화한 적이 있다.
이때 생성되는 api client 방식중 하나가 팩토리였다.

//Api client 요청 함수들이 모인 '객체'를 반환하는 함수
export const DefaultApiFp = function(configuration?: Configuration) {
    const localVarAxiosParamCreator = DefaultApiAxiosParamCreator(configuration)
    return {
        async createGameRoom(gameRoomCreateRequest: GameRoomCreateRequest, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ApiResponseGameRoomCreateResponse>> {
            const localVarAxiosArgs = await localVarAxiosParamCreator.createGameRoom(gameRoomCreateRequest, options);
            const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
            const localVarOperationServerBasePath = operationServerMap['DefaultApi.createGameRoom']?.[localVarOperationServerIndex]?.url;
            return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
        },
      //다른 api client 요청 함수들...
    }
      
//--------------------------팩토리 함수 부분----------------------------//     
export const DefaultApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {
    const localVarFp = DefaultApiFp(configuration)
    return {
        createGameRoom(gameRoomCreateRequest: GameRoomCreateRequest, options?: any): AxiosPromise<ApiResponseGameRoomCreateResponse> {
            return localVarFp.createGameRoom(gameRoomCreateRequest, options).then((request) => request(axios, basePath));
        },
      	//다른 함수들...
    },

코드가 좀 길지만, 핵심은 다음과 같다.
DefaultApiFp에서 비동기로 api 요청을 만들고, DefaultApiFactory에서 해당 요청을 처리하기위한 axios인스턴스와 basePath를 받아 DefaultApiFp에서 반환한 객체의 메서드를 활용한다.

당시에는 둘의 차이점을 모르고, 단순히 DefaultApiFactory에서 함수를 가져다 썼는데 지금보니 팩토리 메서드 패턴이 적용되었고 잘 가져다 쓴게 맞다는 걸 알수 있게되었다.


설명 출처 : https://refactoring.guru/ko/design-patterns/factory-method

profile
모르는 것을 모른다고 하기

0개의 댓글