Dev/python

[python] __new__ 메서드를 활용하여 싱글턴 패턴 구현하기

bskyvision.com 2023. 10. 9. 14:03

파이썬에서 클래스를 정의할 때 __로 시작하여 __로 끝나는 메서드들을 매직 메서드라고 부릅니다. 우리가 자주 만나는 매직 메서드에는 __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  

[3] https://velog.io/@seongwon97/%EC%8B%B1%EA%B8%80%ED%86%A4Singleton-%ED%8C%A8%ED%84%B4%EC%9D%B4%EB%9E%80