[디자인 패턴] 지연초기화 Lazy Initialization

객체를 꼭 필요한 시점에 생성하는 방법

Intro

지연 초기화는 객체를 프로그램 시작 시점에 생성하는 게 아니라, 실제로 필요한 시점에 생성하도록 미루는 기법을 가리킨다. 특히 DB Engine, ML 모델, 외부 API Client 또는 대용량 객체처럼 생성 비용이 큰 리소스를 다룰 때 효과적인 방법이다.

단순히 “필요할 때 객체를 생성한다” 라는 개념처럼 보이지만, 실무에서는 싱글톤, 전역 상태 관리, 서버 워밍업과 함께 자주 사용된다.

1. 지연 초기화의 개념

지연 초기화는 말 그대로 초기화를 지연시키는 것을 가리킨다. 일반적으로는 프로그램이 시작될 때 객체를 바로 만들게 된다.

1
2
3
from sqlalchemy import create_engine

engine = create_engine(...)

반면 지연 초기화는 처음에는 객체를 만들지 않고 None 등을 할당해뒀다가, “객체가 필요해질 때” 혹은 “서버 워밍업 시” 객체를 생성한다.

1
2
3
4
5
6
7
8
9
10
from sqlalchemy import create_engine
from sqlalchemy.engine import Engine

engine: Engine | None = None

def load_engine():
    global engine
    if engine is None:
        engine = create_engine(...)
    return engine

2. 왜 사용할까?

(1) 빠른 초기 실행 속도

프로그램 시작 시 모든 객체를 한꺼번에 만들게 된다면 시작 시간이 길어진다. 따라서 당장 필요 없는 객체는 나중에 필요할 때 새성하도록 하여, 프로그램의 초기 실행을 빠르게 할 수 있다.

(2) 자원 사용량 절감

코드상에 있는 어떤 객체는, 실제로 사용되지 않을 수도 있다. 사용되지도 않는 객체를 메모리에 올리는 것은 낭비이므로, 이를 방지하는 효과도 있다.

또한 지연 초기화는 보통 생성 비용이 큰 객체를 다룰 때 많이 사용되는데, 이러한 객체들은 생성 비용이 크고 자원 점유도 높을 가능성이 많다. 따라서 객체를 필요해질 때까지 생성이 계산을 미뤄 메모리와 같은 여러 자원들을 아낄 수 있다.

이에 해당하는 사례들은 아래와 같다.

  • DB Engine
  • Redis Client
  • S3 Client
  • MLflow Client
  • 머신러닝 모델
  • 대용량 설정 객체
  • 외부 API Client

3. 기본 구조

지연 초기화의 기본 구조는 다음과 같다.

1
2
3
4
5
6
7
8
resource = None

def get_resource():
    global resource
    
    if resource is None:
        resource = create_resource()
    return resource
요소 의미
resource = None 아직 객체를 생성하지 않은 상태
if resource is None 최초의 호출인지 확인
create_resource 실제 객체 생성
return_resource 생성된 객체 반환 / 생성된 객체 재사용

4. DB Engine 예시

예를 들어 SQLAlchemy의 Engine 을 지연초기화한다고 가정해보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from sqlalchemy import create_engine
from sqlalchemy.engine import Engine

_engine: Engine | None = None

def load_db_engine() -> Engine:
global _engine

if _engine is None:
    _engie = create_engine(
        "postgresql+psycopg2://user:password@localhost:5432/app",
        pool_pre_ping=True,
        pool_recycle=3600,
    )
    return _engine
  • (1) 처음 load_db_engine()을 호출하면 _engineNone이다.
  • (2) 따라서 create_engine()이 실행되고, 생성된 Engine 객체가 _engine에 저장된다.
  • (3) 이후 다시 load_db_engine()을 호출하면 _engine이 이미 존재하므로 새로 만들지 않고 기존 객체를 반환한다.

흐름을 도식화해보면 아래와 같다.

1
2
3
4
5
6
 번째 호출 
 _engine is None  create_engine() 실행
 Engine 생성  _engine에 저장  반환

 번째 호출
 _engine is not None  기존 Engine 반환

즉, DB Engine을 한 번만 만들고 재사용하는 구조가 된다.

5. ML 모델 로딩 예시

지연 초기화 기법은 머신러닝 모델 로딩에도 자주 사용된다. 개인적인 경험으로는 특히 API를 통해 추론 서버를 구축할 때 많이 사용하였다.

1
2
3
4
5
6
7
8
9
10
11
ensemble = None

def load_ensemble():
    global ensemble

    if ensemble is None:
        ensemble = EnsembleModel("recommend-model")
        ensemble.load_ensemble_model()
        ensemble.load_member_models()

    return ensemble

앞서 예시를 들었던 사례들과 같이, 이 구조에서는 모델 객체를 처음부터 만들지 않는다.

load_ensemble()이 최초로 호출되는 시점에 모델 객체를 생성하고, 로컬에 저장된 모델 파일을 메모리에 로딩한다.

이후 추천 요청이 들어올 때마다 모델을 다시 로딩하지 않고, 이미 메모리에 올라온 ensemble 객체를 재사용한다.

6. 지연 초기화와 싱글톤, 전역 상태 관리

지연 초기화 기법은 싱글톤, 전역 상태 관리와 함께 자주 사용된다. 그러면 싱글톤과 전역 상태 관리가 무엇인지 살펴보자.

| 개념 | 관심사 | | — | — | | 지연 초기화 | 객체를 언제 만들 것인가 (생성 패턴) | | 싱글톤 | 객체를 몇 개만 만들 것인가 | | 전역 상태 관리 | 객체를 어디에 보관할 것인가 |

  • 지연 초기화는 “언제 만들 것인가” 즉, 디자인 패턴 중 생성 패턴에 해당한다.
  • 싱글톤은 “객체를 몇 개 만들 것인가”라는 설계적 고민에서 나온 패턴이다.
  • 전역 상태 관리는 “어디에 보관할 것인가”에 대한 패턴이다. 만약 생성 시점의 차이로 인해 각기 다른 데이터를 가진 객체가 생성된다면, 데이터가 꼬일 수 있다. 그래서 공통 저장소를 두고 여러 곳에서 같은 상태를 읽고 수정하게 만드는데, 이것이 전역 상태 관리이다.

7. FastAPI에서 사용할 때

FastAPI 서버에서는 지연 초기화를 두 가지 방식으로 사용할 수 있다.

(1) 요청 시 최초 로딩

첫 번째는 실제 요청이 들어왔을 때 처음 로딩하는 방식이다. 이 때 서버 시작은 빠르나, 첫 번째 요청에서 모델 로딩이나 파일 읽기 등이 발생하므로, 첫 요청 응답이 느려질 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
# engine/ensemble.py

ensemble = None

def load_ensemble():
    global ensemble

    if ensemble is None:
        ensemble = EnsembleModel("recommend-model")
        ensemble.load_ensemble_model()
        ensemble.load_member_models()

    return ensemble
1
2
3
4
5
6
7
# main.py
from engine.ensemble import load_ensemble

@app.get("/recommend")
def recommend(user_id: str):
    ensemble = load_ensemble()
    return ensemble.predict(user_id)

(2) startup에서 미리 로딩

두 번째는 FastAPI startup 단계에서 미리 호출하는 방식이다.

1
2
3
4
5
6
7
8
9
10
11
12
# engine/ensemble.py
ensemble = None

def load_ensemble():
    global ensemble

    if ensemble is None:
        ensemble = EnsembleModel("recommend-model")
        ensemble.load_ensemble_model()
        ensemble.load_member_models()

    return ensemble
1
2
3
4
5
6
# main.py
from engine.ensemble import load_ensemble

@app.on_event("startup")
async def startup_event():
    load_ensemble()

이 방식은 구조상으로는 지연 초기화지만, 운영상으로는 서버 시작 시점에 미리 워밍업하는 형태다. 즉, load_ensemble() 함수 자체는 Lazy Loading 구조를 가지고 있지만, FastAPI startup에서 강제로 호출하기 때문에 첫 요청 전에 모델이 준비된다.

개인적으로 모델을 서빙할 때 자주 사용하는 방식이기도 하며, 특히나 MLflow와 같은 외부 모델 레지스트리를 이용할 경우, 또는 다중 worker를 띄우면서 모델 로딩은 한 번만 하게끔 하여 초기 서비스 실행 속도를 높일 때 사용한다.

이 방식은 API 서버에서 많이 사용되는데, 첫 요청 지연을 줄일 수 있고, 서버가 정상적으로 뜨는 시점에 모델 로딩 문제를 미리 확인할 수 있기 때문이다.

8. 프로세스 단위라는 점

지연 초기화와 전역 상태 관리를 서버에서 사용할 때 주의할 점이 있다. 바로 전역 변수는 서버 전체에서 하나가 아니라, 프로세스마다 하나라는 점이다. 따라서 실제 객체 생성을 할 때 이 점을 주의해야 한다.

예를 들어 gunicorn worker를 4개 사용한다고 하자.

1
gunicorn main:app --workers 4 --worker-class uvicorn.workers.UvicornWorker

이 경우 각 worker는 별도의 프로세스다. 따라서 전역 변수도 worker마다 따로 존재한다.

1
2
3
4
worker 1 → ensemble 1개
worker 2 → ensemble 1개
worker 3 → ensemble 1개
worker 4 → ensemble 1개

즉, ensemble이라는 전역 변수를 사용하더라도 서버 전체에서 하나만 존재하는 것은 아니고, 프로세스마다 하나씩 존재하게 되므로 알맞은 시점에 객체 생성을 하게끔 해야 한다.

9. 언제 사용하면 좋을까?

(1) 어울리는 경우

지연 초기화는 다음과 같은 경우에 어울린다.

  • 객체 생성 비용이 큰 경우
  • 객체를 항상 사용하는 것은 아닌 경우
  • 한 번 만든 객체를 계속 재사용할 수 있는 경우
  • DB Engine, Redis Client, ML 모델처럼 공유 리소스가 필요한 경우
  • 서버 시작 시점과 실제 사용 시점을 분리하고 싶은 경우

(2) 어울리지 않는 경우

다음과 같은 경우에는 굳이 사용할 필요가 없다.

  • 객체 생성 비용이 거의 없는 경우
  • 객체 상태가 요청마다 달라져야 하는 경우
  • 전역으로 공유하면 안 되는 사용자별 상태인 경우
  • 생성 실패를 프로그램 시작 시점에 바로 확인해야 하는 경우
  • 테스트 격리가 중요한데 전역 상태 관리가 복잡해지는 경우

10. 마치며

지연 초기화는 단순하지만 실무에서 자주 사용되는 기법이다.

특히 서버 애플리케이션에서는 무거운 리소스를 언제 만들고, 어디에 보관하고, 몇 번 재사용할 것인지가 중요하다. 이때 지연 초기화는 싱글톤, 전역 상태 관리, 캐싱 전략과 함께 사용되는 경우가 많다.

다만 지연 초기화를 사용한다고 해서 항상 좋은 구조가 되는 것은 아니다. 첫 호출 지연, 에러 발생 시점 지연, 전역 상태 증가, 프로세스별 중복 로딩 같은 문제도 함께 고려해야 한다.

결국 중요한 것은 객체를 늦게 만드는 것 자체가 아니다.

무거운 리소스의 생명주기를 명확히 관리하고, 필요한 시점에 안전하게 준비하며, 이후에는 재사용 가능한 구조를 만드는 것이다.

지연 초기화는 이 목적을 달성하기 위한 가장 기본적인 방법 중 하나다.

Comments