Projects

BizKit AI 서버 설계 기록: 긴 요청을 작업으로 나누기

FastAPI의 작업 접수와 Redis Streams·Worker의 실행을 분리한 이유, 상태 조회 계약과 초기 설계의 한계를 정리한 기록이다.

설계 목표와 담당 범위

BizKit AI 서버는 GitHub 활동 분석, 직무 기반 자기소개, 명함 OCR처럼 모델과 외부 API를 함께 호출하는 기능을 처리했다. 나는 AI 서버의 설계와 구현을 맡았다. GPU 모델 서버는 직접 배포했고, FastAPI 서버 배포는 클라우드 팀원이 맡았다.

이 글은 초기 작업 처리 구조를 결정한 이유에 집중한다. 기능이 확장된 뒤의 책임 경계는 AI 서버 아키텍처에, 운영 중 겪은 문제는 개발 회고에 나눠 정리했다. 초기 설계에 있던 리뷰 분석은 최종 제품에서 제외됐다.

문제는 긴 요청을 어떻게 다룰 것인가였다. 모델 추론과 외부 API 응답을 HTTP 요청 하나에서 모두 기다리면, 클라이언트는 작업이 진행 중인지 실패했는지 구분하기 어렵다. 그래서 작업 접수, 실행, 상태·결과 조회를 나누는 방향을 선택했다.

구현과 API 책임

비동기 경로에서 FastAPI는 요청을 검증하고 작업 ID를 만든 뒤 Redis Streams에 등록한다. Worker는 작업을 읽어 외부 데이터 수집, 모델 호출, 결과 파싱을 수행한다.

요청 접수와 모델 실행의 분리요청 접수와 모델 실행의 분리
요청 접수와 모델 실행의 분리좌우로 스크롤하여 전체 보기원본 크게 보기

실제 저장소의 README는 HEX와 직무 분석의 제출 API를 기능별로 구분한다. 단일 POST /ai/tasks로 모든 기능을 접수하는 인터페이스는 아니다.

역할경로
HEX 작업 접수POST /ai/hex/analyze
직무 분석 작업 접수POST /ai/job/analyze
상태 조회GET /ai/tasks/{task_id}
결과 조회GET /ai/tasks/{task_id}/result

기존 동기 API도 남아 있으므로, 여기서는 비동기 경로의 책임만 설명한다.

Redis Streams를 선택한 이유

요청을 받는 서버와 작업을 실행하는 프로세스를 분리하면서, 대기 작업을 전달할 수단이 필요했다. Redis Streams와 Consumer Group을 사용해 작업을 소비하는 구조를 만들었다.

이 선택으로 API 요청과 Worker 실행을 나눌 수 있었다. 대신 작업 ID, 상태 저장, 결과 조회, Worker 실행 환경을 추가로 관리해야 했다. Worker를 늘릴 수는 있지만, 실제 부하에서 처리량이 늘어나는지와 그 정도는 별도 검증이 필요하다.

상태와 실패

소스의 TaskStatus는 pending, running, completed, failed를 정의한다. 세부 진행 단계는 별도 progress 필드로 관리한다. 상태 enum과 작업 안에서 진행 중인 단계를 섞지 않도록 구분했다.

다음은 정상 처리와 실패를 설명하는 개념도이며, 가능한 모든 예외 전이를 열거한 상태 명세는 아니다.

작업 상태: 정상 완료와 실패의 분기작업 상태: 정상 완료와 실패의 분기
작업 상태: 정상 완료와 실패의 분기좌우로 스크롤하여 전체 보기원본 크게 보기

입력 오류는 접수 단계에서 처리하고, 작업 실행 중 실패는 상태와 오류 정보로 남기는 방향을 잡았다. 실패한 작업을 클라이언트가 계속 처리 중인 것으로 오해하지 않게 하는 것이 목적이다.

다만 상태 저장만으로 실행이 취소되거나 중복 요청이 제거되지는 않는다. 외부 모델 서버가 작업을 받아들인 뒤 응답이 지연되는 상황에서는, 클라이언트가 실패로 판단한 시점과 실제 작업 종료 시점이 다를 수 있다. 이는 재시도 정책과 함께 검토해야 할 문제이다.

HEX 설계 변경

GitHub 활동을 규칙으로 환산하는 부분과 생성형 모델의 역할을 분리하려 했다. 처음에는 계산한 점수를 LLM이 보정하는 방향도 계획했지만, 보정의 정확한 근거를 설명하기 어려웠다.

이후 점수는 정형화할 수 있는 지표로 한정하고 리뷰 분석을 별도 역할로 잡았다. 팀에서는 최종적으로 리뷰 기능을 제외했다. 초기 설계의 점수 보정과 리뷰 연동 계획이 그대로 제품에 반영되지는 않았다.

임베딩 서비스 분리

직무 필터와 시맨틱 캐시에 필요한 임베딩 생성·검색은 별도 HTTP 서비스로 분리했다. 메인 AI 서버가 모델 로딩과 Milvus 컬렉션 관리를 직접 담당하지 않도록 하기 위해서이다.

임베딩 생성과 벡터 저장·검색의 책임임베딩 생성과 벡터 저장·검색의 책임
임베딩 생성과 벡터 저장·검색의 책임좌우로 스크롤하여 전체 보기원본 크게 보기

API를 분리하면 구현 책임을 줄일 수 있지만, 모델을 바꿀 때 API 형태만 유지하면 되는 것은 아니다. 기존 문서 벡터와 새 질의 벡터의 공간이 달라질 수 있으므로 재임베딩과 검색 품질 확인이 필요하다. 세부 조건은 임베딩 서비스에 정리했다.

트레이드오프와 한계

비동기 작업 구조는 긴 요청의 접수와 실행을 나누는 방법이다. 모델 계산 시간, 재시도에 따른 중복 작업, GPU 비용 상한을 자동으로 해결하는 방법은 아니다.

실제로 GPU 할당 문제가 발생했을 때 두 계층의 재시도가 중첩되면서 작업과 비용이 늘어나는 사고를 겪었다. 재시도를 추가하기 전에 담당 계층, 전체 시도 횟수, 작업 생존 시간을 함께 정해야 했다. 이는 사고 이후 정리한 설계 기준이다.

결과와 배운 점

FastAPI의 접수, Worker의 실행, 클라이언트의 상태·결과 조회를 분리했다. AI 기능을 완료될 때까지 기다리는 호출이 아니라 상태를 가진 작업으로 다룰 수 있게 됐다.

남은 과제는 상태 전이와 실패 유형, 재시도의 책임을 더 구체적으로 정하는 것이다. 특히 외부 모델 호출이 실패했을 때 남아 있는 작업을 어떻게 처리할지 명세가 필요하다.