'Design Pattern/Creational Patterns(생성 패턴)'에 해당되는 글 2건

- @참고 : 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

개발자 일지

,

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

 

Singleton

Real-World Analogy The government is an excellent example of the Singleton pattern. A country can have only one official government. Regardless of the personal identities of the individuals who form governments, the title, “The Government of X”, is a globa

refactoring.guru

🙌 취지

싱글톤은 클래스가 하나의 인스턴스만 갖도록 보장하며 이 인스턴스에 대한 글로벌 액세스 포인트를 제공할 수 있는 생성 디자인 패턴입니다.

🤔 문제점

싱글톤 패턴은 단일 책임 원칙을 위반하여 동시에 두가지 문제를 해결합니다.

1. 클래스가 한 인스턴스만 가지는지 확인하십시오. 왜 클래스의 인스턴스 수를 제어하고 싶어할까요? 가장 대표적인 이유는 데이터베이스나 파일과 같은 공유자원에 대한 접근 제어를 위해서입니다.

작동 방식은 다음과 같습니다. 객체를 생성하고, 잠시 후 새로이 생성하려한다고 해봅시다. 새로운 객체를 받는 대신 이미 생성된 객체를 받을 것입니다.

이 동작은 생성자 호출이 디자인에 의해 항상 새로운 객체를 반환해야하기 때문에 일반 생성자로 구현하는게 불가능합니다.

쿨라이언트는 항상 같은 객체로 작업하고 있음을 인식하지 못할 수도 있습니다.

2. 해당 인스턴스에 전역적 접근 포인트를 제공하십시오. 필수 객체를 저장하는데 사용한 전역 변수를 기억합니까? 다루기 매우 편리하지만, 코드가 그 변수의 내용을 덮어 쓰고 앱을 충돌시킬 수 있어서 안전하지 않습니다.

전역 변수와 마찬가지로, 싱글톤 패턴은 프로그램 어디에서든 일부 객체에 접근할 수 있게 합니다. 그러면서 다른 코드로 해당 인스턴스를 덮어 쓰지 않도록 보호합니다.

이 문제의 또 다른 측면이 있습니다. 당신은 #1 문제를 해결하는 코드가 프로그램 전체에 흩어져 있는 것을 원하지 않습니다. 특히 나머지 코드가 이미 의존적일 때 그 코드를 한 클래스 안에 갖는게 더 낫습니다.

요즘, 싱글톤 패턴이 매우 유명해져 나열된 문제 중 하나만 해결하더라도 싱글톤 패턴이라 부를지 모릅니다.

😊 해결책

싱글톤의 모든 구현에는 다음 두 단계가 공통적으로 있습니다.

  • 다른 객체가 싱글톤 클래스에 대해 new 연산자를 사용하지 못하도록 기본 생성자를 private으로 만듭니다.
  • 생성자 역할을 하는 정적 생성 메소드를 만듭니다. 후드에서 이 메소드는 private 생성자를 호출하여 객체를 생성하고 정적 필드에 저장합니다. 이 메소드에 대한 다음의 모든 호출은 캐시 된 객체를 반환합니다.

코드가 싱글톤 클래스에 대해 접근할 수 있으면, 싱글톤의 정적 메소드를 호출 할 수 있습니다. 따라서 해당 메소드가 호출될 때마다 동일한 객체가 항상 반환됩니다.

🚗 실세계에 비유

정부는 싱글톤 패턴의 훌륭한 예입니다. 국가는 하나의 공식 정부만을 가질 수 있습니다. 정부를 구성하는 개개인의 개인 신원에 관계없이, 타이틀 "X 정부"는 담당자 그룹을 식별하는 전역적 접근 지점입니다.

🔦구조

1. 싱글톤 클래스는 자체 클래스와 동일한 인스턴스를 반환하는 getInstance 정적 메소드를 선언합니다.

싱글톤의 생성자는 클라이언트 코드에서 감춰져야 합니다. getInstance 메소드를 호출하는 것만이 싱글톤 객체를 얻는 유일한 방법이어야 합니다.

 

 

 

 

# 짜가코드

이 예제에서, 데이터베이스 연결 클래스는 싱글톤 역할을 합니다. 이 클래스는 public 생성자가 없으므로 객체를 얻는 유일한 방법은 getInstance 메소드를 호출하는 것뿐입니다. 이 메소드는 처음 생성된 객체를 캐시하고 모든 후속 호출에서 이를 반환합니다.

//데이터베이스 클래스는 클라이언트가 프로그램 전체에서 동일한 데이터베이스 연결 인스턴스에 접근하게 하는 'getInstance' 메소드를 정의합니다.
class Database is 
	//싱글톤 인스턴스를 저장하기 위한 필드는 정적으로 선언되어야 합니다.
    private static field instance: Database
    
    //싱글톤의 생성자는 'new' 연산자로 직접 생성 호출을 방지하기 위해 항상 프라이빗이어야 합니다.
    private constructor Database() is
    	//데이터베이스 서버로의 실제 연결과 같은 어떤 시작 코드...
        
	//싱그론 인스턴스에 접근을 제어하는 정적 메소드
    public static method getInstance() is
    	if (Database.instance == null) then
        	acquiredThreadLock() and then
            	//이 스레드가 잠금 해제를 기다리는 동안 다른 스레드가 인스턴스를 초기화되지 않았음을 확인하십시오.
                if (Database.instance == null) then
                	Database.instance = new Database()
		return Database.instance
        
	//마지막으로, 싱글톤은 인스턴스에서 실행될 수 있는 비즈니스 로직을 정의해야 한다.
    public method query(sql) is
    	//예를 들어, 앱의 모든 데이터베이스 쿼리는 이 메소드를 거칩니다. 그러므로, 여기에 제한 또는 캐싱 로직을 배치할 수 있습니다.
        //...
        
class Application is
	method main() is
    	Database foo = Database.getInstance()
        foo.query("SELECT ...")
        //...
        Database bar = Database.getInstance()
        bar.query("SELECT ...")
        //변수 'bar'는 변수 'foo'와 동일한 객체를 포함합니다.
    	

💡 적용

  • 프로그램의 클래스가 모든 클라이언트가 사용할 수 있는 단일 인스턴스가 있어야 하는 경우 싱글톤 패턴을 사용하십시오. 예를 들면, 프로그램의 다른 부분에서 공유되는 단일 데이터베이스 객체가 있습니다.
    • 싱글톤 패턴은 특수 생성 메소드를 제외하고 클래스 객체를 생성하는 모든 방법을 비활성화합니다. 이 메소드는 새로운 객체를 생성하거나, 이미 생성된 경우 기존 객체를 반환합니다.
  • 전역 변수에 대한 엄격한 제어가 필요할 때 싱글톤 패턴을 사용하십시오.
    • 전역 변수와 달리, 싱글톤 패턴은 클래스의 단 하나의 인스턴스가 있음을 보장합니다. 싱글톤 클래스 외의 어떤 것도 캐쉬된 인스턴스를 대체할 수 없습니다.
  • 언제든지 이 제한을 조정할 수 있고 여러 개의 싱글톤 인스턴스를 만들 수 있습니다. 변경이 필요한 유일한 코드는 getInstance 메소드의 본문입니다.

📋 구현 방법

  1. 싱글톤 인스턴스를 저장하기 위해 클래스에 프라이빗 정적 필드를 추가하십시오.
  2. 싱글톤 인스턴스를 얻기 위한 퍼블릭 정적 생성 메소드를 선언합니다.
  3. 정적 메소드 내부에 "지연 초기화"를 구현하십시오. 첫번째 호출에 새 객체를 만들어 정적 필드에 담아야 합니다. 메소드는 항상 모든 후속 호출에서 해당 인스턴스를 반환해야 합니다.
  4. 클래스의 생성자를 비공개로 만듭니다. 클래스의 정적 메소드는 여전히 생성자 호출이 가능하지만 다른 객체는 호출 할 수 없습니다.
  5. 클라이언트 코드를 살펴보고 싱글톤 생성자에 대한 모든 직접 호출을 정적 생성 메소드 호출로 대체하십시오.

⚖ 장단점

✔ 클래스가 단일 인스턴스만 가짐을 보장할 수 있습니다.

✔ 해당 인스턴스에 대한 전역적 접근 포인트를 얻습니다.

✔ 싱글톤 객체는 처음 요청된 경우에만 초기화됩니다.

단일 책임 원칙을 위반합니다. 이 패턴은 두가지 문제를 같이 해결합니다.

❌ 싱글톤 패턴은 프로그램의 구성요소가 서로에 대해 너무 많은 정보를 알 때 잘못된 디자인을 숨길 수 있습니다.

❌ 이 패턴은 다중 스레드가 싱글톤 객체를 여러번 만들지 않게 하기 위해 다중 스레드 환경에서 특별한 처리가 필요합니다.

❌ 많은 테스트 프레임워크가 목 객체를 생성할 때 상속에 의존하기 때문에 싱글톤의 클라이언트 코드를 단위 테스트하기 어려울 수 있습니다. 싱글톤 클래스의 생성자가 프라이빗하고 정적 메소드를 재정의하는 것이 대부분의 언어에서 불가능하므로, 싱글톤을 목(mock)으로 하는 창의적인 방법을 고려해야합니다. 아니면 그냥 테스트를 쓰지 마십시오. 또는 싱글톤 패턴을 사용하지 마십시오.

🐱‍🏍 다른 패턴과의 관계

  • Facade 클래스는 대부분의 경우 싱글톤 facade 객체로 충분하기 때문에 종종 싱글톤으로 변환될 수 있습니다.
  • 어떤 방식으로든 객체의 모든 공유 상태를 하나의 플라이급 객체로 줄이면 Flyweight는 싱글톤과 비슷합니다. 그러나 두 패턴에는 두가지 근본적인 차이점이 있습니다.
    1. Flyweight 클래스는 고유한 상태가 다른 여러 인스턴스를 가질 수 있는 반면, 싱글톤 인스턴스는 하나만 있어야 합니다.
    2. 싱글톤 객체는 변경 가능합니다. 플라이급 객체는 변경할 수 없습니다.
  • 추상 팩토리, 빌더, 프로토타입은 모두 싱글톤으로 구현할 수 있습니다.

</> 코드 예제

나이브 싱글톤 (단일 스레드)

조잡한 싱글톤을 구현하는 것은 꽤 쉽습니다. 생성자를 숨기고 정적 생성 메소드를 구현하면 됩니다.

더보기

📄 Singleton.java: Singleton

package com.test.singleton.example.non_thread_safe;

public final class Singleton{
	private static Singleton instance;
	public String value;
	
	private Singleton(String value){
		//다음 코드는 느린 초기화를 모방합니다.
		try {
			Thread.sleep(1000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		this.value = value;
	}
	
	public static Singleton getInstance(String value){
		if (instance == null) {
			instance = new Singleton(value);
		}
		
		return instance;
	}
}

📄 DemoSingleThread.java: Client code

package com.test.singleton.example.non_thread_safe;

public class DemoSingleThread {
	public static void main(String[] args) {
		System.out.println("같은 값이 보이면, 싱글톤이 재사용된 것입니다 (예!!)" + "\n" +
				"다른 값이 표시되면, 2개의 싱글톤이 생성된 것입니다 (우우;;)" + "\n\n" +
				"결과:" + "\n");
		Singleton singleton = Singleton.getInstance("FOO");
		Singleton anotherSingleton = Singleton.getInstance("BAR");
		System.out.println(singleton.value);
		System.out.println(anotherSingleton.value);
	}
}

📄 OutputDemeSingleThread.txt: Execution results

같은 값이 보이면, 싱글톤이 재사용된 것입니다 (예!!)
다른 값이 표시되면, 2개의 싱글톤이 생성된 것입니다 (우우;;)

결과:

FOO
FOO

나이브 싱글톤 (멀티 스레드)

멀티스레드 환경에서 동일한 클래스가 잘못 작동합니다. 멀티 스레드는 생성 메소드를 동시에 호출하고 싱글톤 클래스의 여러 인스턴스를 가져올 수 있습니다.

더보기

📄 Singleton.java: Singleton

package com.test.singleton.example.non_thread_safe;

public final class Singleton {
	private static Singleton instance;
	public String value;
	
	private Singleton(String value) {
		//다음 코드는 느린 초기화를 모방합니다.
		try {
			Thread.sleep(1000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		this.value = value;
	}
	
	public static Singleton getInstance(String value) {
		if (instance == null) {
			instance = new Singleton(value);
		}
		
		return instance;
	}
}

📄 DemoMultiThread.java: Client code

package com.test.singleton.example.non_thread_safe;

public class DemoMultiThread {
	public static void main(String[] args) {
		System.out.println("같은 값이 보이면, 싱글톤이 재사용된 것입니다 (예!!)" + "\n" +
				"다른 값이 표시되면, 2개의 싱글톤이 생성된 것입니다 (우우;;)" + "\n\n" +
				"결과:" + "\n");
		Thread threadFoo = new Thread(new ThreadFoo());
		Thread threadBar = new Thread(new ThreadBar());
		threadFoo.start();
		threadBar.start();
	}
	
	static class ThreadFoo implements Runnable {

		@Override
		public void run() {
			Singleton singleton = Singleton.getInstance("FOO");
			System.out.println(singleton.value);
		}
	}
	
	static class ThreadBar implements Runnable {

		@Override
		public void run() {
			Singleton singleton = Singleton.getInstance("BAR");
			System.out.println(singleton.value);
		}
	}
}

📄 OutputDemoMultiThread.txt: Execution results

같은 값이 보이면, 싱글톤이 재사용된 것입니다 (예!!)
다른 값이 표시되면, 2개의 싱글톤이 생성된 것입니다 (우우;;)

결과:

FOO
BAR

게으른 로딩에 대한 스레드 안전 싱글톤

이 문제를 해결하려면, 싱글톤 객체를 처음 만드는 동안 스레드를 동기화해야 합니다.

더보기

📄 Singleton.java: Singleton

package com.test.singleton.example.non_thread_safe;

public final class Singleton {
	//필드는 이중 점검 잠금이 올바르게 작동하도록 휘발성으로 선언되어야 합니다.
	private static volatile Singleton instance;
	public String value;
	
	private Singleton(String value) {
		this.value = value;
	}
	
	public static Singleton getInstance(String value) {
		//여기서 취한 접근 방식을 이중 점검 잠금(double-checked locking, DCL)이라고 합니다.싱글톤 인스턴스를 동시에 얻으려 시도하는 멀티 스레드 간 경쟁 상태를 방지하여 결과적으로 별도의 인스턴스를 만듭니다.
		//
		//여기에 'result' 변수를 갖는게 완전히 무의미해 보일 수 있습니다. 그러나 자바에서 이중 점검 잠금을 구현할 때 이 로컬 변수를 도입해 해결해야하는 매우 중요한 경고가 있습니다.
		//
		//DCL 이슈에 대해 여기서 더 읽을 수 있습니다.
		//https://en.wikipedia.org/wiki/Double-checked_locking#Usage_in_Java
		Singleton result = instance;
		if (result != null) {
			return result;
		}
		
		synchronized(Singleton.class) {
			if (instance == null) {
				instance = new Singleton(value);
			}
			return instance;
		}
	}
}

📄 DemoMultiThread.java: Client code

package com.test.singleton.example.non_thread_safe;

public class DemoMultiThread {
	public static void main(String[] args) {
		System.out.println("같은 값이 보이면, 싱글톤이 재사용된 것입니다 (예!!)" + "\n" +
				"다른 값이 표시되면, 2개의 싱글톤이 생성된 것입니다 (우우;;)" + "\n\n" +
				"결과:" + "\n");
		Thread threadFoo = new Thread(new ThreadFoo());
		Thread threadBar = new Thread(new ThreadBar());
		threadFoo.start();
		threadBar.start();
	}
	
	static class ThreadFoo implements Runnable {

		@Override
		public void run() {
			Singleton singleton = Singleton.getInstance("FOO");
			System.out.println(singleton.value);
		}
	}
	
	static class ThreadBar implements Runnable {

		@Override
		public void run() {
			Singleton singleton = Singleton.getInstance("BAR");
			System.out.println(singleton.value);
		}
	}
}

📄 OutputDemoMultiThread.txt: Execution results

같은 값이 보이면, 싱글톤이 재사용된 것입니다 (예!!)
다른 값이 표시되면, 2개의 싱글톤이 생성된 것입니다 (우우;;)

결과:

BAR
BAR
블로그 이미지

uchacha

개발자 일지

,