AI 서비스는 왜 개발할 때는 잘 되는데 운영에 가면 터질까?

AI 서비스는 왜 개발할 때는 잘 되는데 운영에 가면 터질까?

AI 개발자가 알아야 할 비동기 프로그래밍 1부

AI 서비스를 개발하는 일은 예전보다 훨씬 쉬워졌다.

Python으로 FastAPI API 서버를 만들고, LLM API를 호출하고, Vector DB를 연결하면 생각보다 빠르게 서비스를 만들 수 있다.

예를 들어 간단한 RAG 서비스를 만든다고 해보자.

글 내용

개발 환경에서 테스트해 보면 꽤 잘 동작한다.

질문을 입력한다.

몇 초 기다린다.

답변이 나온다.

“잘 되는데?”

그런데 이 서비스를 실제 Production 환경에 올리면 이야기가 달라진다.

사용자가 10명, 100명으로 증가하면서 API 응답이 느려지기 시작한다.

어떤 요청은 timeout이 발생한다.

RDB Connection Pool이 부족해질 수도 있고, 외부 LLM API 호출이 밀릴 수도 있다. 메모리가 계속 증가하거나 API 프로세스가 더 이상 새로운 요청을 원활하게 처리하지 못하

는 상황도 생긴다.

경우에 따라서는 Background Worker의 작업이 밀리면서 Queue가 계속 쌓이는 문제도 발생한다.

코드는 똑같은데 왜 개발할 때는 잘 되던 서비스가 운영에 가면 문제가 생길까?

이 질문을 이해하려면 async와 await 문법을 배우기 전에 먼저 알아야 하는 것이 있다.

우리 시스템에서 누가 무엇을 하고 있고, 누가 무엇을 기다리고 있는가?


1. 먼저 ‘서버’라는 말을 구분하자

개발하면서 우리는 흔히 이런 말을 한다.

서버에서 처리하면 됩니다.

하지만 실제 AI 서비스에는 서로 전혀 다른 역할을 하는 여러 프로그램과 시스템이 존재한다.

예를 들어 다음과 같은 구조를 생각해볼 수 있다.

글 내용

여기에는 적어도 몇 가지 서로 다른 역할이 있다.

FastAPI API Server

사용자의 HTTP 요청을 받아 처리하고 HTTP 응답을 반환한다.

예를 들면 다음과 같다.

POST /question
POST /documents
GET  /documents
GET  /status

사용자가 질문을 보내면 API Server가 Vector DB와 LLM 등을 호출하여 결과를 사용자에게 반환할 수 있다.


Background Worker

사용자의 HTTP 요청과 직접 연결되지 않은 작업을 처리한다.

예를 들어 문서 업로드 이후 다음 작업들이 있을 수 있다.

PDF Parsing
    ↓
Chunking
    ↓
Embedding
    ↓
Vector DB 저장

이런 작업을 API 요청 안에서 끝날 때까지 수행하기보다는 Queue에 넣고 Worker가 처리하도록 만들 수 있다.

예를 들면 Celery Worker, RQ Worker 또는 직접 만든 Python Worker 등이 이런 역할을 한다.


외부 AI 서비스

OpenAI, AWS Bedrock 같은 LLM/Embedding 서비스를 사용할 수도 있다.

이 경우 실제 AI 추론은 우리의 FastAPI 프로세스에서 실행되는 것이 아니다.

FastAPI API Server나 Worker는 외부 AI 서비스에 HTTP 요청을 보내고 결과를 기다린다.


데이터 저장 시스템

RDB, Redis, Vector DB, Object Storage 등도 별도의 시스템이다.

예를 들어

PostgreSQL
MySQL
Redis
Qdrant
S3

등이 있다.

따라서 앞으로 이 글에서는 가능한 한 막연하게 ‘서버’라고 표현하지 않고,

FastAPI API Server
Background Worker
LLM API
Embedding API
Vector DB
RDB
Redis
Object Storage

처럼 어떤 역할을 수행하는 시스템인지 구분해서 이야기하겠다.

이 구분이 비동기를 이해하는 데 상당히 중요하다.


2. FastAPI API Server는 생각보다 많은 시간을 기다린다

사용자가 RAG 질문을 보냈다고 생각해보자.

사용자
   │
   │ POST /question
   ▼
FastAPI API Server
   │
   ├─→ Embedding API
   │       ↓
   │     결과 대기
   │
   ├─→ Vector DB
   │       ↓
   │     결과 대기
   │
   ├─→ Reranker
   │       ↓
   │     결과 대기
   │
   ├─→ LLM API
   │       ↓
   │     결과 대기
   │
   └─→ RDB
           ↓
         저장 완료 대기
   │
   ▼
사용자에게 응답

각 단계가 다음 정도 걸린다고 가정해보자.

Embedding API       150ms
Vector DB            80ms
Reranker             300ms
LLM API            4,000ms
RDB 저장              30ms

전체 API 요청에는 약 4~5초가 걸린다.

그렇다면 이 4~5초 동안 FastAPI가 실행되는 CPU가 계속 계산하고 있을까?

대부분의 경우 그렇지 않다.

예를 들어 FastAPI API Server가 외부 LLM API에 요청을 보냈다고 하자.

FastAPI API Server
        │
        │ HTTP Request
        ▼
     LLM API
        │
        │ AI 추론 수행
        │
        │
        ▼
     HTTP Response
        │
        ▼
FastAPI API Server

LLM이 답변을 만드는 동안 실제 AI 추론을 수행하는 것은 LLM Provider 쪽 시스템이다.

우리 FastAPI API Server는 결과가 돌아오기를 기다리고 있다.

즉 API 요청 처리시간이 5초라고 해서 우리 CPU가 5초 동안 계산하고 있다는 의미는 아니다.

이 차이가 중요하다.


3. CPU가 일하는 것과 I/O를 기다리는 것은 다르다

프로그램의 작업을 이해하기 위해 크게 두 종류로 나눠보자.

글 내용

첫 번째는 CPU가 실제로 계산해야 하는 작업이다.

FastAPI / Worker Process
        │
        ▼
      CPU
        │
        ├─ 이미지 변환
        ├─ 대규모 데이터 계산
        ├─ 압축
        ├─ 암호화
        └─ 로컬 ML 모델 추론

이런 작업을 일반적으로 CPU-bound 작업이라고 한다.

CPU가 실제로 바쁘다.

반면 다음과 같은 작업도 있다.

FastAPI API Server / Worker
        │
        ├─ HTTP ─────→ LLM API
        │
        ├─ TCP ──────→ PostgreSQL
        │
        ├─ HTTP ─────→ Vector DB
        │
        ├─ TCP ──────→ Redis
        │
        └─ HTTP ─────→ S3

요청을 보낸 뒤 상대 시스템이 결과를 반환하기까지 기다려야 한다.

이런 작업은 대표적인 I/O-bound 작업이다.

AI 서비스에는 이런 I/O 작업이 굉장히 많다.


4. 문제는 기다리는 것 자체가 아니다

LLM API가 답변을 생성하는 데 5초가 걸리는 것 자체는 반드시 문제가 아니다.

LLM Provider가 답변을 만드는 데 5초가 필요하다면 우리 FastAPI API Server가 그 시간을 0.1초로 만들 수는 없다.

중요한 질문은 이것이다.

FastAPI API Server가 그 5초 동안 다른 사용자의 요청을 처리할 수 있는가?

식당을 하나 생각해보자.

FastAPI API Server가 식당 직원이라고 생각하면 된다.

손님 A가 주문한다.

직원은 주문을 주방에 전달한다.

여기서 주방은 LLM API라고 생각해보자.

손님 A
  │
  ▼
직원(FastAPI)
  │
  │ 주문
  ▼
주방(LLM API)

주방에서 음식을 만드는 데 10분이 걸린다.

그런데 직원이 음식을 받을 때까지 주방 앞에 서 있다고 생각해보자.

손님 A
   │
   ▼
직원
   │
   ▼
주방
   │
   │
   │ 10분...
   │
   │
   ▼
음식 완성

그동안 손님 B가 들어온다.

손님 C도 들어온다.

하지만 직원은 여전히 주방 앞에서 기다리고 있다.

이 식당은 손님 한 명일 때는 아무 문제가 없다.

하지만 점심시간이 되면 상황이 완전히 달라진다.

글 내용

5. 개발 환경에서는 왜 문제가 잘 보이지 않을까?

개발할 때는 대부분 한 사람이 테스트한다.

개발자
  │
  │ POST /question
  ▼
FastAPI API Server
  │
  ├─→ Vector DB
  ├─→ LLM API
  └─→ RDB
  │
  ▼
응답

잘 된다.

다시 질문한다.

또 잘 된다.

그래서 생각한다.

API 잘 만들어졌네.

하지만 Production 환경은 다르다.

User 1 ─┐
User 2 ─┤
User 3 ─┤
User 4 ─┤
        │
        ▼
┌────────────────────┐
│ FastAPI API Server │
└────────────────────┘
        ▲
        │
User 97 ─┤
User 98 ─┤
User 99 ─┤
User100 ─┘

사용자들은 순서대로 한 명씩 들어오지 않는다.

여러 요청이 겹쳐서 들어온다.

바로 여기에서 동시성(Concurrency) 문제가 등장한다.

글 내용

6. 사용자 한 명에게 5초 걸리는 API

다음과 같은 FastAPI Endpoint가 있다고 생각해보자.

POST /question

이 Endpoint가 LLM API를 호출하고 답변을 반환하는 데 평균 5초가 걸린다.

사용자가 한 명이라면 별 문제가 없다.

User A
  │
  ▼
FastAPI
  │
  ├──────── LLM API ────────┐
  │                         │
  │        5초 대기          │
  │                         │
  ◀─────────────────────────┘
  │
  ▼
User A

그런데 동시에 많은 사용자가 요청하면 상황이 달라진다.

만약 어떤 실행 흐름이 LLM 응답을 기다리는 동안 다른 작업을 진행하지 못한다면 그 실행 흐름은 몇 초 동안 묶여 있게 된다.

요청 수가 증가하면 이런 대기들이 계속 겹친다.

결국 API Server가 처리할 수 있는 동시 요청 수와 Worker Process 수, Thread 수, Connection Pool 크기 등의 한계가 드러나기 시작한다.

이때 흔히 나타나는 현상이 있다.

동시 요청 증가
      ↓
처리 대기 증가
      ↓
응답시간 증가
      ↓
Timeout 증가
      ↓
Retry 증가
      ↓
더 많은 요청
      ↓
시스템 부하 증가

개발 환경에서는 거의 볼 수 없었던 현상이다.


7. 여기서 비동기가 등장한다

이제 다시 LLM 호출을 생각해보자.

FastAPI API Server가 LLM API에 요청을 보냈다.

FastAPI
   │
   ├─────────────→ LLM API
   │
   │
   │   응답을 기다려야 함
   │
   │
   ◀────────────── LLM 응답

비동기 방식의 핵심 아이디어는 간단하다.

기다리는 동안 다른 일을 하자.

개념적으로는 다음과 같다.

Request A
   │
   ├──→ LLM API
   │
   │    응답 대기
   │
   ├──────────────┐
                  │
Request B         │
   │              │
   ├──→ Vector DB │
   │              │
   │              │
Request C         │
   │              │
   ├──→ RDB       │
                  │
                  │
LLM 응답 도착 ◀───┘
   │
   ▼
Request A 계속 처리

LLM이 빨라진 것이 아니다.

CPU가 빨라진 것도 아니다.

I/O를 기다리는 동안 같은 실행 환경이 다른 처리 기회를 활용할 수 있게 된 것이다.

이것이 비동기를 이해하는 가장 중요한 출발점이다.

글 내용

8. 그래서 async와 await가 등장한다

Python에서는 이런 코드를 보게 된다.

async def ask_llm():
    result = await llm.generate(...)
    return result

처음 비동기 프로그래밍을 접하면 다음과 같은 용어가 한꺼번에 등장한다.

async
await
coroutine
event loop
task
future

그래서 비동기 프로그래밍을 Python의 어려운 문법이라고 생각하기 쉽다.

하지만 그 전에 기억해야 할 것은 단 하나다.

I/O를 기다리는 동안 다른 일을 할 수 있도록 실행 흐름을 양보한다.

async와 await는 이 아이디어를 프로그램으로 표현하기 위한 도구다.

구체적으로 어떻게 동작하는지는 다음 편에서 자세히 살펴보자.


9. 비동기는 LLM을 빠르게 만드는 기술이 아니다

여기서 매우 중요한 오해 하나를 없애야 한다.

비동기를 사용한다고 LLM API 자체가 빨라지는 것은 아니다.

LLM 응답에 5초가 걸린다면

동기 호출     약 5초
비동기 호출   약 5초

한 사용자가 느끼는 응답시간은 크게 달라지지 않을 수도 있다.

차이가 나타나는 것은 여러 요청이 겹쳐 들어올 때다.

여기서 두 가지 개념을 구분해야 한다.

Latency : 한 요청이 완료되는 데 걸리는 시간.
Throughput : 일정 시간 동안 시스템이 처리할 수 있는 작업량.

비동기는 많은 경우 한 작업의 계산 자체를 빠르게 만드는 기술이라기보다,

I/O 대기가 많은 프로그램이 대기시간을 효율적으로 활용하여 더 많은 작업을 처리할 수 있도록 만드는 방법이다.


10. 비동기는 FastAPI API Server에서만 필요한가?

아니다.

Background Worker에서도 똑같은 문제가 생길 수 있다.

예를 들어 사용자가 PDF 문서 100개를 업로드했다고 생각해보자.

API Server는 작업을 Queue에 넣고 바로 응답한다.

사용자
  │
  ▼
FastAPI API Server
  │
  ▼
Queue
  │
  ▼
Background Worker

Worker가 다음 작업을 수행한다.

S3에서 PDF 다운로드
        ↓
Parsing
        ↓
Chunking
        ↓
Embedding API 호출
        ↓
Vector DB 저장
        ↓
RDB 상태 업데이트

여기에서도 작업의 성격은 서로 다르다.

S3 Download       → I/O 대기
Parsing           → CPU 작업 가능성
Chunking          → CPU 작업
Embedding API     → I/O 대기
Vector DB         → I/O 대기
RDB               → I/O 대기

즉,

API Server냐 Worker냐가 비동기 여부를 결정하는 것이 아니다.

중요한 것은 그 프로세스가 수행하는 작업의 성격이다.

지금 이 프로그램은 CPU로 계산하고 있는가?

아니면,

다른 시스템의 응답을 기다리고 있는가?

이 질문이 먼저다.


11. 그렇다면 모든 것을 async로 만들면 될까?

그렇지 않다.

예를 들어 Worker가 대용량 이미지 변환 작업을 수행한다고 생각해보자.

Background Worker
      │
      ▼
대용량 이미지 처리
      │
      ▼
CPU 100%

이 경우 문제는 기다림이 아니다.

CPU가 실제 계산을 하고 있다.

이런 CPU-bound 작업에 단순히 `async를 붙인다고 CPU가 하나 더 생기는 것은 아니다.

예를 들어 다음과 같은 작업들은 대표적으로 CPU 사용량을 많이 요구할 수 있다.

이미지 변환
영상 인코딩
압축 / 압축 해제
대규모 데이터 변환
OCR
로컬 ML 모델 추론
복잡한 수치 계산

이런 작업은 비동기 I/O와는 다른 접근이 필요하다.

경우에 따라 Thread나 Process를 사용하거나, CPU 작업 전용 Worker를 별도로 구성해야 할 수도 있다.

즉,

I/O 대기가 많다
     ↓
비동기를 검토한다

CPU 계산이 많다
     ↓
Thread / Process / Worker 구조를 검토한다

정도로 우선 구분해서 생각하면 좋다.

물론 실제 시스템에서는 I/O-bound와 CPU-bound 작업이 섞여 있다.

앞서 살펴본 문서 처리 Worker가 대표적인 예다.

Background Worker
      │
      ├─ S3 Download ─────── I/O
      │
      ├─ PDF Parsing ─────── CPU + I/O
      │
      ├─ Chunking ────────── CPU
      │
      ├─ Embedding API ───── I/O
      │
      ├─ Vector DB ───────── I/O
      │
      └─ RDB Update ──────── I/O

그래서 실제 Production 시스템에서는 단순히

“우리 서비스는 async로 만들었습니다.”

라는 말만으로는 시스템이 잘 설계되었는지 판단할 수 없다.

어떤 작업을 어떤 방식으로 실행하고 있는지를 봐야 한다.


12. async def 라고 쓰면 비동기 코드일까?

여기서 또 하나의 중요한 함정이 있다.

다음 코드를 보자.

import requests

async def call_external_api():
    response = requests.get("https://example.com")
    return response.json()

함수 앞에 분명히 async가 붙어 있다.

그렇다면 이 함수는 다른 작업을 방해하지 않고 비동기로 동작할까?

그렇게 단순하지 않다.

requests.get()은 일반적인 동기 HTTP 호출이다.

HTTP 요청을 보낸 뒤 응답이 돌아올 때까지 현재 실행 흐름을 붙잡는다.

즉 겉모습은

async def

이지만 함수 내부에서 blocking I/O를 수행하고 있다.

FastAPI의 비동기 Endpoint에서도 이런 실수는 발생할 수 있다.

@app.get("/question")
async def question():
    result = requests.get("https://llm-api.example.com")
    return result.json()

코드만 보면 비동기 API처럼 보인다.

하지만 내부의 blocking 호출 때문에 해당 Event Loop의 진행을 막을 수 있다.

반면 비동기 HTTP Client를 사용한다면 구조가 달라진다.

import httpx

async def call_external_api():
    async with httpx.AsyncClient() as client:
        response = await client.get("https://example.com")
        return response.json()

여기서는 HTTP 응답을 기다리는 동안 await를 통해 실행 기회를 다른 작업에 넘길 수 있다.

따라서 중요한 것은

async def를 사용했는가?

가 아니라

그 함수 내부의 작업이 실제로
비동기 방식으로 기다릴 수 있는가?

이다.

이 차이를 모르면 코드 전체에 async와 await가 가득한데도 실제 서비스에서는 동시 요청이 제대로 처리되지 않는 일이 생길 수 있다.


13. FastAPI도 자동으로 모든 것을 해결해 주지는 않는다

FastAPI가 비동기를 지원한다고 해서 FastAPI로 만든 API가 자동으로 효율적인 비동기 서비스가 되는 것도 아니다.

예를 들어 구조가 다음과 같다고 해보자.

              FastAPI API Server


async Endpoint

┌─────────┼─────────┐
▼ ▼ ▼
LLM API RDB Vector DB
│ │ │
▼ ▼ ▼
async sync async

LLM 호출과 Vector DB Client는 비동기로 구현되어 있는데 RDB 접근에 사용하는 라이브러리는 동기 방식일 수도 있다.

또는 다음과 같은 경우도 있다.

async Endpoint
     │
     ▼
PDF Parsing
     │
     ▼
CPU 100%

이 경우 PDF Parsing 작업이 오래 걸리면 Event Loop가 다른 요청을 처리할 기회를 얻지 못할 수 있다.

따라서

FastAPI를 사용했다.

비동기 처리가 올바르게 설계되어 있다.

는 전혀 다른 이야기다.

FastAPI는 비동기 프로그램을 만들 수 있는 도구를 제공한다.

그 도구를 어떻게 사용하는지는 개발자의 몫이다.


14. 그렇다고 동기 방식이 나쁜 것도 아니다

여기까지 읽으면 이런 생각이 들 수 있다.

그러면 앞으로 모든 코드를 async로 작성해야 하나?

그럴 필요는 없다.

동기 방식은 훨씬 단순하고 이해하기 쉽다.

예를 들어 하루에 몇 번 실행되는 관리용 Script가 있다고 해보자.

Python Script
    │
    ├─ DB 조회
    │
    ├─ 데이터 가공
    │
    └─ 결과 저장

실행시간이 3초에서 4초가 되는 것이 아무 문제가 없다면 굳이 복잡한 비동기 구조를 만들 이유가 없다.

사용자 요청을 실시간으로 처리하지 않는 간단한 Batch 프로그램도 마찬가지다.

매일 새벽 2시

Batch 시작
    ↓
 DB 조회
    ↓
   처리
    ↓
 파일 생성
    ↓
   종료

10분 안에 끝나면 되는 작업이 동기 방식으로 3분 만에 끝난다면 충분하다.

비동기를 도입하면 얻는 것만 있는 것도 아니다.

코드의 실행 흐름을 이해하기 어려워질 수 있고,

동시 실행에 따른 새로운 문제도 생긴다.

따라서 중요한 것은

비동기를 사용하는 것 자체가 아니라 비동기가 필요한 상황을 판단하는 능력이다.


15. 비동기는 무한 동시 실행을 의미하지 않는다

비동기를 처음 배우고 나면 반대 방향의 실수를 하기도 한다.

“기다리는 동안 다른 작업을 할 수 있다면 가능한 한 많이 동시에 실행하면 빠르지 않을까?”

예를 들어 Embedding API를 호출해야 하는 데이터가 10,000개 있다고 하자.

이론적으로는 다음과 같이 생각할 수 있다.

Embedding 1   ─→
Embedding 2   ─→
Embedding 3   ─→
...
Embedding 10000 ─→

Python에서는 asyncio.gather() 등을 이용해 많은 작업을 동시에 실행할 수도 있다.

하지만 10,000개의 요청을 한꺼번에 실행한다고 해서 좋은 시스템이 되는 것은 아니다.

오히려 다음과 같은 일이 발생할 수 있다.

10,000 Tasks
      │
      ▼
HTTP Connection 급증
      │
      ├─ Connection Pool 고갈
      ├─ Memory 증가
      ├─ 외부 API Rate Limit
      ├─ Socket 증가
      └─ Timeout 증가
              │
              ▼
             Retry
              │
              ▼
          요청량 더 증가
              │
              ▼
             장애

이것은 매우 중요한 사실이다.

비동기는 동시성을 가능하게 하는 기술이지, 무한한 동시성을 가능하게 하는 기술이 아니다.

글 내용

16. 결국 Production에서는 ‘얼마나 동시에’가 중요해진다

이제 질문이 조금 달라진다.

처음에는 이렇게 질문했다.

비동기로 만들 것인가?

하지만 실제 서비스를 운영할 때는 더 중요한 질문이 생긴다.

동시에 몇 개까지 실행할 것인가?

예를 들어 FastAPI API Server가 외부 LLM API를 호출한다고 해보자.

FastAPI API Server

Request 1 ──────→ LLM
Request 2 ──────→ LLM
Request 3 ──────→ LLM
...
Request 100 ────→ LLM

비동기 구조라면 많은 요청이 동시에 LLM 응답을 기다릴 수 있다.

그런데 시스템에는 여러 한계가 존재한다.

FastAPI 동시 요청
        │
        ├─ HTTP Connection Pool
        │
        ├─ DB Connection Pool
        │
        ├─ Vector DB 처리량
        │
        ├─ LLM API Rate Limit
        │
        ├─ Memory
        │
        └─ Network

어느 하나라도 한계를 넘으면 병목이 발생한다.

예를 들어 RDB Connection Pool이 20개인데 동시에 500개의 요청이 DB Connection을 요구한다면 500개가 모두 즉시 DB를 사용할 수는 없다.

결국 일부 요청은 기다려야 한다.

그래서 Production에서는 단순히 async를 사용하는 것을 넘어

Concurrency Limit
Semaphore
Connection Pool
Timeout
Rate Limit
Queue
Backpressure
Worker

같은 개념이 중요해진다.


17. API Server와 Background Worker는 문제의 모습도 다르다

FastAPI API Server와 Background Worker를 구분해서 보는 이유가 여기에도 있다.

API Server의 가장 중요한 특징은 사용자가 응답을 기다리고 있다는 것이다.

사용자
  │
  │ Request
  ▼
FastAPI API Server
  │
  │ 처리
  ▼
Response
  │
  ▼
사용자

처리가 밀리면 사용자가 직접 느낀다.

2초
 ↓
5초
 ↓
15초
 ↓
30초
 ↓
Timeout

반면 Background Worker는 조금 다르다.

FastAPI
   │
   ▼
Queue
   │
   ├─ Task 1
   ├─ Task 2
   ├─ Task 3
   ├─ Task 4
   └─ ...
        │
        ▼
Background Worker

Worker의 처리속도보다 작업이 들어오는 속도가 빠르면 Queue가 쌓인다.

작업 유입

100 task/sec
     │
     ▼
   Queue
     │
     ▼
Worker 처리

50 task/sec

매초 100개의 작업이 들어오는데 Worker가 50개밖에 처리하지 못한다면 매초 50개의 작업이 밀린다.

1분이면 3,000개다.

10분이면 30,000개다.

API Server가 바로 죽지 않더라도 서비스는 이미 정상적인 상태가 아니다.

따라서 Worker에서는

Queue Length
Processing Rate
Worker Concurrency
Task Duration
Retry
Memory
CPU

같은 요소를 함께 봐야 한다.


18. 그래서 ‘서버가 터졌다’는 말도 정확하지 않다

현장에서 흔히 듣는 말이 있다.

서버가 터졌어요.

그런데 실제로 무엇이 문제인지 보면 전혀 다른 원인들이 있을 수 있다.

"서버가 느려졌다"

               │
        ├─ FastAPI Event Loop가 Blocking됨
        │
        ├─ API Worker가 모두 점유됨
        │
        ├─ RDB Connection Pool 고갈
        │
        ├─ HTTP Connection Pool 고갈
        │
        ├─ Background Queue 적체
        │
        ├─ Worker CPU 100%
        │
        ├─ Memory 부족
        │
        ├─ 외부 LLM API 지연
        │
        └─ 외부 API Rate Limit

겉으로 사용자에게 보이는 현상은 비슷할 수 있다.

“응답이 안 온다.”

하지만 원인은 완전히 다르다.

그래서 Production 문제를 해결하려면

‘서버가 느리다’가 아니라 ‘어떤 프로세스가 어떤 자원을 기다리고 있는가’를 봐야 한다.

이 관점은 비동기를 공부하면서 반드시 익혀야 한다.


19. AI 서비스는 ‘계산 서버’라기보다 ‘오케스트레이터’에 가까워지고 있다

특히 최근의 AI 서비스를 보면 이런 특징이 더욱 강해진다.

RAG 서비스를 생각해보자.

                    ┌──────────────┐
사용자 ───────────→ │ FastAPI API │
└──────┬───────┘

┌───────────────┼────────────────┐
│ │ │
▼ ▼ ▼
Embedding API Vector DB RDB


Reranker


LLM API

Agent 서비스는 더 심하다.

FastAPI API Server
        │
        ▼
      Agent
        │
        ├─→ LLM API
        │
        ├─→ Search API
        │
        ├─→ Database
        │
        ├─→ MCP / Tool
        │
        ├─→ 다른 사내 API
        │
        └─→ 다시 LLM API

우리 프로그램이 모든 계산을 직접 수행하는 것이 아니다.

여러 시스템에 일을 요청하고,

기다리고,

결과를 받고,

다른 시스템에 다시 요청하고,

마지막 결과를 조합한다.

따라서 현대적인 AI 애플리케이션의 API Server나 Worker는 많은 경우

여러 시스템 사이의 작업을 조율하는 오케스트레이터(Orchestrator) 역할을 한다.

이런 구조에서는 I/O 대기를 어떻게 다루느냐가 시스템 전체 처리량에 큰 영향을 미친다.

그래서 AI 서비스 개발자에게 비동기는 선택적인 Python 고급 문법 정도로만 보기 어렵다.


20. 우리가 정말 공부해야 하는 것

이번 시리즈에서 공부하려는 것은 단순히 다음 문법을 외우는 것이 아니다.

async def
await
asyncio.create_task()
asyncio.gather()

궁극적으로 다음 질문에 답할 수 있어야 한다.

사용자 요청이 들어왔다.
        │
        ▼
어느 프로세스가 받는가?
        │
        ▼
FastAPI API Server인가?
Background Worker인가?
        │
        ▼
그 프로세스는 지금
무엇을 하고 있는가?
        │
        ├─ CPU 계산?
        │
        └─ I/O 대기?
                │
                ▼
그 대기가 다른 작업을 막는가?
                │
                ▼
동시에 몇 개까지 실행되는가?
                │
                ▼
그 숫자를 제한하고 있는 것은 무엇인가?

여기까지 생각할 수 있다면 코드도 다르게 보이기 시작한다.

예를 들어 이런 질문을 하게 된다.

LLM API 호출에 5초가 걸릴 때 FastAPI API Server로 동시에 500개의 요청이 들어오면 어떻게 되는가?

이 Endpoint 안에서 사용하는 DB Driver는 동기인가 비동기인가?

HTTP Client의 Connection Pool은 몇 개인가?

Vector DB가 10초 동안 느려지면 API 요청들은 어디에서 기다리게 되는가?

Background Worker가 처리할 수 있는 속도보다 Queue에 작업이 들어오는 속도가 빠르면 어떻게 되는가?

CPU-bound 작업을 FastAPI Event Loop 안에서 직접 실행하고 있지는 않은가?

외부 LLM API가 응답하지 않을 때 우리는 몇 초까지 기다릴 것인가?

이 질문들은 AI 모델에 대한 질문이 아니다.

하지만 AI 서비스를 Production에서 운영하려면 반드시 답해야 하는 질문들이다.


마치며

AI 서비스를 만드는 진입장벽은 빠르게 낮아지고 있다.

Python과 FastAPI를 사용하고 LLM API, Vector DB, RDB를 연결하면 비교적 짧은 시간에도 동작하는 서비스를 만들 수 있다.

하지만

동작하는 서비스를 만드는 것과 많은 사용자가 동시에 사용하는 서비스를 만드는 것은 다른 문제다.

개발 환경에서는 보통 이런 구조다.

개발자 한 명
     ↓
FastAPI API Server
     ↓
외부 서비스
     ↓
   응답

Production에서는 이렇게 된다.

                수많은 사용자


FastAPI API Server

┌───────────┼───────────┐
▼ ▼ ▼
LLM API Vector DB RDB


Queue


Background Worker

┌───────────┼───────────┐
▼ ▼ ▼
S3 Embedding API RDB

이제 단순히

“API 호출이 성공하는가?”

만 확인해서는부족하다.

Production에서는 질문이 달라져야 한다.

FastAPI API Server에 동시에 100개의 요청이 들어오면 어떻게 되는가?

그 요청들은 어디에서 기다리고 있는가?

LLM API를 기다리는 동안 FastAPI의 Event Loop는 다른 요청을 처리할 수 있는가?

Background Worker 하나가 긴 작업을 수행하고 있을 때 다른 작업들은 어디에서 기다리는가?

Worker가 처리하는 속도보다 Queue에 작업이 들어오는 속도가 빠르면 어떻게 되는가?

RDB Connection Pool이 20개인데 동시에 100개의 작업이 Connection을 요구하면 어떻게 되는가?

외부 API가 30초 동안 응답하지 않으면 FastAPI API Server나 Worker는 그동안 무엇을 하고 있는가?

CPU-bound 작업을 Event Loop 안에서 실행하면 다른 요청에는 어떤 영향을 미치는가?

이 질문에 답하기 시작하면 비동기는 더 이상 단순한 Python 문법이 아니다.

시스템의 구조를 이해하는 방법이 된다.

그리고 비동기 프로그래밍을 공부하면서 가장 먼저 기억해야 할 것은 의외로 간단하다.

누가 무엇을 기다리고 있는가?

FastAPI API Server가 LLM API를 기다리고 있는가?

Background Worker가 S3 다운로드를 기다리고 있는가?

RDB Connection을 얻지 못해 요청이 기다리고 있는가?

아니면 프로그램은 기다리는 것이 아니라 실제로 CPU를 사용해 계산하고 있는가?

이것부터 구분해야 한다.

그다음 질문은 이것이다.

기다리는 동안 다른 일을 할 수 있는가?

바로 여기에서 비동기 프로그래밍이 시작된다.

하지만 아직 중요한 질문 하나가 남아 있다.

비동기 = 동시에 실행?

동시성 = 병렬 처리?

async = non-blocking?

await = 기다리는 것?

이 용어들은 비슷하게 느껴지지만 같은 의미가 아니다.

특히 async와 await를 사용하면서도 이 차이를 정확히 이해하지 못하면 코드가 어떻게 실행되는지 예상하기 어려워진다.

다음 글에서는 코드를 본격적으로 작성하기 전에 이 개념부터 정리해보자.


다음 글

AI 개발자가 알아야 할 비동기 프로그래밍 2부

동기와 비동기의 진짜 차이 — async는 동시에 실행한다는 뜻일까?

2부에서는 다음 네 가지를 구분하는 것부터 시작한다.

Synchronous   vs   Asynchronous

Blocking      vs   Non-blocking

Concurrency   vs   Parallelism

그리고 이것들이 FastAPI에서 실제로 어떤 의미를 갖는지 살펴본다.

예를 들어 다음과 같은 상황이다.

@app.get("/question")
async def question():
    result = await call_llm()
    return result

await를 만났는데 왜 프로그램은 멈추지 않는 것일까?

아니, 정확히 말하면 무엇은 멈추고 무엇은 멈추지 않는 것일까?

하나의 Python 프로세스가 여러 사용자의 요청을 처리한다는 것은 실제로 무엇을 의미할까?

Event Loop는 무엇을 하고 있는가?

그리고 흔히 말하는

“비동기는 동시에 여러 작업을 실행한다.”

라는 설명은 정확한 표현일까?

2부에서는 식당의 직원 한 명으로 다시 돌아가 이 문제를 하나씩 풀어본다.

직원 한 명이 여러 테이블을 담당할 수 있다고 해서 직원이 여러 명이 된 것은 아니다.

이 차이를 이해하면 Concurrency와 Parallelism의 차이, 그리고 Python 비동기 프로그래밍의 핵심인 Event Loop가 왜 필요한지도 자연스럽게 이해할 수 있다.

여기서 잠깐, Event Loop란 무엇일까

앞에서부터 Event Loop라는 용어가 조금씩 등장하기 시작했다.

비동기 프로그래밍을 처음 접한다면 이름부터 조금 어렵게 느껴질 수 있다.

하지만 지금 단계에서는 복잡하게 생각할 필요가 없다.

Event Loop는 아주 단순하게 말하면,

“지금 실행할 수 있는 작업을 찾아 실행하고, 기다려야 하는 작업은 잠시 내려놓았다가 준비되면 다시 이어서 실행하도록 관리하는 반복 실행 구조” 라고 이해하면 된다.

예를 들어 FastAPI API Server가 다음 세 개의 요청을 처리하고 있다고 생각해보자.

Request A
   ↓
LLM API 호출
   ↓
응답 기다림 ─────────────┐
                      │
Request B             │
   ↓                  │
Vector DB 호출         │
   ↓                  │
응답 기다림 ────────┐    │
                 │    │
Request C        │    │
   ↓             │    │
RDB 호출          │    │
   ↓             │    │
응답 기다림         │    │
                 │    │
                 ▼    ▼
               응답이 도착하면
               해당 작업 계속

Request A가 LLM API의 응답을 기다린다고 해서 Event Loop까지 함께 가만히 기다릴 필요는 없다.

A가 기다리는 동안 실행할 수 있는 Request B의 작업을 처리하고,

B 역시 I/O를 기다리게 되면 Request C의 작업을 처리할 수 있다.

개념적으로 보면 Event Loop는 계속 이런 질문을 하는 셈이다.

┌──────────────────────────┐
│ 지금 실행할 수 있는 작업은?    │
└────────────┬─────────────┘
             │
             ▼
          실행한다
             │
             ▼
       I/O를 기다려야 하나?
          /          \
        YES           NO
         │             │
         ▼             ▼
   잠시 실행을 양보     계속 실행
         │
         ▼
  다른 작업을 확인
         │
         ▼
  I/O 완료 알림 도착
         │
         ▼
  이전 작업을 다시 진행
         │
         └──────────→ 반복

그래서 이름도 Event Loop다.

I/O 완료 같은 이벤트를 확인하면서 실행 가능한 작업들을 계속 처리하는 흐름이 반복된다.

Python의 asyncio에서는 이 Event Loop가 비동기 작업을 실행하는 핵심 역할을 한다.

그리고 await는 이 구조와 밀접한 관계가 있다.

result = await call_llm()

이 코드를 아주 개념적으로 표현하면,

“LLM 응답이 필요하니 나는 지금 여기서 진행할 수 없다. 기다리는 동안 다른 실행 가능한 작업이 있다면 그쪽에 실행 기회를 넘기고, 결과가 준비되면 나를 다시 진행시켜 달라.”

정도로 생각하면 된다.

여기서 중요한 점이 하나 있다.

Event Loop가 여러 작업을 관리한다고 해서 CPU 하나가 동시에 여러 Python 코드를 실행하고 있다는 뜻은 아니다.

Event Loop는 실행 가능한 작업을 빠르게 전환하면서 I/O 대기시간을 활용한다.

그래서 비동기 프로그래밍에서 중요한 것은 Event Loop를 오래 붙잡지 않는 것이다.

예를 들어 다음처럼 오래 걸리는 CPU 작업을 Event Loop에서 직접 실행한다고 생각해보자.

Event Loop
    │
    ▼
PDF Parsing 시작
    │
    │
    │ CPU 작업 중...
    │
    │
    ▼
PDF Parsing 완료
    │
    ▼
다음 작업

PDF Parsing 코드가 CPU를 계속 사용하면서 실행권을 놓지 않는다면 Event Loop는 그동안 다른 비동기 작업으로 넘어갈 기회를 얻지 못할 수 있다.

그래서 다른 요청들은 실행 가능한 상태여도 기다리게 된다.

Request A ─→ PDF Parsing ───────────────→ 완료
                    │
                    │ Event Loop 점유
                    │
Request B ──────────┼──────────── 기다림
Request C ──────────┼──────────── 기다림
Request D ──────────┴──────────── 기다림

이것이 흔히 말하는 “Event Loop를 막았다(Block the Event Loop)” 라는 상황이다.

지금은 여기까지만 이해하면 충분하다.

정리하면,

Event Loop는 기다리는 작업과 실행할 수 있는 작업 사이를 조율하면서 비동기 프로그램이 효율적으로 움직일 수 있도록 관리하는 핵심 실행 구조다.

그리고 앞으로 async, await, Coroutine, Task를 하나씩 살펴보면 Event Loop가 실제로 이 작업들을 어떻게 관리하는지도 자연스럽게 이해할 수 있게 된다.

Event Loop의 내부 동작은 다음 편에서 조금 더 자세히 살펴보자.



최신글