C++

디자인 패턴 1 - 팩토리 메서드 패턴(C++)

workbench34 2025. 1. 10. 20:05

 디자인 패턴 중 팩토리 메서드 패턴생성 패턴에 속한다. 팩토리 메서드는 부모 클래스에서 객체들을 생성할 수 있는 인터페이스를 제공하지만, 자식 클래스들이 생성될 객체들의 유형을 변경할 수 있도록 하는 생성 패턴이다.

 

 예시를 들어보자. 내가 물류 관리 앱을 개발하였다 가정하겠다. 앱의 첫 번째 버전은 트럭 운송만을 처리할 수 있어 대부분의 코드가 Truck(트럭) 클래스에 있다. 얼마 후  나의 앱이 유명해져 이번엔 해상 물류 회사로부터 선박 운송을 처리할 수 있는 기능을 앱에 추가해 달라고 한다. 좋은 일이지만 현재 대부분의 코드는 Truck 클래스에 결합되어 있어 Ship(선박) 클래스를 추가하려면 전체 코드 베이스를 변경해야 하는 상황에 처할 것이다. 결국 많은 조건문이 운송 수단 객체들의 클래스에 따라 앱의 행동을 바꾸는 매우 복잡한 코드가 작성되게 될 것이다.

 

 이 상황에 처하지 않으려면 어떻게 해야할까? 이때 사용하는것이 팩토리 메서드 패턴이다. 팩토리 메서드 패턴은 new 연산자를 사용하여 객체 생성 직접 호출들을 특별한 팩토리 메서드에 대한 호출들로 대체하라고 제안한다. 이 뜻이 무엇인지 아직 이해가 안갈 것 이다. 아래에 약간의 예시를 들어 설명해 주겠다.

 

기존 방식:

C++

// 직접 객체 생성
Product* product = new ConcreteProductA();

위 코드처럼, 우리가 원하는 객체를 생성하기 위해서 new 연산자를 직접 사용하여 메모리를 할당하고 객체를 생성하는 것이 일반적인 방법이다. 그러나 만약 product에 대해 새로운 객체를 생성하려면 또 new ConcreteProductA를 사용하여 생성해야한다. 그렇게 되면 코드를 수정해야하는 부분이 많아질 가능성이 높다. (ConcreteProductA의 이름을 바꾼다던가 또는 ConcreteProductA가 아닌 ConcreteProductB를 사용해야하는 순간이 온다던가 이런 여러가지 요소를 의미한다.)

 

팩토리 메서드 패턴을 사용하는 방식:

 

C++

// 팩토리 메서드를 사용한 객체 생성
Creator* creator = new ConcreteCreatorA();
Product* product = creator->factoryMethod();

그러나 이렇게 팩터리 메서드 패턴에서는 객체 생성을 담당하는 별도의 매서드(factoryMethod)를 제공한다. 이 메서드를 호출하면 원하는 종류의 객체를 생성할 수 있다. 이렇게 되면 만약 product의 객체를 바꾸어야할때 creator만 수정해 준다면 다른 product1, product2 등을 수정하지 않고도 모두 객체를 바꿀 수 있게 된다. 즉, 팩토리 메서드 패턴은 객체 생성을 추상화하여 코드의 유연성을 높이고, 테스트하기 쉽게 만들며, 시스템의 결합도를 낮추는 효과를 제공한다.

 

짤막 상식으로 팩터리 메서드에서 반환된 객체는 종종 제품이라고 부르기에 product를 예시로 넣었다.

 

전체적인 구조를 보자.

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

 

다음은 예시 코드 전문이다.

/**
 * Product 인터페이스는 모든 구체적인 product들이 구현해야 하는 연산들을 선언합니다.
 */
class Product {
 public:
  virtual ~Product() {}
  virtual std::string Operation() const = 0;
};

/**
 * Concrete Products는 Product 인터페이스의 다양한 구현을 제공합니다.
 */
class ConcreteProduct1 : public Product {
 public:
  std::string Operation() const override {
    return "{Result of the ConcreteProduct1}";
  }
};

class ConcreteProduct2 : public Product {
 public:
  std::string Operation() const override {
    return "{Result of the ConcreteProduct2}";
  }
};

/**
 * Creator 클래스는 Product 클래스의 객체를 반환해야 하는 factory method를 선언합니다. 
 * Creator의 서브클래스들은 보통 이 메서드의 구현을 제공합니다.
 */
class Creator {
  /**
   * Creator는 factory method의 기본 구현을 제공할 수도 있습니다.
   */
 public:
  virtual ~Creator(){};
  virtual Product* FactoryMethod() const = 0;
  /**
   * 이름과는 달리, Creator의 주요 책임은 product를 생성하는 것이 아닙니다. 
   * 보통, Creator는 factory method에 의해 반환된 Product 객체에 의존하는 핵심 비즈니스 로직을 포함합니다. 
   * 서브클래스들은 factory method를 오버라이딩하고 다른 유형의 product를 반환함으로써 간접적으로 이 비즈니스 로직을 변경할 수 있습니다.
   */
  std::string SomeOperation() const {
    // factory method를 호출하여 Product 객체를 생성합니다.
    Product* product = this->FactoryMethod();
    // 이제, product를 사용합니다.
    std::string result = "Creator: The same creator's code has just worked with " + product->Operation();
    delete product;
    return result;
  }
};

/**
 * Concrete Creators는 결과 product의 유형을 변경하기 위해 factory method를 오버라이딩합니다.
 */
class ConcreteCreator1 : public Creator {
  /**
   * 메서드의 시그니처는 여전히 추상 product 유형을 사용합니다. 
   * 비록 구체적인 product가 실제로 메서드에서 반환되지만, 
   * 이렇게 하면 Creator는 구체적인 product 클래스와 독립적으로 유지될 수 있습니다.
   */
 public:
  Product* FactoryMethod() const override {
    return new ConcreteProduct1();
  }
};

class ConcreteCreator2 : public Creator {
 public:
  Product* FactoryMethod() const override {
    return new ConcreteProduct2();
  }
};

/**
 * 클라이언트 코드는 기본 인터페이스를 통해 구체적인 creator의 인스턴스와 함께 작동합니다. 
 * 클라이언트가 기본 인터페이스를 통해 creator와 계속해서 작업하는 한, 
 * 어떤 creator의 서브클래스든 전달할 수 있습니다.
 */
void ClientCode(const Creator& creator) {
  // ...
  std::cout << "Client: I'm not aware of the creator's class, but it still works.\n"
            << creator.SomeOperation() << std::endl;
  // ...
}

/**
 * Application은 구성 또는 환경에 따라 creator의 유형을 선택합니다.
 */
int main() {
  std::cout << "App: Launched with the ConcreteCreator1.\n";
  Creator* creator = new ConcreteCreator1();
  ClientCode(*creator);
  std::cout << std::endl;
  std::cout << "App: Launched with the ConcreteCreator2.\n";
  Creator* creator2 = new ConcreteCreator2();
  ClientCode(*creator2);

  delete creator;
  delete creator2;
  return 0;
}

얼핏 보면 복잡해보이나 그리 복잡한 방식의 패턴은 아니다. 마지막으로 어떤 상황 에서 팩토리 메서드를 써야하는지 알아보고 끝내겠다. 꼭 이런 상황에 쓰는 것이 아닌, 이런 방식도 있다는 예시를 들어주는 것이다.

 

- 팩토리 메서드는 당신의 코드가 함께 작동해야 하는 객체들의 정확한 유형들과 의존관계들을 미리 모르는 경우 사용한다.

 

- 팩토리 메서드는 당신의 라이브러리 또는 프레임워크의 사용자들에게 내부 컴포넌트들을 확장하는 방법을 제공하고 싶을 때 사용한다.

 

- 팩토리 메서드는 기존 객체들을 매번 재구축하는 대신 이들을 재사용하여 시스템 리소스를 절약하고 싶을 때 사용한다.

 

다음에는 생성 패턴의 다른 패턴인 싱글톤 패턴을 알아보도록 하겠다.