glasses.dev
#Cloud Run#Serverless#PostgreSQL#AsyncPG#Architecture

Cloud Run과 AsyncPG: 서버리스 환경에서의 DB 연결 전략

2025-11-23
5 min read

Cloud Run과 AsyncPG: 서버리스 환경에서의 DB 연결 전략

서버리스(Serverless) 환경인 Google Cloud Run에 FastAPI 애플리케이션을 배포하면서 겪었던 기술적 난관들과 그 해결 과정을 공유합니다. 단순한 설정 오류가 아닌, 서버리스의 특성비동기 DB 드라이버의 내부 동작을 이해해야만 해결할 수 있었던 문제들에 집중했습니다.


1. Cloud Run의 Lifecycle과 Cold Start: 초기화 전략의 중요성

문제의 발단: "Container failed to start"

로컬에서는 완벽하게 동작하던 앱이 Cloud Run 배포 직후 Container failed to start 에러와 함께 종료되었습니다. 로그에는 AttributeError가 찍혀 있었지만, 근본적인 원인은 애플리케이션의 초기화 방식에 있었습니다.

기술적 분석: Blocking Initialization의 위험성

기존 코드는 lifespan 이벤트 핸들러에서 DB 연결을 Blocking 방식으로 처리하고 있었습니다.

@asynccontextmanager
async def lifespan(app: FastAPI):
    await connect_to_db()  # 연결 실패 시 여기서 Exception 발생 -> 앱 기동 실패
    yield

Cloud Run과 같은 서버리스 환경에서는 Cold Start 시간이 매우 중요합니다. 컨테이너가 시작되고 PORT 리스닝을 시작하기 전까지의 시간이 길어지거나 예외가 발생하면, 플랫폼은 이를 "배포 실패"로 간주하고 컨테이너를 즉시 종료시킵니다.

DB 연결과 같은 외부 의존성은 네트워크 지연이나 일시적 장애로 인해 언제든 실패할 수 있습니다. 이를 앱의 기동(Startup)과 강하게 결합(Tight Coupling)시키면, 일시적인 DB 장애가 전체 서비스의 배포 실패로 이어지는 **단일 실패 지점(SPOF)**이 됩니다.

해결: Graceful Degradation 패턴 적용

DB 연결 실패를 치명적 오류(Fatal Error)로 처리하지 않고, 경고(Warning) 수준으로 낮추어 애플리케이션이 일단 기동되도록 변경했습니다.

async def connect_to_db():
    try:
        pool = await asyncpg.create_pool(...)
    except Exception as e:
        # 연결 실패를 로그로 남기되, 예외를 전파하지 않음
        logger.error(f"DB Connection failed: {e}")
        # 헬스 체크 엔드포인트는 정상 응답하도록 하여 컨테이너 생존 보장
    else:
        logger.info("DB Connected Successfully")

이 패턴을 통해 컨테이너는 정상적으로 시작되어 로그를 수집할 수 있게 되었고, 문제의 원인을 파악할 수 있는 관측성(Observability)을 확보했습니다.


2. Cloud SQL Auth Proxy와 Unix Socket: 추상화의 누수

문제의 발단: SSL Handshake Error

앱이 기동된 후 로그에서 발견된 것은 ssl.CertificateError였습니다. 우리는 Cloud Run의 --add-cloudsql-instances 옵션을 사용하여 Cloud SQL Auth Proxy를 사이드카 패턴으로 실행하고 있었고, 이를 통해 Unix Domain Socket으로 연결을 시도했습니다.

기술적 분석: asyncpg의 연결 수립 과정

asyncpg는 PostgreSQL의 고성능 비동기 드라이버입니다. 문제는 DATABASE_URL 파싱 로직과 소켓 처리 방식의 미묘한 차이에서 발생했습니다.

일반적으로 postgres://user:pass@/path/to/socket 형태의 DSN(Data Source Name)을 사용할 때, 드라이버는 이를 호스트네임으로 해석하거나, 기본적으로 SSL 연결을 시도할 수 있습니다. 하지만 Cloud SQL Auth Proxy가 제공하는 Unix Socket은 이미 로컬 보안 채널이므로 SSL이 불필요하며, 오히려 SSL 핸드셰이크 시도 시 프로토콜 불일치로 연결이 실패합니다.

해결: 명시적 설정 주입 (Explicit Configuration)

DSN 파싱에 의존하는 암시적(Implicit) 설정 대신, 명시적(Explicit)으로 연결 파라미터를 주입하여 드라이버의 동작을 제어했습니다.

# 명시적으로 호스트 경로와 SSL 비활성화를 지정
pool = await asyncpg.create_pool(
    user=DB_USER,
    host="/cloudsql/project:region:instance", # Unix Socket 경로
    database=DB_NAME,
    ssl=False  # Unix Socket은 이미 안전하므로 SSL 비활성화 필수
)

이 설정을 통해 asyncpg는 불필요한 네트워크 스택(TCP/IP)을 거치지 않고 커널 레벨의 소켓 통신을 수행하게 되어 성능과 안정성을 모두 확보했습니다.


3. 결과: 안정적인 서비스 운영

이러한 아키텍처 개선을 통해 Swifty 서비스는 Cloud Run 환경에서 안정적으로 동작하게 되었습니다.

메인 대시보드운동 기록 인증식단 기록 인증
DB에서 실시간 데이터 로딩이미지 업로드 및 DB 저장복합 데이터 처리 및 저장

Loading diagram...

💡 Cloud Run 배포를 위한 체크리스트

비슷한 환경을 구축하려는 엔지니어들을 위해 핵심 체크리스트를 정리했습니다.

  1. 초기화 분리 (Decouple Initialization): lifespan이나 on_startup에서 외부 의존성 연결이 실패하더라도 앱이 죽지 않도록 예외 처리를 하세요. 컨테이너가 살아야 로그도 볼 수 있습니다.
  2. 소켓 vs TCP 명확화: Cloud SQL 연결 시 Unix Socket을 사용한다면, DB 드라이버단에서 SSL을 명시적으로 끄세요(ssl=False 또는 sslmode=disable). 소켓 통신은 이미 안전합니다.
  3. URL 파싱 주의: 비밀번호에 특수문자가 있거나 소켓 경로를 사용할 때는 URL string 방식보다 Object/Keyword Argument 방식으로 설정을 주입하는 것이 훨씬 안전하고 명확합니다.

이러한 시행착오들이 여러분의 서버리스 여정에 도움이 되기를 바랍니다. 🚀

댓글이나 피드백을 남겨주세요

(Giscus 또는 Utterances 댓글 컴포넌트가 들어갈 자리)