Generic Host / BackgroundService를 데스크톱 앱에 가지고 들어오는 이유 - 기동・수명・graceful shutdown의 정리가 꽤 편해진다
· 小村 豪 · C#, .NET, Generic Host, BackgroundService, WPF, WinForms, Windows 개발, 설계
Windows 도구나 상주계 앱을 약간 기르면, UI의 외측에 있는 처리가 서서히 늘어납니다.
정기 폴링, 파일 감시, 재접속, 큐 처리, 기동 시 초기화, 종료 시 flush.
처음에는 Form_Load나 OnStartup이나 Task.Run으로 버티지만, 그대로 자라면 누가 개시하고 누가 멈추며 누가 예외를 보는지가 애매해집니다.
async / await의 쓰는 방식 그 자체보다 앞서, 처리의 수명을 누가 가질지 결정하는 편이 좋은 장면입니다.
거기서 효과적인 것이 .NET의 Generic Host와 BackgroundService입니다.
UI 스레드 측의 async / await에 대해서는,
WPF / WinForms의 async/await와 UI 스레드를 한 장으로 정리 - await 후의 돌아갈 곳, Dispatcher, ConfigureAwait, .Result / .Wait()의 막힘 포인트
나
C# async/await의 베스트 프랙티스 - Task.Run과 ConfigureAwait의 판단표
와 이어지는 이야기입니다.
이번에는 그 더 외측에 있는 「앱 전체의 기동과 정지」의 정리에 좁힙니다.
실무에서 점차 썩기 쉬운 것은 대개 이 부근입니다.
- 폼이나 ViewModel의 여기저기서
Task.Run이 생긴다 - 상주 루프의 정지 조건이
bool플래그로 흩어진다 - 종료 시에 아직 동작하고 있는 처리가 있어, 가끔 닫히지 않는다
- 로그 / 설정 / DI의 입구가 기술별로 따로따로가 된다
Environment.Exit로 정리하고 싶어져서,finally가 날아간다
이 글에서는 주로 .NET 6 이후의 WPF / WinForms / 상주계 Windows 앱을 전제로,
왜 Generic Host / BackgroundService가 수수하게 효과적인지,
어디까지 가지고 들어오면 이득인지, 어디서 거칠게 하면 진흙탕이 되는지를 정리합니다.
용어를 먼저 맞춘다
이 종류의 이야기는 용어의 의미가 애매한 채로는 갑자기 읽기 어려워집니다. 그래서 이 글에서 사용하는 용어를 처음에 대략 고정합니다.
- Generic Host
- .NET 앱의 「기동」 「의존 관계」 「설정」 「로그」 「정지」를 한꺼번에 돌봐주는 토대입니다.
- ASP.NET Core만의 구조가 아니라, 콘솔, worker, 데스크톱 앱에서도 사용할 수 있습니다.
- Host /
IHost- build한 후의 실체입니다.
- 이것을
StartAsync로 기동하고,StopAsync로 멈춥니다.
- Hosted Service
- host의 수명에 매달려 개시・정지되는 상주 처리입니다.
IHostedService를 구현하거나, 통상은BackgroundService를 계승해 씁니다.
BackgroundServiceIHostedService의 쓰기 쉬운 구현 보조입니다.- 길게 달리는 본체를
ExecuteAsync에 쓸 수 있으므로, 감시 루프나 정기 처리를 정리하기 쉬워집니다.
- lifetime
- 이 글에서는 「그 처리가 언제 시작되고, 언제 끝나며, 누가 멈출 책임을 가지는지」라는 의미로 사용합니다.
- 단순한 생존 시간이라기보다, 개시 책무와 정지 책무를 포함한 수명 관리입니다.
- graceful shutdown
- 강제 종료가 아니라, 멈출 신호를 내고 진행 중의 처리를 가능한 한 정리해 나서 종료하는 것입니다.
- 예를 들어 「다음 주기를 시작하지 않는다」 「큐를 어디까지 흘릴지 결정한다」 「close나 flush를 기다린다」가 여기에 들어갑니다.
- DI
- Dependency Injection의 약자로, 의존 오브젝트의 조립을 호출 측에 직접 쓰지 말고 컨테이너 경유로 받는 방식입니다.
- 이 글에서는 「logger나 설정이나 reader를 new 축제로 하지 않고, 입구에서 한꺼번에 구성한다」 정도의 이해로 충분합니다.
요컨대 이 이야기는
「BackgroundService라는 편리한 클래스의 소개」만이 아니라,
앱 전체의 기동과 정지를 host로 모으고, 상주 처리의 lifetime을 설계로서 가지는 이야기
라고 생각하고 읽으면 쫓기 쉽습니다.
1. 먼저 결론(한마디로)
- Generic Host는 데스크톱 앱에서도 기동과 lifetime 관리의 토대로서 꽤 유력합니다.
BackgroundService는 「길게 사는 처리」를Task.Run던져놓기가 아니라 관리된 수명에 싣기 위한 그릇입니다.- 실무에서 가장 효과적인 것은 개시 책무 / 정지 책무 / 예외 감시 / 로그 / DI / 설정을 1곳의 설계로 모을 수 있는 것입니다.
StartAsync는 짧게, 길게 달리는 본체는ExecuteAsync, 종료 시의 뒷정리는StopAsync로 나누면 꽤 읽기 쉬워집니다.- 상주 앱, 트레이 앱, 장치 감시, 정기 동기, 순서 있는 후처리, 재접속 루프는 특히 상성이 좋습니다.
- 반대로, 버튼을 누를 때만 1회 동작하는 처리까지 전부
BackgroundService로 하면 약간 거창해집니다. StopAsync는 편리하지만 프로세스 크래시나 강제 종료의 보험이 아닙니다. 거기에 뒷정리를 너무 기대지 않는 것도 중요합니다.
요컨대 데스크톱 앱에서 Generic Host / BackgroundService가 효과적인 것은,
「백그라운드 처리가 있으니까」가 아니라,
「그 백그라운드 처리의 수명을 UI의 덤으로가 아니라 설계로서 가지고 싶으니까」입니다.
2. 먼저 한 장으로 정리
2.1. 전체상
먼저 이 그림으로 보면 이야기가 꽤 빠릅니다.
flowchart LR
A["데스크톱 앱 기동<br/>(WPF / WinForms)"] --> B["Host를 Build / StartAsync"]
B --> C["DI / Logging / Configuration 준비"]
B --> D["HostedService.StartAsync"]
D --> E["BackgroundService.ExecuteAsync"]
E --> F["PeriodicTimer / Queue / 재접속 / 감시 루프"]
C --> G["MainWindow / MainForm을 표시"]
F --> H["상태 업데이트 / 로그 / 외부 I/O"]
H --> I["UI는 필요한 장소만 Dispatcher / Invoke"]
J["사용자 종료 / Fatal error / StopApplication"] --> K["IHost.StopAsync"]
K --> L["CancellationToken 통지"]
L --> M["HostedService.StopAsync"]
M --> N["접속 close / flush / graceful shutdown"]
UI 앱에서 흔한 것은 Program.cs / App.xaml.cs / Form_Load / Closing / Task.Run / Timer / static singleton에 조금씩 책무가 흩어지는 형태입니다.
Host를 넣으면 대략 다음 분담으로 할 수 있습니다.
- UI: 화면, 입력, 표시
- HostedService / BackgroundService: 상주 처리, 감시, 큐 처리, 정기 처리
- DI 서비스: 실제 업무 로직, 외부 접속, 설정, 로그
이 분할 방식을 할 수 있는 것만으로도 리뷰의 쉬움이 꽤 바뀝니다.
2.2. 두는 장소의 판단표
| 하고 싶은 것 | 두는 장소의 제1후보 | 이유 |
|---|---|---|
| 기동 직후의 가벼운 초기화 | StartAsync |
기동에 참가하는 짧은 처리로서 의미가 명확 |
| 길게 사는 감시 / 폴링 / 재접속 | ExecuteAsync |
서비스 수명과 함께 달리게 하기 쉽다 |
| 종료 시의 정지 통지 / flush / close | StopAsync |
CancellationToken과 함께 graceful shutdown을 쓰기 쉽다 |
| 의존 관계의 구성, 설정, 로그 | Host.CreateApplicationBuilder |
입구를 1곳으로 모을 수 있다 |
| 화면 업데이트 | UI 측 | worker에서 직접 UI를 건드리지 않는 편이 사고가 적다 |
| 버튼 누름마다의 1회 처리 | 통상의 async 메서드 |
HostedService로 하지 않아도 좋은 경우가 많다 |
| 순서 있는 백그라운드 후처리 | Channel<T> + BackgroundService |
던져놓기보다 수명과 상한을 관리하기 쉽다 |
Host를 넣는 가치는 무언가를 「비동기로 할 수 있다」는 것보다, 어디에 둬야 할지의 판단이 명확해지는 것에 있습니다.
3. 왜 데스크톱 앱에서 효과적인가
3.1. UI와 상주 처리의 책무를 나누기 쉽다
데스크톱 앱은 UI가 주역으로 보이지만, 실무에서 무거워지는 것은 대개 UI의 외측입니다.
예를 들어:
- 10초마다의 상태 동기
- 장치나 서버와의 재접속
- 파일 감시와 취입
- 큐에 쌓인 후처리
- 로그 전송이나 메트릭스 송신
- 기동 시의 캐시 warm-up
이들은 「화면의 이벤트」가 아니라 앱 전체의 수명에 매달리는 처리입니다.
여기를 폼이나 윈도우의 코드 비하인드에 살게 하면, 화면을 닫았을 때 멈추는 책임, 예외를 주울 책임, 재시행이나 backoff를 결정하는 책임이, UI의 사정과 섞이기 시작합니다.
BackgroundService를 사용하면,
「이 처리는 앱이 동작하고 있는 동안 계속 산다」
라는 선언이 코드의 형태로 나옵니다.
이것이 수수하게 강합니다.
3.2. 기동・정지・예외의 입구를 한 곳으로 모을 수 있다
Host를 사용하지 않는 desktop app에서도 ServiceCollection, ConfigurationBuilder, LoggerFactory를 개별로 나열하면 비슷한 것을 할 수 있습니다.
다만, 그 형태는 대개 조금씩 흩어집니다.
- DI는
Program.cs - 설정은 독자 static
- 로그는 별도 factory
- 종료 처리는
ApplicationExit - 상주 처리는
Task.Run
이 상태로도 처음에는 동작합니다. 그러나 몇 달 후에 보고 돌아오면 누가 앱의 수명을 가지고 있는가가 보이기 어려워집니다.
Generic Host를 사용하면,
- 서비스 등록
- 구성 읽기
- 로그 구성
- hosted service의 기동
- 정지 통지
IHostApplicationLifetime에 의한 전체 정지
가 같은 틀에 들어갑니다.
즉, 「이 앱은 어떻게 기동하고 어떻게 멈추는가」의 입구를 1곳으로 모으기 쉬운 것입니다. 상주계 앱에서는 여기가 나중에 효과가 옵니다.
3.3. graceful shutdown을 설계에 넣기 쉽다
상주 처리는 시작하기보다 멈추는 편이 어렵습니다. 정말로 그렇습니다. 개시는 3행이라도 종료는 진흙 맛이 납니다.
예를 들어 종료 시에는:
- 진행 중의 I/O를 취소하고 싶다
- 다음 주기를 개시하지 않도록 하고 싶다
- 큐의 잔건을 어디까지 흘릴지 결정하고 싶다
- 소켓이나 COM 오브젝트를 닫고 싶다
- 로그 flush나 상태 보존을 기다리고 싶다
이 부근을 FormClosing으로 모으면 화면 사정과 섞여 힘들어집니다.
Host / BackgroundService라면 CancellationToken과 StopAsync가 있으므로,
「멈추기 위한 경로」가 처음부터 있습니다.
물론 마법은 아닙니다.
크래시나 kill에서는 StopAsync가 불리지 않는 경우도 있습니다.
그래도 「정상 종료 시는 이 루트로 멈춘다」라는 설계가 있는 것만으로 꽤 조용해집니다.
3.4. DI / 로그 / 설정이 처음부터 갖춰진다
Generic Host의 좋은 점은 BackgroundService뿐만이 아닙니다.
Host.CreateApplicationBuilder로 DI / 구성 / 로그의 토대가 갖춰진다appsettings.json이나 환경 변수를 그대로 사용하기 쉽다ILogger<T>를 UI도 worker도 같은 유파로 사용할 수 있다- 필요하면
IOptions<T>계로 설정을 모을 수 있다
특히 Windows 도구 안건에서는, 「처음에는 작았으므로 거칠게 static으로 가지고 있던 설정이나 logger가 나중에 힘들어진다」 는 것이 꽤 자주 있습니다.
여기를 처음부터 host에 실어두면, 앱이 약간 살찌기 시작했을 때의 숨 차는 것이 줄어듭니다.
4. 어울리는 케이스
Generic Host / BackgroundService가 특히 효과적이기 쉬운 것은 예를 들어 다음과 같은 케이스입니다.
- 트레이 상주 앱 정기 동기, 감시, 알림, 재접속이 있다
- 장치 / 카메라 / 소켓 접속 앱 접속 유지, 감시, 재시행, 상태 취득이 있다
- 파일 연계 도구 감시, 취입 큐, 순서 있는 처리가 있다
- 사내 도구의 비대화 예방 처음에는 작지만, 설정・로그・외부 I/O가 늘어날 것 같다
- 종료 품질이 중요한 앱 닫을 때 중도 반쪽 상태를 남기고 싶지 않다
반대로, 다음과 같은 케이스에서는 갑자기 host를 넣지 않아도 좋을 수 있습니다.
- 단발 기동해 1회만 처리하고 끝나는 작은 도구
- 배경 처리가 거의 없고, UI 이벤트만으로 완결되는 화면
- 의존 관계나 설정이 거의 늘지 않는, 정말로 작은 사내 보조 도구
즉, Host는 「필수」가 아닙니다.
다만 상주 처리가 2개 이상 보이기 시작하면 꽤 적극적으로 검토해도 좋습니다.
나중에 Task.Run 식민지를 정리하기보다 훨씬 쌉니다.
5. 최소 구성 예(WPF 예)
예로서 WPF에서 host를 기동하고, 5초마다 외부 상태를 읽는 BackgroundService를 동작시키는 최소 구성을 씁니다.
WinForms에서도 입구가 Main / ApplicationContext로 바뀌는 것뿐이고 사고방식은 거의 같습니다.
5.1. App.xaml.cs
using System.Windows;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace DesktopHostSample;
public partial class App : Application
{
private IHost? _host;
protected override async void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
HostApplicationBuilder builder = Host.CreateApplicationBuilder(e.Args);
builder.Services.Configure<HostOptions>(options =>
{
options.ShutdownTimeout = TimeSpan.FromSeconds(15);
});
builder.Services.AddSingleton<MainWindow>();
builder.Services.AddSingleton<StatusStore>();
builder.Services.AddScoped<IDeviceStatusReader, DeviceStatusReader>();
builder.Services.AddHostedService<DevicePollingBackgroundService>();
_host = builder.Build();
await _host.StartAsync();
MainWindow mainWindow = _host.Services.GetRequiredService<MainWindow>();
mainWindow.Show();
}
protected override async void OnExit(ExitEventArgs e)
{
if (_host is not null)
{
await _host.StopAsync();
_host.Dispose();
}
base.OnExit(e);
}
}
이 형태의 포인트는 3개입니다.
- host의 기동을 UI 표시 전에 한다
- 종료 시에
StopAsync를 명시적으로 await한다 - DI / hosted service / shutdown timeout을 입구에서 모은다
OnExit을 async로 하는 것 자체는 UI 프레임워크 사정으로 약간 신경을 써야 하지만,
「종료 시에 host를 멈춘다」는 흐름을 분명히 써 두는 의미는 큽니다.
5.2. BackgroundService
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace DesktopHostSample;
public sealed class DevicePollingBackgroundService(
IServiceScopeFactory scopeFactory,
StatusStore statusStore,
ILogger<DevicePollingBackgroundService> logger) : BackgroundService
{
public override async Task StartAsync(CancellationToken cancellationToken)
{
logger.LogInformation("Device polling service is starting.");
await base.StartAsync(cancellationToken);
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
logger.LogInformation("Device polling loop started.");
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(5));
while (await timer.WaitForNextTickAsync(stoppingToken))
{
try
{
using IServiceScope scope = scopeFactory.CreateScope();
IDeviceStatusReader reader =
scope.ServiceProvider.GetRequiredService<IDeviceStatusReader>();
DeviceStatus status = await reader.ReadAsync(stoppingToken);
statusStore.Update(status);
}
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
{
break;
}
catch (Exception ex)
{
logger.LogError(ex, "Device polling failed.");
}
}
logger.LogInformation("Device polling loop finished.");
}
public override async Task StopAsync(CancellationToken cancellationToken)
{
logger.LogInformation("Device polling service is stopping.");
await base.StopAsync(cancellationToken);
logger.LogInformation("Device polling service stopped.");
}
}
여기서 중요한 것은 ExecuteAsync를
「관리된 while 루프」로서 자연스럽게 쓰는 것입니다.
- 주기는
PeriodicTimer - 정지는
stoppingToken - 예외는 로깅
scoped한 의존이 필요하면 매번 scope를 자른다
이 형태로 해 두면, 「지금 이 상주 처리는 어디서 시작되고, 어디서 멈추며, 어디서 실패가 보이는가」 가 꽤 읽기 쉬워집니다.
5.3. 상태 공유는 UI 직결로 하지 않는다
worker에서 UI 오브젝트를 직접 만지면 결국 거기서 UI 스레드 문제가 재발합니다.
그래서 우선은:
- worker는 상태 스토어나 메시징 층을 업데이트한다
- UI는 자신의 컨텍스트에서 그 상태를 읽는다 / 반영한다
라는 분리 편이 안전합니다.
예를 들어 StatusStore는 다음과 같은 얇은 공유 층으로 해 둘 수 있습니다.
namespace DesktopHostSample;
public sealed class StatusStore
{
private readonly object _gate = new();
private DeviceStatus _current = DeviceStatus.Empty;
public DeviceStatus Current
{
get
{
lock (_gate)
{
return _current;
}
}
}
public void Update(DeviceStatus next)
{
lock (_gate)
{
_current = next;
}
}
}
public sealed record DeviceStatus(string Message)
{
public static readonly DeviceStatus Empty = new("No Data");
}
UI로의 즉시 통지가 필요하면 Dispatcher / BeginInvoke / 이벤트 / messenger 등을 사용합니다.
다만, 그 책임은 UI 경계에서 가지는 편이 섞이지 않습니다.
6. StartAsync / ExecuteAsync / StopAsync의 분할 방식
이 3가지가 섞이면 읽는 이의 머릿속이 금방 탁해집니다. 우선은 다음 분할 방식이 꽤 안정적입니다.
6.1. StartAsync
StartAsync는 기동에 참가하는 짧은 처리를 두는 장소입니다.
어울리는 것:
- 기동 로그
- 가벼운 구독 개시
- 즉시 끝나는 초기 상태의 준비
base.StartAsync전후에서의 최소한의 정서
어울리지 않는 것:
- 몇십 초나 걸리는 warm-up
- 무한 루프
- 무거운 I/O를 나열하는 본체 처리
StartAsync를 무겁게 하면 앱 전체의 시작까지 둔하게 보입니다.
여기는 「개시의 신호」를 쓰는 장소, 정도로 생각하면 사고가 적습니다.
6.2. ExecuteAsync
ExecuteAsync는 서비스 수명의 본체입니다.
어울리는 것:
- 폴링
- 감시 루프
- 재접속 루프
Channel<T>를 읽는 컨슈머- 주기 처리
- 「정지까지 산다」 처리 전반
여기서의 요령은 3가지입니다.
CancellationToken을 처음부터 마지막까지 통과시킨다- 예외로 루프 전체가 말없이 죽지 않도록 한다
- 리트라이나 backoff를 임시방편으로 너무 늘리지 않는다
BackgroundService는 편리하지만, 방치하면 「뭐든 빨아들이는 거대 루프」도 됩니다.
실처리는 별도 서비스로 잘라내고, ExecuteAsync 자체는 수명 관리와 오케스트레이션으로 기울이는 편이 읽기 쉽습니다.
6.3. StopAsync
StopAsync는 정상 종료 시의 정리를 하는 장소입니다.
어울리는 것:
- 정지 로그
- 타이머 / 구독 / 감시의 해제
- 명시적으로 close / flush하고 싶은 리소스의 정리
base.StopAsync를 통한 종료 대기
다만, StopAsync에 전부를 너무 기대하지 않는 것도 중요합니다.
- 프로세스가 떨어졌다
- 강제 종료되었다
- OS 측에서 kill되었다
이 종류의 종료에서는 애초에 통과하지 않는 경우가 있습니다.
그래서,
- 영속화는 가능한 한 평상시에 작게 끝낸다
- 종료 시에만 정합이 잡히지 않는 설계로 하지 않는다
- cleanup은 idempotent로 해 둔다
이 부근이 중요합니다. 종료 시만으로 세계를 구하려고 하면 대개 탁해집니다.
6.4. .NET 10 이후의 주의
2025년 이후의 변경점으로서, .NET 10에서는 BackgroundService.ExecuteAsync의 전체가 백그라운드 태스크로서 실행되는 거동으로 바뀌었습니다.
이전에는 최초의 await 전의 동기 부분이 기동 시에 다른 서비스의 개시를 블로킹하는 약간 알기 어려운 거동이 있었습니다.
이 변경으로 ExecuteAsync의 「최초의 몇 줄이 기동을 무겁게 하고 있던」 사고는 줄어들기 쉬워졌습니다.
다만, 그래도 설계상은
- 기동에 참가하는 짧은 처리 →
StartAsync - 길게 달리는 본체 →
ExecuteAsync
로 나누는 편이 읽기 쉽습니다.
기동 타이밍을 더 엄밀히 제어하고 싶다면 IHostedLifecycleService까지 시야에 들어갑니다.
이 부근은 상주 앱이 살쪄왔을 때 효과적인 수수한 논점입니다.
7. 자주 있는 안티패턴
7.1. Window_Loaded / Form_Shown에서 무한 루프를 시작한다
처음은 편합니다. 하지만 정지 책무와 예외 책무가 UI 측에 딱 붙습니다.
「화면이 닫히면 멈춘다」 「최소화 to tray에서는 멈추지 않는다」 「설정 변경 시는 재기동한다」 같은 조건이 늘기 시작하면 금방 힘들어집니다.
7.2. Task.Run을 던져놓는다
Task.Run 자체가 나쁜 것은 아닙니다.
나쁜 것은 수명과 예외의 주인이 없는 것입니다.
특히 상주 처리를 Task.Run(async () => { while (...) { ... } })로 시작하면,
- 언제 끝나는가
- 누가 기다리는가
- 예외는 어떻게 보는가
- 종료 시에 어디까지 기다리는가
가 애매해집니다.
이것이 BackgroundService에 실리는 것만으로도 꽤 정리하기 쉬워집니다.
7.3. BackgroundService에서 UI를 직접 만진다
이것은 지뢰입니다. UI 스레드 문제와 lifetime 문제가 한꺼번에 섞입니다.
worker는 UI를 직접 만지지 않고,
- 상태
- 이벤트
- 메시지
- queue
중 어느 것으로 경계를 두는 편이 안전합니다.
7.4. StopAsync에만 중요한 저장 처리를 모은다
StopAsync는 정상 종료의 도움은 되지만 최후의 심판이 아닙니다.
종료 시에만 저장하지 않는다, 종료 시에만 flush하지 않는다, 종료 시에만 일관성이 맞지 않는다,
는 설계라면 크래시로 무너집니다.
7.5. host를 사용하고 있는데 Environment.Exit로 거칠게 떨어뜨린다
이것도 자주 있습니다.
「이제 귀찮으니 떨어뜨리자」
로 Environment.Exit을 부르면,
host가 가지고 있는 graceful shutdown의 경로를 스스로 끊어버립니다.
치명적 에러로 전체 종료하고 싶다면,
우선은 IHostApplicationLifetime.StopApplication()을 사용해,
멈추기 위한 정규 루트를 통과시키는 편이 자연스럽습니다.
8. 리뷰 시의 체크리스트
Generic Host / BackgroundService를 사용하는 desktop app의 리뷰에서는 다음을 순서대로 보면 알기 쉽습니다.
- 그 처리는 앱 수명에 매달리는 처리인가, 단순한 UI 이벤트 처리인가
- 기동 책무가
StartAsync/ExecuteAsync/StopAsync에 적절히 나뉘어 있는가 StartAsync가 너무 무거워지고 있지 않은가ExecuteAsync가CancellationToken을 마지막까지 넘기고 있는가scoped한 의존을 hosted service에서 직접 쥐고 있지 않은가- worker가 UI 오브젝트를 직접 만지고 있지 않은가
- 예외가 말없이 쥐어뭉개지고 있지 않은가
- 재시행 루프가 무한 고빈도로 되어 있지 않은가
- 종료 시의 대기 시간에 상한이 있는가
Environment.Exit이나 프로세스 kill 전제의 종료가 섞여 있지 않은가
이 체크리스트로 보면, 「일단 Host 넣었습니다」 와 「수명을 설계로서 정리할 수 있습니다」 의 차이가 꽤 보이기 쉬워집니다.
9. 대강의 사용 구분
| 하고 싶은 것 | 먼저 고르는 것 |
|---|---|
| 앱 전체의 DI / 로그 / 설정을 맞춘다 | Host.CreateApplicationBuilder |
| 상주 루프를 동작시킨다 | BackgroundService |
| 일정 간격으로 돌린다 | PeriodicTimer + BackgroundService |
| 순서 있는 후처리를 흘린다 | Channel<T> + BackgroundService |
| scoped service를 사용한다 | IServiceScopeFactory.CreateScope() |
| 정상 종료를 전체에 통지한다 | IHostApplicationLifetime.StopApplication() |
| UI 업데이트 | UI 측에서 Dispatcher / Invoke |
| 1회만의 화면 조작 | 통상의 async 메서드 |
| 기동 시의 엄밀한 라이프사이클 제어 | IHostedLifecycleService를 검토 |
10. 정리
데스크톱 앱에 Generic Host / BackgroundService를 가지고 들어오는 이유는,
「Web 같은 쓰는 법을 하고 싶어서」가 아닙니다.
정말로 효과적인 것은 다음 3가지입니다.
- 기동과 정지의 책무를 1곳으로 모을 수 있다
- 길게 사는 처리의 수명을 설계로서 가질 수 있다
- graceful shutdown을 후추가가 아니라 입구에서부터 다룰 수 있다
Windows 도구나 상주계 앱은 처음에는 작더라도, 감시, 동기, 재접속, 큐, 로그, 설정이 조금씩 늘어납니다. 그때 UI 코드의 덤으로 운용하면 나중에 조용히 힘들어집니다.
반대로,
- UI는 UI
- 상주 처리는 hosted service
- 실처리는 DI 서비스
- 종료는
StopAsync와CancellationToken
으로 나누는 것만으로도 꽤 정돈됩니다.
화려함은 없습니다. 다만 이런 수수한 설계는 실무에서 제대로 효과가 옵니다. 「닫으면 가끔 이상해진다」 「어디서 멈추고 있는지 모른다」 같은 싫은 끈적거림을 줄여 줍니다.
Windows 도구나 상주계 앱에서 BackgroundService화, 기동 / 정지 설계, 감시 루프, COM / 소켓 / 파일 감시의 수명 정리, 종료 시 불구의 구분 등으로 막혀 있는 경우는 설계 리뷰나 방침 정리부터 상담해 주세요.
11. 참고 자료
관련 기사
같은 태그를 공유하는 최신 기사입니다. 더 가까운 주제로 지식을 넓힐 수 있습니다.
Windows Forms, WPF, WinUI 중 어느 것으로 할까 - 신규 개발, 기존 자산, 배포, UI 표현의 판단표
Windows 데스크톱 앱을 C#/.NET으로 새로 만들 때 WinForms·WPF·WinUI 중 무엇을 고를지, 신규 개발과 기존 자산, 배포 방식, UI 표현력, 팀 문화의 다섯 축으로 비교한 한 장짜리 판단표를 제시하여 독자가 자기 프로젝트...
.NET의 Generic Host란 무엇인가 - DI, 설정, 로그, BackgroundService를 먼저 정리
Generic Host의 정체를, DI・설정・로그・BackgroundService와 Host.CreateApplicationBuilder / WebApplicationBuilder의 관계로 정리합니다. 어디서 효과적이고 어디서 과잉인지를 .NET...
PeriodicTimer / System.Threading.Timer / DispatcherTimer의 사용 구분 - .NET의 정기 실행을 먼저 정리
PeriodicTimer는 async 루프, System.Threading.Timer는 ThreadPool callback, DispatcherTimer는 WPF UI 스레드라는 책무 차이를 정리하고, .NET 6 이후의 worker・WPF에서 ...
WPF / WinForms의 async/await와 UI 스레드를 한 장으로 정리 - await 후의 돌아갈 곳, Dispatcher, ConfigureAwait, .Result / .Wait()의 막힘 포인트
WPF/WinForms에서 async/await가 헷갈리는 핵심—await 후의 복귀 스레드, Dispatcher와 Invoke의 사용 구분, ConfigureAwait(false)의 진짜 의미, .Result/.Wait()로 화면이 굳는 이유까...
FileSystemWatcher 사용법과 주의점 - 누락, 중복 알림, 완료 판정의 함정
Windows .NET 파일 감시에서 FileSystemWatcher의 이벤트를 완료 알림으로 오인하기 쉬운 함정과, 재스캔 요청·원자적 claim·idempotency를 축으로 누락과 중복을 견디는 안전한 설계 패턴을 정리합니다.
관련 토픽
이 기사와 가까운 토픽 페이지입니다. 기사를 출발점 삼아 관련 서비스와 다른 기사로 이어집니다.
Windows 기술 토픽
Windows 개발, 장애 조사, 기존 자산 활용에 관한 KomuraSoft LLC 기사를 모은 토픽 허브입니다.
Generic Host & 앱 아키텍처
Generic Host, BackgroundService, DI, 구성, 로깅, 앱 수명 설계를 정리한 토픽 페이지입니다.
UI 스레드 & 타이머
WPF / WinForms UI 스레드, async 흐름, Dispatcher 사용, 타이머 판단을 정리한 토픽 페이지입니다.
이 주제와 연결되는 서비스
이 기사는 다음 서비스 페이지로 이어집니다. 가까운 입구부터 확인해 주세요.
Windows 앱 개발
상주 처리, 장비 연동, 운영 로그, 유지 보수 가능한 구조가 필요한 Windows 데스크톱 애플리케이션을 지원합니다.
기술 상담 & 설계 리뷰
설계 방향, 아키텍처 경계, 수명 관리, 기존 Windows 자산 처리 방법을 정리하는 데 도움을 드립니다.
저자 프로필
기사 저자의 프로필 페이지입니다.
Go Komura
합동회사 코무라소프트 대표
Windows 소프트웨어 개발, 기술 상담, 장애 조사를 중심으로 재현이 어려운 장애 조사와 기존 자산이 남아 있는 프로젝트에 강점이 있습니다.
공개 링크