Abseil Tip 42 초기화 메서드보다 팩토리 함수를 선호하세요


title: “주간 팁 #42: 초기화 메서드보다 팩토리 함수를 선호하세요” layout: tips sidenav: side-nav-tips.html published: true permalink: tips/42 type: markdown order: “042” —

원래 2013-05-10에 totw/42로 게시됨
작성자: Geoffrey Romer (gromer@google.com)
2017-12-21 개정

“공장을 짓는 사람은 성전을 세우고, 그곳에서 일하는 사람은 그곳에서 예배를 드린다. 각자에게 비난이 아닌 경의와 칭찬이 주어져야 한다.” – 캘빈 쿨리지

예외가 허용되지 않는 환경(예: Google 내부)에서는 C++ 생성자가 반드시 성공해야 합니다. 생성자는 호출자에게 실패를 알릴 방법이 없기 때문입니다. 물론 abort()를 사용할 수도 있지만, 이는 프로그램 전체를 강제 종료시키므로 프로덕션 코드에서는 일반적으로 허용되지 않습니다.

클래스의 초기화 논리가 실패 가능성을 피할 수 없는 경우, 흔히 사용되는 접근 방식 중 하나는 초기화 메서드(“init 메서드”라고도 함)를 제공하는 것입니다. 이 메서드는 실패할 가능성이 있는 초기화 작업을 수행하고 반환값을 통해 실패를 알립니다. 사용자는 일반적으로 객체를 생성한 직후 이 메서드를 호출하고, 초기화에 실패하면 객체를 바로 소멸시키는 것을 전제로 합니다. 하지만 이러한 가정은 항상 문서화되지도, 항상 지켜지지도 않습니다. 사용자가 초기화 전에 다른 메서드를 호출하거나, 초기화 실패 후에도 메서드를 호출하는 일이 너무 쉽습니다. 때로는 클래스 자체가 이러한 동작을 조장하기도 합니다. 예를 들어 초기화 전에 객체를 구성하도록 하거나, 초기화 실패 후 오류를 읽도록 하는 메서드를 제공하는 경우가 있습니다.

이 설계는 사용자에게 최소 두 가지, 종종 세 가지의 뚜렷한 상태(초기화 완료, 초기화되지 않음, 초기화 실패)를 유지해야 한다는 것을 의미합니다. 이러한 설계를 성공적으로 유지하려면 많은 규율이 필요합니다. 클래스의 모든 메서드는 호출 가능한 상태를 명확히 지정해야 하고, 사용자는 이러한 규칙을 준수해야 합니다. 이 규율이 느슨해지면 클라이언트 개발자는 의도와 상관없이 동작하는 코드를 작성하기 쉬워지고, 유지 보수성이 급격히 떨어지게 됩니다. 결국 구현이 곧 인터페이스가 되어버립니다. (Hyrum의 법칙 참조)

다행히도 이러한 단점을 해결할 간단한 대안이 있습니다: 팩토리 함수를 제공하여 클래스 인스턴스를 생성하고 초기화한 후, 포인터나 absl::optional(참고: )로 반환하여 실패를 null로 나타내는 방법입니다. 아래는 unique_ptr<>를 사용하는 간단한 예입니다:

// foo.h
class Foo {
 public:
  // 팩토리 메서드: Foo를 생성하고 반환합니다.
  // 실패 시 null을 반환할 수 있습니다.
  static std::unique_ptr<Foo> Create();

  // Foo는 복사할 수 없습니다.
  Foo(const Foo&) = delete;
  Foo& operator=(const Foo&) = delete;

 private:
  // 클라이언트가 생성자를 직접 호출할 수 없습니다.
  Foo();
};

// foo.c
std::unique_ptr<Foo> Foo::Create() {
  // Foo의 생성자가 private이므로 new를 사용해야 합니다.
  return absl::WrapUnique(new Foo());
}

이 패턴은 두 가지 장점을 모두 제공합니다: 팩토리 함수 Foo::Create()는 생성자처럼 완전히 초기화된 객체만 노출하면서도, 초기화 메서드처럼 실패를 알릴 수 있습니다. 팩토리 함수의 또 다른 장점은 반환 타입의 하위 클래스를 반환할 수 있다는 점입니다(absl::optional을 반환 타입으로 사용하는 경우는 예외). 이를 통해 사용자 코드를 업데이트하지 않고도 다른 구현을 대체하거나, 사용자 입력에 따라 동적으로 구현 클래스를 선택할 수 있습니다.

이 접근 방식의 주요 단점은 힙에 할당된 객체에 대한 포인터를 반환하므로 스택에서 작동하도록 설계된 “값 타입(value-like)” 클래스에는 적합하지 않다는 점입니다. 하지만 이러한 클래스는 대개 복잡한 초기화를 필요로 하지 않습니다. 또한, 파생 클래스 생성자가 기본 클래스를 초기화해야 할 때는 팩토리 함수를 사용할 수 없으므로 초기화 메서드가 기본 클래스의 보호된(protected) API에 필요할 수 있습니다. 하지만 공개된(public) API는 여전히 팩토리 함수를 사용할 수 있습니다.