__new__+Lock, 또는 메타클래스로 확장.파이썬은 모듈을 한 번만 로드해요. 그래서 모듈 전역에 올려두면 사실상 싱글톤처럼 굴어요.
저는 설정 관리에 이렇게 씁니다.
settings.py
# settings.py
class Settings:
def __init__(self):
self.debug = False
self.theme = "light"
settings = Settings() # ← 모듈 전역: 사실상 싱글톤
app.py
# app.py
from settings import settings
def main():
print("초기:", settings.debug, settings.theme) # False light
settings.debug = True
settings.theme = "dark"
other() # 다른 함수(다른 파일이어도 OK)
print("마무리:", settings.debug, settings.theme)
def other():
from settings import settings
print("other에서 보는 값:", settings.debug, settings.theme) # True dark (동일 인스턴스)
if __name__ == "__main__":
main()
실행하면 대충 이런 로그가 떠요
초기: False light
other에서 보는 값: True dark
마무리: True dark
✔️ 간단하고 실전에서 제일 많이 씀. “진짜 클래스로 싱글톤 만들어야 하나?” 싶으면 이걸 먼저 고려하세요.
__new__ + Lock (스레드 안전 버전)여러 스레드가 동시에 만들어도 딱 한 개만 생기게 하고 싶을 때 썼습니다.
# singleton_config.py
import threading
import concurrent.futures
class Config:
_instance = None
_lock = threading.Lock()
def __new__(cls, *args, **kwargs):
# double-checked locking
if cls._instance is None:
with cls._lock:
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self):
# __init__는 여러 번 불릴 수 있어서 가드 필요
if getattr(self, "_initialized", False):
return
self._initialized = True
self._data = {}
def set(self, k, v): self._data[k] = v
def get(self, k, d=None): return self._data.get(k, d)
if __name__ == "__main__":
def worker(i):
c = Config()
c.set("last_writer", i)
return id(c)
with concurrent.futures.ThreadPoolExecutor(max_workers=8) as ex:
ids = list(ex.map(worker, range(50)))
print("인스턴스 id 개수:", len(set(ids))) # 1이면 성공
print("마지막 작성자:", Config().get("last_writer"))
제 실행 결과(예)
인스턴스 id 개수: 1
마지막 작성자: 49
✔️ TIL:
__init__는 여러 번 불릴 수 있으니_initialized가드 없으면 초기화가 중복됩니다.
로거/메트릭 등 여러 타입을 각각 싱글톤으로 만들고 싶을 때 편했어요.
# singleton_meta.py
import threading
class SingletonMeta(type):
_instances = {}
_lock = threading.Lock()
def __call__(cls, *args, **kwargs):
with cls._lock:
if cls not in cls._instances:
cls._instances[cls] = super().__call__(*args, **kwargs)
return cls._instances[cls]
class Logger(metaclass=SingletonMeta):
def __init__(self): self.history = []
def log(self, msg):
self.history.append(msg)
print(f"[LOG] {msg}")
class Metrics(metaclass=SingletonMeta):
def __init__(self): self.counters = {}
def incr(self, key, n=1): self.counters[key] = self.counters.get(key, 0) + n
if __name__ == "__main__":
a, b = Logger(), Logger()
print("로거 동일?", id(a) == id(b)) # True
a.log("hello"); b.log("world")
print("히스토리:", a.history) # ['hello', 'world']
짧은 메모
__init__ 가드 빼먹으면 값이 자꾸 리셋됨.**“자식 클래스가 반드시 구현해야 하는 메서드”**를 선언해두는 것.
파이썬에선 abc 모듈의 ABC, @abstractmethod 사용.
# abstract_animal.py
from abc import ABC, abstractmethod
class Animal(ABC):
@abstractmethod
def speak(self) -> str: ...
class Dog(Animal):
def speak(self) -> str: return "멍멍"
class Cat(Animal):
def speak(self) -> str: return "야옹"
if __name__ == "__main__":
pets = [Dog(), Cat()]
for p in pets: print(p.speak())
# Animal() # ← TypeError: 추상 메서드 남아있어서 인스턴스화 불가
실행 느낌
멍멍
야옹
✔️
Animal()은 직접 못 만듭니다. 자식이speak를 반드시 구현해야 해요.
개발하면서 결제수단을 늘려도 호출부 코드를 안 고치고 싶어서 이렇게 잡았습니다.
# payment.py
from abc import ABC, abstractmethod
class PaymentProcessor(ABC):
@property
@abstractmethod
def name(self) -> str: ...
@abstractmethod
def pay(self, amount: int) -> None: ...
class CardProcessor(PaymentProcessor):
def __init__(self, merchant_id: str):
self._merchant_id = merchant_id
@property
def name(self) -> str: return "CARD"
def pay(self, amount: int) -> None:
print(f"[{self.name}] 승인 {amount}원 (MID={self._merchant_id})")
class BankTransferProcessor(PaymentProcessor):
@property
def name(self) -> str: return "BANK_TRANSFER"
def pay(self, amount: int) -> None:
print(f"[{self.name}] 계좌이체 {amount}원 처리")
def checkout(processor: PaymentProcessor, amount: int):
print(f"결제 시작: {processor.name} / {amount}원")
processor.pay(amount)
print("결제 완료\n")
if __name__ == "__main__":
checkout(CardProcessor("M1234"), 15000)
checkout(BankTransferProcessor(), 39000)
실행하면
결제 시작: CARD / 15000원
[CARD] 승인 15000원 (MID=M1234)
결제 완료
결제 시작: BANK_TRANSFER / 39000원
[BANK_TRANSFER] 계좌이체 39000원 처리
결제 완료
제가 좋았던 점
checkout)는 인터페이스만 믿고 호출. 새 결제수단 추가해도 수정 없음.@property에도 @abstractmethod를 같이 써서 추상 프로퍼티 만들 수 있음.@classmethod/@staticmethod와 함께).“설정은 한 개”, “결제는 여러 구현”을 동시에 쓰면 아래처럼 됩니다.
# combo_example.py
from abc import ABC, abstractmethod
import threading
# --- Singleton Settings ---
class Settings:
_instance = None
_lock = threading.Lock()
def __new__(cls):
if cls._instance is None:
with cls._lock:
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self):
if getattr(self, "_initialized", False): return
self._initialized = True
self.api_key = "DUMMY_API_KEY"
self.currency = "KRW"
# --- Abstract Payment Interface ---
class Payment(ABC):
@abstractmethod
def pay(self, amount: int) -> None: ...
class CardPayment(Payment):
def __init__(self):
self.settings = Settings() # 싱글톤 주입
def pay(self, amount: int) -> None:
print(f"[CARD] {amount}{self.settings.currency} 결제 (API={self.settings.api_key})")
class TransferPayment(Payment):
def __init__(self):
self.settings = Settings() # 싱글톤 주입
def pay(self, amount: int) -> None:
print(f"[BANK] {amount}{self.settings.currency} 이체 (API={self.settings.api_key})")
def order(processor: Payment, amount: int):
processor.pay(amount)
if __name__ == "__main__":
s = Settings()
s.currency = "KRW" # 한 번 바꾸면 모든 결제에서 같은 설정 사용
order(CardPayment(), 12000)
order(TransferPayment(), 45000)
제 실행 결과
[CARD] 12000KRW 결제 (API=DUMMY_API_KEY)
[BANK] 45000KRW 이체 (API=DUMMY_API_KEY)
싱글톤
__new__ + Lock + __init__ 가드추상 메서드
from abc import ABC, abstractmethod@property와도 조합 가능 (추상 프로퍼티)__new__ 싱글톤에 Lock 추가하고 id()로 진짜 한 개인지 확인읽어주셔서 감사합니다!
혹시 위 코드 복붙해서 돌려보다가 로그가 다르게 나온 부분 있으면, 어떤 환경/상황이었는지 댓글로 남겨주세요. 제가 재현해보고 글 업데이트할게요 :)