[개발공부] 싱글톤(Singleton) 과 추상 메서드(Abstract Method)

DD[Dev_Diary]·2025년 8월 28일
  • 싱글톤: “프로그램에서 딱 1개만 있어야 하는 객체” 만들기. (설정/로거/커넥션 등)
  • 추상 메서드: “자식이 반드시 구현해야 할 메서드” 강제(팀 규약 만들기).
  • 파이썬은 모듈 전역 객체가 사실상 싱글톤처럼 동작. 필요하면 __new__+Lock, 또는 메타클래스로 확장.

1) 싱글톤: 진짜로 한 개만 만들기

1-1. 제일 쉬운 방법: 모듈 전역 객체

파이썬은 모듈을 한 번만 로드해요. 그래서 모듈 전역에 올려두면 사실상 싱글톤처럼 굴어요.
저는 설정 관리에 이렇게 씁니다.

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

✔️ 간단하고 실전에서 제일 많이 씀. “진짜 클래스로 싱글톤 만들어야 하나?” 싶으면 이걸 먼저 고려하세요.


1-2. 클래스로 구현: __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 가드 없으면 초기화가 중복됩니다.


1-3. 여러 클래스를 싱글톤으로? → 메타클래스 한 방

로거/메트릭 등 여러 타입을 각각 싱글톤으로 만들고 싶을 때 편했어요.

# 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']

짧은 메모

  • 공용 Lock으로 간단히 스레드 안전 확보.
  • “클래스별로 한 개씩”이 필요할 때 깔끔.

1-4. 제가 당한(…) 실수 모음

  • 전역 상태 남발: 테스트 지옥. 가능하면 의존성 주입(함수/생성자 인자) 섞어 쓰기.
  • 초기화 중복: __init__ 가드 빼먹으면 값이 자꾸 리셋됨.
  • 멀티스레딩: Lock 없이 싱글톤 만들었다가 2개 생긴 적 있음…(테스트에서만 뜨는 유령 버그 느낌)

2) 추상 메서드: 팀 규약(계약) 강제하기

2-1. 핵심 개념 한 줄

**“자식 클래스가 반드시 구현해야 하는 메서드”**를 선언해두는 것.
파이썬에선 abc 모듈의 ABC, @abstractmethod 사용.

2-2. 제일 작은 예제: 동물 울음

# 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반드시 구현해야 해요.


2-3. 실전 예제: 결제 인터페이스 통일

개발하면서 결제수단을 늘려도 호출부 코드를 안 고치고 싶어서 이렇게 잡았습니다.

# 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)는 인터페이스만 믿고 호출. 새 결제수단 추가해도 수정 없음.
  • 팀에 “필수 메서드/프로퍼티”를 강제할 수 있어 코드가 일정해짐.

2-4. 추상 메서드 관련 TIL

  • @property에도 @abstractmethod를 같이 써서 추상 프로퍼티 만들 수 있음.
  • 클래스/정적 메서드도 추상화 가능 (@classmethod/@staticmethod와 함께).
  • 구현 하나라도 빼먹으면 인스턴스화 시점에 바로 에러를 줘서 초반에 잡음.

3) 콤보: 싱글톤 설정 + 추상 메서드 결제

“설정은 한 개”, “결제는 여러 구현”을 동시에 쓰면 아래처럼 됩니다.

# 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)

4) 유지보수 팁 (실무 감각)

  • 전역 싱글톤 최소화: 정말 공용이어야 하는 것만(설정/로거). 도메인 로직은 가급적 의존성 주입으로.
  • 인터페이스 먼저 잡기: 추상 메서드로 규약부터 잡으면, 팀원이 병렬로 구현하기 쉬움.
  • 테스트하기 좋게: 인터페이스만 보고 목(Mock)/페이크(Fake)를 끼워 넣기 쉬워짐.

5) 치트시트

  • 싱글톤

    • 쉬운 길: 모듈 전역 객체
    • 클래스로: __new__ + Lock + __init__ 가드
  • 추상 메서드

    • from abc import ABC, abstractmethod
    • @property와도 조합 가능 (추상 프로퍼티)
    • 호출부는 인터페이스만 의존 → 확장 쉬움

6) 체크리스트 (바로 실습)

  • 모듈 전역 싱글톤으로 앱 설정 뚝딱 만들어보기
  • __new__ 싱글톤에 Lock 추가하고 id()로 진짜 한 개인지 확인
  • 추상 클래스 하나 만들고 구현 클래스 2개 이상 작성
  • 결제/알림/스토리지 등 도메인으로 바꿔서 콤보 예제 돌려보기

읽어주셔서 감사합니다!
혹시 위 코드 복붙해서 돌려보다가 로그가 다르게 나온 부분 있으면, 어떤 환경/상황이었는지 댓글로 남겨주세요. 제가 재현해보고 글 업데이트할게요 :)

profile
AI로 유용한 서비스 개발을 꿈꾸는 A린이

0개의 댓글