StudyDad Loop 제품을 만들며 배운 운영과 설계를 기록합니다.

FamBlend를 중심으로 실제 구현, 운영 메모, GitHub 포트폴리오를 연결해 쌓아가는 StudyDad의 작업 기록입니다.

성장과 기술/시스템 설계

API timeout을 피하는 잡 큐와 비동기 워커 패턴

박세식 2026. 7. 12. 12:00

운영 콘솔 아키텍처 시리즈 7/9

운영 콘솔에는 버튼을 누르면 파일을 만들어 내려받는 기능이 자주 있다. 엑셀, CSV, 집계표, 분석 리포트 같은 것들이다. 처음에는 단순하게 생각하기 쉽다.

사용자가 요청한다. 서버가 데이터를 조회한다. 파일을 만든다. 응답으로 내려준다.

작은 데이터라면 이 방식도 괜찮다. 하지만 작업이 수 초에서 수십 초 걸리기 시작하면 동기 HTTP 요청 안에서 끝내는 방식은 금방 한계에 부딪힌다.

문제 상황

리포트 생성은 다음 특성을 가졌다.

  • 여러 테이블에서 데이터를 조회한다.
  • 파일 생성 시간이 데이터 양에 따라 달라진다.
  • 브라우저가 응답을 기다리는 동안 사용자는 아무것도 알 수 없다.
  • API Gateway나 프록시에는 timeout이 있다.
  • 실패했을 때 어디까지 진행됐는지 추적해야 한다.

동기 API로 만들면 사용자는 오래 기다려야 하고, 서버는 요청 하나를 오래 붙잡고 있어야 한다. 중간에 timeout이 나면 실제 작업은 끝났는지 실패했는지도 애매해진다.

선택지

첫 번째 선택지는 동기 응답을 유지하는 것이다. 구현은 단순하지만 timeout과 사용자 경험이 문제다.

두 번째 선택지는 서버에서 파일을 만든 뒤 바로 다운로드 URL로 redirect하는 것이다. 작업 시간이 짧다면 가능하지만, 오래 걸리는 경우에는 여전히 HTTP 요청을 붙잡는다.

세 번째 선택지는 리포트 생성을 job으로 모델링하는 것이다. API는 job을 만들고 즉시 job id를 반환한다. 실제 파일 생성은 worker가 비동기로 처리한다. 사용자는 상태를 조회하다가 완료되면 다운로드한다.

세 번째 방식을 선택했다.

최종 결정

오래 걸리는 리포트 생성은 동기 API가 아니라 비동기 job으로 처리한다.

구현 구조

작업 상태는 report_job 같은 테이블에 저장한다.

컬럼 의미
id job id
status PENDING, IN_PROGRESS, COMPLETE, FAILED
requested_by 요청자
parameters 리포트 생성 조건
result_path 결과 파일 위치
error_message 실패 이유
created_at 생성 시각
updated_at 마지막 갱신 시각

사용자가 리포트 생성을 요청하면 API는 바로 파일을 만들지 않는다.

POST /api/reports
  -> validate request
  -> insert report_job
  -> trigger worker
  -> return { jobId }

worker는 비동기로 실행된다. Lambda를 쓴다면 async invocation을 사용할 수 있고, 상주 서버 환경이라면 별도 queue와 worker process를 둘 수 있다.

Worker
  -> read job parameters
  -> update status to IN_PROGRESS
  -> query data
  -> generate report.xlsx
  -> upload to object storage
  -> update status to COMPLETE

프론트엔드는 상태를 폴링한다.

GET /api/jobs/{jobId}
  -> { status: "IN_PROGRESS" }
  -> { status: "COMPLETE", downloadUrl: "..." }

얻은 것

API 응답이 빨라졌다. 사용자는 리포트 생성을 요청하고 바로 job id를 받는다. 브라우저는 긴 HTTP 연결을 붙잡지 않아도 된다.

timeout 문제도 사라졌다. API 요청과 실제 작업 실행이 분리되어 있기 때문에, API Gateway나 브라우저 timeout에 작업 전체가 묶이지 않는다.

운영 대응도 쉬워졌다. job table을 보면 어떤 요청이 진행 중이고, 무엇이 실패했는지 확인할 수 있다. 실패한 job을 다시 실행하는 기능도 만들 수 있다.

사용자 경험도 개선된다. "파일 생성 중", "완료", "실패" 상태를 화면에 명확히 보여줄 수 있다. 사용자는 기다리는 동안 다른 작업을 할 수 있다.

포기한 것

구현은 확실히 복잡해진다. API, worker, job table, 상태 조회, 파일 저장소가 필요하다. 작은 리포트 하나라면 과한 구조일 수 있다.

실패 처리도 신경 써야 한다. worker가 중간에 죽었을 때 job은 계속 IN_PROGRESS로 남을 수 있다. 일정 시간 이상 갱신되지 않은 job을 실패 처리하거나 재시도하는 정책이 필요하다.

같은 요청을 여러 번 눌렀을 때 중복 job이 생길 수도 있다. 요청 파라미터와 사용자 기준으로 idempotency key를 두는 것도 고려할 수 있다.

다시 설계한다면

초기에는 polling으로 충분하지만, 작업 시간이 길고 사용자가 많아지면 알림 기반으로 바꿀 수 있다. WebSocket, Server-Sent Events, 이메일, 사내 알림 등 선택지는 많다.

또한 worker를 Lambda self-invoke로 시작했다면, 작업 수가 늘어날 때는 명시적인 queue를 도입하는 편이 좋다. queue가 있으면 retry, dead letter queue, 처리량 제어가 쉬워진다.

정리

오래 걸리는 작업은 HTTP 요청 안에서 끝내려 하지 않는 것이 좋다.

리포트 생성을 job으로 모델링하면 timeout을 피할 수 있고, 상태 추적과 실패 대응이 쉬워진다. 사용자는 빠르게 응답을 받고, 시스템은 무거운 작업을 독립적으로 처리한다. 운영 콘솔에서 파일 생성 기능이 커지기 시작하면 가장 먼저 고려할 만한 패턴이다.

함께 볼 GitHub 저장소

Next Step 이 글은 StudyDad 작업 루프의 한 조각입니다.

글에서 정리한 생각은 GitHub의 코드와 포트폴리오로 이어지고, 일부는 FamBlend 같은 제품 실험으로 확장됩니다.