research

런타임에 한 번만 생성할 것을 굳이 클래스로 만들어야 할까?

런타임에 한 번만 생성할 것을 굳이 클래스로 만들어야 할까?

핵심 질문

"프로그램 runtime 중에 한 번만 생성할 클래스를 굳이 클래스로 만들 필요가 있을까? 만약 클래스로 관리해야 한다면 효과적인 방법은 무엇인가?"

클래스가 하는 일 3가지

클래스는 원래 세 가지를 묶어준다:

  1. 상태 (state) — 데이터를 담는다
  2. 행동 (behavior) — 메서드로 그 데이터를 다룬다
  3. 정체성 (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 정도)만 |

핵심 원칙: "하나만 만들겠다"는 결정을 클래스 안에 숨기지 마라. 그 결정은 앱 시작점에서 명시적으로 하는 게 낫다.


관련