
.NET AI 시스템에서 CancellationToken이 더 중요한 이유
요약
.NET 환경에서 AI 워크로드의 특성을 고려할 때 CancellationToken 사용의 중요성을 강조합니다. LLM 호출과 스트리밍 응답 등 긴 실행 시간이 필요한 AI 작업에서 리소스 낭비를 방지하기 위한 필수적인 엔지니어링 패턴을 다룹니다.
핵심 포인트
- AI 워크로드는 일반 CRUD보다 실행 시간이 길어 취소 처리가 필수적임
- CancellationToken 미사용 시 토큰 및 컴퓨팅 자원 낭비 발생
- 스트리밍 응답 및 RAG 파이프라인에서 리소스 효율성 극대화 가능
- ASP.NET Core 및 IChatClient 등 전 계층에 취소 토큰 전달 권장
CancellationToken은 .NET에서 가장 과소평가된 AI 엔지니어링 기능 중 하나입니다.
새로운 기능이라서가 아닙니다.
AI 워크로드(Workload)는 서로 다른 런타임 프로필(Runtime profile)을 가지고 있기 때문입니다.
일반적인 애플리케이션 호출은 밀리초(milliseconds) 단위로 끝날 수 있습니다.
LLM 호출은 몇 초가 걸릴 수 있습니다.
스트리밍 응답(Streaming response)은 토큰이 생성되는 동안 계속 실행될 수 있습니다.
임베딩 파이프라인(Embedding pipeline)은 수천 개의 청크(Chunks)를 처리할 수 있습니다.
도구 호출(Tool call)은 또 다른 느린 네트워크 요청을 트리거할 수 있습니다.
그리고 때로는, 사용자가 이미 떠나버리기도 합니다.
탭을 닫았을 수도 있습니다.
다른 페이지로 이동했을 수도 있습니다.
HTTP 요청이 타임아웃(Timeout) 되었을 수도 있습니다.
백그라운드 작업(Background job)이 중단되었을 수도 있습니다.
배포(Deployment)가 종료되고 있을 수도 있습니다.
취소(Cancellation) 처리가 없다면, 애플리케이션은 더 이상 아무도 필요로 하지 않는 비용이 많이 드는 작업을 계속 수행하게 될 수 있습니다.
이는 다음과 같은 결과를 의미합니다:
- 낭비되는 토큰 (Wasted tokens)
- 낭비되는 컴퓨팅 자원 (Wasted compute)
- 불필요한 도구 호출 (Unnecessary tool calls)
- 느려지는 종료 프로세스 (Slower shutdowns)
- 노이즈가 많은 트레이스 (Noisy traces)
- 악화된 리소스 사용 (Worse resource usage)
.NET에서 이것은 특별한 AI 문제만이 아닙니다.
AI 시스템에서 훨씬 더 눈에 띄게 나타나는 일반적인 엔지니어링 문제입니다.
CancellationToken을 전달하십시오.
ASP.NET Core 엔드포인트(Endpoint)로부터.
HttpContext.RequestAborted로부터.
에이전트(Agent) 호출로.
IChatClient 호출로.
임베딩 생성(Embedding generation)으로.
검색 레이어(Retrieval layer)로.
데이터베이스 쿼리(Database query)로.
도구 실행(Tool execution)으로.
특히 IAsyncEnumerable을 사용하여 스트리밍 응답을 보낼 때 더욱 중요합니다. 백엔드가 생성을 멈추기 훨씬 전에 UI가 듣기를 중단할 수 있기 때문입니다.
AI 엔지니어링은 더 나은 프롬프트(Prompts), 더 나은 모델(Models), 또는 더 나은 프레임워크(Frameworks)에 관한 것만이 아닙니다.
그것은 요청의 생명주기(Lifecycle)를 존중하는 것에 관한 것이기도 합니다.
AI 워크로드가 이 문제를 드러내는 이유
.NET에 취소(Cancellation) 기능이 존재하는 이유는 오래 걸리는 작업이 중단될 수 있는 협력적인 방식(Cooperative way)이 필요하기 때문입니다.
이는 항상 중요했습니다.
하지만 전통적인 CRUD 워크로드는 종종 이 문제를 숨깁니다.
만약 요청이 데이터베이스에서 한 행을 읽고 30밀리초 만에 반환된다면, 취소를 무시하는 비용은 작습니다.
여전히 잘못된 방식이지만, 극적인 문제를 일으키는 경우는 드뭅니다.
AI 워크로드(workloads)는 상황을 바꿉니다.
LLM 호출은 수 초 동안 외부 HTTP 연결을 열어둘 수 있습니다.
스트리밍 채팅 엔드포인트(streaming chat endpoint)는 브라우저 탭이 닫힌 후에도 계속해서 토큰(tokens)을 생성할 수 있습니다.
RAG 요청은 사용자가 유용한 정보를 보기 전에 검색(retrieval), 재순위화(reranking), 프롬프트 구성(prompt construction), 그리고 모델 생성(model generation)을 수행할 수 있습니다.
데이터 수집 작업(ingestion job)은 수천 개의 청크(chunks)에 대해 임베딩(embeddings)을 생성할 수 있습니다.
에이전트(agent)는 다른 API를 호출하는 도구(tools)를 호출할 수 있습니다.
런타임 프로필(runtime profile)이 더 넓고, 느리며, 비용이 많이 듭니다.
이것이 바로 취소(cancellation)가 단순한 정리 작업의 세부 사항이 아니게 되는 이유입니다.
취소는 비용 및 신뢰성 모델(cost and reliability model)의 일부가 됩니다.
멘탈 모델 (The Mental Model)
CancellationToken은 타임아웃 버튼이 아닙니다.
스레드 중단(thread abort)도 아닙니다.
부수 효과(side effects)를 마법처럼 되돌리지도 않습니다.
또한 원격 제공자(remote provider)가 작업이나 과금을 즉시 중단한다는 것을 보장하지도 않습니다.
이것은 협력적 신호(cooperative signal)입니다.
호출자(caller)는 "이 작업은 더 이상 필요하지 않음"이라고 말합니다.
피호출자(callee)는 안전하게 중단할 수 있는 지점을 스스로 결정합니다.
AI 시스템에서 이러한 차이가 중요한 이유는 작업이 종종 여러 경계를 넘나들기 때문입니다:
- HTTP 요청 (HTTP request)
- 검색 (retrieval)
- 임베딩 또는 재순위화 (embedding or reranking)
- 모델 호출 (model call)
- 스트리밍 응답 (streaming response)
- 도구 실행 (tool execution)
- 로깅 및 트레이싱 (logging and tracing)
만약 어느 시점에서든 토큰이 사라진다면, 파이프라인(pipeline)의 나머지 부분은 계속 실행될 수 있습니다.
흔한 실패 사례는 개발자가 취소의 존재를 잊어버리는 것이 아닙니다.
흔한 실패 사례는 취소가 첫 번째 메서드 시그니처(method signature)에만 존재한다는 것입니다.
HTTP 경계에서 시작하기
ASP.NET Core에서 엔드포인트의 CancellationToken 매개변수는 요청 중단 토큰(request-aborted token)에 바인딩됩니다.
그것이 보통 여러분이 원하는 첫 번째 토큰입니다.
app.MapPost("/ask", async (
AskRequest request,
IChatClient chatClient,
...
중요한 점은 엔드포인트(endpoint) 구문이 아닙니다.
중요한 점은 토큰(token)이 모델 경계(model boundary)를 넘어간다는 것입니다.
모델이 생성하는 동안 사용자가 연결을 끊는다면, 애플리케이션은 단순히 답변을 버리기 위해 계속해서 답변을 기다려서는 안 됩니다.
더 큰 규모의 애플리케이션에서는 보통 엔드포인트에서 모델을 직접 호출하기보다 애플리케이션 서비스(application service)로 토큰을 전달합니다.
app.MapPost("/ask", async (
AskRequest request,
AssistantService assistant,
...
그러면 서비스가 AI 워크플로(workflow)를 소유하게 되지만, 요청(request)이 여전히 생명주기(lifecycle)를 소유합니다.
public sealed class AssistantService(
IRetrievalService retrieval,
IChatClient chatClient)
...
그 패턴은 다음과 같습니다:
- 경계(boundary)에서 토큰을 수락할 것
- 모든 비동기 작업(async operation)에 토큰을 전달할 것
CancellationToken.None으로 대체하지 말 것- 코드가 AI 계층(layer)에 진입한 후에도 전달을 중단하지 말 것
스트리밍(Streaming)이 취소를 더 중요하게 만드는 이유
스트리밍은 취소 처리를 무시했을 때 가장 놓치기 쉬운 부분입니다.
브라우저, 모바일 앱, 또는 프론트엔드 스트림 리더(stream reader)가 사라진 후에도 백엔드는 계속해서 토큰을 생성할 수 있습니다.
사용자의 관점에서는 대화가 종료되었습니다.
백엔드의 관점에서는 모델이 여전히 작동 중일 수 있습니다.
이는 낭비되는 작업입니다.
스트리밍 엔드포인트(streaming endpoints)의 경우, 모델 호출 시 토큰을 전달하고, 쓰기 루프(write loop)가 깔끔하게 종료되는 데 도움이 된다면 선택적으로 응답 쓰기(response writes) 시에도 토큰을 전달하십시오.
app.MapGet("/ask/stream", async (
string question,
IChatClient chatClient,
...
여기에는 두 가지 유용한 세부 사항이 있습니다.
첫째, 모델 스트리밍 호출이 토큰을 전달받습니다.
둘째, 각 응답 쓰기(response write)가 동일한 토큰을 전달받습니다.
스트리밍은 단일 작업이 아니기 때문에 이 점이 중요합니다.
스트리밍은 일련의 순서(sequence)입니다.
각 토큰 청크(token chunk)는 중단할 수 있는 또 다른 기회입니다.
사용자 인터페이스(UI)가 듣기를 중단한다면, 백엔드도 이를 인지해야 합니다.
요청이 중단(aborted)된다면, 모델 스트림(model stream)도 멈춰야 합니다.
배포(deployment)가 종료 중이라면, 엔드포인트(endpoint)는 단지 이미 시작되었다는 이유만으로 긴 스트림을 계속 유지해서는 안 됩니다.
WriteAsync와 FlushAsync에 토큰을 전달하는 것도 괜찮지만, 더 중요한 부분은 대개 상류(upstream) 취소입니다.
클라이언트가 연결을 끊으면, ASP.NET Core는 이미 응답 쓰기를 중단하거나 실패할 수도 있습니다.
진정으로 비용이 많이 드는 작업은 모델 호출(model call), 검색(retrieval), 도구 실행(tool execution), 또는 아무도 읽지 않을 응답을 위해 계속 데이터를 생성하는 임베딩 생성(embedding generation)입니다.
만약 취소 토큰(cancellation token) 파라미터를 노출하지 않는 스트리밍 API를 사용한다면, 대신 열거(enumeration) 지점에서 .WithCancellation(cancellationToken)을 사용하십시오.
API 문서에서 해당 패턴을 명시적으로 기대하지 않는 한, 두 메커니즘 모두에 동일한 토큰을 전달하지 마십시오.
RAG 파이프라인에도 동일한 규율이 필요합니다
RAG 시스템은 종종 하나의 "질문(ask)" 엔드포인트 뒤에 여러 개의 비용이 많이 드는 작업들을 숨겨둡니다.
단일 사용자 질문은 다음과 같은 작업을 수행할 수 있습니다:
- 쿼리 재작성 (rewrite the query)
- 임베딩 생성 (generate an embedding)
- 벡터 인덱스 검색 (search a vector index)
- 소스 문서 가져오기 (fetch source documents)
- 결과 재순위화 (rerank results)
- 프롬프트 구축 (build the prompt)
- 모델 호출 (call the model)
- 답변 스트리밍 (stream the answer)
취소(Cancellation)는 이 전체 체인을 관통하여 전달되어야 합니다.
public sealed class RagAssistant(
IQueryRewriter rewriter,
IRetriever retriever,
...
이것은 지루해 보일 수 있습니다.
하지만 그것이 핵심입니다.
AI 시스템에서의 취소는 영리한 프레임워크를 필요로 해서는 안 됩니다.
그것은 일반적인 메서드 계약(method contract)의 일부여야 합니다.
만약 검색(retrieval)이 EF Core를 기반으로 한다면, 거기에도 토큰을 전달하십시오.
public Task<List<DocumentChunk>> LoadChunksAsync(
IReadOnlyCollection<string> chunkIds,
CancellationToken cancellationToken)
...
만약 검색이 벡터 데이터베이스(vector database)나 검색 서비스(search service)를 기반으로 한다면, 동일한 규칙이 적용됩니다.
외부로 나가는 SDK 호출이 토큰을 받아야 합니다.
임베딩 작업은 취소가 빈번하게 발생하는 지점(hotspots)입니다
임베딩 생성 (Embedding generation) 또한 취소가 무시되는 또 다른 지점입니다.
이는 종종 요청 경로 (request path) 외부에서 실행되기 때문에, 개발자들은 이를 단순히 완료될 때까지 실행될 수 있는 배치 작업 (batch work)으로 취급하곤 합니다.
때로는 그것이 괜찮을 수도 있습니다.
하지만 데이터 수집 (ingestion) 작업은 배포, 종료 또는 운영상의 개입 중에 여전히 깔끔하게 중단되어야 합니다.
만약 수천 개의 청크 (chunks)를 처리한다면, 배치 사이사이에 취소 여부를 확인하십시오.
public async Task IndexDocumentsAsync(
IEnumerable<DocumentChunk> chunks,
IEmbeddingGenerator<string, Embedding<float>> embeddingGenerator,
...
배치 경계 (batch boundary)는 자연스러운 안전 지점 (safe point)입니다.
단순히 토큰이 취소되었다는 이유만으로 로컬 인메모리 투영 (in-memory projection) 작업 중간에 중단하고 싶지는 않을 것입니다.
하지만 다음의 비용이 많이 드는 임베딩 배치 (batch of embeddings)를 생성하기 전에는 중단하고 싶을 것입니다.
데이터 수집 (ingestion) 파이프라인의 경우, 취소는 종료 동작 (shutdown behavior)에도 유용합니다.
자신의 중단 토큰 (stopping token)을 무시하는 백그라운드 서비스 (background service)는 배포를 더 느리고 예측 불가능하게 만들 수 있습니다.
public sealed class EmbeddingWorker(
EmbeddingQueue queue,
DocumentIndexer indexer) : BackgroundService
...
stoppingToken은 단순한 장식이 아닙니다.
이는 호스트 (host)가 워커 (worker)에게 프로세스가 중단되려 한다는 것을 알려주는 것입니다.
도구(Tools) 또한 취소가 필요합니다
도구 호출 (Tool calling)은 이 문제를 덜 중요하게 만드는 것이 아니라, 오히려 더 중요하게 만듭니다.
에이전트 도구 (agent tool)는 여전히 애플리케이션 코드입니다.
데이터베이스를 쿼리하거나, 내부 API를 호출하거나, 검색 서비스 (search service)를 호출하거나, 파일을 읽거나, 또는 다른 모델 호출을 트리거할 수도 있습니다.
만약 부모 요청 (parent request)이 취소된다면, 도구는 불필요한 작업을 계속해서 수행해서는 안 됩니다.
[Description("Searches internal documentation for relevant snippets.")]
public static Task<IReadOnlyList<SearchResult>> SearchDocsAsync(
string query,
...
모델은 토큰에 대해 알 필요가 없습니다.
여러분의 애플리케이션이 알아야 합니다.
이것은 중요한 경계입니다.
모델이 제공하는 인자(arguments)는 신뢰할 수 없는 입력(untrusted input)입니다.
CancellationToken은 여러분의 런타임(runtime)에서 제공됩니다.
에이전트 추상화(agent abstractions) 때문에 도구 실행(tool execution)이 여전히 여러분의 애플리케이션 생명주기(application lifecycle)에 속해 있다는 사실을 잊지 마십시오.
이는 여러분의 도구 프레임워크가 IServiceProvider와 CancellationToken을 모델이 제공하는 파라미터가 아닌, 런타임이 제공하는 파라미터로 취급한다는 것을 전제로 합니다.
만약 프레임워크가 모든 메서드 파라미터를 모델 스키마(schema)에 노출한다면, 애플리케이션 서비스나 생명주기 토큰(lifecycle tokens)을 그런 방식으로 노출하지 마십시오.
타임아웃은 정책이고, 취소는 배관(Plumbing)이다
취소 토큰(Cancellation tokens)은 종종 타임아웃(timeouts)을 구현하는 데 사용되지만, 이 둘은 같은 것이 아닙니다.
타임아웃은 정책 결정(policy decision)입니다.
예를 들어:
- 이 채팅 엔드포인트(endpoint)는 30초 후에 중단되어야 한다
- 이 검색(retrieval) 호출은 2초 후에 중단되어야 한다
- 이 임베딩(embedding) 배치(batch)는 5분 후에 중단되어야 한다
- 이 백그라운드 워커(background worker)는 호스트(host)가 종료될 때 중단되어야 한다
토큰은 그러한 결정이 코드를 통해 전달되는 방식입니다.
요청 취소 토큰(request cancellation token)과 내부 타임아웃(internal timeout)이 모두 필요하다면, 정책이 가시적인 경계(edge)에서 이 둘을 연결하십시오.
app.MapPost("/ask", async (
AskRequest request,
AssistantService assistant,
...
이렇게 하면 다음 두 가지 케이스를 분리할 수 있습니다:
- 사용자가 떠난 경우
- 시스템이 작업이 너무 오래 걸렸다고 결정한 경우
이 두 경우는 동일한 방식으로 로그를 남기거나, 경고를 보내거나, 재시도(retry)해서는 안 됩니다.
하지 말아야 할 것
제가 가장 먼저 찾아내는 실수는 CancellationToken.None입니다.
await chatClient.GetResponseAsync(
messages,
cancellationToken: CancellationToken.None);
이것은 "호출자가 사라지더라도 계속 진행하라"는 뜻입니다.
때로는 이것이 의도적일 수도 있습니다.
하지만 대부분의 경우, 이는 실수입니다.
또 다른 실수는 토큰을 전달받기는 하지만, 첫 번째 호출에서만 사용하고 마는 것입니다.
public async Task<string> AnswerAsync(
string question,
CancellationToken cancellationToken)
...
검색 (Retrieval) 호출은 취소될 수 있습니다.
모델 (Model) 호출은 취소될 수 없습니다.
그것이 바로 토큰을 놓쳐서는 안 되는 정확한 지점입니다. 왜냐하면 모델 호출은 종종 작업 중 가장 느리고 비용이 많이 드는 부분이기 때문입니다.
취소를 실패(Failure)처럼 로깅하는 것은 노이즈를 생성합니다
예상된 취소 (Expected cancellation)는 실패 (Failure)와 같지 않습니다.
스트리밍 답변이 생성되는 동안 사용자가 브라우저 탭을 닫는다면, 그것은 모델 장애 (Model outage)가 아닙니다.
호스트가 종료 중이고 백그라운드 임베딩 (Embedding) 워커가 배치 (Batch) 사이에서 중단된다면, 그것은 인제스션 (Ingestion) 오류가 아닙니다.
취소는 별도로 로깅하십시오.
try
{
await assistant.AnswerAsync(question, cancellationToken);
...
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기