Self-Dev/Design Patterns R&D

[13] 디자인 패턴 목록 - 생성 디자인 패턴 - 팩토리 메서드

Khadra 2024. 6. 7. 21:38

출처 : 디자인 패턴에 뛰어들기 - 알렉산더 슈베츠 도서


팩토리 메서드 패턴

가상 생성자, Factory Method 이름으로도 불린다.

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


문제

예시 - 1)
앱의 첫 번째 버전은 트럭 운송만 처리할 수 있어서 대부분의 코드가 Truck(트럭) 클래스에 결합되어 있다.
앱에 Ship(선박) 클래스를 추가하려면 전체 코드 베이스를 변경해야 하고, 차후 다른 유형의 교통수단을 추가할 때도 마찬가지 문제가 발생한다. 이로 인해 복잡한 코드와 많은 조건문이 생기게 된다.

나머지 코드가 이미 기존 클래스들에 결합되어 있다면 프로그램에 새 클래스를 추가하는 일은 그리 간단하지 않는다.

현재 대부분의 코드는 Truck 클래스에 결합되어 있다.
앱에 Ship (선박) 클래스를 추가하려면 전체 코드 베이스를 변경해야 한다.
또한 차후 앱에 다른 유형의 교통수단을 추가하려면 아마도 다시 전체 코드 베이스를 변경해야 할 것이다.
그러면 결과적으로 많은 조건문이 운송 수단 객체들의 클래스에 따라 앱의 행동을 바꾸는 매우 복잡한 코드가 작성될 것이다.

해결책

팩토리 메서드 패턴은 (new 연산자를 사용한) 객체 생성 직접 호출들을 특별한 팩토리 메서드에 대한 호출들로 대체하라고 제안한다.

  • 객체들은 여전히 new 연산자를 통해 생성되지만 팩토리 메서드 내에서 호출되고 있다.
  • 참고로 팩토리 메서드에서 반환된 객체는 종종 제품이라고도 불린다.

자식 클래스들은 팩토리 메서드가 반환하는 객체들의 클래스를 변경할 수 있다.

팩토리 메서드 패턴은 객체 생성 직접 호출들을 팩토리 메서드에 대한 호출들로 대체한다.
이를 통해 자식 클래스에서 팩토리 메서드를 오버라이딩하고, 생성되는 제품들의 클래스를 변경할 수 있게 된다.
단, 자식 클래스들은 공통 기초 클래스 또는 공통 인터페이스가 있는 경우에만 다른 유형의 제품들을 반환할 수 있다.

모든 제품들은 같은 인터페이스를 따라야 한다.

모든 제품들은 같은 인터페이스를 따라야 한다.
예를 들어 Truck과 Ship 클래스들은 모두 Transport 인터페이스를 구현해야 하며, 이 인터페이스는 deliver(배달) 메서드를 선언한다.

팩토리 메서드를 사용하는 클라이언트 코드는 다양한 자식 클래스들에서 반환되는 여러 제품 간의 차이를 알지 못한다.
클라이언트 코드는 모든 제품을 추상 Transport로 간주하며, deliver 메서드가 어떻게 작동하는지는 중요하지 않다.

요약

팩토리 메서드 패턴은 객체 생성의 책임을 부모 클래스에서 자식 클래스로 이동시켜 코드의 유연성과 확장성을 높이는 디자인 패턴이다. 이를 통해 코드의 복잡성을 줄이고 유지 보수를 쉽게 할 수 있다.


구조

  • 1. 제품
    • 인터페이스를 선언하며, 모든 객체에 공통인 생성자와 메서드를 정의한다.
  • 2. 구상 제품
    • 제품 인터페이스의 다양한 구현체이다.
  • 3. 크리에이터(Creator)
    • 새로운 제품 객체를 반환하는 팩토리 메서드를 선언한다.
    • 이 메서드의 반환 유형은 제품 인터페이스와 일치해야 한다.
    • 팩토리 메서드는 추상으로 선언하여 자식 클래스들이 자신만의 버전을 구현하도록 강제할 수 있다. 또는 기본값 제품 유형을 반환하도록 설정할 수 있다.
    • 크리에이터의 주요 역할은 제품을 생성하는 것이 아니라, 제품과 관련된 핵심 비즈니스 로직을 수행하는 것이다.
  • 4. 구상 크리에이터
    • 기초 팩토리 메서드를 오버라이드하여 다른 유형의 제품을 반환한다.
    • 팩토리 메서드는 새로운 인스턴스를 생성할 필요가 없으며, 기존 객체를 캐시, 객체 풀 또는 다른 소스로부터 반환할 수도 있다.

요약

팩토리 메서드 패턴의 구조는 제품 인터페이스, 다양한 구상 제품, 크리에이터 클래스, 그리고 이를 확장한 구상 크리에이터들로 구성된다.
이 패턴은 객체 생성의 책임을 자식 클래스들에 위임하여 유연성과 확장성을 높인다.
크리에이터 클래스는 핵심 비즈니스 로직을 수행하며, 팩토리 메서드는 이 로직을 제품 생성과 분리하는 역할을 한다.


의사코드

아래 예시는 어떻게 팩토리 메서드가 클라이언트 코드를 구상 UI 클래스들과 결합하지 않고도 크로스 플랫폼 UI 요소들을 생성할 수 있는지를 보여준다.

크로스 플랫폼 다이얼로그(대화 상자) 예시

  • 기초 Dialog(대화 상자) 클래스는 여러 UI 요소들을 사용하여 대화 상자를 렌더링한다.
    • 다양한 운영 체제에서 이러한 요소들은 약간씩 다르게 보일 수 있지만 여전히 일관되게 작동해야 한다.
    • 예를 들어, 윈도우에서의 버튼은 리눅스에서도 여전히 버튼이어야 한다.
  • 팩토리 메서드를 적용하면, 대화 상자 로직을 각 운영 체제에 맞게 반복해서 재작성할 필요가 없다.
    • 기초 Dialog 클래스 내에서 버튼을 생성하는 팩토리 메서드를 선언하면 나중에 팩토리 메서드에서 윈도우 유형의 버튼들을 반환하는 Dialog 자식 클래스를 생성할 수 있다.
    • 이 자식 클래스는 기초 클래스로부터 Dialog의 대부분의 코드를 상속받지만, 팩토리 메서드 덕분에 윈도우 유형의 버튼들도 렌더링할 수 있다.
  • 이 패턴이 작동하려면 기초 Dialog 클래스가 추상 버튼들과 함께 작동해야 한다.
    • 참고로 추상 버튼은 모든 구상 버튼들이 따르는 인터페이스 또는 기초 클래스이다.
    • 이렇게 해야 대화 상자 코드가 버튼 유형에 관계없이 작동합니다.

물론, 위 접근 방식을 다른 UI 요소들에도 적용할 수 있지만, 대화 상자에 새로운 팩토리 메서드를 추가할 때마다 이 프로그램은 추상 팩토리 패턴에 더 가까워진다.

//크리에이터 클래스는 제품 클래스의 객체를 반환해야 하는 팩토리 메서드를 선언합니다. 
//크리에이터의 자식 클래스들은 일반적으로 이 메서드의 구현을 제공합니다.
‘class Dialog is
//크리에이터는 팩토리 메서드의 일부 디폴트 구현을 제공할 수도 있습니다.
  abstract method createButton():Button

  // 크리에이터의 주 업무는 제품을 생성하는 것이 아닙니다. 크리에이터는
  // 일반적으로 팩토리 메서드에서 반환된 제품 객체에 의존하는 어떤 핵심
  // 비즈니스 로직을 포함합니다. 자식 클래스들은 팩토리 메서드를 오버라이드 한
  // 후 해당 메서드에서 다른 유형의 제품을 반환하여 해당 비즈니스 로직을
  // 간접적으로 변경할 수 있습니다.
  method render() is
    // 팩토리 메서드를 호출하여 제품 객체를 생성하세요.
    Button okButton = createButton()
    // 이제 제품을 사용하세요.
    okButton.onClick(closeDialog)
    okButton.render()


// 구상 크리에이터들은 결과 제품들의 유형을 변경하기 위해 팩토리 메서드를
// 오버라이드합니다.
class WindowsDialog extends Dialog is
  method createButton():Button is
    return new WindowsButton()

class WebDialog extends Dialog is
  method createButton():Button is
    return new HTMLButton()


// 제품 인터페이스는 모든 구상 제품들이 구현해야 하는 작업들을 선언합니다.
interface Button is
  method render()
  method onClick(f)

// 구상 제품들은 제품 인터페이스의 다양한 구현을 제공합니다.
class WindowsButton implements Button is
  method render(a, b) is
    // 버튼을 윈도우 스타일로 렌더링하세요.
  method onClick(f) is
    // 네이티브 운영체제 클릭 이벤트를 바인딩하세요.

class HTMLButton implements Button is
  method render(a, b) is
    // 버튼의 HTML 표현을 반환하세요.
  method onClick(f) is
    // 웹 브라우저 클릭 이벤트를 바인딩하세요.


class Application is
  field dialog: Dialog

  // 앱은 현재 설정 또는 환경 설정에 따라 크리에이터의 유형을 선택합니다.
  method initialize() is
    config = readApplicationConfigFile()

    if (config.OS == "Windows") then
      dialog = new WindowsDialog()
    else if (config.OS == "Web") then
      dialog = new WebDialog()
    else
      throw new Exception("Error! Unknown operating system.")

  // 클라이언트 코드는 비록 구상 크리에이터의 기초 인터페이스를 통하는 것이긴
  // 하지만 구상 크리에이터의 인스턴스와 함께 작동합니다. 클라이언트가
  // 크리에이터의 기초 인터페이스를 통해 크리에이터와 계속 작업하는 한 모든
  // 크리에이터의 자식 클래스를 클라이언트에 전달할 수 있습니다.
  method main() is
    this.initialize()
    dialog.render()

요약

의사코드는 팩토리 메서드 패턴을 사용하여 클라이언트 코드를 구상 UI 클래스와 결합하지 않고 크로스 플랫폼 UI 요소들을 생성하는 방법을 보여주고 있다.
기초 Dialog 클래스에서 팩토리 메서드를 사용해 운영 체제에 맞는 UI 요소를 생성하고, 이를 통해 코드 재사용성을 높이며 운영 체제 간의 일관성을 유지한다.


주의사항

1. 팩토리 메서드는 함께 작동해야 하는 객체들의 정확한 유형과 의존관계를 미리 알 수 없을 때 사용하세요..

  • 팩토리 메서드는 제품 생성 코드를 제품을 사용하는 코드와 분리한다. 이렇게 하면 제품 생성자 코드를 독립적으로 확장하기 쉬워진다.
  • 예를 들어, 앱에 새로운 제품을 추가하려면 새로운 크리에이터 자식 클래스를 생성하고, 해당 클래스 내부의 팩토리 메서드를 오버라이드하기만 하면 된다.

2. 팩토리 메서드는 라이브러리나 프레임워크의 사용자들에게 내부 컴포넌트를 확장하는 방법을 제공하고 싶을 때 사용하세요.

  • 상속은 라이브러리나 프레임워크의 기본 동작을 확장하는 가장 쉬운 방법이다. 그러나 프레임워크는 어떻게 표준 컴포넌트 대신 자식 클래스를 사용해야 한다는 것을 알 수 있는지를 생각해야 한다.
  • 해결책은 프레임워크 전체에서 컴포넌트를 생성하는 코드를 단일 팩토리 메서드로 줄이고, 이를 오버라이드할 수 있도록 하는 것이다.
  • 예를 들어, 오픈 소스 UI 프레임워크를 사용하여 앱을 작성하고, 둥근 버튼이 필요할 때, 기초 프레임워크 클래스에서 자식 클래스 UIWithRoundButtons를 만들어 createButton 메서드를 오버라이드한다.
  • 이 메서드는 기초 클래스에 Button 객체를 반환하지만, 자식 클래스는 RoundButton 객체를 반환한다. 이제 UIFramework 클래스 대신 UIWithRoundButtons 클래스를 사용하면 된다.

3. 팩토리 메서드는 기존 객체를 재구축하는 대신 재사용하여 시스템 리소스를 절약하고 싶을 때 사용하세요.

  • 이러한 요구는 데이터베이스 연결, 파일 시스템 및 네트워크처럼 시스템 자원을 많이 사용하는 대규모 객체들을 처리할 때 자주 발생한다.
  • 기존 객체를 재사용하는 경우
    • 먼저 생성된 모든 객체를 추적하기 위해 일부 스토리지를 생성해야 한다.
    • 누군가 객체를 요청하면 프로그램은 해당 풀에서 유휴 객체를 찾아 클라이언트 코드에 반환해야 한다.
    • 유휴 객체가 없으면 새로운 객체를 생성하여 풀에 추가해야 한다.
  • 이 많은 양의 코드를 중복하지 않도록 한 곳에 모아야 한다.
    • 가장 확실하고 편리한 위치는 객체의 클래스 생성자일 수 있지만, 생성자는 항상 새로운 객체를 반환해야 한다.
  • 따라서 새 객체를 생성하고 기존 객체를 재사용할 수 있는 일반적인 메서드가 필요하다.

요약

팩토리 메서드는 코드가 함께 작동해야 하는 객체의 정확한 유형을 미리 알지 못할 때 유용하다.
객체 생성과 사용을 분리하여 코드를 확장하기 쉽게 만든다.
또한, 라이브러리나 프레임워크의 사용자가 내부 컴포넌트를 확장할 수 있도록 도와준다.
마지막으로, 팩토리 메서드는 시스템 리소스를 절약하기 위해 기존 객체를 재사용하는 데 유용하다.


구현 방법

1. 모든 제품이 같은 인터페이스를 따르도록 하세요.

  • 이 인터페이스는 모든 제품에서 의미가 있는 메서드들을 선언해야 한다.

2. 크리에이터 클래스 내부에 빈 팩토리 메서드를 추가하세요.

  • 이 메서드의 반환 유형은 공통 제품 인터페이스와 일치해야 한다.

3. 크리에이터의 코드에서 제품 생성자들에 대한 모든 참조를 찾으세요.

  • 이 참조들을 하나씩 팩토리 메소드에 대한 호출‘로 교체하면서 제품 생성‘코드를 팩토리 메서드로 추출해야 한다.
  • 반환된 제품의 유형을 제어하기 위해 팩토리 메서드에 임시 매개변수를 추가해야 할 수도 있다.
  • 이 시점에서 팩토리 메서드의 코드는 꽤 복잡할 수 있다.
  • 예를 들어 인스턴트화할 제품 클래스를 선택하는 큰 switch 문장이 있을 수 있다.

4. 이제 팩토리 메서드에 나열된 각 제품 유형에 대한 크리에이터 자식 클래스들의 집합을 생성한 후, 자식 클래스들에서 팩토리 메서드를 오버라이딩하고 기초 메서드에서 생성자 코드의 적절한 부분들을 추출하세요.

5. 제품 유형이 너무 많아 모든 제품에 대하여 자식 클래스들을 만드는 것이 합리적이지 않을 경우, 자식 클래스들의 기초 클래스의 제어 매개변수를 재사용할 수 있다.

  • 예를 들어, 다음과 같은 클래스 계층구조가 있다고 가정한다.
  • Mail(우편) 기초 클래스의 자식 클래스들은 AirMail(항공우편)과 GroundMail(지상우편)이며, Transport(운송수단) 클래스의 자식 클래스들은 Plane(비행기), Truck(트럭), 그리고 Train(기차)이다.
  • AirMail(항공우편) 클래스는 Plane(비행기) 객체만 사용하지만, GroundMail(지상우편)은 Truck 과 Train 객체들 모두 사용할 수 있다.
  • 이 두 가지 경우를 모두 처리하기 위해 새 자식 클래스(예: TrainMail(기차우편))를 만들 수도 있으나, 다른 방법도 있다.
  • 클라이언트 코드가 받으려는 제품을 제어하기 위해 GroundMail 클래스의 팩토리 메서드에 전달인자(argument)를 전달하는 방법이다.

6. 추출이 모두 끝난 후 기초 팩토리 메서드가 비어 있으면, 해당 팩토리 메서드를 추상화할 수 있다.

  • 팩토리 메서드가 비어 있지 않으면, 나머지를 그 메서드의 디폴트 행동으로 만들 수 있다.

요약

팩토리 메서드 구현 방법(요약)

  1. 모든 제품이 같은 인터페이스를 따르도록 한다.
  2. 크리에이터 클래스에 빈 팩토리 메서드를 추가한다.
  3. 제품 생성자에 대한 참조를 팩토리 메서드 호출로 교체한다.
  4. 각 제품 유형에 대한 자식 클래스를 생성하고, 팩토리 메서드를 오버라이드한다.
  5. 모든 제품에 자식 클래스를 만드는 것이 합리적이지 않을 경우, 제어 매개변수를 재사용한다.
  6. 기초 팩토리 메서드가 비어 있으면 추상화하고, 비어 있지 않으면 디폴트 행동으로 만든다.

장단점

1. 크리에이터와 구상 제품들이 단단하게 결합되지 않도록 할 수 있다.

  • 제품 생성과 사용을 분리하여 코드의 유연성을 높인다.

2. 단일 책임 원칙

  • 제품 생성 코드를 프로그램의 한 위치로 이동하여 코드를 더 쉽게 유지관리할 수 있다.

3. 개방/폐쇄 원칙

  • 기존 클라이언트 코드를 훼손하지 않고 새로운 유형의 제품들을 프로그램에 도입할 수 있다.

4. 복잡성 증가

  • 패턴을 구현하기 위해 많은 새로운 자식 클래스들을 도입해야 하므로 코드가 더 복잡해질 수 있다.
  • 가장 좋은 방법은 크리에이터 클래스들의 기존 계층구조에 패턴을 도입하는 것이다.

요약

장점

  • 결합도 감소: 크리에이터와 구상 제품들이 단단하게 결합되지 않도록 한다.
  • 단일 책임 원칙: 제품 생성 코드를 중앙화하여 유지보수성을 높인다.
  • 개방/폐쇄 원칙: 기존 코드를 수정하지 않고 새로운 제품을 추가할 수 있다.

단점

  • 복잡성 증가: 많은 새로운 자식 클래스들이 도입되어 코드가 더 복잡해질 수 있다.

다른 패턴과의 관계

1. 진화 과정

  • 많은 디자인은 복잡성이 낮고 자식 클래스들을 통해 더 많은 커스터마이징이 가능한 팩토리 메서드로 시작한다.
  • 그러므로 더 유연하면서도 더 복잡한 추상 팩토리, 프로토타입 또는 빌더 패턴으로 발전해나간다.

2. 추상 팩토리

  • 추상 팩토리 클래스들은 팩토리 메서드들의 집합을 기반으로 하는 경우가 많다.
  • 그러나 당신은 또한 프로토타입을 사용하여 추상 팩토리의 구상 클래스들의 생성 메서드들을 구현할 수도 있다.

3. 반복자와의 관계

  • 팩토리 메서드를 반복자와 함께 사용한다.
  • 이를 통해 컬렉션 자식 클래스들이 해당 컬렉션들과 호환되는 다양한 유형의 반복자들을 반환하도록 할 수 있다.

4. 프로토타입과의 비교

  • 프로토타입은 상속을 기반으로 하지 않으므로 상속과 관련된 단점들이 없다.
  • 반면에 프로토타입은 복제된 객체의 복잡한 초기화가 필요하다.
  • 팩토리 메서드는 상속을 기반으로 하지만 초기화 단계가 필요하지 않다.

5. 템플릿 메서드와의 관계

  • 팩토리 메서드는 템플릿 메서드의 특수화라고 생각할 수 있다.
  • 동시에 대규모 템플릿 메서드의 한 단계의 역할을 팩토리 메서드가 할 수 있다.

요약

  • 팩토리 메서드는 복잡성이 낮고 유연한 디자인을 시작점으로, 추상 팩토리, 프로토타입, 빌더 패턴으로 발전할 수 있다.
  • 팩토리 메서드는 반복자와 함께 사용되어 다양한 유형의 반복자를 반환할 수 있다.
  • 프로토타입 패턴은 상속의 단점을 피할 수 있지만 복잡한 초기화가 필요하다.
  • 팩토리 메서드는 템플릿 메서드의 특수화로 간주될 수 있으며, 템플릿 메서드의 한 단계를 담당할 수 있다.