- @참고 : https://refactoring.guru/design-patterns/factory-method

 

Factory Method

/ Design Patterns / Creational Patterns Also known as: Virtual Constructor Factory Method Intent Factory Method is a creational design pattern that provides an interface for creating objects in a superclass, but allows subclasses to alter the type of objec

refactoring.guru

🙌 취지

팩토리 메소드는 수퍼클래스에서 객체를 생성하기 위한 인터페이스를 제공하지만, 서브클래스가 작성 될 객체의 타입을 변경할 수 있는 생산적 디자인 패턴인입니다.

🤔 문제점

물류 관리 어플리케이션을 만들고 있다고 생각해보십시오. 앱의 첫 번째 버전은 트럭 운송 만 처리 할 수 있으므로 대부분의 코드는 Truck 클래스 내에 있습니다.

잠시 후, 앱이 꽤 유명해집니다. 매일 해상 운동 회사로부터 앱에 해상 물류를 통합하기 위해 수십 건의 요청을 받습니다.

나머지 코드가 이미 기존 클래스에 연결되어 있으면 프로그램에 새 클래스를 추가하는 것은 그리 간단치 않습니다.

좋은 소식이지요? 그러나 코드는 어떻나요? 현재, 대부분의 코드는 Truck 클래스에 연결되어 있습니다. Ships를 앱에 추가하려면 전체 코드베이스를 변경해야합니다. 또한, 나중에 다른 유형의 운송을 앱에 추가하기로 결정한다면, 모든 이 변경를 다시 해야할지 모릅니다.

그 결과로, 운송 객체 클래스에 따라 앱의 동작을 전환하는 조건부으로 가득찬 매우 불쾌한 코드가 생깁니다.

😊 해결책

팩토리 메소드 패턴은 직접 객체 생성 호출(new 연산자를 사용한)을 특수 팩토리 메소드 호출로 대체하는 걸 제안합니다. 걱정하지 마십세요. 객체는 여전히 new 연산자를 통해 생성되지만, 팩토리 메소드 내부에서 호출됩니다. 팩토리 메소드에 의해 반환된 객체는 종종 "제품(products)"라 불립니다.

서브클래스는 팩토리 메소드에 의해 반환되는 객체의 클래스를 변경할 수 있습니다.

언뜻보기에, 이 변화는 의미 없을 수 있습니다. 생성자 호출을 프로그램의 한부분에서 다른부분으로 옮겼을 뿐입니다. 그러나, 다음 사항을 고려하세요. 이제 서브클래스 안의 팩토리 메소드를 오버라이드 할 수 있고 메소드에 의해 생성된 제품 클래스를 변경할 수 있습니다.

그러나 약간의 제한이 있습니다. 서브클래스는 이 제품이 공통의 베이스 클래스나 인터페이스를 갖을 때에만 다른 타입의 제품을 반환할 수 있습니다. 또한, 베이스 클래스의 팩토리 메소드는 이 인터페이스로 반환 타입이 선언되어 있어야 합니다.

모든 제품은 동일한 인터페이스를 따라야 합니다.

예를 들어, TruckShip 클래스는 Transport 인터페이스를 구현해야하며,  deliver라는 메소드를 선언합니다. 각각의 클래스는 이 메소드를 다르게 구현합니다. 트럭은 화물을 육상으로 운송하고, 배는 해상으로 운송합니다. RoadLogistics 클래스의 팩토리 메소드는 트럭 객체를 반환하는 반면, SeaLogistics 클래스의 팩토리 메소드는 배를 반환합니다.

모든 제품 클래스가 공통 인터페이스를 구현하는 한, 객체를 클라이어트 코드에 중단 없이 전달할 수 있습니다.

팩토리 메소드를 사용하는 코드(종종 클라이언트 코드라 불리는)는 다양한 서브클래스에서 반환한 실제 프로덕트 사이에 차이를 보이지 않습니다. 클라이언트는 모든 프로덕트를 추상 Transport로 취급합니다. 클라이언트는 모든 전송 객체가 deliver 메소드를 갖고 있어야 한다는 알지만, 정확한 작동 방식은 클라이언트에게 중요하지 않습니다.

🔦구조

1. Product는 크리에이터와 그 서브클래스가 생성할 수 있는 모든 객체에 공통인 인터페이스를 선언합니다.

2. Concrete Products는 제품 인터페이스의 다른 구현입니다.

3. Creator 클래스는 새 프로덕트 객체를 반환하는 팩토리 메소드를 선언합니다. 이 메소드의 반환 타입이 프로덕트 인터페이스와 일치해야합니다.

팩토리 메소드를 추상으로 선언하여 모든 서브클래스가 고유한 버전의 메소드를 구현하게 할 수 있습니다. 대안으로, 베이스 팩토리 메소드는 몇가지 기본 프로덕트 타입을 반환할 수 있습니다.

이름과 맞지 않게, 프로덕트 생성은 크리에이터의 주요 책임이 아닙니다. 일반적으로, 크리에이터 클래스에는 이미 프로덕트와 관련된 일부 핵심 비즈니스 로직이 있습니다. 팩토리 메소드는 이 로직이 구체적 프로덕트 클래스로부터 분리되도록 돕습니다. 비유를 하자면, 큰 소프트웨어 개발 회사는 프로그래머를 위한 교육 부서를 운영할 수 있습니다. 그러나, 회사 전체의 주 기능은 코드를 작성하는 것이지 프로그래머를 기르는게 아닙니다.

4. Concrete Creators는 다른 제품 타입을 반환하도록 기본 팩토리 메소드를 재정의합니다.

팩토리 메소드는 항상 새 인스턴스를 생성할 필요가 없습니다. 캐시, 객체 풀, 또는 다른 소스에서 기존 객체를 반환할 수 있습니다.

# 짜가코드

이 예제는 팩토리 메소드를 사용하여 클라이언트 코드를 구체적인 UI 클래스에 연결하지 않고 플랫폼 간 UI 요소를 작성하는 방법을 보여줍니다.

플랫폼 간 대화의 예

기본 대화 상자 클래스는 다른 UI 요소를 사용하여 창을 렌더링합니다. 다양한 운영 체제에서, 이러한 요소는 약간 다르게 보일 수 있지만, 여전히 일관되게 동작해야 합니다. 윈도우의 버튼은 여전히 리눅스의 버튼입니다.

팩토리 메소드가 작동하면, 각 운영 체제에 대한 대화 상자의 로직을 재작성할 필요가 없습니다. 기본 대화상자 클래스 안에 버튼을 생성하는 팩토리 메소드를 선언하면, 나중에 팩토리 메소드에서 윈도우 스타일 버튼을 반환하는 대화 상자 서브 클래스를 생성할 수 있습니다. 그런 다음 서브클래스는 대부분의 대화 상자의 코드를 기본 클래스로에서 상속하지만, 팩토리 메소드 덕분에, 화면에 윈도우 모양의 버튼을 렌더링할 수 있습니다.

이 패턴이 작동하려면 기본 대화상자 클래스는 추상 버튼(모든 구체적인 버튼이 따르는 기본 클래스나 인터페이스)와 함께 작동해야 합니다. 이런 방식으로 대화 상자의 코드는 작동하는 버튼의 유형에 관계없이 작동합니다.

물론, 이 방법을 다른 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 WindowButton implements Button is
	method render(a, b) is
    	//윈도우 스타일로 버튼을 렌더링합니다.
	method onClick(f) is
    	//OS 본연의 클릭 이벤트를 바인딩합니다.
        
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 프레임워크를 사용하여 앱을 작성한다고 상상해 보세요. 당신의 앱은 둥근 버튼이 있어야 하지만, 프레임워크는 네모난 것만 제공합니다. 당신은 표준 Button 클래스를 영광스러운 RoundButton 서브 클래스로 확장합니다. 그러나 이제 주 UIFramework 클래스에 기본 버튼 대신 새 버튼 서브클래스를 사용하도록 지시해야합니다.
    • 이를 위해, 기본 프레임워크 클래스에 서브클래스 UIWithRoundButtons를 생성하고 createButton 메소드를 재정의합니다. 이 메소드가 기본 클래스에서 Button 객체를 반환하는 동안, 서브클래스가 RoundButton 객체를 반환합니다. 이제 UIFramework 대신 UIWithRoundButtons 클래스를 사용하세요. 그게 다입니다!
  • 매번 기존 객체를 재 구축하는 대신 재사용하여 시스템 자원을 절약하려면 팩토리 메소드를 사용하십시오.
    • 데이터베이스 연결, 파일 시스템, 및 네트워크 자원과 같은 리소스를 많이 사용하는 대규모 객체를 다룰 때 종종 필요를 느낍니다.
    • 기존 객체를 재사용하기 위해 뭘 해야하는지 생각해봅시다.
      1. 우선, 생성된 모든 객체를 추적하려면 저장소를 만들어야 합니다.
      2. 객체를 요청하면, 프로그램은 해당 풀 내에서 사용가능한 객체를 찾아야합니다.
      3. ... 그런 다음 클라이언트 코드에 반환해야 합니다.
      4. 사용가능한 객체가 없는 경우, 프로그램은 새로운 객체를 생성해야 합니다.(그리고 풀에 추가해야합니다.)
    • 코드가 많습니다! 그리고 모두 한 곳에 담겨야 중복 코드로 프로그램을 오염시키지 않습니다.
    • 이 코드를 배치할 수 있는 가장 분명하고 편리한 위치는 재사용하려는 객체를 가진 클래스의 생성자일 겁니다. 그러나, 정의에 의하면 생성자는 항상 새 객체를 반환해야 합니다. 기존 인스턴스를 반환할 수 없습니다.
    • 따라서, 새 객체를 생성 할 뿐만 아니라 아니라 기존 것을 재사용할 수 있는 일반적인 메소드가 필요합니다. 그것은 팩토리 메소드와 매우 흡사합니다.

📋 구현 방법

1. 모든 제품이 동일한 인터페이스를 따르게 만드세요. 이 인터페이스는 모든 제품에 적합한 메소드를 선언해야합니다.

2. 크리에이터 클래스 내부에 빈 팩토리 메소드를 추가하세요. 메소드의 반환 타입은 공통 제품 인터페이스와 일치해야 합니다.

3. 크리에이터의 코드에서 제품 생성에 대한 모든 참조를 찾으세요. 제품 생성 코드를 팩토리 메소드로 추출하면서 하나씩, 팩토리 메소드 호출로 대체하세요.

반환 프로덕트의 타입을 제어하기 위해 팩토리 메소드에 임시 파라미터를 추가해야 할 수도 있습니다.

이 시점에서, 팩토리 메소드의 코드는 흉해 보일 수 있습니다. 인스턴스화 할 제품 클래스를 선택하는 큰 switch 연산자가 있을 수 있습니다. 그러나 걱정하지 마세요. 곧 고칠겁니다.

4. 이제 팩토리 메소드에 나열된 각 제품 타입에 대한 크리에이터 서브클래스 세트를 만드세요. 서브클래스에 팩토리 메소드를 재정의하고 기본 메소드에서 적절한 생성 코드 부분을 추출하세요.

5. 너무 많은 제품 유형이 있어 모든 서브클래스를 생성하는 게 적절하지 않으면, 서브클래스 안의 기본 클래스의 제어 파라미터를 재사용할 수 있습니다.

예를 들어, 다음과 같은 클래스 계층이 있다고 가정해보세요. 몇개의 서브클래스가 있는 기본 Mail 클래스: AirMail 과 GroundMail; Transport 클래스는 Plane, Truck 및 Train 입니다. AirMail 클래스가 Plane 객체만 사용하지만, GroundMail 은 Truck 과 Train 객체 모두 사용할 수 있습니다. 두 경우 모두를 다루기 위해 새 서브클래스(TrainMail)를 생성할 수 지만, 다른 선택지가 있습니다. 클라이언트 코드는 GroundMail 클래스의 팩토리 메소드에 인수를 전달하여 수신할 제품을 제어할 수 있습니다.

6. 모든 추출 후, 기본 팩토리 메소드가 비면, 추상으로 만들 수 있습니다. 남은게 있으면, 메소드의 기본 동작으로 만들 수 있습니다.

⚖ 장단점

✔ 크리에이터와 구체적인 제품 사이의 긴밀한 연결을 피합니다.

✔ 단일 책임 원칙(Single Responsibility Principle). 제품 작성 코드를 프로그램의 한 곳으로 옮겨 코드가 지원하기 편하게 할 수 있습니다.

✔ 개방/폐쇄 원칙(Open/Closed Principle). 기존 클라이언트 코드를 손상시키지 않고 새로운 유형의 제품을 프로그램에 도입할 수 있습니다. 

❌ 패턴을 구현하기 위해 많은 새로운 서브클래스를 도입해야하므로 코드는 더 복잡해질 수 있습니다. 가장 좋은 시나리오는 기존 제작자 클래스 계층에 패턴을 도입할 때입니다.

🐱‍🏍 다른 패턴과의 관계

  • 많은 설계는 Factory Method를 사용하여 시작하고(서브클래스를 통해 덜 복잡하고 사용자 정의 가능) Abstract Factory, Prototype 또는 Builder로 발전합니다(보다 유연하지만, 더 복잡함).
  • 추상 팩토리 클래스는 팩토리 메소드 세트를 기반으로 하지만, 이 클래스의 메소드를 작성하기 위해 프로토타입을 사용할 수도 있습니다.
  • 팩토리 메소드이터레이터와 함께 사용하면 컬렉션 서브클래스가 컬렉션과 호환되는 다른 유형의 이터레이터를 반환할 수 있습니다.
  • 프로토타입은 상속에 기반하지 않으므로 단점이 없습니다. 반면에, 프로토타입은 복제된 객체의 복잡한 초기화가 필요합니다. 팩토리 메소드는 상속에 기반하지만 초기화 단계가 필요하지 않습니다.
  • 팩토리 메소드템플릿 메소드의 전문화입니다. 동시에 팩토리 메소드는 큰 템플릿 메소드의 단계 역할을 할 수 있습니다.

</> 코드 예제

플랫폼 간 GUI 요소 제작

이 예제에서, 버튼은 제품의 역할을 하고, 대화 상자는 크리에이터 역할을 합니다.

다른 유형의 대화 상자는 고유 타입이 요소가 필요합니다. 그렇기 때문에 각 대화 상자 유형에 대한 서브클래스를 만들고 팩토리 메소드를 재정의합니다.

📁 buttons

📄 buttons/Button.java: Common product interface

package com.test.factorymethod.buttons;
/**
 * 모든 버튼에 대한 공통 인터페이스
 */
public interface Button {
	void render();
	void onClick();
}

📄 buttons/HtmlButton.java: Concrete product

package com.test.factorymethod.buttons;

/**
 * HTML 버튼 구현.
 */
public class HtmlButton implements Button {

	@Override
	public void render() {
		System.out.println("<button>Test Button</button>");
		onClick();
	}

	@Override
	public void onClick() {
		System.out.println("Click! Button says - 'Hello World!'");
	}
	
}

📄 buttons/WindowsButton.java: Concrete product

package com.test.factorymethod.buttons;

import java.awt.Color;
import java.awt.FlowLayout;
import java.awt.Font;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.SwingConstants;

/**
 * 윈도우 버튼 구현.
 */
public class WindowsButton implements Button {
	JPanel panel = new JPanel();
	JFrame frame = new JFrame();
	JButton button;

	@Override
	public void render() {
		frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		JLabel label = new JLabel("Hello World!");
		label.setOpaque(true);
		label.setBackground(new Color(235, 233, 126));
		label.setFont(new Font("Dialog", Font.BOLD, 44));
		label.setHorizontalAlignment(SwingConstants.CENTER);
		panel.setLayout(new FlowLayout(FlowLayout.CENTER));
		frame.getContentPane().add(panel);
		panel.add(label);
		onClick();
		panel.add(button);
		
		frame.setSize(320, 200);;
		frame.setVisible(true);
		onClick();
	}

	@Override
	public void onClick() {
		button = new JButton("Exit");
		button.addActionListener(new ActionListener() {

			@Override
			public void actionPerformed(ActionEvent e) {
				frame.setVisible(false);
				System.exit(0);
				
			}
		});
	}
}

📁 factory

📄 factory/Dialog.java: Base creator

package com.test.factorymethod.factory;

import com.test.factorymethod.buttons.Button;

/**
 * 기본 팩토리 클래스. "팩토리"는 클래스의 역할이 아닙니다. 다른 프로덕트를 만들어야하는 핵심 비즈니스 로직이 있어야 합니다.
 */
public abstract class Dialog {
	
	public void renderWindow() {
		// ... 다른 코드 ...
		Button okButton = createButton();
		okButton.render();
	}
	
	/**
	 * 서브클래스는 특정 버튼 객체를 생성하기 위해 이 메소드를 재정의할 것입니다.
	 * @return
	 */
	public abstract Button createButton();
}

📄 factory/HtmlDialog.java: Concrete creator

package com.test.factorymethod.factory;

import com.test.factorymethod.buttons.Button;
import com.test.factorymethod.buttons.HtmlButton;

/**
 * HTML 대화 상자는 HTML 버튼을 생성합니다.
 */
public class HtmlDialog extends Dialog {

	@Override
	public Button createButton() {
		return new HtmlButton();
	}
	
}

📄 factory/WindowsDialog.java: One more concrete creator

package com.test.factorymethod.factory;

import com.test.factorymethod.buttons.Button;
import com.test.factorymethod.buttons.WindowsButton;

public class WindowsDialog extends Dialog {

	@Override
	public Button createButton() {
		return new WindowsButton();
	}
	
}

📄 Demo.java: Client code

package com.test.factorymethod;

import com.test.factorymethod.factory.Dialog;
import com.test.factorymethod.factory.HtmlDialog;
import com.test.factorymethod.factory.WindowsDialog;

public class Demo {
	private static Dialog dialog;
	
	public static void main(String[] args) {
		configure();
		runBusinessLogin();
	}
	
	/**
	 * 구체적 팩토리는 일반적으로 구성이나 환경 옵션에 따라 선택됩니다.
	 */
	private static void configure() {
		if (System.getProperty("os.name").equals("Windows 10")) {
			dialog = new WindowsDialog();
		} else {
			dialog = new HtmlDialog();
		}
		
	}
	
	/**
	 * 모든 클라이언트 코드는 추상 인터페이스를 통해 팩토리와 제품과 함께 작동해야 합니다. 이 방법은 어떤 팩토리와 작업하는지 그리고 어떤 종류의 프로덕트를 반환하는지 신경쓰지하지 않습니다.
	 */
	private static void runBusinessLogin() {
		dialog.renderWindow();
		
	}
	
}

 

블로그 이미지

uchacha

개발자 일지

,