싱글턴(Singleton) 패턴 : 기초The Singleton Pattern: The Basics

파이썬에서 싱글턴 패턴의 다양한 구현 방법Different ways to implement the singleton pattern in Python

written by tiaz0128

싱글턴(Singleton)

생성자가 여러 차례 호출되더라도 실제로 생성되는 객체는 하나이고 최초 생성 이후에 호출된 생성자는 최초의 생성자가 생성한 객체를 리턴한다. ~ 위키백과 ~

싱글턴(Singleton) 패턴은 특정 클래스의 인스턴스가 프로그램 전체에서 단 하나만 생성되도록 보장하는 패턴입니다.

그냥 전역 변수로 쓰면 안돼?

전역 변수를 쓰지 않고 싱글턴 패턴을 쓰는 가장 큰 이유는 접근 제어 때문입니다.

  • 전역 변수: 전역 변수는 애플리케이션의 어느 곳에서나 직접 접근하고 수정할 수 있습니다. 이는 데이터의 무결성을 보장하기 어렵게 만들고, 예기치 않은 변경으로 인한 버그의 원인이 될 수 있습니다.
  • 싱글턴 패턴: 싱글턴 패턴을 사용하면, 인스턴스의 생성과 접근을 제어하는 메서드를 통해 객체에 접근합니다. 이는 객체의 상태와 생명주기를 더 잘 제어할 수 있게 해주며, 데이터의 무결성을 유지하는 데 도움이 됩니다.

언제 써야 하나

굳이 여러번 만들어질 필요가 없는 객체를 생각하면 됩니다. 대표적인 경우는 아래와 같습니다.

  • 데이터베이스 연결
  • 로깅
  • 설정 정보
  • 캐시

파이썬에서 싱글턴 구현하기

파이썬에서 여러가지 방법으로 싱글턴을 구현 할 수 있습니다. 여기에서는 5가지 방법으로 싱글턴을 구현해보고 각각의 특성을 알아보겠습니다.

  1. GoF(Gang of Four)
  2. __new__
  3. metaclass
  4. Eager
  5. Thread Safe

싱글턴 : GoF(Gang of Four)

클래스 다이어그램

%%{
  init: {
    'theme': 'base',
    'themeVariables': {
      'primaryColor': '#2a3844',
      'lineColor': '#fff',
      'primaryTextColor': '#fff',
      'tertiaryColor': '#fff'
    }
  }
}%%

classDiagram
    direction BT

    class Singleton{
        -_instance: Singleton

        +get_instance() Singleton
    }

구현

가장 클래식한 방법으로 GoF에서 말하는 패턴입니다.

  1. __init__ 메서드를 Override해서 사용하지 못하게 한다.
  2. 클래스 메서드 get_instance를 호출하여 인스턴스를 생성 또는 이미 만들어져 있는 인스턴스를 리턴 한다.
gof/singleton.py
from typing import Self

class Singleton:
    _instance = None

    def __init__(self) -> None:
        raise RuntimeError("Call instance() method")

    @classmethod
    def get_instance(cls) -> Self:
        if not cls._instance: 
            cls._instance = super().__init__(cls)
        return cls._instance
gof/test_gof_singleton.py
from gof.singleton import Singleton

def test_gof_singleton():
    obj_1 = Singleton.get_instance()
    obj_2 = Singleton.get_instance()

    assert obj_1 is obj_2

싱글턴 : __new__

클래스 다이어그램

%%{
  init: {
    'theme': 'base',
    'themeVariables': {
      'primaryColor': '#2a3844',
      'lineColor': '#fff',
      'primaryTextColor': '#fff',
      'tertiaryColor': '#fff'
    }
  }
}%%

classDiagram
    direction BT

    class Singleton{
        -_instance: Singleton

        +__new__() Singleton
    }

구현

파이썬에서 가장 일반적으로 구현할 수 있는 방법입니다.

  1. __new__ 메서드를 오버라이드(Override) 한다.
  2. __new__ 메서드에서 인스턴스를 생성 또는 이미 만들어져 있는 인스턴스를 리턴 한다.
new/singleton.py
from typing import Self

class Singleton:
    _instance = None

    def __new__(cls) -> Self:
        if not cls._instance:
            cls._instance = super().__new__(cls)

        return cls._instance
new/test_new_singleton.py
from new.singleton import Singleton

def test_new_singleton():
    obj_1 = Singleton()
    obj_2 = Singleton()

    assert obj_1 is obj_2

싱글턴 : 메타클래스(metaclass)

메타클래스(metaclass)는 클래스의 클래스입니다. 즉, 메타클래스는 클래스를 생성하는 것입니다. 파이썬에서 모든 것은 객체이며, 클래스도 객체입니다. 따라서 클래스를 생성하는 것 역시 객체인데, 이를 메타클래스라고 합니다.

사용자 정의 메타클래스를 만드는 방법은 크게 두 가지가 있습니다.

  1. type을 상속받아서 메타클래스를 만들고 메서드를 오버라이드
  2. type을 사용하여 동적으로 클래스를 생성하는 방식

클래스 다이어그램

%%{
  init: {
    'theme': 'base',
    'themeVariables': {
      'primaryColor': '#2a3844',
      'lineColor': '#fff',
      'primaryTextColor': '#fff',
      'tertiaryColor': '#fff'
    }
  }
}%%

classDiagram
    direction BT

    class SingletonMeta{
        - _instances: dict

        - __call__() Singleton
    }

    class Singleton{
        + some_business_logic()
    }

    Singleton --|> SingletonMeta

구현

type을 상속하여 메타클래스를 만드는 방법을 이용하여 싱글턴 패턴을 만들 수 있습니다.

  1. type 을 상속하는 metaclass를 정의한다.
  2. __call__ 메서드를 오버라이드 한다.
  3. 만들고 싶은 싱글턴 클래스에 metaclass를 지정 합니다.
meta/meta.py
import logging

class SingletonMeta(type):
    _instances = {}

    def __call__(cls, *args, **kwargs):
        logging.info("metaclass __call__")

        if cls not in cls._instances:
            instance = super().__call__(*args, **kwargs)
            cls._instances[cls] = instance
        
        return cls._instances.get(cls)
meta/singleton.py
from meta.meta import SingletonMeta

class Singleton(metaclass=SingletonMeta):
    def some_business_logic(self):
        ...
meta/test_meta_singleton.py
from meta.singleton import Singleton

def test_meta_singleton():
    obj_1 = Singleton()
    obj_2 = Singleton()

    assert obj_1 is obj_2

metaclass=를 사용해서 해당 클래스의 생성 과정에 개입하는 클래스를 지정 한다고 생각하면 됩니다. 여기서는 __call__ 메서드를 오버라이드 했기 때문에 객체가 생성되는 시점에 SingletonMeta 메타클래스의 __call__ 메서드가 호출 되는 것입니다.

INFO     root:singleton_meta.py:8 metaclass __call__
INFO     root:singleton_meta.py:8 metaclass __call__

앞서 본 싱글턴 패턴은 클래스 자체에 싱글턴을 구현한 반면, 메타클래스는 상속하는 형태로 코드를 작성 할 수 있습니다. 보다 객체지향스럽게 코드를 작성하는데 도움이 됩니다.

마무리

지금까지 싱글턴 패턴의 기본 개념과 파이썬에서 구현할 수 있는 세 가지 방법에 대해 알아보았습니다. 😊

  • GoF 스타일의 전통적인 구현 방식
  • Python의 __new__ 메서드를 활용한 방식
  • 메타클래스를 활용한 객체지향적 구현 방식

각각의 구현 방식은 저마다의 특징이 있습니다. GoF 방식은 가장 전통적이지만 직관적이며, __new__ 방식은 파이썬스러운 구현이 가능합니다. 메타클래스를 활용한 방식은 상속을 통해 재사용성이 높은 코드를 작성할 수 있다는 장점이 있습니다.

다음으로

다음글에서 계속 됩니다.

싱글턴(Singleton) 패턴 : 심화에서는 남은 두 가지 구현 방식인 Eager Initialization과 Thread Safe한 싱글턴 구현에 대해 자세히 다루도록 하겠습니다. 특히 멀티스레드 환경에서 안전하게 싱글턴을 구현하는 방법에 대해 깊이 있게 살펴보겠습니다.

Singleton

Even if the constructor is called multiple times, only one object is actually created, and constructors called after the first creation return the object created by the first one. ~ Wikipedia ~

The singleton pattern guarantees that only one instance of a given class is ever created across the whole program.

Why not just use a global variable?

The biggest reason to use the singleton pattern instead of a global variable is access control.

  • Global variable: A global variable can be accessed and modified directly from anywhere in the application. That makes data integrity hard to guarantee and can cause bugs through unexpected changes.
  • Singleton pattern: With the singleton pattern, you access the object through a method that controls how the instance is created and accessed. This gives you better control over the object’s state and lifecycle, and helps keep the data intact.

When should you use it?

Think of objects that have no reason to be created more than once. Typical cases include:

  • Database connections
  • Logging
  • Configuration
  • Caches

Implementing a singleton in Python

There are several ways to implement a singleton in Python. Here, we’ll implement it in five ways and look at the characteristics of each.

  1. GoF(Gang of Four)
  2. __new__
  3. metaclass
  4. Eager
  5. Thread Safe

Singleton: GoF (Gang of Four)

Class diagram

%%{
  init: {
    'theme': 'base',
    'themeVariables': {
      'primaryColor': '#2a3844',
      'lineColor': '#fff',
      'primaryTextColor': '#fff',
      'tertiaryColor': '#fff'
    }
  }
}%%

classDiagram
    direction BT

    class Singleton{
        -_instance: Singleton

        +get_instance() Singleton
    }

Implementation

This is the most classic approach, the pattern as described by the GoF.

  1. Override the __init__ method so it can’t be used.
  2. Call the class method get_instance to create the instance, or return the one that already exists.
gof/singleton.py
from typing import Self

class Singleton:
    _instance = None

    def __init__(self) -> None:
        raise RuntimeError("Call instance() method")

    @classmethod
    def get_instance(cls) -> Self:
        if not cls._instance: 
            cls._instance = super().__init__(cls)
        return cls._instance
gof/test_gof_singleton.py
from gof.singleton import Singleton

def test_gof_singleton():
    obj_1 = Singleton.get_instance()
    obj_2 = Singleton.get_instance()

    assert obj_1 is obj_2

Singleton: __new__

Class diagram

%%{
  init: {
    'theme': 'base',
    'themeVariables': {
      'primaryColor': '#2a3844',
      'lineColor': '#fff',
      'primaryTextColor': '#fff',
      'tertiaryColor': '#fff'
    }
  }
}%%

classDiagram
    direction BT

    class Singleton{
        -_instance: Singleton

        +__new__() Singleton
    }

Implementation

This is the most common way to implement it in Python.

  1. Override the __new__ method.
  2. In __new__, create the instance, or return the one that already exists.
new/singleton.py
from typing import Self

class Singleton:
    _instance = None

    def __new__(cls) -> Self:
        if not cls._instance:
            cls._instance = super().__new__(cls)

        return cls._instance
new/test_new_singleton.py
from new.singleton import Singleton

def test_new_singleton():
    obj_1 = Singleton()
    obj_2 = Singleton()

    assert obj_1 is obj_2

Singleton: metaclass

A metaclass is the class of a class. In other words, a metaclass is what creates classes. In Python, everything is an object, and classes are objects too. So the thing that creates a class is itself an object, and that’s what we call a metaclass.

There are two main ways to create a custom metaclass:

  1. Inherit from type to make a metaclass and override its methods
  2. Use type to create classes dynamically

Class diagram

%%{
  init: {
    'theme': 'base',
    'themeVariables': {
      'primaryColor': '#2a3844',
      'lineColor': '#fff',
      'primaryTextColor': '#fff',
      'tertiaryColor': '#fff'
    }
  }
}%%

classDiagram
    direction BT

    class SingletonMeta{
        - _instances: dict

        - __call__() Singleton
    }

    class Singleton{
        + some_business_logic()
    }

    Singleton --|> SingletonMeta

Implementation

You can build the singleton pattern by creating a metaclass that inherits from type.

  1. Define a metaclass that inherits from type.
  2. Override the __call__ method.
  3. Set the metaclass on the class you want to make a singleton.
meta/meta.py
import logging

class SingletonMeta(type):
    _instances = {}

    def __call__(cls, *args, **kwargs):
        logging.info("metaclass __call__")

        if cls not in cls._instances:
            instance = super().__call__(*args, **kwargs)
            cls._instances[cls] = instance
        
        return cls._instances.get(cls)
meta/singleton.py
from meta.meta import SingletonMeta

class Singleton(metaclass=SingletonMeta):
    def some_business_logic(self):
        ...
meta/test_meta_singleton.py
from meta.singleton import Singleton

def test_meta_singleton():
    obj_1 = Singleton()
    obj_2 = Singleton()

    assert obj_1 is obj_2

Think of metaclass= as naming the class that steps into the creation process of this class. Here, because we overrode the __call__ method, the SingletonMeta metaclass’s __call__ method is called at the moment the object is created.

INFO     root:singleton_meta.py:8 metaclass __call__
INFO     root:singleton_meta.py:8 metaclass __call__

Whereas the earlier singletons implement the pattern inside the class itself, a metaclass lets you write the code in an inheritance style. That helps you write more object-oriented code.

Wrapping up

So far, we’ve covered the basic concept of the singleton pattern and three ways to implement it in Python. 😊

  • The traditional GoF-style implementation
  • An implementation using Python’s __new__ method
  • An object-oriented implementation using a metaclass

Each implementation has its own characteristics. The GoF approach is the most traditional yet intuitive, and the __new__ approach allows a Pythonic implementation. The metaclass approach has the advantage of letting you write highly reusable code through inheritance.

What’s next

Continued in the next post.

In The Singleton Pattern: Advanced, we’ll cover the remaining two implementations in detail: Eager Initialization and a Thread Safe singleton. In particular, we’ll take an in-depth look at how to implement a singleton safely in a multithreaded environment.

python 디자인 패턴 싱글턴

tiaz0128

Eat Sleep Coding.

Never Never GiveUp.

Security  |  BackEnd  |  Multi Cloud