언제 AI 요청을 백그라운드 작업(Background Job)으로 전환해야 하는가
요약
AI 요청을 동기식 요청으로 유지할지 백그라운드 작업으로 전환할지 결정하는 기준을 제시합니다. 작업의 소유권, 내구성, 재시도 필요성 및 사용자 경험을 고려하여 적절한 실행 모델을 선택하는 방법을 다룹니다.
핵심 포인트
- 작업의 소유권이 HTTP 교환에 있는지 애플리케이션에 있는지에 따라 모델이 결정됨
- 결과가 배포나 네트워크 중단 후에도 유지되어야 한다면 백그라운드 작업이 필요함
- 스트리밍은 사용자 경험을 위한 것이지 작업의 내구성을 보장하는 메커니즘이 아님
- 복잡한 파이프라인이나 부하 평준화가 필요한 경우 백그라운드 작업이 적합함
LLM 호출은 예측 불가능한 네트워크 지연 시간(Latency)을 유발하지만, 지연 시간 그 자체만으로 실행 모델을 결정하지는 않습니다. AI 요청은 HTTP 요청이 종료된 후에도 그 결과가 여전히 중요할 때 백그라운드 작업 (Background Job)으로 전환되어야 합니다.
30초가 걸리는 응답이라도 여전히 대화형 스트리밍 요청 (Interactive streaming request)에 속할 수 있습니다. 반면, 5초짜리 작업이라도 영구적인 작업 (Durable work)을 트리거하거나, 배포 중에도 생존해야 하거나, 원래의 HTTP 요청 외부에서 재시도(Retry) 및 중복 처리(Duplicate handling)가 필요하다면 이미 백그라운드 작업이 필요할 수 있습니다.
그 경계는 소유권(Ownership)에 있습니다:
- 동기식 요청 (Synchronous request)은 현재의 HTTP 교환에 의해 소유됩니다.
- 스트리밍 요청 (Streaming request)은 여전히 해당 교환에 의해 소유되지만, 작업이 계속되는 동안 부분적인 출력을 반환합니다.
- 백그라운드 작업 (Background job)은 요청이 수락된 후 애플리케이션에 의해 소유됩니다.
이러한 차이는 API 계약 (API contract), 취소 모델 (Cancellation model), 재시도 동작 (Retry behavior), 상태 저장 (State storage) 및 운영 책임 (Operational responsibilities)을 변화시킵니다.
라이프사이클 결정부터 시작하기
큐 (Queue)를 추가하기 전에, 클라이언트가 연결을 끊거나 애플리케이션이 재시작될 때 어떤 일이 일어나야 하는지 질문하십시오.
| 실행 모델 (Execution model) | 적합한 경우 | 중요한 제한 사항 |
|---|---|---|
| 동기식 요청 (Synchronous request) | 결과가 즉시 필요한 짧고 제한된 작업 | 호출자와 서버가 요청을 활성 상태로 유지해야 함 |
| ... |
다음 조건이 모두 충족될 때는 작업을 요청 내부에 유지하십시오:
- 호출자가 결과를 즉시 필요로 함
- 작업의 실행 시간이 제한되어 있음
- 연결이 끊긴 호출자에게는 더 이상 결과가 필요하지 않음
- 전체 요청을 재시도하는 것이 안전함
- 호스팅 경로가 연결을 충분히 오래 열어둘 수 있음
다음 중 하나 이상의 조건이 충족되면 백그라운드 작업으로 이동하십시오:
- 결과가 브라우저 새로고침, 네트워크 중단 또는 배포 (deployment) 후에도 유지되어야 함
- 작업에 여러 모델 (model), 검색 (retrieval) 또는 도구 (tool) 단계가 포함됨
- 호출자에게 진행 상황 (progress) 또는 상태 페이지가 필요함
- 원래의 HTTP 요청을 다시 제출하지 않고도 재시도 (retries)가 이루어져야 함
- 워크로드에 큐 기반의 부하 평준화 (queue-based load leveling) 또는 동시성 제한 (concurrency limits)이 필요함
- 나중에 검색할 수 있는 아티팩트 (artifact)를 생성함
- 중복 제출이 중복 비용이나 부작용 (side effects)을 초래할 수 있음
이는 채팅에만 국한되지 않습니다. 문서 추출 (document extraction), 대규모 요약 작업 (large summarization jobs), 배치 분류 (batch classification), 평가 실행 (evaluation runs), 인제스션 파이프라인 (ingestion pipelines), 오디오 전사 (audio transcription) 및 보고서 생성 (report generation)이 일반적인 후보입니다.
스트리밍 (Streaming)은 내구성 메커니즘이 아닙니다
스트리밍은 사용자 경험 (user-experience) 문제를 해결합니다. 스트리밍을 통해 호출자는 전체 결과가 준비되기 전에 토큰 (tokens), 이벤트 (events) 또는 진행 상황 (progress)을 볼 수 있습니다.
하지만 다음과 같은 상황에서는 해결책이 되지 않습니다:
- 브라우저가 닫힐 때
- 리버스 프록시 (reverse proxy)가 연결을 종료할 때
- 애플리케이션 인스턴스 (application instance)가 재시작될 때
- 클라이언트가 최종 응답을 놓친 후 재시도할 때
- 워크플로 (workflow)가 몇 분 동안 계속되어야 할 때
- 다른 장치에서 나중에 결과를 검색해야 할 때
스트리밍 작업 또한 내구성이 있는 작업 (durable job)에 의해 뒷받침될 수 있지만, 이는 별개의 설계입니다. 이 경우 스트림은 진행 상황이 존재하는 유일한 장소가 아니라, 영구 저장된 작업 이벤트 (persisted job events)에 대한 하나의 뷰 (view)가 됩니다.
상호작용 자체가 제품인 경우에는 스트리밍을 사용하십시오. 작업을 완료하고 기록하는 것이 제품인 경우에는 작업 (job)을 사용하십시오.
요청 기반 (request-bound) 버전은 간단하지만 취약합니다
문서 분석 (document-analysis) 엔드포인트의 직접적인 버전은 작성하기 쉽습니다:
app.MapPost(
"/documents/{documentId:guid}/analysis",
async (
...
이 방식은 호출자가 기다릴 것으로 예상되고 작업이 안전하게 반복될 수 있는 경우에 유효한 설계입니다.
분석 작업이 요청(Request)보다 더 오래 지속되어야 하는 경우에는 이 방식이 적합하지 않습니다. 전체 작업 과정에 요청 취소 토큰(Request Cancellation Token)을 전달하면 호출자가 떠날 때 작업을 올바르게 중단할 수 있지만, 이는 내구성(Durability)과는 정반대의 개념입니다. 그렇다고 토큰을 무시하는 것도 내구성이 있는 해결책은 아닙니다. 작업은 여전히 안정적인 식별자(Identifier)도, 영속화된 상태(Persisted Status)도, 프로세스 재시작 후의 신뢰할 수 있는 복구 경로(Recovery Path)도 갖지 못하기 때문입니다.
엔드포인트에는 다른 계약(Contract)이 필요합니다.
미완료된 결과가 아닌, 작업 리소스(Job Resource)를 반환하라
비동기 처리(Asynchronous Processing)를 위해서는 초기 요청이 작업을 검증 및 수락하고, 안정적인 작업 식별자(Job Identifier)를 생성한 뒤 빠르게 반환해야 합니다.
일반적인 HTTP 형태는 다음과 같습니다:
POST /documents/{documentId}/analysis-jobs
-> 202 Accepted
-> Location: /analysis-jobs/{jobId}
...
202 Accepted는 분석이 성공했다는 의미가 아닙니다. 이는 서버가 해당 작업을 처리할 책임을 수락했다는 의미입니다.
최소한의 엔드포인트로 해당 계약을 명시적으로 구현할 수 있습니다:
app.MapPost(
"/documents/{documentId:guid}/analysis-jobs",
async (
...
AnalysisJobSubmissionService는 중요한 애플리케이션 경계(Application Boundary)입니다. 이 서비스는 다음을 수행해야 합니다:
- 인증된 호출자가 해당 문서를 분석할 권한이 있는지 확인합니다.
- 멱등성 키(Idempotency Key)의 범위를 호출자 또는 테넌트(Tenant) 및 작업(Operation)으로 제한합니다.
- 키를 요청 지문(Request Fingerprint)에 바인딩합니다.
- 해당 범위의 키가 이미 기존 제출 건에 속해 있는지 확인합니다.
- 저장된 지문과 들어온 지문이 일치하는 경우에만 기존 작업을 반환합니다.
- 키가 존재하지만 지문이 다른 경우에는 요청을 거부합니다.
- 새 작업을
Pending(대기 중) 상태로 영속화합니다. - 작업 식별자가 워커(Worker)에게 전달되도록 준비합니다.
- 엔드포인트로 작업을 반환합니다.
지문(Fingerprint)은 documentId, 작업 유형(operation type), 프롬프트 또는 워크플로 버전(workflow version), 그리고 관련 옵션과 같이 작업의 의미를 변경하는 모든 입력을 포함해야 합니다. 서로 다른 지문에 대해 동일한 키를 재사용할 경우, 관련 없는 작업을 반환하는 대신 충돌(conflict) 응답을 생성해야 합니다. 데이터베이스의 고유 제약 조건(unique database constraint)을 통해 스코프가 지정된 키(scoped key)를 강제함으로써, 동시에 제출된 작업들이 모두 생성되는 상황을 방지할 수 있습니다.
행(row)을 영속화하는 것과 큐 메시지(queue message)를 발행하는 것은 두 개의 별개 쓰기 작업입니다. 이 두 작업 사이에서 작업이 유실되는 것이 허용되지 않는다면, 아웃박스 패턴(outbox pattern)이나 대기 중인 작업을 안정적으로 복구할 수 있는 다른 설계를 사용하십시오. 데이터베이스 커밋(commit) 이후 보호되지 않은 상태로 큐 전송을 수행하면 실패 구간(failure window)이 발생합니다.
작업 라이프사이클(Job lifecycle) 영속화
큐 메시지가 작업의 존재를 나타내는 유일한 기록이 되어서는 안 됩니다. API와 워커(worker)가 모두 사용할 수 있는 영구적인 작업 리소스(persistent job resource)를 유지하십시오.
실제적인 작업 기록에는 일반적으로 다음 항목들이 포함됩니다:
- 작업 ID (job ID)
- 소유자 또는 테넌트 ID (owner or tenant ID)
- 작업 유형 (operation type)
- 입력 참조 (input reference)
- 멱등성 키 (idempotency key)
- 요청 지문 (request fingerprint)
- 상태 (state)
- 생성 및 업데이트 타임스탬프 (creation and update timestamps)
- 애플리케이션 처리 시도 횟수 (application processing attempt count)
- 프롬프트 또는 워크플로 버전 (prompt or workflow version)
- 모델 또는 배포 식별자 (model or deployment identifier)
- 결과 참조 (result reference)
- 안전한 에러 코드 및 메시지 (safe error code and message)
- 취소 요청 타임스탬프 (cancellation request timestamp)
큐가 해당 데이터를 위해 명시적으로 설계되고 관리되지 않는 한, 큐 메시지에 가공되지 않은 프롬프트(raw prompts), 개인 문서, 또는 자격 증명(credentials)을 포함하지 마십시오. 작업 ID를 포함하는 작은 메시지가 재시도, 검사 및 보안 측면에서 더 용이합니다. 워커는 시스템 기록(system of record)으로부터 권한이 부여된 입력을 로드할 수 있습니다.
닫힌 상태 집합(closed set of states)을 사용하십시오. 예를 들어:
public enum AnalysisJobStatus
{
Pending,
...
종료 상태(Terminal states)는 종료 상태로 유지되어야 합니다. 중복된 큐 전달이 이미 Succeeded(성공), Failed(실패), 또는 Canceled(취소) 상태인 작업에 도달하면, 프로세서는 모델을 다시 실행하지 않고 종료해야 합니다.
워커를 HTTP 요청과 분리하기
작업 제출 후의 실행 권한은 워커(Worker)가 가집니다. BackgroundService는 유용한 .NET 호스팅 경계(hosting boundary)이지만, 내구성(durability)은 BackgroundService 자체가 아니라 큐(queue)와 작업 저장소(job store)에서 나옵니다.
public sealed class AnalysisJobWorker(
IAnalysisJobQueue queue,
IServiceScopeFactory scopeFactory,
...
샘플에서는 큐의 의미론(semantics)이 중요하기 때문에 애플리케이션 추상화(application abstractions)를 사용합니다. CompleteAsync는 성공적인 처리를 확인(acknowledge)합니다. FailAsync는 예외를 큐의 재시도(retry) 또는 데드 레터(dead-letter) 동작에 매핑해야 합니다. ReleaseAsync는 호스트가 종료될 때 중단된 전달(interrupted delivery)을 다시 사용할 수 있도록 만듭니다. 정산(Settlement)은 호스트 종료 토큰(shutdown token)이 이미 취소되었을 수 있으므로 짧고 독립적인 타임아웃을 사용합니다. 백엔드 브로커(backing broker)에 적합한 타임아웃과 정산 동작을 선택하십시오.
백엔드 큐는 이러한 작업들이 무엇을 보장할 수 있는지를 결정합니다:
- 경계가 지정된
Channel<T>는 단일 프로세스 내부에서 백프레셔(backpressure)를 제공할 수 있지만, 해당 프로세스가 중단되면 큐에 대기 중인 항목들은 사라집니다. - Azure Service Bus와 같은 내구성 있는 브로커(durable broker)는 재시작 후에도 생존하며 재전달(redelivery)을 지원할 수 있지만, 프로세서는 중복 전달(duplicate delivery)을 처리해야 합니다.
- 데이터베이스 기반 큐는 작업 상태와 디스패치(dispatch)를 가깝게 유지할 수 있지만, 안전한 점유(claiming)와 동시성 제어(concurrency control)가 필요합니다.
인메모리 큐(in-memory queue)를 내구성 있는 것으로 설명하지 마십시오. 이는 작업이 일회성이거나 영구적인 작업 저장소(persistent job store)로부터 재구성될 수 있는 경우에만 적절합니다.
브로커의 전달 횟수(delivery count)와 애플리케이션의 처리 시도(processing attempts)는 서로 연관되어 있지만 서로 다른 신호입니다. 재전달(Redelivery)은 비즈니스 처리가 시작되기 전에 발생할 수 있는 반면, 한 번의 전달 내에 여러 번의 내부 시도가 포함될 수 있습니다. 전송 진단(transport diagnostics)에는 브로커 메타데이터를 사용하고, 워크플로(workflow)에 중요한 처리 시도는 작업 저장소에 기록하십시오.
워커는 싱글톤 호스트 서비스(singleton hosted service)로 등록됩니다. 각 작업마다 의존성 주입(dependency-injection) 범위를 생성하면, 프로세서가 워커의 전체 수명 동안 인스턴스를 유지하지 않고도 DbContext와 같은 스코프 서비스(scoped services)를 사용할 수 있습니다.
처리를 멱등적(Idempotent)으로 만들기
중복이 발생할 수 있는 지점은 두 곳입니다:
- 호출자가 첫 번째 응답을 받지 못해 제출을 재시도하는 경우.
- 확인(acknowledgement) 전 처리가 실패하여 큐(queue)가 작업을 재전송하는 경우.
제출 멱등성 키(submission idempotency key)는 첫 번째 케이스를 처리합니다. 지속적인 작업 상태(persistent job state)와 멱등적 프로세서(idempotent processor)는 두 번째 케이스를 처리합니다.
비용이 많이 드는 모델 호출을 하기 전에, 프로세서는 작업을 로드하여 여전히 작업이 필요한지 결정해야 합니다. 부수 효과(side effect)를 커밋하기 전에, 동일한 효과가 이미 적용되지 않았는지 확인해야 합니다.
해당 확인 작업에는 원자적 점유(atomic claim)가 포함되어야 합니다. 현재 상태, 행 버전(row version)과 같은 낙관적 동시성 토큰(optimistic concurrency token), 또는 시간 제한이 있는 임대(time-bound lease)에 의해 보호되는 조건부 업데이트를 통해 작업을 Pending에서 Running으로 이동시키십시오. 만약 점유에 실패한다면, 다른 워커(worker)가 해당 작업을 소유하고 있는 것이므로 현재의 전달(delivery)은 종료되거나 해제되어야 합니다. 또한, 충돌이 발생한 워커가 작업을 영원히 Running 상태로 방치하지 않도록 임대(lease)에는 만료(expiry) 및 복구 규칙이 필요합니다.
모델 전용 작업의 경우, 호출을 반복하면 비용만 중복될 수 있습니다. 메시지를 보내거나, 레코드를 업데이트하거나, 도구(tools)를 호출하는 작업의 경우, 작업을 반복하면 비즈니스 효과(business effects)도 중복될 수 있습니다. 이러한 효과들은 자체적인 멱등성 경계(idempotency boundary)를 가져야 합니다. 모델이 특정 동작이 이미 발생했는지 여부를 결정하는 책임을 가져서는 안 됩니다.
요청 취소와 작업 취소를 분리하기
제출 엔드포인트의 취소 토큰(cancellation token)은 HTTP 요청에 속합니다. 이는 제출을 검증하고 영속화하는 동안에는 유용합니다. 하지만 수락된 작업의 수명(lifetime)이 되어서는 안 됩니다.
API가 202 Accepted를 반환하고 나면, 취소는 애플리케이션 작업이 됩니다. 전형적인 API는 다음과 같은 형태를 노출합니다:
POST /analysis-jobs/{jobId}/cancellation
해당 엔드포인트는 CancellationRequested를 기록합니다. 워커는 비용이 많이 드는 경계(boundaries) 직전에 해당 상태를 확인하며, 가능한 경우 모델, 검색(retrieval), 도구 호출에 작업별 취소 신호를 전달합니다.
DELETE /analysis-jobs/{jobId}는 리소스를 의도적으로 삭제하는 것이 취소(cancellation)를 요청하는 것을 의미할 때도 방어 가능한 설계입니다. 만약 작업(job)이 계속 유지되면서 상태(state)만 변경되는 것이라면, 명시적인 취소 작업을 통해 해당 계약(contract)을 더 명확하게 만들 수 있습니다.
취소는 협력적(cooperative)입니다. 애플리케이션은 여전히 부분적인 결과(partial results)와 외부 부수 효과(external side effects)에 대해 어떻게 처리할지 결정해야 합니다. 이미 발송된 이메일은 취소할 수 없습니다. 부분적으로 작성된 보고서는 정리가 필요할 수 있습니다. 도구 호출(tool call)은 취소보다는 보상(compensation) 작업이 필요할 수도 있습니다.
호스트 종료(Host shutdown)는 또 다릅니다. stoppingToken은 워커(worker)에게 프로세스가 중단되고 있음을 알립니다. 내구성이 있는 큐(durable queue)를 사용하는 경우, 완료되지 않은 작업은 비즈니스 실패로 조용히 표시되기보다는 재전송(redelivery)될 수 있도록 해제되어야 합니다.
내부 정보를 유출하지 않고 유용한 상태를 노출하기
상태 엔드포인트(status endpoint)는 공개 계약(public contract)의 일부이지, 운영 로그 뷰어가 아닙니다.
호출자가 조치를 취할 수 있는 정보를 반환하세요:
{
"jobId": "c7d8d96c-45ef-4c40-9a96-d6d12b9db4ec",
"status": "Running",
...
}
가공되지 않은 예외 메시지(raw exception messages), 모델 페이로드(model payloads), 자격 증명(credentials), 또는 내부 큐(internal queue)의 세부 정보를 반환하지 마세요. 실패를 안정적인 에러 코드(error codes)와 안전한 설명으로 매핑하세요. 지원 팀이 공개된 실패와 내부 텔레메트리(telemetry)를 연관 지을 수 있도록 돕는 경우에만 응답에 트레이스 ID(trace IDs)를 포함하세요.
모든 상태 및 취소 요청에 대해 작업 소유자 또는 테넌트(tenant)를 기준으로 권한을 부여(Authorize)하세요. 작업 ID를 알고 있다는 사실만으로 다른 사용자의 입력, 결과 또는 실패 세부 정보를 읽을 수 있어서는 안 됩니다.
알림을 내구성이 있는 상태로 만들지 않고 완료를 푸시하기
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기