콘텐츠로 이동

02. 똑똑한 백엔드 창고 만들기 (FastAPI & DB)

"Serverless 환경에서도 우리는 강력한 Python 백엔드를 가질 수 있습니다."

단순히 데이터를 저장하는 것을 넘어, AI가 개입하고 수강신청의 복잡한 규칙을 계산할 백엔드 서버를 Firebase Functions 위에 구축해 봅시다.


1. FastAPI Integration: 서버리스와 파이썬의 만남

Firebase Functions v2는 기본적으로 개별 함수 단위로 동작하지만, 우리는 FastAPI를 통째로 얹어 일반적인 웹 서버처럼 다룰 것입니다.

🛠️ FastAPI를 Firebase Functions에 통합하기

1️⃣ 필수 패키지 설치

functions/requirements.txt에 다음 패키지들을 추가합니다:

firebase-functions>=0.5.0
firebase-admin>=6.5.0
fastapi
uvicorn

2️⃣ 수동 ASGI 어댑터 구현

Firebase Functions v2는 ASGI(비동기 서버 인터페이스)를 직접 지원하지 않습니다. 하지만 수동 ASGI 어댑터를 구현하면 FastAPI의 모든 기능을 사용할 수 있습니다.

📌 참고: 이 구현 방식은 Reddit 커뮤니티 토론을 참조했습니다.

functions/main.py에 다음 코드를 작성합니다:

# functions/main.py

from firebase_functions import https_fn, options
import firebase_admin
from firebase_admin import initialize_app
from fastapi import FastAPI
import asyncio
import json

# Firebase Admin 초기화 (중복 방지)
if not firebase_admin._apps:
    initialize_app()

# 전역 지역 설정 (서울)
options.set_global_options(region="asia-northeast3")

# FastAPI 앱 생성
app = FastAPI()

@app.get("/api/health")
def health_check():
    return {"status": "ok"}

@https_fn.on_request()
def fastapi_handler(req: https_fn.Request) -> https_fn.Response:
    """
    수동 ASGI 어댑터 구현
    Firebase Functions v2에서 FastAPI를 실행하기 위해 ASGI 프로토콜을 직접 구현
    """
    try:
        # ASGI scope 구성
        asgi_request = {
            "type": "http",
            "method": req.method,
            "path": req.path,
            "headers": [(k.lower().encode(), v.encode()) for k, v in req.headers.items()],
            "query_string": req.query_string or b"",
        }

        # ASGI receive 함수
        async def receive():
            return {"type": "http.request", "body": req.get_data() or b"", "more_body": False}

        # 응답 데이터 수집
        response_body, response_headers, response_status = [], [], 200

        # ASGI send 함수
        async def send(message):
            nonlocal response_body, response_headers, response_status
            if message["type"] == "http.response.start":
                response_status = message.get("status", 200)
                response_headers = message.get("headers", [])
            elif message["type"] == "http.response.body":
                response_body.append(message.get("body", b""))

        # FastAPI를 asyncio로 실행
        async def run_asgi():
            await app(asgi_request, receive, send)

        asyncio.run(run_asgi())

        # 응답 조합
        full_body = b"".join(response_body)
        headers_dict = {
            k.decode() if isinstance(k, bytes) else k: v.decode() if isinstance(v, bytes) else v
            for k, v in response_headers
        }

        return https_fn.Response(response=full_body, status=response_status, headers=headers_dict)

    except Exception as e:
        return https_fn.Response(
            response=json.dumps({"error": f"Internal Server Error: {str(e)}"}),
            status=500,
            headers={"Content-Type": "application/json"},
        )

주요 포인트:

  • Firebase Functions v2는 ASGI를 직접 지원하지 않음
  • ASGI 프로토콜(scope, receive, send)을 수동으로 구현
  • asyncio.run()으로 FastAPI의 비동기 핸들러를 실행
  • 모든 FastAPI 기능(라우팅, validation, async/await) 사용 가능

2. Firestore Design: NoSQL 데이터 모델링

전통적인 엑셀 방식(SQL)과 달리, Firestore는 JSON 문서(Document) 형태의 자유로운 구조를 가집니다.

🗄️ 수강신청 시스템 설계 (Collection)

이제부터는 AI와 함께 시스템을 설계하는 단계입니다. 단순히 코드만 짜달라고 하는 게 아니라, 기획서(prd.md)를 보고 데이터 구조를 먼저 설계하도록 시키세요.

  1. AI에게 prd.md 읽히기: 프로젝트 루트의 prd.md 내용을 복사해서 AI에게 제공합니다.
  2. 클래스 다이어그램 요청: 기획서를 바탕으로 필요한 데이터 모델들을 찾고, 그 관계를 그려달라고 합니다.
  3. Firestore 컬렉션 설계: 다이어그램을 바탕으로 NoSQL인 Firestore에 어떻게 저장할지 결정합니다.

AI 프롬프트 예시

prd.md 파일을 참고해서 수강신청 시스템에 필요한 데이터 모델을 설계해줘.

  1. 먼저 이 시스템에 필요한 클래스 다이어그램(Class Diagram)을 그려서 보여줘. (User, Course, Enrollment 등)
  2. 그리고 이걸 Firestore(NoSQL) 데이터베이스에 저장하려면 어떤 컬렉션(Collection)들이 필요할지 제안해줘.

💡 DB Seeding (초기 데이터 주입)

아무것도 없는 DB는 테스트하기 어렵습니다. AI에게 요청하여 seed.py 스크립트를 만들고, 강의 목록을 로컬 에뮬레이터에 한 방에 밀어 넣어 봅시다.

# 로컬 에뮬레이터에 데이터 주입
export FIRESTORE_EMULATOR_HOST="127.0.0.1:8080"
export GCLOUD_PROJECT="course-registration-79d64"
python seed.py

확인 방법: 에뮬레이터 UI(http://localhost:4000)의 Firestore 탭을 클릭해보세요. courses 컬렉션에 데이터가 가득 찬 것을 볼 수 있습니다!

AI 프롬프트 예시

seed.py 파일을 만들어서 firebase emulator에 강의, 학생, 수강 정보 샘플 데이터를 만들어 넣어야해.

  1. docs/prd.md 파일을 참고해서 기본 모델을 만들어주고, 그 모델을 바탕으로 firestore 컬렉션을 생성하는 구조로 만들어줘.
  2. 만들어진 샘플데이터는 테스트 용도로 사용할거야.

3. CORS & Safety: 보안 울타리 치기

백엔드가 완성되어도 프론트엔드에서 접속하지 못한다면 무용지물입니다. CORS(Cross-Origin Resource Sharing) 설정을 통해 허락된 도메인만 접속을 허용합니다.

# functions/main.py 일부
from fastapi.middleware.cors import CORSMiddleware

app.add_middleware(
    CORSMiddleware,
    allow_origins=[
        "http://localhost:5173",  # 로컬 개발
        "http://127.0.0.1:5002",  # Firebase Hosting Emulator
        "http://localhost:5002",
        "https://<project-id>.web.app", # Firebase Hosting 도메인
        "https://<project-id>.firebaseapp.com",
    ],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

비용 폭탄 방지 설정 (Resource Limits)

서버리스는 호출된 만큼 돈을 냅니다. 무한 루프나 공격에 대비해 전역 옵션에 제한 설정을 반드시 추가해야 합니다.

# functions/main.py 전역 설정
options.set_global_options(
    region="asia-northeast3",
    max_instances=10,       # 최대 10개 인스턴스 제한 (DDoS 방어)
    memory=options.MemoryOption.MB_256, # 최소 메모리 사용
    timeout_sec=30,         # 30초 초과 시 강제 종료
)

4. [Check] 첫 번째 API 호출 테스트

모든 설정이 끝났다면, 브라우저나 Postman을 통해 우리 백엔드가 살아있는지 확인합니다.

  • Endpoint: http://127.0.0.1:5001/<project-id>/asia-northeast3/fastapi_handler/api/health
  • Expected Response: {"status": "ok"}

5. 핵심 정리

용어 설명 비유
FastAPI 빠르고 현대적인 Python 웹 프레임워크 주방의 최신식 조리기구
NoSQL (Firestore) 정해진 틀 없이 유연하게 데이터를 쌓는 방식 포스트잇 게시판
CORS 다른 도메인에서의 리소스 요청을 관리하는 보안 정책 외부인 출입 검문소
ASGI 비동기 웹 서버와 애플리케이션 간의 표준 인터페이스 주문서 전달용 통신 시스템

준비 완료! 이제 똑똑한 두뇌(FastAPI)와 든든한 창고(Firestore)가 마련되었습니다. 다음 04장에서는 이 복잡한 시스템을 실제 배포하기 전, 내 컴퓨터 안에서 완벽하게 시뮬레이션하고 검증하는 에뮬레이터 및 TDD 전략을 배워보겠습니다!


실습 과제: FastAPI의 @app.get("/api/courses") 엔드포인트를 만들고, Firestore에서 전체 강의 목록을 가져와 JSON으로 반환하는 코드를 AI와 함께 완성해 보세요.