FastAPI로 만드는 AI 서비스 백엔드: 비동기 API 설계 기초

입출력이 많은 AI 서비스에서 동기와 비동기 처리를 구분해 설계하는 기본기

공들여 만든 AI 모델이 있어도, 다른 애플리케이션이 호출할 수 있는 서비스 형태로 배포되지 않으면 실제로 쓰이기 어렵다. 특히 외부 LLM API를 호출하거나 데이터베이스를 조회하는 등 결과를 받기까지 시간이 걸리는 입출력 작업이 많은 AI 서비스는, 전통적인 동기 방식 웹 프레임워크로 구현하면 하나의 요청이 처리되는 동안 다른 요청이 모두 대기하는 병목이 쉽게 발생한다. 사용자는 응답이 늦어질수록 서비스를 이탈하기 쉬우므로, 이런 대기 시간을 어떻게 줄이느냐가 AI 백엔드 설계의 핵심 과제가 된다.

FastAPI가 AI 백엔드에 적합한 이유

FastAPI는 파이썬 기반의 현대적인 웹 프레임워크로, 프레임워크 설계 단계부터 비동기 처리를 기본으로 삼고 있어 입출력 작업이 많은 AI 서비스와 궁합이 좋다. 코드에 타입 힌트를 붙이는 것만으로 요청과 응답 데이터의 유효성을 자동으로 검사해주고, Swagger UI 같은 대화형 API 문서를 자동 생성해주기 때문에 프론트엔드 개발자와 협업할 때도 설명 문서를 따로 만드는 수고가 줄어든다.

동기와 비동기의 차이: 은행 창구 비유

창구 직원이 한 명뿐인 은행을 생각해보면 동기 방식을 이해하기 쉽다. 한 고객의 업무가 오래 걸리는 일이면, 뒤에 선 모든 고객은 그 일이 끝날 때까지 하염없이 기다려야 한다. 반면 비동기 방식은 멀티태스킹이 가능한 직원과 같다. 다른 부서 확인이 필요한 일이 생기면 해당 고객에게 번호표를 주고 곧바로 다음 고객 업무를 처리하다가, 확인이 끝나면 다시 첫 번째 고객 업무를 마무리한다. FastAPI의 async/await 구문이 바로 이런 방식으로 동작하며, 대기 시간 동안 다른 요청을 처리해 전체 처리량을 끌어올린다.

실제 팁: 비동기 엔드포인트 설계 방법

기본적인 비동기 엔드포인트 작성하기

함수를 async def로 선언하면 FastAPI가 알아서 비동기적으로 요청을 처리한다. 아래는 외부 AI 서비스를 호출하는 상황을 흉내 낸 예시다.

from fastapi import FastAPI
import asyncio

app = FastAPI()

@app.post("/predict")
async def predict(text: str):
    # 외부 API 호출이나 DB 조회 등 느린 I/O 작업을 시뮬레이션
    await asyncio.sleep(5)
    return {"prediction": f"'{text}'에 대한 예측 결과입니다."}

이 엔드포인트는 5초간 대기하지만, 그동안 서버가 멈추는 것이 아니라 다른 요청을 동시에 받아 처리할 수 있다는 점이 핵심이다.

CPU 연산이 오래 걸리는 작업 분리하기

async/await는 입출력 작업에만 효과가 있다. 모델 추론처럼 순수 연산이 오래 걸리는 작업을 async def 안에서 그대로 실행하면 비동기의 이점을 살릴 수 없고 여전히 다른 요청을 막는다. 이럴 때는 별도의 스레드 풀에서 실행해야 한다.

from fastapi import FastAPI
from fastapi.concurrency import run_in_threadpool
import time

def cpu_bound_inference(text: str) -> str:
    time.sleep(5)
    return f"'{text}'에 대한 CPU 기반 추론 결과"

@app.post("/cpu_predict")
async def cpu_predict(text: str):
    prediction = await run_in_threadpool(cpu_bound_inference, text=text)
    return {"prediction": prediction}

일반 def로 선언한 엔드포인트는 FastAPI가 자동으로 스레드 풀에서 실행해주므로, 상황에 맞게 async 키워드를 붙이거나 빼는 것만으로도 두 종류의 작업을 안전하게 분리할 수 있다.

엔드포인트 유형별로 나눠 설계하기

실제 서비스에서는 하나의 백엔드 안에 외부 API를 호출하는 엔드포인트와 무거운 연산을 수행하는 엔드포인트가 섞여 있는 경우가 많다. 이 둘을 처음부터 구분해서 설계해두면, 나중에 어떤 엔드포인트가 병목의 원인인지 찾기 쉬워진다. 입출력 위주 엔드포인트는 비동기 라이브러리로 통일하고, 연산 위주 엔드포인트는 스레드 풀이나 별도 워커로 분리하는 원칙을 팀 내에서 미리 정해두는 편이 유지보수에 도움이 된다.

주의사항

Async All the Way 원칙

비동기의 이점을 온전히 누리려면 async def 함수 안에서 호출하는 라이브러리도 비동기를 지원해야 한다. 데이터베이스 접근에는 동기용 드라이버 대신 비동기 드라이버를, HTTP 요청에는 동기 클라이언트 대신 비동기 클라이언트를 사용해야 한다. 동기 라이브러리를 하나라도 호출하는 순간 그 함수는 다시 다른 요청을 막는 구간이 되어버린다.

동시성과 병렬성을 혼동하지 않기

비동기는 여러 작업을 번갈아 처리하는 동시성을 제공할 뿐, 여러 코어에서 동시에 연산이 실행되는 병렬성을 의미하지 않는다. async는 단일 CPU 작업 자체의 속도를 빠르게 해주지 않으므로, 무거운 계산을 실제로 병렬화하려면 여러 워커 프로세스를 두거나 더 강력한 하드웨어를 고려해야 한다. 이 차이를 모른 채 비동기로 바꾸기만 하면 성능이 좋아질 것이라 기대하면, 정작 연산이 무거운 구간에서는 개선을 체감하기 어렵다.

정리

FastAPI는 뛰어난 처리 성능과 네이티브 비동기 지원 덕분에 AI 서비스 백엔드를 구축하는 데 유용한 선택지다. 입출력 위주 작업은 async def로 처리하고 연산 위주 작업은 스레드 풀로 분리하는 것만으로도, 외부 서비스 호출이 잦은 환경에서 동시 처리 성능과 응답성을 함께 확보할 수 있다. 여기에 자동 문서화와 데이터 유효성 검사 기능까지 더해지면, 개발 생산성과 서비스 안정성을 동시에 높이는 백엔드를 비교적 적은 수고로 만들 수 있다.

핵심 요약

  • FastAPI는 비동기 처리를 기본으로 설계되어 입출력이 많은 AI 서비스에 적합하다.
  • async/await는 입출력 작업의 대기 시간 동안 다른 요청을 처리할 수 있게 해준다.
  • CPU 연산이 오래 걸리는 작업은 async def 대신 스레드 풀에서 실행해야 한다.
  • 비동기 라이브러리를 일관되게 사용해야 Async All the Way 원칙이 유지된다.

초보자가 자주 실수하는 포인트

  • 모델 추론 같은 연산 작업까지 async def 안에서 그대로 실행해 오히려 다른 요청을 막는 경우
  • 동기 방식 DB 드라이버나 HTTP 클라이언트를 async 함수 안에서 그대로 호출하는 경우
  • 비동기를 도입하면 연산 속도 자체가 빨라진다고 오해하는 경우

체크리스트

  • 입출력 위주 엔드포인트와 연산 위주 엔드포인트를 구분했는가
  • async def 함수 안에서 동기 라이브러리를 호출하고 있지 않은가
  • 연산이 오래 걸리는 작업을 스레드 풀이나 별도 워커로 분리했는가
  • 동시성과 병렬성의 차이를 팀원들이 이해하고 있는가

이 글은 초보자 기준으로 이해하기 쉽게 정리되었으며, 내용은 운영 과정에서 순차적으로 보완될 수 있습니다.