본문 바로가기
C++

Tip of the Week #42: Prefer Factory Functions to Initializer Methods

by 개발자J의일상 2022. 5. 20.
반응형

Originally posted as totw/42 on 2013-05-10

By Geoffrey Romer (gromer@google.com)

Revised 2017-12-21

 

예외가 허용되지 않는 환경(예: Google 내)에서 C++ 생성자는 호출자에게 실패를 보고할 방법이 없기 때문에 효과적으로 성공해야 한다. 물론 abort()를 사용할 수 있지만 그렇게 하면 전체 프로그램이 충돌하며 이는 종종 상품화 코드에서 허용되지 않는다.

 

클래스의 초기화 로직이 실패 가능성을 피할 수 없는 경우, 한가지 일반적인 접근 방식은 클래스에 초기화 메서드(initializer method)("init method"라고도 함)를 제공하는 것이다. 이 메서드는 실패할 수 있는 초기화 작업을 수행하고, return값을 통해 실패를 알린다. 일반적으로 사용자는 생성 직후 이 메서드를 호출하고 실패하면 사용자가 객체를 즉시 파괴한다고 가정한다.

그러나 이러한 가정이 항상 문서화되거나 항상 준수되는 것은 아니다. 초기화 전이나 초기화가 실패한 후에 사용자가 다른 메서드를 호출하기 시작하는 것은 너무나 쉽다. 때때로 class는 실제로 이러한 행동을 권장한다, 예를들어 초기화하기 전에 객체를 구성하거나 초기화가 실패한 후 객체에서 오류를 읽는 메서드를 제공한다.

 

이 디자인은 사용자가 볼 수 있는 최소한 두 가지 상태, 그리고 종종 세 가지(초기화됨, 초기화되지 않음, 초기화 실패)로 클래스를 유지 관리하는 데 전념한다.

이러한 디자인 작업을 수행하려면 많은 훈련이 필요하다:

클래스의 모든 메서드는 호출할 수 있는 상태를 지정해야 하며 사용자는 이러한 규칙을 준수해야 한다. 이 원칙이 어긋나면 클라이언트 개발자는 지원하려는 의도와 상관없이 작동되는 어떤 코드든 작성하는 경향이 있다. 이러한 일이 발생되면, 고객이 의존하기 시작한 사전 초기화 방법 메서드 호출의 조합을 당신의 구현에서 지원해야 하기 때문에 유지 보수성이 급격히 떨어진다. 사실상, 당신의 구현이 당신의 인터페이스가 되었다. (하이럼의 법칙 참조)

 

다행히도 이러한 단점이 없는 간단한 대안이 있다:

클래스의 인스턴스를 생성 및 초기화하고 이를 포인터로 반환하거나 absl::optional(TotW #123 참조)로 반환하는 팩토리 함수(Factory function)를 제공하고, 실패를 나타내기 위해 null을 사용한다. 다음은 unique_ptr<>을 사용하는 toy example이다.

// foo.h
class Foo {
 public:
  // Factory method: creates and returns a Foo.
  // May return null on failure.
  static std::unique_ptr<Foo> Create();

  // Foo is not copyable.
  Foo(const Foo&) = delete;
  Foo& operator=(const Foo&) = delete;

 private:
  // Clients can't invoke the constructor directly.
  Foo();
};

// foo.c
std::unique_ptr<Foo> Foo::Create() {
  // Note that since Foo's constructor is private, we have to use new.
  return absl::WrapUnique(new Foo());
}

 

많은 경우 이 패턴을 두 가지 장점을 모두 제공한다:

팩토리 함수 Foo::Create()는 생성자와 같이 완전히 초기화된 객체만 노출하지만 초기화 메소드처럼 실패를 나타낼 수 있다.

팩토리 함수의 또 다른 장점은 반환 유형의 모든 하위 클래스 인스턴스를 반환할 수 있다는 것이다(absl::optional을 반환 유형으로 사용하는 경우에는 불가능함).

이를 통해 사용자 코드를 업데이트 하지 않고 다른 구현으로 교체하거나 사용자 입력에 따라 구현 클래스를 동적으로 선택할 수도 있다.

 

이 접근 방식의 주요 단점은 힙 할당 객체(heap-allocated object)에 대한 포인터를 반환하므로, 스택에서 작동하도록 설계된 "value-like" 클래스에는 적합하지 않다는 것이다. 그러나 이러한 클래스는 일반적으로 처음부터 복잡한 초기화가 필요하지 않다. 팩토리 함수는 파생 클래스 생성자가 그것의 base를 초기화해야 할 때도 사용할 수 없으므로 base 클래스의 보호된 API에 초기화 메서드가 필요한 경우가 있다. 하지만 public API는 여전히 팩토리 함수를 사용할 수 있다.

300x250

댓글