웹 어플리케이션 운영시 파일을 생성하고 다운로드 링크로 제공할시에 정상적인 플로우로 다운로드 및 임시파일 삭제가 이루어지면 문제가 없지만 간혹 프로세스 진행중에 브라우저를 닫거나 예기치 못한 오류로 종료가 되었을때 임시 파일이 서버에 쌓이는 문제가 있습니다.
이러한 문제를 해결하기 위해 .NET Timer Queue에 정리하는 프로세스를 예약해놓고 주기적으로 해당 동작이 수행되게 구성합니다.
서버 자동 삭제의 장점
- 브라우저 상태와 무관
- 확실한 정리 보장
- 관리 편의성
- 리소스 효율적 사용
Timer의 동작 원리
_timer = new Timer(callback, state, dueTime, period);
```
**내부 동작:**
1. Timer 생성 시 OS의 타이머 큐에 등록
2. 대기 중에는 CPU/쓰레드 사용 안 함 (리소스 0%)
3. 지정된 시간이 되면 OS가 하드웨어 인터럽트 발생
4. ThreadPool에서 쓰레드를 빌려 콜백 실행
5. 실행 완료 후 쓰레드 반납
**비유:**
알람시계를 맞춰놓고 자는 것과 같습니다. 알람 시간까지 당신은 아무것도 하지 않고(CPU 사용 없음), 시간이 되면 알람이 울립니다.
### 리소스 사용량
```
Timer 1개당:
- 메모리: ~1KB
- CPU: 0% (대기 중)
- 쓰레드: 실행 시점에만 잠깐 사용
10개의 Timer를 돌려도:
- 메모리: ~10KB
- CPU: 여전히 0%
```
.NET Framework 4.8 MVC 구현
1. 서비스 클래스 생성
// Services/TempFileCleanupService.cs
using System;
using System.IO;
using System.Threading;
using System.Web;
public class TempFileCleanupService
{
private static Timer _timer;
public static void Start(params string[] folderPaths)
{
// 다음 정각까지 대기 시간 계산
var now = DateTime.Now;
var nextHour = now.Date.AddHours(now.Hour + 1);
var delay = nextHour - now;
_timer = new Timer(
Cleanup,
folderPaths,
delay, // 첫 실행: 다음 정각
TimeSpan.FromHours(1) // 이후: 1시간마다
);
}
private static void Cleanup(object state)
{
string[] folders = state as string[];
foreach (var folder in folders)
{
CleanupFolder(folder);
}
}
private static void CleanupFolder(string folderPath)
{
try
{
if (!Directory.Exists(folderPath))
return;
var files = Directory.GetFiles(folderPath);
int deletedCount = 0;
foreach (var file in files)
{
try
{
var fileInfo = new FileInfo(file);
// 30분 이상 된 파일 삭제
if (DateTime.Now - fileInfo.CreationTime > TimeSpan.FromMinutes(30))
{
File.Delete(file);
deletedCount++;
}
}
catch (Exception ex)
{
// 개별 파일 삭제 실패 시 로깅
System.Diagnostics.Debug.WriteLine($"파일 삭제 실패: {file}, {ex.Message}");
}
}
if (deletedCount > 0)
{
System.Diagnostics.Debug.WriteLine($"{folderPath}: {deletedCount}개 파일 삭제됨");
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"{folderPath} 정리 실패: {ex.Message}");
}
}
// 선택사항: 명시적 종료
public static void Stop()
{
_timer?.Dispose();
_timer = null;
}
}
2. Global.asax.cs에서 시작
public class MvcApplication : HttpApplication
{
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);
// 임시 파일 자동 정리 시작
TempFileCleanupService.Start(
Server.MapPath("~/Temp"),
Server.MapPath("~/Content/Present"),
Server.MapPath("~/Uploads/Temp")
);
}
// 선택사항: 애플리케이션 종료 시 정리
protected void Application_End()
{
TempFileCleanupService.Stop();
}
}
.NET 8 Blazor Server 구현
Blazor에서는 IHostedService를 사용하는 것이 표준 방식입니다.
1. 서비스 클래스 생성
// Services/TempFileCleanupService.cs
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
public class TempFileCleanupService : IHostedService, IDisposable
{
private Timer _timer;
private readonly ILogger<TempFileCleanupService> _logger;
private readonly IWebHostEnvironment _env;
private readonly IConfiguration _config;
public TempFileCleanupService(
ILogger<TempFileCleanupService> logger,
IWebHostEnvironment env,
IConfiguration config)
{
_logger = logger;
_env = env;
_config = config;
}
public Task StartAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("임시 파일 정리 서비스 시작");
// 다음 정각까지 대기
var now = DateTime.Now;
var nextHour = now.Date.AddHours(now.Hour + 1);
var delay = nextHour - now;
_timer = new Timer(
Cleanup,
null,
delay,
TimeSpan.FromHours(1)
);
return Task.CompletedTask;
}
private void Cleanup(object state)
{
try
{
// appsettings.json에서 설정 읽기
var folders = _config.GetSection("TempFileCleaning:Folders").Get<string[]>();
var maxAgeMinutes = _config.GetValue<int>("TempFileCleaning:MaxAgeMinutes");
foreach (var folder in folders)
{
string fullPath = Path.Combine(_env.ContentRootPath, folder);
CleanupFolder(fullPath, maxAgeMinutes);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "파일 정리 중 오류 발생");
}
}
private void CleanupFolder(string folderPath, int maxAgeMinutes)
{
try
{
if (!Directory.Exists(folderPath))
return;
var files = Directory.GetFiles(folderPath);
int deletedCount = 0;
foreach (var file in files)
{
try
{
var fileInfo = new FileInfo(file);
if (DateTime.Now - fileInfo.CreationTime > TimeSpan.FromMinutes(maxAgeMinutes))
{
File.Delete(file);
deletedCount++;
}
}
catch (Exception ex)
{
_logger.LogError(ex, $"파일 삭제 실패: {file}");
}
}
if (deletedCount > 0)
{
_logger.LogInformation($"{folderPath}: {deletedCount}개 파일 삭제됨");
}
}
catch (Exception ex)
{
_logger.LogError(ex, $"폴더 정리 실패: {folderPath}");
}
}
public Task StopAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("임시 파일 정리 서비스 종료");
_timer?.Change(Timeout.Infinite, 0);
return Task.CompletedTask;
}
public void Dispose()
{
_timer?.Dispose();
}
}
2. appsettings.json 설정
{
"Logging": {
"LogLevel": {
"Default": "Information"
}
},
"TempFileCleaning": {
"Folders": [
"wwwroot/Temp",
"wwwroot/Uploads/Temp",
"Content/Present"
],
"MaxAgeMinutes": 30
}
}
3. Program.cs에서 등록
var builder = WebApplication.CreateBuilder(args);
// Razor Pages, Blazor 등 기본 서비스 등록
builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor();
// 백그라운드 서비스 등록
builder.Services.AddHostedService<TempFileCleanupService>();
var app = builder.Build();
// 미들웨어 설정
app.UseStaticFiles();
app.UseRouting();
app.MapBlazorHub();
app.MapFallbackToPage("/_Host");
app.Run();
```
## IHostedService 동작 원리
많은 분들이 `StartAsync`가 어떻게 호출되는지 궁금해하십니다.
### 호출 흐름
```
1. Program.cs에서 서비스 등록:
builder.Services.AddHostedService<TempFileCleanupService>();
→ "이 서비스는 백그라운드 서비스입니다" 라고 표시
2. app.Run() 실행:
→ ASP.NET Core 런타임이 시작됨
3. 런타임이 자동으로:
→ 모든 IHostedService를 찾음
→ 각 서비스의 StartAsync() 호출
4. 애플리케이션 실행 중...
5. 애플리케이션 종료 시:
→ 모든 IHostedService의 StopAsync() 호출
MVC vs Blazor 비교
// MVC (.NET Framework 4.8)
// 수동으로 직접 호출
protected void Application_Start()
{
TempFileCleanupService.Start(folders); // 개발자가 호출
}
// Blazor (.NET 8)
// 프레임워크가 자동 호출
builder.Services.AddHostedService<TempFileCleanupService>();
// app.Run() 시점에 자동으로 StartAsync() 실행
차이점
| 항목 | .NET Framework MVC | .NET 8 Blazor |
| 진입점 | Global.asax.cs | Program.cs |
| 시작 방식 | 수동 호출 | 자동 호출 (IHostedService) |
| 설정 방식 | 코드에 직접 | appsettings.json |
| 로깅 | Debug.WriteLine | ILogger (구조화된 로깅) |
| 종료 처리 | Application_End | StopAsync (자동) |
| DI 지원 | 제한적 | 완전 지원 |
주의사항
1. 폴더 권한 확인
IIS 애플리케이션 풀 계정이 해당 폴더에 대한 삭제 권한을 가지고 있어야 합니다.
2. 네트워크 드라이브 사용 시
네트워크 드라이브의 파일을 삭제할 때는 네트워크 지연을 고려해야 합니다.
관련문서
Background tasks with hosted services in ASP.NET Core
Learn how to implement background tasks with hosted services in ASP.NET Core.
learn.microsoft.com
'C# > 이모저모' 카테고리의 다른 글
| WebSocket 채팅 시스템 (0) | 2026.03.22 |
|---|---|
| WebSocket (0) | 2026.03.22 |
| 웹 애플리케이션(.NET) (0) | 2025.11.30 |
| IIS(Internet Information Services) (0) | 2025.11.27 |
| 파일 생성 및 다운로드 관리(물리 경로 vs 가상 경로)_Web (0) | 2025.11.27 |