런타임에 한 번만 생성할 것을 굳이 클래스로 만들어야 할까?
핵심 질문
"프로그램 runtime 중에 한 번만 생성할 클래스를 굳이 클래스로 만들 필요가 있을까? 만약 클래스로 관리해야 한다면 효과적인 방법은 무엇인가?"
클래스가 하는 일 3가지
클래스는 원래 세 가지를 묶어준다:
- 상태 (state) — 데이터를 담는다
- 행동 (behavior) — 메서드로 그 데이터를 다룬다
- 정체성 (identity) —
instance1 is instance2같은 개체 구분
"딱 한 번만 만들 것"이라면 정체성은 의미가 없다. 비교할 두 번째 인스턴스가 없으니까. 결국 남는 건 상태 + 행동인데, 이걸 꼭 클래스로 해야 하냐는 게 핵심 질문이다.
대안 비교
1. 모듈 레벨 변수 (Python / Go / JS)
# db.py
import sqlite3
conn = sqlite3.connect("db.sqlite")
def execute(query):
return conn.execute(query)
Python 모듈은 첫 import 때 한 번만 실행되고 이후엔 캐싱된다. 구조 없이 사실상 싱글턴.
| | | |---|---| | ✅ | 단순함. 오버엔지니어링 없음 | | ✅ | Python에서 가장 관용적인 방법 | | ❌ | 인터페이스가 없어 테스트에서 mock 교체 어려움 | | ❌ | 초기화 타이밍 제어 어려움 (import되면 바로 실행) | | ❌ | Java/C++ 같은 언어에서는 선택지 자체가 없음 |
2. Singleton 패턴 (GoF)
class DatabaseManager:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance.conn = sqlite3.connect("db.sqlite")
return cls._instance
| | | |---|---| | ✅ | 클래스로 캡슐화 — 메서드, 타입 힌트, 인터페이스 제공 | | ✅ | 초기화 타이밍 제어 가능 (첫 호출 때) | | ❌ | 숨겨진 전역 상태 — 의존성이 코드에 안 보임 | | ❌ | 테스트가 어렵다 — 테스트끼리 상태 공유, mock 교체 어렵고 teardown 필요 | | ❌ | SRP 위반 — 클래스가 자기 생명주기를 스스로 관리함 |
3. Dependency Injection
# 앱 시작점 (composition root)
conn = sqlite3.connect("db.sqlite")
db = DatabaseManager(conn) # 평범한 클래스, 싱글턴 아님
# 필요한 곳에 주입
order_service = OrderService(db)
user_service = UserService(db)
DatabaseManager는 평범한 클래스다. "하나만 만들겠다"는 결정을 클래스 안에 숨기지 않고, 앱 시작점(composition root)에서 명시적으로 한다.
| | |
|---|---|
| ✅ | 의존성이 보인다 — OrderService(db) 한 줄에서 db를 쓴다는 게 드러남 |
| ✅ | 테스트가 쉽다 — OrderService(mock_db)로 끝 |
| ✅ | 나중에 멀티 DB가 필요해지면 다른 인스턴스 넘기면 됨 |
| ❌ | 인스턴스를 계속 넘겨줘야 해서 보일러플레이트 증가 |
| ❌ | 큰 앱에선 IoC 컨테이너가 필요해짐 |
4. IoC 컨테이너 (Spring / FastAPI Depends 등)
DI를 프레임워크가 관리해주는 것. "이 타입은 싱글턴으로 관리해"를 컨테이너에게 위임.
# FastAPI 예시
def get_db():
return db_instance # 앱 시작 때 만들어둔 것
@router.get("/orders")
def get_orders(db: DatabaseManager = Depends(get_db)):
...
| | | |---|---| | ✅ | DI의 장점 + 보일러플레이트 감소 | | ❌ | 프레임워크 학습 비용 |
결론
| 상황 | 추천 | |---|---| | Python 소규모, 단순 유틸 | 모듈 레벨 변수 | | 테스트 필요 or 중간 이상 규모 | DI (composition root에서 생성) | | 프레임워크 쓰는 중간~대규모 | IoC 컨테이너 | | GoF Singleton | 특수한 경우 (Logger 정도)만 |
핵심 원칙: "하나만 만들겠다"는 결정을 클래스 안에 숨기지 마라. 그 결정은 앱 시작점에서 명시적으로 하는 게 낫다.
관련
- overview — Singleton 패턴 전체 지도
- research/larman-ooad/10-gof-patterns|GoF 패턴 in Larman — Singleton + Factory 조합
- learn/Languages/Python/python-lifecycle-resource-management|Python 리소스 관리 — Python 구현 예시