[python] __new__ 메서드를 활용하여 싱글턴 패턴 구현하기
파이썬에서 클래스를 정의할 때 __로 시작하여 __로 끝나는 메서드들을 매직 메서드라고 부릅니다. 우리가 자주 만나는 매직 메서드에는 __init__(), __str__(), __repr__() 등이 있습니다.
오늘은 매직 메서드 중 하나인 __new__() 메서드에 대해 알아보도록 하겠습니다. 또한 __new__() 메서드를 활용하여 싱글턴 패턴을 구현해보겠습니다.
__new__() 메서드
__new__() 메서드는 항상 __init__() 메서드보다 먼저 실행됩니다.
class Person:
def __new__(cls, *args, **kwargs):
print("__new__ 호출")
print(cls)
print(args)
print(kwargs)
obj = super().__new__(cls)
return obj
def __init__(self, name="홍길동", age=0):
print("__init__ 호출")
print(self)
self.name = name
self.age = age
p1 = Person("이순신", 39)
# __new__ 호출
# <class '__main__.Person'>
# ('이순신', 39)
# {}
# __init__ 호출
# <__main__.Person object at 0x102599690>
print(p1)
# <__main__.Person object at 0x102599690>
위 예제의 결과 로그를 보시면 __new__() 메서드가 __init__() 메서드보다 먼저 호출된 것을 확인하실 수 있습니다.
그리고 __new__() 메서드가 호출되었을 때는 객체가 생성되지 않고, __init__() 메서드가 호출되었을 때 메모리 상에 객체가 생성되는 것도 확인할 수 있습니다.
__new__() 메서드로 싱글턴 패턴 구현하기
그런데 왜 굳이 __new__() 메서드를 알아야 하냐고 반문하시는 분들도 계실 것 같습니다. 사실 많은 경우에는 몰라도 별 문제가 없습니다. 하지만, 클래스로 생성될 객체의 수에 제한을 줘야하는 경우에는 __new__() 메서드를 유용하게 사용할 수 있습니다.
싱글턴(singleton) 패턴은 어떤 클래스의 객체를 한번 생성하면 다시 그 클래스로 객체 생성을 시도하더라도 다시 새로운 객체를 생성하지 않고 이미 생성된 객체를 활용하게 하는 디자인 패턴입니다.
일반적으로 클래스로 객체를 찍어내면 생성할 때마다 새로운 객체가 생성됩니다. 새로운 객체가 생성되었다는 것은 메모리 상 다른 곳에 새로운 객체가 존재한다는 뜻입니다. id() 함수로 생성된 객체의 주소를 살펴보면 다른 것을 알 수 있습니다.
class Person:
pass
p1 = Person()
p2 = Person()
print(id(p1)) # 4347071824
print(id(p2)) # 4347071888
그런데 만약 Person이라는 클래스로 생성된 객체는 단 한 개만 존재해야 한다면, Sigleton 클래스를 만든 후 그것을 상속받아서 Person 클래스를 만들면 됩니다.
class Singleton:
def __new__(cls, *args, **kwargs):
print("__new__() 호출")
if not hasattr(cls, "_instance"):
cls._instance = super().__new__(cls)
return cls._instance
class Person(Singleton):
def __init__(self, name, age):
print("Person 클래스 __init__() 메서드 호출")
self.name = name
self.age = age
p1 = Person("심교훈", 35)
# __new__() 호출
# Person 클래스 __init__() 메서드 호출
p2 = Person("문태호", 36)
# __new__() 호출
# Person 클래스 __init__() 메서드 호출
p3 = Person("황병일", 36)
# __new__() 호출
# Person 클래스 __init__() 메서드 호출
print(id(p1)) # 4374797136
print(id(p2)) # 4374797136
print(id(p3)) # 4374797136
print(p1.name) # 황병일
print(p2.name) # 황병일
print(p3.name) # 황병일
print(p1 is p2) # True
print(p2 is p3) # True
print(p3 is p1) # True
위 결과에서 확인할 수 있듯이 Person 클래스로 찍어낸 p1, p2, p3는 모두 동일한 객체임을 알 수 있습니다. 새롭게 생성을 시도해도 속성만 바뀔 뿐 메모리 상 위치는 동일합니다.
메모리 내 객체 개수 비교
gc 모듈을 활용하여 현재 메모리에서 관리 중인 객체를 확인해보겠습니다. 우선 Person 클래스를 정의할 때 Singleton 클래스를 상속받지 않은 상태로 확인해보겠습니다.
import gc
class Person:
def __init__(self, name, age):
print("Person 클래스 __init__() 메서드 호출")
self.name = name
self.age = age
p1 = Person("심교훈", 35)
p2 = Person("문태호", 36)
p3 = Person("황병일", 36)
print(id(p1)) # 4367194448
print(id(p2)) # 4367195984
print(id(p3)) # 4367196048
print(p1.name) # 심교훈
print(p2.name) # 문태호
print(p3.name) # 황병일
print(p1 is p2) # False
print(p2 is p3) # False
print(p3 is p1) # False
gc.collect()
objects = gc.get_objects()
print(f"메모리 내 객체 수: {len(objects)}") # 메모리 내 객체 수: 4738
메모리 내 객체 수는 4738개 입니다.
이번에는 싱글턴 패턴을 적용한 후 객체 수를 살펴보겠습니다.
import gc
class Singleton:
def __new__(cls, *args, **kwargs):
print("__new__() 호출")
if not hasattr(cls, "_instance"):
cls._instance = super().__new__(cls)
return cls._instance
class Person(Singleton):
def __init__(self, name, age):
print("Person 클래스 __init__() 메서드 호출")
self.name = name
self.age = age
p1 = Person("심교훈", 35)
p2 = Person("문태호", 36)
p3 = Person("황병일", 36)
print(id(p1)) # 4365818256
print(id(p2)) # 4365818256
print(id(p3)) # 4365818256
print(p1.name) # 황병일
print(p2.name) # 황병일
print(p3.name) # 황병일
print(p1 is p2) # True
print(p2 is p3) # True
print(p3 is p1) # True
gc.collect()
objects = gc.get_objects()
print(f"메모리 내 객체 수: {len(objects)}") # 메모리 내 객체 수: 4736
4736개로 딱 2개만큼 생성된 객체의 수가 적음을 확인할 수 있습니다.
4738 - 4736 = 2
3개의 객체가 생성되지 않고 1개의 객체가 재사용된 것이므로 싱글턴 패턴이 잘 적용된 것입니다.
참고자료
[1] https://weeklyit.code.blog/2019/12/24/2019-12%EC%9B%94-3%EC%A3%BC-python%EC%9D%98-__init__%EA%B3%BC-__new__/.
[2] https://wikidocs.net/69361