.NET 비동기 작업과 CancellationToken
들어가며
업무를 하며 코드 작성중 컨벤션에 맞게 프로젝트의 Application 디렉토리에 InitializeHostedService 라는 파일이 있기에 찾아보며 만들어보고자 했다. 이 파일에서 일반적으로 StartAsync 와 StopAsync 메서드를 정의한다고 했고 여기서 CancellationToken 이라는걸 처음 보았다.
찾아보니 .NET은 애플리케이션에서 백그라운드 작업을 구현할 때 앱 종료 시 안전하게 작업을 중단할 수 있도록 CancellationToken 메커니즘을 제공한고 한다. 예를 들어 데이터베이스 쓰기 작업 중에 앱이 갑자기 종료되면 데이터 손상, 불완전한 트랜잭션, 리소스 누수가 발생할 수 있기 때문이다.
이번 글에서는 간단하게 CancellationToken의 동작 원리와 생명주기를 정리하고자 한다.
CancellationToken의 역할
CancellationToken은 비동기 작업을 안전하게 취소하기 위한 신호 체계다. 앱이 종료 요청을 받으면 토큰이 취소 상태가 되고, 실행 중인 작업들은 이를 확인하여 안전하게 중단된다.
기본 구조
1
2
3
4
5
6
7
8
9
10
11
public class DataSyncService : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
await DoWorkAsync(stoppingToken);
await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);
}
}
}
이 CancellationToken 은 신호만 제공한다. 작업을 자동으로 멈추지 않으므로 코드에서 명시적으로 확인해야 한다.
IHostedService 생명주기
ASP.NET Core의 백그라운드 서비스는 IHostedService 인터페이스를 통해 관리된다.
1
2
3
4
5
public interface IHostedService
{
Task StartAsync(CancellationToken cancellationToken);
Task StopAsync(CancellationToken cancellationToken);
}
StartAsync의 CancellationToken
- 앱 시작 중 취소 시 신호를 받는다
- 드물게 발생 (ex. 앱 시작 중 Ctrl+C)
StopAsync의 CancellationToken
- 기본 5초 타임아웃
- 5초 안에 종료되지 않으면 강제 종료
토큰의 생명주기
이 5단계 생명주기에 대해 각각 알아보자.
1. 생성
ASP.NET Core는 앱 시작 시 내부적으로 CancellationTokenSource를 생성한다.
1
2
3
4
5
6
7
// 간소화한 ASP.NET Core 내부 동작
var _applicationStopping = new CancellationTokenSource();
foreach (var service in _hostedServices)
{
service.StartAsync(_applicationStopping.Token);
}
2. 전달
생성된 토큰은 참조 복사로 여러 서비스에 전달된다. 모든 서비스가 같은 토큰 인스턴스를 참조한다.
3. 사용
각 서비스는 토큰을 확인하면서 작업을 수행한다.
1
2
3
4
5
6
7
8
9
10
11
// 방법 1: 직접 확인
while (!token.IsCancellationRequested)
{
// 작업 수행
}
// 방법 2: 예외 발생
token.ThrowIfCancellationRequested();
// 방법 3: 콜백 등록
token.Register(() => Console.WriteLine("취소됨"));
4. 취소
앱 종료 요청 시 토큰이 취소된다.
5. 정리
만든 사람이 정리한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// ASP.NET Core가 만든 토큰 → 건드리지 않음
public Task StartAsync(CancellationToken cancellationToken)
{
// 이 토큰은 ASP.NET Core가 Dispose
}
// 직접 만든 토큰 → 직접 정리
public class MyService : IHostedService, IDisposable
{
private CancellationTokenSource _cts = new();
public void Dispose()
{
_cts?.Dispose();
}
}
토큰 공유의 의미
하나의 토큰은 모든 작업에 영향을 준다.
중요한 점은 하나의 토큰이 취소되면 그 토큰을 공유하는 모든 작업이 취소 신호를 받는다.
1
2
3
4
5
6
7
8
9
// 3개 서비스가 같은 토큰 공유
Service1.StartAsync(mainToken);
Service2.StartAsync(mainToken);
Service3.StartAsync(mainToken);
// Ctrl+C 누름
Cancel();
// 결과: 3개 서비스 모두 동시에 취소 신호
예외는 토큰을 취소시키지 않는다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Service1 : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken token)
{
throw new Exception("Crashed"); // 예외 발생
}
}
public class Service2 : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken token)
{
while (!token.IsCancellationRequested)
{
Console.WriteLine("Still running"); // 계속 실행됨
}
}
}
Service1의 예외는 토큰에 영향을 주지 않는다. 토큰은 오직 Cancel() 호출로만 취소된다.
독립적 제어가 필요하다면?
별도의 토큰을 생성하여 연결한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Service1 : BackgroundService
{
private CancellationTokenSource _ownCts = new();
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
var combined = CancellationTokenSource.CreateLinkedTokenSource(
stoppingToken, // 앱 종료
_ownCts.Token // 개별 제어
);
while (!combined.Token.IsCancellationRequested)
{
await DoWork(combined.Token);
}
combined.Dispose();
}
public void StopMe()
{
_ownCts.Cancel(); // 이 서비스만 중단
}
}
프로젝트별 토큰 독립성
솔루션에 여러 프로젝트가 있을 때, 각 프로젝트는 독립적인 프로세스로 실행되며 각자의 토큰을 가진다.
1
2
3
4
5
6
7
8
9
Solution
├─ WebAPI 프로젝트
│ └─ CancellationTokenSource #1
│
├─ Web 프로젝트
│ └─ CancellationTokenSource #2
│
└─ Worker 프로젝트
└─ CancellationTokenSource #3
특징:
- 각 프로젝트는 독립적인 프로세스
- 한 프로젝트 종료 ≠ 다른 프로젝트 종료
- 프로젝트 간 토큰 공유 불가능 (다른 프로세스)
WebAPI 프로젝트를 종료해도 Web 프로젝트는 계속 실행된다. 다만 HTTP 요청은 연결 실패 에러를 받게 된다.
마치며
CancellationToken은 비동기 작업의 안전한 종료를 위한 핵심 메커니즘이다.
핵심 원칙:
| 개념 | 설명 |
|---|---|
| 신호 전달 | 토큰은 신호만 제공, 작업이 직접 확인 필요 |
| 공유 범위 | 하나의 토큰 취소 = 모든 공유 작업 취소 신호 |
| 예외와 무관 | 예외는 토큰을 취소시키지 않음 |
| 소유권 | 만든 사람이 Dispose 책임 |
| 독립 제어 | 별도 제어 필요 시 Linked Token 사용 |
안전한 종료는 견고한 애플리케이션의 기본이다.
CancellationToken을 제대로 활용하면 예기치 않은 종료 상황에서도 데이터 무결성을 보장할 수 있다.





