1. 소개
비동기(또는 오래 걸리는) 작업을 시작하면 사용자(또는 다른 코드)가 갑자기 "멈춰! 이제 필요 없어! 그만해!"라고 요구할 수 있습니다. 예를 들면, 사용자가 거대한 파일 다운로드를 중단하거나 프로그램 창을 닫았거나, 큰 데이터베이스에서의 조회를 취소할 수 있습니다. 취소 지원이 없다면 프로그램은 계속 실행되며 자원을 낭비하게 됩니다 — 사용자와 시스템을 배려하는 좋은 방식이 아니죠.
일반적인 취소 시나리오:
- 파일 다운로드 취소 또는 데이터 전송 취소.
- 사용자 요청으로 복잡한 데이터 처리에서 빠르게 빠져나오기.
- 더 이상 필요하지 않은 긴 작업을 일시 중지/중단할 때.
취소는 반응성이 좋고 자원을 아끼는 애플리케이션을 만드는 비밀 무기입니다.
.NET에서 비동기 작업을 어떻게 취소하나?
.NET에서는 "취소 토큰"(CancellationToken) 개념을 사용해서 긴 작업을 취소합니다. 이 객체를 작업의 모든 구성요소에 전달합니다. 누군가 취소를 요청하면 토큰이 관련된 모든 부분에 즉시 알립니다. 실제로는 빨간 깃발과 같아서 먼저 본 부분이 멈추는 식입니다.
이 메커니즘은 두 가지 주요 클래스로 구현됩니다:
- CancellationTokenSource — 취소 토큰을 생성하고 관리합니다.
- CancellationToken — 비동기 메서드에 전달되어 취소 가능하게 만듭니다.
중요: 취소 토큰 자체가 코드를 강제 중단하지는 않습니다. 단지 "신호"를 보내는 역할을 하며, 실제로 어떻게 반응할지는 애플리케이션이 결정합니다.
2. 취소 토큰 생성과 작업 취소
간단한 예제로 어떻게 동작하는지 살펴보겠습니다(학습용 콘솔 앱을 확장해나간다고 생각하세요).
예제: 취소 가능한 간단한 비동기 작업
using System;
using System.Threading;
using System.Threading.Tasks;
namespace DemoApp
{
class Program
{
static async Task Main()
{
// 소스 토큰을 생성합니다
CancellationTokenSource cts = new CancellationTokenSource();
// 비동기 작업을 시작합니다
Task longRunningTask = DoWorkAsync(cts.Token);
Console.WriteLine("작업을 취소하려면 아무 키나 누르세요...");
Console.ReadKey();
// 취소를 요청합니다
cts.Cancel();
try
{
await longRunningTask;
}
catch (OperationCanceledException)
{
Console.WriteLine("작업이 취소되었습니다!");
}
}
// 취소를 지원하는 비동기 메서드
static async Task DoWorkAsync(CancellationToken cancellationToken)
{
for (int i = 0; i < 10; i++)
{
// 취소 신호를 확인합니다
cancellationToken.ThrowIfCancellationRequested();
Console.WriteLine($"단계 실행 {i + 1}/10...");
await Task.Delay(1000); // 1초 지연
}
Console.WriteLine("작업이 성공적으로 완료되었습니다!");
}
}
}
어떻게 동작하나?
- 우리는 CancellationTokenSource(cts)를 만들고, 거기서 토큰(cts.Token)을 얻습니다.
- 이 토큰을 비동기 작업에 전달합니다.
- DoWorkAsync() 내부에서 주기적으로 ThrowIfCancellationRequested()로 취소를 검사합니다. 사용자가 취소를 요청하면 이 메서드는 OperationCanceledException을 던지고 작업은 중단됩니다.
- Main()에서는 아무 키나 누르면 cts.Cancel()을 호출해 중단 신호를 보냅니다.
만약 cancellationToken.IsCancellationRequested를 확인하지 않거나 ThrowIfCancellationRequested()를 호출하지 않으면, 작업은 계속 실행됩니다 — 토큰은 정보용 플래그일 뿐입니다.
3. CancellationToken: 내부 구조와 약간의 팁
취소 토큰은 메서드와 작업 사이에서 쉽게 전달할 수 있는 객체입니다. 이로 인해 유연성이 생깁니다:
- 같은 토큰을 여러 비동기/동기 작업에서 재사용할 수 있습니다.
- 하나의 CancellationTokenSource에서 생성된 토큰을 여러 작업이 쓰면 "그룹 취소"를 쉽게 구현할 수 있습니다.
- 취소 토큰은 비침입적입니다: 무시해도 기존 코드처럼 동작합니다.
취소 관리: 어디서 어떻게 토큰을 확인해야 할까?
취소 플래그를 확인해야 하는 위치는 논리적으로 의미 있는 곳입니다: 루프 내부, 긴 처리의 각 단계, 단계 간 전환 지점 등에서 확인하세요.
// 언제든지 확인 가능
if (cancellationToken.IsCancellationRequested)
{
Console.WriteLine("작업이 취소되었습니다! 종료합니다...");
return;
}
// 또는 간단히 예외를 던지게
cancellationToken.ThrowIfCancellationRequested();
보통 ThrowIfCancellationRequested()를 많이 씁니다 — 호출한 쪽에서 잡을 수 있는 특수 예외를 던집니다.
4. 표준 라이브러리의 비동기 메서드
많은 .NET 클래스와 메서드(특히 비동기 메서드)가 기본적으로 CancellationToken을 지원합니다. 이를 적극적으로 사용해서 작업을 "올바르게" 중단하세요.
파일 비동기 읽기 예제:
using System.IO;
using System.Threading;
using System.Threading.Tasks;
class FileDemo
{
public static async Task ReadFileWithCancelAsync(string filePath, CancellationToken cancellationToken)
{
using FileStream stream = File.OpenRead(filePath);
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken)) > 0)
{
// 데이터를 처리합니다...
// 만약 취소 토큰이 요청되면, ReadAsync는 스스로 OperationCanceledException을 던집니다
}
}
}
FileStream.ReadAsync와 CancellationToken에 대해서는 공식 문서를 참고하세요.
5. 유용한 팁
타임아웃도 취소의 한 형태다!
지정한 시간이 지나면 자동으로 작업을 취소하도록 설정할 수 있습니다. CancellationTokenSource를 이렇게 만들면 됩니다:
// 예: 5초 타임아웃을 가진 CancellationTokenSource 생성
CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
5초 후에 토큰이 자동으로 "세트"되고, 해당 토큰을 사용하는 모든 작업은 다음 검사 시 중단 신호를 받습니다. 무한 대기를 막는 데 유용합니다.
취소 시 어떤 일이 일어나나?
소스에서 Cancel()을 호출하면, 그 토큰을 사용하는 모든 메서드가 상태 변화를 알게 됩니다. 하지만 코드가 토큰을 확인하지 않으면 취소는 실제로 적용되지 않습니다.
흔한 실수: 모든 비동기/긴 작업에 토큰을 전달하는 것을 잊는 것. 일부는 취소되고 일부는 계속 실행되는 상황이 생깁니다.
시각화: 취소 흐름
sequenceDiagram
participant Main as 메인 스레드
participant CTS as CancellationTokenSource
participant Task as 비동기 작업
Main->>CTS: CTS 생성, 토큰 획득
Main->>Task: 토큰을 작업에 전달
Note over Task: 작업이 주기적으로 토큰 확인
Main->>CTS: Cancel() 호출
CTS-->>Task: 토큰이 "취소됨" 상태로 변경
Task-->>Main: OperationCanceledException을 던짐
Main->>Main: 예외를 잡고 종료 처리
취소가 주로 사용되는 곳
- 비동기 다운로드와 서버 요청: 인터넷이 불안정하거나 사용자가 중단할 때 취소 가능.
- 큰 계산 작업: 타임아웃이나 사용자의 중단 요청으로 중지 가능.
- 네트워크 작업, 파일 처리, 백그라운드에서 큰 컬렉션을 처리할 때.
이제 비동기 작업 취소 기본은 이해했습니다 — 애플리케이션이 더 빠르고 사용자 친화적으로 동작하게 될 거예요!
6. 팁과 흔한 실수
잊지 마세요 취소 토큰을 지원하는 모든 메서드와 호출에 토큰을 전달해야 합니다. 한 군데라도 빠뜨리면 작업이 멈추지 않을 수 있습니다.
토큰을 정기적으로 확인하세요 — 특히 긴 루프, 파일 처리, 대용량 업로드/다운로드 등에서. IsCancellationRequested나 ThrowIfCancellationRequested()를 사용하세요.
강제로 스레드나 작업을 밖에서 종료하려고 하지 마세요: 취소 토큰은 "멈춰달라"는 요청이지, 스레드를 강제 종료하는 도구가 아닙니다.
표준 라이브러리의 ReadAsync, Delay, HttpClient.SendAsync 같은 함수들은 이미 토큰을 통해 취소를 지원합니다. 꼭 활용하세요!
취소를 처리할 때는 정확히 OperationCanceledException을 잡으세요 — 이는 요청에 의해 정상적으로 취소되었음을 알리는 특수 예외입니다.
GO TO FULL VERSION