본문으로 건너뛰기

"Backend" 태그 — 3개 게시물

백엔드 엔지니어링, FastAPI, DB 관련 글

모든 태그 보기

Scaling PostgreSQL to power 800 million ChatGPT users

· 약 9분
최재훈
LEAD (AI Research Engineer), Brain Crew

TL;DR

OpenAI는 ChatGPT와 API를 지원하기 위해 단일 Primary PostgreSQL 인스턴스와 50개의 Read Replica를 활용해 80만 사용자를 지원하며, 연간 10배 이상의 트래픽 증가를 처리하고 있습니다. MVCC(Multi-Version Concurrency Control)의 write amplification 문제를 완화하기 위해 write-heavy 워크로드를 샤딩된 시스템(Azure Cosmos DB)으로 마이그레이션하고, 쿼리 최적화, 워크로드 격리, 연결 풀링, 캐싱 등의 최적화를 통해 PostgreSQL을 초당 수백만 쿼리를 처리하는 규모로 확장했습니다. 이는 적절한 엔지니어링과 최적화를 통해 단일 Primary 아키텍처로도 대규모 read-heavy 워크로드를 충분히 지원할 수 있음을 증명합니다.

Key Takeaways

  • 단일 Primary 아키텍처의 가능성: Read-heavy 워크로드에 대해서는 샤딩 없이도 Primary 하나와 다수의 Read Replica로 대규모 트래픽을 처리할 수 있으며, 이는 샤딩에 수반되는 복잡한 애플리케이션 변경을 피할 수 있게 함
  • MVCC의 근본적 한계 이해: PostgreSQL의 MVCC 구현은 update 시 전체 row를 복사하여 write/read amplification을 유발하고, dead tuple, table bloat, autovacuum 튜닝 등 운영 복잡도를 증가시키므로 write-heavy 워크로드는 다른 시스템으로 분리해야 함
  • 계층화된 트래픽 관리: 우선순위 기반 워크로드 격리, PgBouncer를 통한 연결 풀링, 캐시 락/리스 메커니즘을 통해 트래픽 급증 시 cascading failure를 방지하고 시스템 안정성을 확보할 수 있음
  • 쿼리 최적화의 중요성: ORM이 생성한 다중 테이블 조인(12개 테이블 조인 사례)과 같은 expensive query 하나가 전체 서비스 장애를 유발할 수 있으므로, 복잡한 join은 애플리케이션 레이어로 이동하고 SQL 동작을 엄격히 검토해야 함
  • 단일 장애 지점(SPOF) 완화 전략: 대부분의 critical read를 replica로 오프로드하고, HA 모드로 hot standby를 운영하며, 각 region에 충분한 headroom을 가진 다수의 replica를 배치하여 Primary 장애 시에도 read 서비스는 유지할 수 있도록 설계

상세 내용

OpenAI의 PostgreSQL 스케일링 여정

OpenAI는 수년간 PostgreSQL을 ChatGPT와 API의 핵심 데이터 시스템으로 운영해왔습니다. 사용자 기반이 급격히 증가하면서, 지난 1년간 PostgreSQL 부하는 10배 이상 증가했고 계속해서 빠르게 상승하고 있습니다.

이러한 성장을 지탱하기 위한 프로덕션 인프라 개선 작업을 통해 새로운 인사이트를 얻었습니다: PostgreSQL은 많은 사람들이 생각했던 것보다 훨씬 더 큰 read-heavy 워크로드를 안정적으로 지원할 수 있도록 확장 가능합니다.

현재 OpenAI는 단일 Primary Azure PostgreSQL Flexible Server 인스턴스와 전 세계 여러 region에 분산된 약 50개의 read replica를 통해 8억 명의 사용자를 위한 대규모 글로벌 트래픽을 지원하고 있습니다. Azure Database for PostgreSQL은 완전 관리형 서비스로서 compute와 storage를 분리한 아키텍처를 제공하며, zone redundant 고가용성을 지원하여 동일 Azure region 내 availability zone 간 동기식 복제를 통해 무손실 failover를 가능하게 합니다.

초기 설계의 한계

ChatGPT 출시 후 트래픽이 전례 없는 속도로 증가했습니다. 이를 지원하기 위해 애플리케이션과 PostgreSQL 데이터베이스 레이어 모두에서 광범위한 최적화를 신속히 구현했고, 인스턴스 크기를 늘리는 scale-up과 read replica를 추가하는 scale-out을 수행했습니다.

단일 Primary 아키텍처가 OpenAI 규모의 요구사항을 충족시킬 수 있다는 것은 놀랍게 들릴 수 있지만, 실제로 이를 작동시키는 것은 단순하지 않습니다. PostgreSQL 과부하로 인한 여러 심각한 장애(SEV)를 경험했으며, 이들은 종종 동일한 패턴을 따릅니다:

  • 캐싱 레이어 장애로 인한 광범위한 캐시 미스
  • CPU를 포화시키는 expensive 다중 조인(multi-way join) 급증
  • 새 기능 출시로 인한 write storm

리소스 사용률이 증가하면 쿼리 지연시간이 늘어나고 요청이 타임아웃되기 시작합니다. 재시도는 부하를 더욱 증폭시키고, 전체 ChatGPT 및 API 서비스를 저하시킬 수 있는 악순환을 촉발합니다.

부하 상태에서의 악순환

PostgreSQL MVCC의 문제점

PostgreSQL은 read-heavy 워크로드에 대해 잘 확장되지만, write 트래픽이 많은 기간에는 여전히 어려움을 겪습니다. 이는 주로 PostgreSQL의 MVCC(Multi-Version Concurrency Control) 구현 때문입니다.

MVCC의 기본 개념은 DBMS가 여러 쿼리가 가능한 한 서로 간섭 없이 동시에 데이터베이스에 읽고 쓸 수 있도록 하는 것입니다. 쿼리가 실행될 때 DBMS는 트랜잭션이 시작된 시점의 데이터베이스 스냅샷을 관찰합니다(snapshot isolation). 이 접근 방식은 reader가 데이터에 접근하는 것을 차단하는 명시적인 레코드 락의 필요성을 제거합니다.

그러나 PostgreSQL의 MVCC 구현에는 심각한 문제가 있습니다:

Write Amplification: 쿼리가 tuple을 업데이트하거나 단일 필드만 수정할 때도 전체 row를 복사하여 새 버전을 생성합니다. Write가 많은 워크로드에서는 상당한 write amplification이 발생합니다.

Read Amplification: 쿼리가 최신 버전을 검색하기 위해 여러 tuple 버전(dead tuple)을 스캔해야 하므로 read amplification도 증가합니다.

운영 복잡도: Table과 index bloat, index 유지관리 오버헤드 증가, 복잡한 autovacuum 튜닝 등 추가적인 문제를 야기합니다.

Carnegie Mellon University의 Andy Pavlo 교수와 함께 작성한 블로그 "The Part of PostgreSQL We Hate the Most"에서 이러한 이슈에 대한 심층 분석을 제공하고 있으며, 이는 PostgreSQL Wikipedia 페이지에서도 인용되고 있습니다. 이 글에서는 PostgreSQL의 MVCC 구현이 MySQL, Oracle, Microsoft SQL Server를 포함한 다른 주요 관계형 DBMS 중 최악이라고 지적합니다.

초당 수백만 쿼리로 PostgreSQL 확장하기

이러한 한계를 완화하고 write 압력을 줄이기 위해 다음과 같은 전략을 채택했습니다:

Write 워크로드 마이그레이션

샤딩 가능한(수평 파티셔닝 가능한) write-heavy 워크로드를 Azure Cosmos DB와 같은 샤딩된 시스템으로 마이그레이션하고, 불필요한 write를 최소화하도록 애플리케이션 로직을 최적화했습니다. 또한 현재 PostgreSQL 배포에 새로운 테이블 추가를 더 이상 허용하지 않으며, 새 워크로드는 기본적으로 샤딩된 시스템을 사용합니다.

현재 인프라가 발전했음에도 PostgreSQL은 샤딩되지 않은 상태로, 단일 Primary 인스턴스가 모든 write를 처리합니다. 주된 이유는 기존 애플리케이션 워크로드를 샤딩하는 것이 매우 복잡하고 시간이 많이 걸리며, 수백 개의 애플리케이션 엔드포인트를 변경해야 하고 몇 달 또는 몇 년이 걸릴 수 있기 때문입니다. 워크로드가 주로 read-heavy이고 광범위한 최적화를 구현했기 때문에, 현재 아키텍처는 여전히 트래픽 증가를 지원할 충분한 여유를 제공합니다.

Primary 부하 감소

과제: 단일 writer만 있는 경우 write를 확장할 수 없습니다. write 급증은 Primary를 빠르게 과부하시켜 ChatGPT 및 API와 같은 서비스에 영향을 줄 수 있습니다.

솔루션: Primary에서 read와 write 모두 가능한 한 부하를 최소화하여 write 급증을 처리할 충분한 용량을 확보합니다. Read 트래픽은 가능한 한 replica로 오프로드됩니다. 그러나 write 트랜잭션의 일부인 일부 read 쿼리는 Primary에 남아야 합니다. 이러한 경우 쿼리가 효율적이고 느린 쿼리를 피하도록 보장하는 데 집중합니다.

쿼리 최적화

과제: PostgreSQL에서 여러 expensive 쿼리를 식별했습니다. 과거에는 이러한 쿼리의 볼륨 급증이 대량의 CPU를 소비하여 ChatGPT와 API 요청을 모두 느리게 만들었습니다.

솔루션: 12개 테이블을 조인하는 매우 비용이 많이 드는 쿼리를 발견했으며, 이 쿼리의 급증이 과거 고심각도 SEV의 원인이었습니다. 복잡한 다중 테이블 조인은 가능한 한 피해야 합니다. 조인이 필요한 경우 쿼리를 분해하고 복잡한 조인 로직을 애플리케이션 레이어로 이동하는 것을 고려해야 합니다.

이러한 문제 쿼리 중 다수는 ORM(Object-Relational Mapping) 프레임워크에 의해 생성되므로, 생성된 SQL을 주의 깊게 검토하고 예상대로 동작하는지 확인하는 것이 중요합니다. 또한 idle_in_transaction_session_timeout과 같은 timeout을 구성하여 장기 실행 idle 쿼리가 autovacuum을 차단하는 것을 방지해야 합니다.

단일 장애 지점(SPOF) 완화

과제: Read replica가 다운되면 트래픽을 다른 replica로 라우팅할 수 있습니다. 그러나 단일 writer에 의존한다는 것은 단일 장애 지점이 있다는 의미이며, 다운되면 전체 서비스가 영향을 받습니다.

솔루션: 가장 중요한 요청은 read 쿼리만 포함합니다. Primary의 단일 장애 지점을 완화하기 위해 writer에서 replica로 이러한 read를 오프로드하여 Primary가 다운되어도 해당 요청이 계속 서비스될 수 있도록 합니다.

Primary 장애를 완화하기 위해 Hot Standby와 함께 고가용성(HA) 모드로 Primary를 실행합니다. Hot Standby는 지속적으로 동기화되는 replica로 항상 트래픽을 인계받을 준비가 되어 있습니다. PostgreSQL에서는 Primary 서버가 continuous archiving 모드로 작동하고 각 standby 서버는 continuous recovery 모드로 작동하며 Primary에서 WAL 파일을 읽습니다. Azure PostgreSQL 팀은 매우 높은 부하에서도 이러한 failover가 안전하고 안정적으로 유지되도록 상당한 작업을 수행했습니다.

워크로드 격리

과제: 특정 요청이 PostgreSQL 인스턴스에서 불균형적으로 많은 리소스를 소비하는 상황이 자주 발생합니다. 이는 동일한 인스턴스에서 실행되는 다른 워크로드의 성능 저하로 이어질 수 있습니다.

솔루션: "noisy neighbor" 문제를 완화하기 위해 워크로드를 전용 인스턴스로 격리하여 리소스 집약적 요청의 급증이 다른 트래픽에 영향을 주지 않도록 합니다. 구체적으로 요청을 low-priority와 high-priority tier로 분할하고 별도의 인스턴스로 라우팅합니다. 이렇게 하면 low-priority 워크로드가 리소스 집약적이 되어도 high-priority 요청의 성능이 저하되지 않습니다.

연결 풀링(Connection Pooling)

과제: 각 인스턴스에는 최대 연결 제한이 있습니다(Azure PostgreSQL에서 5,000개). 연결이 부족하거나 idle 연결이 너무 많이 누적되기 쉽습니다. 이전에 모든 사용 가능한 연결을 소진시킨 connection storm으로 인한 장애가 있었습니다.

솔루션: PgBouncer를 프록시 레이어로 배포하여 데이터베이스 연결을 풀링합니다. Statement 또는 transaction 풀링 모드로 실행하면 연결을 효율적으로 재사용하여 활성 클라이언트 연결 수를 크게 줄일 수 있습니다. 또한 연결 설정 지연시간을 줄입니다: 벤치마크에서 평균 연결 시간이 50밀리초에서 5밀리초로 감소했습니다.

Region 간 연결과 요청은 비용이 많이 들 수 있으므로 프록시, 클라이언트, replica를 동일한 region에 배치하여 네트워크 오버헤드와 연결 사용 시간을 최소화합니다.

각 read replica에는 여러 PgBouncer 파드를 실행하는 자체 Kubernetes 배포가 있습니다. 동일한 Kubernetes Service 뒤에서 여러 Kubernetes 배포를 실행하여 파드 간 트래픽을 로드 밸런싱합니다.

PostgreSQL 프록시로서의 PgBouncer

캐싱 전략

과제: 캐시 미스의 급증은 PostgreSQL 데이터베이스에 read 급증을 유발하여 CPU를 포화시키고 사용자 요청을 느리게 만들 수 있습니다.

솔루션: PostgreSQL의 read 압력을 줄이기 위해 캐싱 레이어를 사용하여 대부분의 read 트래픽을 제공합니다. 그러나 캐시 hit rate가 예기치 않게 떨어지면 캐시 미스의 burst가 대량의 요청을 직접 PostgreSQL로 푸시할 수 있습니다.

캐시 미스 storm 동안 과부하를 방지하기 위해 캐시 락킹(및 리싱) 메커니즘을 구현하여 특정 키에 대해 미스가 발생한 단일 reader만 PostgreSQL에서 데이터를 가져오도록 합니다. 여러 요청이 동일한 캐시 키에 대해 미스가 발생하면 한 요청만 락을 획득하고 다른 요청은 해당 요청이 데이터를 가져올 때까지 대기합니다.

결론

OpenAI의 경험은 적절한 최적화와 엔지니어링을 통해 PostgreSQL을 초당 수백만 쿼리를 처리하고 수억 명의 사용자를 지원하는 규모로 확장할 수 있음을 보여줍니다. MVCC의 근본적인 한계를 이해하고 이를 완화하기 위한 전략적 접근(write 워크로드 분리, 쿼리 최적화, 워크로드 격리, 연결 풀링, 캐싱)을 통해 단일 Primary 아키텍처의 한계를 극복하고 안정적인 서비스를 제공할 수 있었습니다.

References

GPU Programming 101

· 약 5분
최재훈
LEAD (AI Research Engineer), Brain Crew

TL;DR

GPU 프로그래밍은 CPU와 근본적으로 다른 병렬 처리 아키텍처를 활용하는 기술입니다. CPU가 순차적 처리에 최적화된 반면, GPU는 수천 개의 코어로 대규모 병렬 연산을 수행합니다. AI Research Engineer라면 딥러닝 모델 학습 최적화를 위해 GPU의 메모리 계층 구조(글로벌/공유/레지스터 메모리), 스레드 조직화(그리드/블록/워프), 그리고 메모리 접근 패턴(coalescing)을 이해해야 합니다. CUDA나 OpenCL 같은 프레임워크를 통해 GPU를 프로그래밍하며, 효율적인 커널 설계가 성능의 핵심입니다.

Key Takeaways

  • 대규모 병렬성 활용: GPU는 수천 개의 경량 코어를 통해 단순한 연산을 대량으로 처리하는데 특화되어 있어, 행렬 연산이 많은 딥러닝에 최적입니다.
  • 메모리 계층 최적화: 글로벌 메모리(느림, 대용량) → 공유 메모리(빠름, 블록 공유) → 레지스터(매우 빠름, 스레드 전용) 순으로 활용하면 성능을 극대화할 수 있습니다.
  • 메모리 접근 패턴: 연속된 스레드가 연속된 메모리를 접근하는 coalesced access 패턴을 유지해야 메모리 대역폭을 효율적으로 사용할 수 있습니다.
  • 스레드 조직화 이해: 워프(32개 스레드 단위) 단위로 동작하므로, 분기문(if-else)은 워프 내 발산(divergence)을 일으켜 성능을 저하시킬 수 있습니다.
  • 프레임워크 선택: CUDA(NVIDIA 전용, 성숙한 생태계)와 OpenCL(크로스 플랫폼) 중 프로젝트 요구사항에 맞게 선택하고, PyTorch/TensorFlow 같은 고수준 라이브러리가 내부적으로 어떻게 GPU를 활용하는지 이해하면 디버깅과 최적화에 유리합니다.

상세 내용

GPU vs CPU: 아키텍처의 근본적 차이

GPU Programming 101

CPU와 GPU는 서로 다른 목적으로 설계된 프로세서입니다. CPU는 복잡한 제어 로직과 큰 캐시를 가진 소수의 강력한 코어로 구성되어, 순차적 처리와 분기 예측에 최적화되어 있습니다. 반면 GPU는 간단한 제어 유닛을 가진 수천 개의 작은 코어로 구성되어, 동일한 연산을 대량의 데이터에 병렬로 적용하는 SIMT(Single Instruction, Multiple Threads) 아키텍처를 채택합니다.

AI/ML 워크로드에서 GPU가 압도적인 이유는 명확합니다. 신경망 학습의 핵심인 행렬 곱셈, 컨볼루션 연산, 활성화 함수 적용 등은 모두 수백만 개의 독립적인 연산으로 분해될 수 있으며, 이는 GPU의 대규모 병렬 처리 능력과 완벽하게 부합합니다.

GPU 메모리 계층 구조

GPU 프로그래밍에서 성능 최적화의 핵심은 메모리 계층을 이해하고 활용하는 것입니다:

글로벌 메모리 (Global Memory)

  • 가장 크지만 가장 느린 메모리 (수백 사이클 지연)
  • 모든 스레드에서 접근 가능
  • 일반적으로 수 GB ~ 수십 GB 용량
  • CPU의 메인 메모리와 유사한 역할

공유 메모리 (Shared Memory)

  • 블록 내 스레드들이 공유하는 고속 메모리
  • 레지스터보다 느리지만 글로벌 메모리보다 100배 이상 빠름
  • 스레드 간 데이터 교환 및 재사용에 활용
  • 일반적으로 블록당 48~96 KB

레지스터 (Registers)

  • 각 스레드 전용의 가장 빠른 메모리
  • 지연 시간이 거의 없음
  • 로컬 변수가 저장되는 공간
  • 제한적이므로 과도한 사용 시 occupancy 감소

효율적인 메모리 사용 패턴은 다음과 같습니다:

  1. 글로벌 메모리에서 데이터를 공유 메모리로 로드
  2. 공유 메모리에서 여러 번 재사용하며 연산 수행
  3. 결과를 다시 글로벌 메모리에 저장

이 패턴은 느린 글로벌 메모리 접근을 최소화하고, 빠른 공유 메모리의 지역성(locality)을 활용합니다.

스레드 조직화: 그리드, 블록, 워프

GPU 프로그래밍에서는 스레드를 계층적으로 조직합니다:

그리드 (Grid)

  • 전체 커널 실행 단위
  • 여러 블록으로 구성
  • 1D, 2D, 3D 구조 가능

블록 (Block)

  • 스레드의 그룹
  • 같은 블록 내 스레드는 공유 메모리 사용 가능
  • 동기화 가능 (__syncthreads())
  • 일반적으로 128~1024 스레드로 구성

워프 (Warp)

  • 32개 스레드의 실행 단위 (NVIDIA 기준)
  • 동일한 명령어를 동시에 실행
  • 워프 내 분기(branch divergence) 발생 시 직렬화되어 성능 저하

최적화 팁:

  • 블록 크기는 워프 크기(32)의 배수로 설정
  • 워프 내 조건 분기 최소화
  • 메모리 접근은 coalesced pattern 유지 (연속된 스레드가 연속된 메모리 접근)

프로그래밍 모델과 프레임워크

CUDA (Compute Unified Device Architecture)

  • NVIDIA GPU 전용
  • 가장 성숙한 생태계와 도구
  • C/C++ 확장 형태
  • cuDNN, cuBLAS 등 최적화된 라이브러리 제공
__global__ void vectorAdd(float *A, float *B, float *C, int N) {
int idx = blockIdx.x * blockDim.x + threadIdx.x;
if (idx < N) {
C[idx] = A[idx] + B[idx];
}
}

OpenCL (Open Computing Language)

  • 크로스 플랫폼 (NVIDIA, AMD, Intel 등)
  • 이식성이 높지만 CUDA 대비 복잡한 API
  • 다양한 하드웨어 지원 필요 시 선택

고수준 프레임워크

  • PyTorch, TensorFlow: 자동 GPU 가속
  • Numba, CuPy: Python에서 GPU 커널 작성
  • Triton: OpenAI의 GPU 프로그래밍 언어

AI Research Engineer를 위한 실전 가이드

프로파일링과 디버깅

  • NVIDIA Nsight Systems/Compute로 병목 지점 분석
  • 커널 실행 시간, 메모리 전송 시간 측정
  • Occupancy 확인 (이론적 최대 대비 실제 활용률)

일반적인 최적화 전략

  1. 메모리 대역폭 최적화: Coalesced access, 불필요한 전송 제거
  2. 연산 강도 증가: 메모리 접근 대비 연산 비율 높이기
  3. Occupancy 최적화: 블록 크기, 레지스터 사용량 조절
  4. 텐서 코어 활용: Mixed precision training (FP16/BF16)

딥러닝 특화 최적화

  • Fused kernels: 여러 연산을 하나의 커널로 결합
  • Memory pooling: 반복적 할당/해제 오버헤드 제거
  • Gradient accumulation: 큰 배치를 여러 작은 배치로 분할
  • Flash Attention: 메모리 효율적인 attention 구현

실무에서의 고려사항

GPU 프로그래밍은 추상화 수준에 따라 접근 방식이 달라집니다:

  • 고수준 (대부분의 경우): PyTorch/TensorFlow 사용, 프레임워크 최적화 기능 활용
  • 중간 수준: 커스텀 CUDA 커널 작성 (PyTorch custom ops)
  • 저수준: 전체 시스템 최적화, 새로운 아키텍처 구현

대부분의 AI Research Engineer는 고수준 프레임워크를 주로 사용하지만, GPU 동작 원리를 이해하면:

  • 예상치 못한 성능 저하 원인 파악 가능
  • 메모리 부족(OOM) 문제 해결 전략 수립
  • 모델 아키�처 설계 시 하드웨어 친화적 선택
  • 프로파일링 결과를 올바르게 해석

References

  • 원본 문서: GPU Programming 101 (Backend-Engineering, Architecture)

Fastapi 모범사례 15선

· 약 5분
최재훈
LEAD (AI Research Engineer), Brain Crew

TL;DR

FastAPI를 프로덕션 환경에서 안정적으로 운영하기 위한 15가지 핵심 모범 사례를 다룹니다. 블로킹 작업 시 동기 함수 사용, 비동기 라이브러리 활용, 무거운 작업의 분리, 보안을 위한 문서 노출 제한, 구조화된 로깅, 그리고 Gunicorn+Uvicorn 조합의 배포 전략까지 실무에서 즉시 적용 가능한 패턴을 제시합니다. 특히 async/await의 올바른 사용과 의존성 주입을 통한 코드 재사용성 향상이 핵심입니다.

Key Takeaways

  • async/def 선택 기준: 블로킹 작업(파일 I/O, 동기 DB 쿼리)은 def로, 논블로킹 작업은 async def로 정의하여 FastAPI가 적절한 스레드 풀에서 처리하도록 설계
  • ML 추론 작업 분리: CPU/GPU 집약적인 작업은 Triton, TensorFlow Serving 등 전용 엔진으로 오프로드하고, FastAPI는 요청 검증과 라우팅만 담당
  • 의존성 주입 활용: DB 연결, 인증, 검증 로직을 의존성으로 분리하여 코드 재사용성을 높이고 테스트 용이성 확보
  • 보안 강화: 프로덕션에서 Swagger/ReDoc 비활성화, env 파일로 민감 정보 관리, Pydantic Settings로 설정 검증
  • 구조화된 로깅: structlog/loguru로 request_id 기반 컨텍스트 로깅을 구현하여 분산 환경에서 추적 가능성 확보

상세 내용

비동기 처리의 올바른 이해

FastAPI에서 async defdef의 선택은 성능에 직접적인 영향을 미칩니다. async 엔드포인트는 메인 스레드에서 실행되므로, 블로킹 작업(time.sleep(), requests.get(), 동기 DB 클라이언트)을 포함하면 전체 서버가 멈추게 됩니다.

블로킹 작업 처리 방법:

# ❌ 잘못된 방법 - 서버가 10초간 멈춤
@app.get("/")
async def endpoint():
time.sleep(10)

# ✅ 올바른 방법 - Thread Pool에서 처리
@app.get("/")
def endpoint():
time.sleep(10)

반대로 비동기 지원 라이브러리를 활용하면 더 높은 동시성을 달성할 수 있습니다:

# ✅ 비동기 라이브러리 활용
async def endpoint():
await asyncio.sleep(1)
async with httpx.AsyncClient() as client:
await client.get(url)
client = AsyncIOMotorClient()
await client.db.collection.find_one()

무거운 작업의 아키텍처 분리

머신러닝 추론: NVIDIA Triton, TensorFlow Serving, TorchServe 같은 전용 추론 엔진을 사용하면 FastAPI는 경량화된 API 게이트웨이 역할만 수행합니다. 이는 GPU 자원을 효율적으로 사용하고, 추론 서버의 독립적인 스케일링을 가능하게 합니다.

오래 걸리는 작업: Queue + Worker 패턴으로 비동기 처리를 구현합니다:

  1. FastAPI → 메시지 큐(Redis, RabbitMQ)로 작업 전달
  2. 별도 워커가 큐에서 작업 처리
  3. 결과를 DB에 저장
  4. 클라이언트는 polling 또는 webhook으로 결과 조회

간단한 백그라운드 작업:

@app.post("/register")
async def register_user(user_data: UserCreate, bg_tasks: BackgroundTasks):
bg_tasks.add_task(send_email, user_data.email)
bg_tasks.add_task(event_user_registered, user_data.email)
return {"message": "OK"}

⚠️ BackgroundTasks는 서버 재시작 시 작업이 손실되므로, 중요한 작업은 메시지 큐를 사용하세요.

의존성 주입 패턴

의존성 주입은 1-3번 규칙이 동일하게 적용되며, 코드 재사용성을 크게 향상시킵니다.

DB 연결 관리:

@asynccontextmanager
async def lifespan(app: FastAPI):
app.state.pool = await create_pool()
yield
await app.state.pool.close()

async def get_conn(request: Request):
async with request.app.state.pool.acquire() as conn:
yield conn

@app.get("/data")
async def endpoint(db_conn = Depends(get_conn)):
# 커넥션 풀 재사용

검증 로직 분리:

async def validate_owner(
post_id: int,
user = Depends(get_user)
):
post = await db.get(post_id)
if post.user_id != user.id:
raise HTTPException(403)
return post

@app.put("/posts/{post_id}")
async def update_post(post = Depends(validate_owner)):
# 검증 로직 재사용

Pydantic 기반 검증 및 설정 관리

커스텀 Base 모델:

class CustomBaseModel(BaseModel):
class Config:
alias_generator = to_camel
populate_by_name = True
json_encoders = {
datetime: datetime.isoformat,
Decimal: str,
ObjectId: str
}

Validator 활용:

class UserIn(BaseModel):
email: EmailStr
age: int = Field(gte=18)

@validator("email")
def must_be_corporate(cls, v):
if not v.endswith("@company.com"):
raise ValueError("Must be a company email")
return v

응답 자동 검증:

# ✅ FastAPI가 자동으로 검증 및 직렬화
@app.get("/user", response_model=UserOut)
async def get_user():
return {"id": 1, "name": "Alice"} # dict 반환만으로 충분

설정 관리:

from pydantic_settings import BaseSettings

class Settings(BaseSettings):
db_url: str
api_key: str
debug: bool

settings = Settings() # .env 자동 로드

보안 강화

문서 노출 제한:

app = FastAPI(
docs_url=None if PRODUCTION else "/docs",
redoc_url=None if PRODUCTION else "/redoc",
openapi_url=None if PRODUCTION else "/openapi.json",
)

프로덕션에서 API 문서를 노출하면 공격자에게 엔드포인트 구조, 파라미터 형식, 인증 방식을 알려주는 것과 같습니다.

환경 변수 관리:

# .env
API_KEY="secret_key"
DATABASE_URL="postgresql://..."

# .gitignore
.env

# .env.example (템플릿)
API_KEY="your_api_key_here"
DATABASE_URL="your_database_url"

구조화된 로깅

structlog 미들웨어:

class LoggingMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
structlog.contextvars.clear_contextvars()
structlog.contextvars.bind_contextvars(
request_id=str(uuid.uuid4())
)
logger.info("request_received",
method=request.method,
path=request.url.path
)
response = await call_next(request)
logger.info("request_completed",
status_code=response.status_code)
return response

로그 출력 예시:

{
"method": "GET",
"path": "/hello",
"event": "request_received",
"request_id": "d22f859d-443d-9a19-5b8073dd27b6",
"level": "info",
"timestamp": "2025-06-28T17:13:08.501954Z"
}

분산 환경에서는 FileBeat → Elasticsearch → Kibana 같은 중앙 로깅 시스템을 구축하여 여러 인스턴스의 로그를 추적할 수 있습니다.

프로덕션 배포 전략

Gunicorn + Uvicorn 조합:

# 개발 환경
uvicorn main:app --reload

# 프로덕션 환경
gunicorn main:app \
--workers 4 \
--worker-class uvicorn.workers.UvicornWorker \
--bind 0.0.0.0:8000

uvloop 성능 향상: Node.js의 libuv 기반 고성능 이벤트 루프를 제공합니다. 설치만 하면 FastAPI가 자동으로 감지하여 사용합니다.

Worker 수 결정: 일반 공식은 (CPU 코어 * 2) + 1이지만, 부하 테스트를 통해 최적값을 찾는 것이 중요합니다. I/O 바운드 애플리케이션은 더 많은 워커가, CPU 바운드는 적은 워커가 유리할 수 있습니다.

Docker 컨테이너화: Docker를 사용하면 일관된 환경 보장, 쉬운 스케일링, Kubernetes 같은 오케스트레이션 도구와의 통합이 가능합니다.

References