파이썬 싱글턴 패턴

김세환·2021년 3월 19일
0

파이썬-객체지향

목록 보기
5/5
post-thumbnail

싱글톤

싱글톤 디자인 패턴은 글로벌하게 접근 가능한 하나의 객체를 제공하는 패턴이다.
로깅이나 데이터베이스 관련 작업, 프린터 스풀러와 같은 동일한 리소스에 대한 동시 요청의 충돌을 방지하기 위해 하나의 인스턴스를 공유하는 작업에 주로 사용한다.
예를 들어, 데이터 베이스의 일관성 유지를 위해 DB에 작업을 수행하는 하나의 데이터베이스 객체가 필요한 경우 또는
여러 서비스의 로그를 한개의 로그파일에 순차적으로 동일한 로깅객체에 사용해 남기는 경우에 사용한다 !

싱글톤 패턴의 목적?

  1. 클래스에 대한 단일 객체 생성
  2. 전역 객체 제공
  3. 공유된 리소스에 대한 동시 접근 제어

싱글톤 패턴의 구조

생성자를 private으로 선언하고 객체를 초기화 하는 static 함수를 만들면 간단히 구현 가능하다.

첫 호출에 객체가 생성되고 클래스는 동일한 객체를 계속 반환한다 !

하지만....

파이썬에서는 private으로 생성자(__init__)를 선언 할수가 없다.

따라서 여러 구현방법이 제시되고, 우리는 적절한것을 찾아서 사용하면 된다!

싱글톤 패턴 구현 (자바)

파이썬으로 구현하는 싱글턴을 보기 전에, private으로 생성자를 선언 가능한 Java에서는 싱글톤 패턴을 어떻게 구현하는지 코드로 확인해보자!

public class Singleton {
    private static Singleton singleton = new Singleton();
    // 정적 필드에 Singleton 생성자를 이용해서 singleton 객체를 한번 생성한다

    private Singleton() {};
    // 생성자는 외부 접근이 불가능하게 private 으로 접근제한한다

    static Singleton getInstance() {
        return singleton;
    }
    // 스태틱 메서드로 유일한 인스턴스를 리턴하는 스태틱 메서드를 작성한다.
    //static 리턴타입(싱글턴 객체의 타입이 리턴) getInstance 메서드 이름

    public static void main(String[] args) {
        Singleton a = Singleton.getInstance();
        Singleton b = Singleton.getInstance();

        if (a == b) {
            System.out.println("같은 인스턴스");
        }
        else {
            System.out.println("다른 인스턴스");
        }
    }
}

UML 다이어그램에 있는 그대로 구현하면 끝!

싱글톤 패턴 구현(파이썬)

class Singleton(object):
    """
    하나의 싱글톤 인스턴스를 생성
    이미 생성된 인스턴스가 있다면 재사용
    """
    def __new__(cls, *args, **kwargs):
        """
        *args와 **kwargs는 무슨의미일까?
        여러 가변인자를 받겠다고 명시하는 것이며, *args는 튜플형태로 전달, **kwargs는 키:값 쌍의 사전형으로 전달된다.
        def test(*args, **kwargs):
            print(args)
            print(kwargs)
        
        test(5,10,'hi', k='v')
        결과 : (5, 10, 'hi') {'k': 'v'}
        """
        if not hasattr(cls, 'instance'):
            cls.instance = super(Singleton, cls, *args, **kwargs).__new__(cls, *args, **kwargs)
        return cls.instance

if __name__ == '__main__':
    s = Singleton()
    print("객체 생성", s)
    s1 = Singleton()
    print("객체 생성", s1)

실제 실행 결과는 다음과 같다.

위 코드에서 __new__ 매직메서드를 오버라이딩해 객체를 생서한다.
__new__메서드는 s객체가 이미 존재하는지 확인한 이후, hasattr 함수는 cls객체가 instance 속성을 갖고있는지 확인.
cls.instance 라는 어트리뷰트가 없는 경우에 한해서 생성자를 호출해 객체를 찍어낸다.
cls.instance 가 이미 있을경우, 기존 객체를 재사용한다 !

게으른 초기화 (Lazy instantiation)

게으른 초기화는 싱글톤 패턴을 기반으로 하는 초기화 방식이다.
모듈을 임포트 할 때, 아직 필요하지 않은 시점에 객체를 미리 생성하는 경우가 있다!
이 때 게으른 초기화를 사용하는 것인데.. 게으른 초기화는 인스턴스가 필요한 시점에 생성하는 방법이다!

코드를 통해 확인해보자

class Singleton(object):
    __instance = None

    def __init__(self):
        if not Singleton.__instance:
            print("객체가 아직 없음 !")
        else:
            print("객체가 이미 생성되어있음 !", self.get_instance())

    @classmethod
    def get_instance(cls):
        if not cls.__instance:
            cls.__instance = Singleton()
        return cls.__instance


if __name__ == "__main__":
    s = Singleton()  # 클래스를 초기화는 했지만.. 객체를 생성하진 않았다.
    print("객체 생성!", Singleton.get_instance())
    s1 = Singleton()

결과 !

실제 나의 적용 사례..

작년 여름 산업용 게이트웨이인 Revolution Pi용 모니터링 어플리케이션(웹)을 개발했을 때의 일이다.

잘 개발하고 나서 테스트를 해보면, 5~10분 뒤에 어플리케이션 서버가 뻗어버리는 이슈가 있었다.

나의 미숙한 코드개발로 인해서 데이터를 수집하는 쪽 객체가 계속해서 생겨나고 있었고 메모리 누수로 인해 어플리케이션 서버가 뻗어버리는 현상이였다.

이 때 어디서 주워들은 싱글턴패턴이 기억나서 적용해보았는데, 매우 잘 해결되었던 기억이 있다!

그 코드를 확인해보자 !

import revpimodio2
import json
import os
from util import get_profile

TEST_JSON = {
    "sensor_list":{
        "InputValue_1" : "차압계",
        "InputValue_2" : "송풍기",
        "InputValue_3" : "배풍기",
        "RTDValue_1" : "온도"
    },
    "IMGPATH" : "/dev/piControl0",
    "data_information" : {
        "InputValue_1" : {
            "originalRange" : [4000, 20000],
            "changedRange" : [4, 20]
        },
        "InputValue_2" : {
            "originalRange" : [4000, 20000],
            "changedRange" : [4, 20]
        },
        "InputValue_3" : {
            "originalRange" : [4000, 20000],
            "changedRange" : [4, 20]
        },
        "RTDValue_1" : {
            "originalRange" : [0, 10000],
            "changedRange" : [0, 100]
        }
    }
} 

#for Test
PROFILE = None

def singleton(cls):
    instances = {}
    def getinstance():
        if cls not in instances:
            instances[cls] = cls()
        return instances[cls]
    return getinstance

@singleton
class RevolutionPi:

    def __init__(self):
        self.profile_path = "/home/pi/ksg_edge_deploy/socket_project/config.json" #revpi edge path
        self._profile = get_profile(self.profile_path)
        self.image_path = self._profile.get("IMGPATH")
        self.sensor_profile = self._profile.get("sensor_list")
        self.normalization_profile = self._profile.get("data_information")
        self.sampling_time = 0.02 #20ms
        self.before_buffer = []
        self.after_buffer = []
        self.rev = revpimodio2.RevPiModIO(autorefresh = True, procimg = self.image_path)
        self.rev.cycletime = 1000
        self.IO = self.rev.io
    
    def get_data(self):
        sensor_list = list(self.sensor_profile.keys())
        rev_data = [0]*len(sensor_list)  
        for idx in range(len(sensor_list)):
            rev_data[idx] = getattr(self.IO, sensor_list[idx]).value
        self.before_buffer = rev_data #list

    def data_normalization(self):
        self.get_data()
        self.after_buffer = []
        profile = self.normalization_profile

        for i,v in enumerate(profile):
            input_start = profile.get(v).get('originalRange')[0]
            input_end = profile.get(v).get('originalRange')[1]
            change_start = profile.get(v).get('changedRange')[0]
            change_end = profile.get(v).get('changedRange')[1]
            n = (change_end - change_start) / (input_end - input_start)
            processed_data = self.before_buffer[i] * n + change_start - input_start * n
            self.after_buffer.append(float(round(processed_data,2)))

찾아보니까 특정 클래스에 싱글톤을 적용시키는 방법이 매우 여러가지가 있었다.
이 때 제일 매력적으로 나에게 느껴진것은 데코레이터를 이용한 싱글톤 적용인데, 위 코드에서 이 부분이다.

def singleton(cls):
    instances = {}
    def getinstance():
        if cls not in instances:
            instances[cls] = cls()
        return instances[cls]
    return getinstance

@singleton
class RevolutionPi:
    pass

결론...

내가 싱글턴패턴을 적용했던 이유는 단순한 메모리 누수를 막기위한 조치였다.
사실은 코드 발적화가 원인인 메모리 누수라서 좋은 해결법은 아니였던 것 같다.

하지만! 이번에 다시 공부해보니, 객체의 유일성을 보장해주어야 하는 특수 케이스에 대해서 싱글턴을 적용한다는것을 이해하였다!

이로서 한 발자국 더 나아감!

profile
DevOps 엔지니어로 핀테크 회사에서 일하고있습니다. 아직 많이 부족합니다.

0개의 댓글