Singleton Pattern
- 글로벌하게 접근 가능한 한 개의 객체만을 생성하는 패턴
사용 이유
- 로깅, DB작업, 프린터 스풀러 등 어플리케이션 리소스에 대한 동시 요청 충돌을 막기 위해 한개의 인스턴스만 생성
- ex) 일관성을 위해 DB 접근 객체를 하나만 생성
- 클래스에 대한 단일 전역 객체 제공
- 공유된 리소스에 대한 동시 제어
1. 구현 방법
- Constructor를 private으로 선언
- 객체를 초기화 하는 static함수, 객체를 return하는 static함수 구현
Python에서 구현
class Singleton(object):
def __new__(cls):
if not hasattr(cls, 'instance'):
cls.instance = super(Singleton, cls).__new__(cls)
return cls.instance
s = Singleton()
print("Object created", s)
s1 = Singleton()
print("Object created", s1)
super(Singleton, cls):Singleton클래스 자신의 부모에 접근합니다ㅣhasattr을 통해 클래스 자체에서instance를 갖고 있는지 확인한 후 없으면instance객체를 생성합니다.cls: 클래스 그 자체를 의미하는 변수
실행 결과

- 매 요청마다 같은 객체를 반환
__init__과 self를 사용한다면
class Singleton(object):
def __init__(self):
if not hasattr(self, 'instance'):
self.instance = super(Singleton, self).__init__()
return self.instance
s = Singleton()
print("Object created", s)
s1 = Singleton()
print("Object created", s1)
실행 결과

- 매 요청마다 다른 객체를 반환
2. Lazy Initialization
- 클래스를 생성하면서 필요하지 않은 시점에 객체가 미리 만들어 질 수 있다.
- 객체가 꼭 필요한 시점에만 객체를 생성
- 클래스를 초기화 한 후 함수를 통해 객체를 생성
Python에서 구현
class Singleton:
__instance = None
def __init__(self):
if not Singleton.__instance:
print("__init__ method called...")
else:
print("Instance already created:", self.getInstance())
@classmethod
def getInstance(cls):
if not cls.__instance:
cls.__instance = Singleton()
return cls.__instance
s = Singleton() # 클래스 초기화, 객체 생성 X
Singleton.getInstance() # 객체 생성
s1 = Singleton().getInstance() # getInstance를 통해 싱글턴 객체에 접근
print(s1)
s2 = Singleton()
print(s2)
__instance: 클래스 전역 객체classmethod인getInstance를 통해 객체 생성, 접근
실행 결과

s2에 대해…
- 위 코드에서
getInstance()를 통해 반환 받은 객체의 주소는 0x103cf8208 입니다. - s1을 print하면 동일한 주소에 대한 객체가 반환됩니다.
- 반면 s2는
Singleton생성자를 통해 반환받은 객체를 쥐고 있으며 s1이 담고 있는 객체의 주소와는 다릅니다. - s2는 정확하게 말하면
getInstance를 통해 생성된 즉, 싱글턴 패턴을 통해 생성된 객체가 아닌 이름이Singleton인 클래스의 생성자를 통해 생성된 객체입니다. - 두 객체는 완전히 다른 객체이며, 싱글턴 패턴을 통해 생성된 객체에 접근하기 위해선
getInstance메서드를 통해 접근해야 합니다.
Java에서 구현
public class Singleton {
private static Singleton instance = null;
private Singleton(){} // 외부에서 생성자에 접근 불가능
public static Singleton getInstance(){ // Singleton instance 반환
if(instance == null){
instance = new Singleton();
}
return instance;
}
public void printInstance(){
System.out.println("Singletone instance");
}
public static void main(String[] args) {
Singletone singleton = Singletone.getInstance();
singletone.printInstance();
}
}
- 생성자에 접근하지 못하게 한 후
getInstance를 통해서만 객체에 접근 static이기 때문에 매 번 같은 객체를 반환
Python vs Java
- python은 클래스 자체를 제어하는 것이 가능하므로 생성자를 실행한 후
classmethod를 통해 전역 객체 생성 - Java는
static메서드를 통해 전역 객체를 생성
Module Singleton
- 파이썬에서 import방식에 의해 모든 모듈은 싱글톤입니다.
작동 방식
- Python은 모듈이 import되었는지 확인
- 됐다면, 해당 객체를 반환하고 아니면 import하여 instance화
- 모듈은 import하는 순간 초기화. 하지만, 같은 모듈을 import하면 초기화 하지 않는다.
3. Monostate Singleton Pattern
- 모든 객체가 같은 상태를 공유하는 패턴
- 한 객체의 데이터의 유일성을 보장할 수 있는 방법
- 객체를 파생해도 동일한 상태를 공유
단점
- 객체가 사용되지 않더라도 메모리 공간을 차지한다
- 생성과 소멸이 잦으며, 많은 비용이 소모된다.
구현 예시
class Borg:
__shared_state = {"1": "2"}
def __init__(self):
self.x = 1
self.__dict__ = {}
b1 = Borg()
b2 = Borg()
b2.x = 4
print("b1 is :", b1)
print("b2 is :", b2)
print("b1 x: ", b1.x)
print("b2 x: ", b2.x)
print("b1 dict: ", b1.__dict__)
print("b2 dict: ", b2.__dict__)
__dict__: 클래스 내부 속성 변수를 dictionary 관리하는 변수self.x를 선언해도self.__dict__를 사용하면 속성변수x에 접근할 수 없게된다.- 이후 b2.x = 4 를 통해 x 변수를 dictionary로 관리
- 해당 코드에서
__shared_state에 있는 데이터를 dictionary로 관리하게 되고 모든 인스턴스에서 공유하게 된다.
실행 결과

__new__를 사용한 구현
class Book(object):
_shared_state = {}
def __new__(cls, *args, **kwargs):
obj = super(Book, cls).__new__(cls, *args, **kwargs)
obj.__dict__ = cls._shared_state
print(obj)
return obj
book1 = Book() # 다른 객체이지만 상태를 공유한다.
book2 = Book()
book1.x = 1
book2.y = 2
print(book1.__dict__)
print(book2.__dict__)
실행 결과

4. Singleton and Meta class
Meta class
- 클래스의 클래스
- 클래스 그 자체는 메타 클래스의 인스턴스
- 이미 정의된 클래스를 통해 새로운 형식의 클래스 생성 가능
- 상속과 유사한 기능처럼 보인다
Meta class vs Inheritance
- 보통 메타 클래스는 OOP의 제약을 벗어난 구현을 위해 사용
- 한 객체가 메타 클래스로 부터 받은 method를 호출해도 메타 클래스의 해당 메서드를 찾지 않는다.
- 다만 객체가 생성될 시에 메타 클래스가 미리 생성
- python이 인터프리터를 사용하기에 가능
- 상속과 다르게 부모-자식관계로 묶여 있지 않으며, 서로 다른 객체
- OOP의 제약을 벗어난 극도로 dynamic한 프로그래밍 시에 사용 추천
예시

- python에서 모든 것은 객체이다
- type 클래스가 int 클래스의 메타클래스
- int가 type을 재정의
python에서 클래스
- 기본적으로 python에서
class를 사용해 정의한 클래스는 클래스 이면서 객체이다 - 클래스 그 자체이기도 하지만 동시에 객체이기도 하다
type을 통해 클래스의 자료형을 확인
class Car:
pass
print(type(Car))
# 출력 결과
<class 'type'>
- python에서 모든 클래스는 클래스 이며
type이라는 클래스의 객체이기도 하다
type
type은 python에서 자료형을 확인하는 함수이지만, 또 다른 기능으로 클래스를 생성하는 기능이 있다.- 인자를 보면 type(
name,bases,dict)의 인자를 받는다- name: 클래스 명
- base: 베이스 클래스
- dict: 속성값
- 인자로 클래스를 이루는 정의를 받아 클래스를 반환
- 그리고 이 클래스는 객체가 되기도 한다
- class Car 라는 코드는 사실상
type이 실행되어 클래스(이면서 객체)를 반환하는 과정을 거친다. - 메타 클래스는 클래스의 클래스 이며 클래스를 생성하는 클래스이다.
- 즉, 메타 클래스는 클래스 생성을 제어할 수 있다.
type은 메타 클래스이며, 클래스를 생성하는 메타클래스이다.
type을 통한 클래스 및 인스턴스 생성
# type을 통해 Wing 클래스를 생성하고 A에 저장
A = type("Wing", (), {"x": 1})
print(A)
print(A.__dict__)
# A를 통해 Wing class의 인스턴스를 생성 후 a1에저장, 값을 확인
a1 = A()
print(a1.x)
출력 결과

그래서…
- 메타 클래스를 통해 클래스와 객체 생성을 제어할 수 있으며, 이는 싱글톤을 생성하는 용도로 사용할 수 있다는 것과 같다
메타 클래스를 통한 싱글턴 생성
class MetaSingleton(type):
_instances = {}
# 클래스를 함수처럼 사용할 때 호출되는 메서드
# ex) a = A(), a()
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super(MetaSingleton, cls).__call__(*args, **kwargs)
return cls._instances[cls]
class Book(metaclass=MetaSingleton):
pass
book1 = Book()
book2 = Book()
print(book1)
print(book2)
출력 결과

5. Examples
데이터베이스
- 여러 서비스가 한 개의 DB를 공유하는 구조
- 안정된 서비스를 위해
- DB의 일관성을 보존해야 하며, 연산간 충돌이 없어야 한다.
- 다수의 DB 연산을 처리하려면 메모리와 CPU를 효율적으로 사용해야 한다.
싱글턴을 통해 하나의 DB 접속 객체 생성
import sqlite3
# 객체를 싱글턴으로 만드는 역할
class MetaSingleton(type):
_instances = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super(MetaSingleton, cls).__call__(*args, **kwargs)
return cls._instances[cls]
# MetaSingleton으로 인해 1개의 Database 객체만 생성
class Database(metaclass=MetaSingleton):
connection = None
def connect(self):
if self.connection is None:
self.connection = sqlite3.connect("db.sqlite3")
self.cursorobj = self.connection.cursor()
return self.cursorobj
db1 = Database().connect()
db2 = Database().connect()
print(db1)
print(db2)
MetaSingleton메타 클래스에 의해Database객체는 싱글턴으로 생성- 웹 앱이 DB 요청을 할 때 마다
Database클래스 객체를 한개만 생성하여 DB 동기화를 보장- 리소스를 하나만 사용하여 CPU, 메모리 효율적 사용
인프라 상태 확인
class StatusCheck:
_instance = None
# 싱글턴으로 StatueCheck 객체 생성
# 클래스 메서드인 __new__를 사용
def __new__(cls, *args, **kwargs):
if not StatusCheck._instance:
StatusCheck._instance = super(StatusCheck, cls).__new__(cls, *args, **kwargs)
return StatusCheck._instance
# 싱글턴에서 공동으로 공유하는 자원
def __init__(self):
self._servers = []
def addServer(self):
self._servers.append("Server 1")
self._servers.append("Server 2")
self._servers.append("Server 3")
self._servers.append("Server 4")
def changeServer(self):
self._servers.pop()
self._servers.append("Server 5")
# 동일한 두 객체
status_check1 = StatusCheck()
status_check2 = StatusCheck()
status_check1.addServer()
print("Schedule statue check for servers (1)")
for i in range(4):
print("Checking ", status_check1._servers[i])
status_check2.changeServer()
print("Schedule statue check for servers (2)")
for i in range(4):
print("Checking ", status_check2._servers[i])
실행 결과

- 동일한 객체
status_check1,status_check2에서_servers배열을 조작
6. 정리
싱글턴의 단점
- 같은 객체에 여러 참조자가 있을 수 있다.
- 전역 객체에 종속적인 클래스간 관계가 복잡하며, 전역 객체 수정이 다른 클래스에 영향을 미칠 수 있다.
싱글턴을 사용하는 상황
- 어플리케이션에서 풀, 캐시, 설정 등 한 개의 객체만 필요한 경우에 생성하여 사용
- 글로벌 액세스를 제공해야 하는 경우
- 클래스 객체가 한 개만 필요한 경우에 사용