수정 이력(6건, 최종 수정 2026년 09월 03일)
이 글에 적용한 변경 사항의 기록입니다. 보관해 둔 수정 전 버전은 DOI가 부여된 고정 URL에서 읽을 수 있습니다.
- Codex 리뷰에 따라 상담·문의 링크에 /ko/ 로케일 접두를 붙였습니다. 본문의 기술적인 주장은 바꾸지 않았습니다.
- 관련 기사 링크를 한국어 permalink에 맞추는 등 CI가 지적한 표시용 수정을 반영했습니다. 본문의 기술적인 주장은 바꾸지 않았습니다.
- permalink·저자 표기·지식 맵 래퍼·깨진 내부 링크 등 CI가 지적한 표시용 수정을 반영했습니다. 본문의 기술적인 주장은 바꾸지 않았습니다.
- 기사 맨 앞에 「이 기사의 지식 맵」 절을 추가했습니다. 본문에서 다루는 개념과 그 관계를 요약·그림·상세 페이지 링크로 모은 것입니다. 본문의 주장은 바꾸지 않았습니다.
- 외부 리뷰(1283건) 대응으로 본문을 업데이트했습니다. 개별 변경 내용은 아래 이력을 참조하세요.
- gMSA와 SCM을 처음 나올 때 풀어 썼고, SCM과 서비스 프로세스의 주고받기(시작 요청부터 중지까지의 상태 전이와 시작·중지의 시간 제한)를 시퀀스 다이어그램으로 만들었습니다. 등록되었는지 확인하는 절을 새로 두고, `sc.exe`와 PowerShell로 확인하는 방법, `services.msc`의 화면 경로, 이벤트 뷰어에서 소스를 어디서 보는지 덧붙였습니다.
- 최초 공개
이 글을 인용하기(DOI(등록된 아카이브): 10.5281/zenodo.21635344)
아래 DOI는 이전에 등록된 아카이브를 가리키며 현재 본문과 다를 수 있습니다. 현재 본문을 참조할 때는 이 페이지의 URL을 사용하세요.
Go Komura (2026). 「Windows서비스 만드는 법과 운영 ── 작업 스케줄러와의 구분부터 BackgroundService의 서비스화까지」. 합동회사 코무라소프트. https://comcomponent.com/ko/blog/windows-service-worker-service-guide/
- DOI(등록된 아카이브)
- 10.5281/zenodo.21635344
- DOI(마지막 등록 버전)
- 10.5281/zenodo.21635345
「5분마다 작업 스케줄러로는 더 이상 감당이 안 된다」「장치에서 오는 데이터를 상시 대기하는 감시 데몬을 상주시키고 싶다」「폴더에 파일이 놓이면 수 초 이내에 처리하고 싶다」. 정기 실행 상담을 이어 가다 보면, 요구사항이 커진 끝에서 반드시 이 이야기가 나옵니다.
이 블로그에서는 Power Automate로 하는 업무 자동화, 작업 스케줄러의 안전한 운영 설계로 자동화 이야기를 이어 왔습니다. 작업 스케줄러 기사의 제8장에서 「분 단위 폴링이나 상시 감시가 필요해지면 상주 프로세스의 영역」이라는 선을 짚었지만, 그렇다면 실제로 Windows서비스를 어떻게 만들고 운영에 올릴 것인가. 여기를 애매하게 둔 채 「일단 서비스화」하면 「멈출 수 없는 서비스」「죽은 채로 아무도 모르는 서비스」「LocalSystem으로 무엇이든 돌아가는 서비스」가 생깁니다.
이 기사에서는 상주 처리를 Windows서비스로 둘지 판단하는 표, 서비스 구조의 최소 지식(SCM=서비스 제어 관리자·시작 유형·세션 0), .NET 8 Worker Service로 구현하기, sc.exe로 등록과 복구 옵션, 실행 계정 선택, 그리고 안전한 중지 처리──를 실무에서 정하는 순서로 정리합니다.
1. 먼저 결론
- 구분의 축 ── 수 분 이상 간격의 정기 처리라면 작업 스케줄러로 충분합니다. 상시 대기(소켓, named pipe, FileSystemWatcher)·초 단위 반응·장애 시 자동 복구가 요구사항에 들어가면 Windows서비스입니다.
- UI는 표시할 수 없습니다 ── Windows Vista 이후 서비스는 세션 0에서 동작하며, 사용자와 직접 대화할 수 없습니다(UI를 표시할 수 없습니다). UI가 필요하면 서비스와 UI 앱을 나누고 프로세스 간 통신(IPC)으로 대화하도록 설계합니다.1
- 구현 경로 ── .NET에서 만드는 법은 Worker Service 템플릿+
Microsoft.Extensions.Hosting.WindowsServices의AddWindowsService(IHostBuilder계열이면UseWindowsService)가 공식 경로입니다. 콘솔 앱으로 그대로 실행할 수 있어 개발·디버그가 기존 서비스 개발보다 훨씬 수월해졌습니다.2 - 예외와 재시작 ── .NET 6 이후
BackgroundService안의 처리되지 않은 예외는 기본값으로 호스트를 중지시킵니다(BackgroundServiceExceptionBehavior.StopHost). 다만 이는 「정상 중지」이므로 SCM(서비스 제어 관리자)의 복구 옵션에 의한 재시작이 동작하지 않습니다. 재시작하고 싶은 실패는 0이 아닌 종료 코드로 프로세스를 내립니다.32 - 실행 계정 ── 관성에 따라 LocalSystem으로 두지 마세요.
sc.exe create의 기본값이 LocalSystem이므로, 의식하지 않으면 최고 권한으로 움직이기 시작합니다.4 로컬에서 끝나는 작업이면 LocalService나 가상 계정, 도메인에서 공유나 DB에 접근하면 gMSA(Group Managed Service Account, 그룹 관리 서비스 계정. 비밀번호를 도메인 측이 자동 관리하는 전용 계정)가 정석입니다.5 - 복구와 중지 ── 복구 옵션(
sc.exe failure)과 중지 처리(HostOptions.ShutdownTimeout, 기본 30초6)는 등록 시점에 설계합니다. 「중지에 응답하지 않는 서비스」는 운영에서 가장 꺼리는 존재입니다.
그림의 실선은 항상 성립하는 관계, 점선은 조건이 붙는 관계입니다(성립 조건은 상세 페이지의 관계별 설명에 적혀 있습니다). 관계 전체 목록(총 19건, 근거와 확신도 포함)과 주요 개념의 정의는 지식 맵 상세 페이지에 정리되어 있습니다(일본어). 데이터: JSON-LD / Turtle
2. 언제 서비스로 둘 것인가 ── 작업 스케줄러·서비스·상주 앱 판단표
상주처럼 보이는 요구사항이 나왔을 때의 선택은 세 가지입니다. 작업 스케줄러, Windows서비스, 그리고 「시작 프로그램에 등록한 상주 앱」(알림 영역에 상주하는 유형). 성격을 나란히 정리합니다.
| 관점 | 작업 스케줄러 | Windows서비스 | 상주 앱(시작 프로그램 등록) |
|---|---|---|---|
| 시작 타이밍 | 시각·이벤트 계기로 그때그때 시작 | OS 시작 시(로그온 전)부터 상주 | 사용자 로그온 시부터 |
| 로그온이 필요 없는가 | 필요하지 않게 할 수 있음(비대화형 실행) | 불필요 | 필요(로그오프하면 사라짐) |
| UI | 표시할 수 없음(비대화형 구성에서는) | 표시할 수 없음(세션 0) | 표시할 수 있음(알림 영역·대화상자) |
| 상시인가 정기인가 | 정기(시간 단위〜일 단위가 적소) | 상시 | 상시(다만 사용자 세션 안) |
| 권한 | 작업마다 실행 계정 지정 | 서비스 계정(제6장) | 로그온 사용자의 권한 |
| 감시·복구 | 이력+자체 알림 | SCM의 복구 옵션(자동 재시작) | 없음(직접 만듦) |
| 배포 수고 | XML / PowerShell로 등록 | sc.exe / 설치 프로그램 | Run 키 등에 등록 |
판단의 축은 다음과 같습니다.
- 하루 수 회〜시간 단위의 정기 처리이고, 처리 사이에 상태를 갖지 않으면 작업 스케줄러입니다. 여기를 굳이 서비스화해서 스스로 타이머를 관리하는 것은 과하고, 작업 스케줄러 기사에서 쓴 운영 설계가 훨씬 싸게 끝납니다.
- 상시 대기가 본질──TCP나 파이프에서 요청 대기, FileSystemWatcher로 하는 폴더 감시, 큐의 순차 처리, 초 단위 반응──이면 Windows서비스입니다. 「5분마다 작업」으로 잘게 나눠 폴링하기 시작했다면, 그것은 상주 프로세스를 열화된 형태로 다시 구현하고 있는 신호입니다.
- UI가 본체(알림 영역의 조작, 사용자에게 띄우는 팝업)이면 상주 앱입니다. 다만 로그온하지 않으면 동작하지 않으므로 서버 용도에는 쓸 수 없습니다. 「백그라운드 처리는 상시 필요하지만 UI도 필요」하면, 다음 장과 같이 서비스와 UI 앱의 두 프로세스로 나눕니다.
하나 더, 놓치기 쉬운 판단 재료가 복구입니다. 작업 스케줄러의 작업은 실패해도 다음 스케줄까지 방치되지만, 서비스는 SCM이 자동 재시작을 맡아 줍니다(제5장). 「야간에 죽어도 아침까지는 스스로 복귀해 있기를 바란다」는 요구사항은, 그 자체로 서비스화의 이유가 됩니다.
3. 서비스 구조의 최소 지식 ── SCM·시작 유형·세션 0
3.1 SCM과 시작 유형
Windows서비스의 주체는 서비스 제어 관리자(SCM)입니다. 서비스 등록부를 관리하고, 시작·중지 요청을 중개하며, 실패 시 복구 동작을 실행합니다. 서비스 프로세스는 SCM과 규약대로 대화할 수 있는 구조여야 하지만, 이 부분은 .NET 라이브러리가 대신해 주므로(제4장), 우리가 설계로 정하는 것은 「시작 유형」「실행 계정」「복구」「중지」의 네 가지입니다.
시작 유형(Startup type)은 네 가지입니다.4
| 시작 유형 | 동작 | 쓰는 곳 |
|---|---|---|
| 자동(auto) | OS 시작 시 시작 | 상시 가동 서비스의 기본 |
| 자동·지연 시작(delayed-auto) | 다른 자동 서비스보다 조금 뒤에 시작 | 업무 서비스는 우선 이것. 시작 직후의 혼잡과 의존 대상이 아직 시작되지 않은 상태를 피함 |
| 수동(demand) | 요청이 있을 때만 시작 | 다른 앱에서 시작되는 보조 서비스 |
| 사용 안 함(disabled) | 시작 불가 | 중지 조치·봉인 |
업무 서비스는 지연 시작을 기본으로 둡니다. OS 시작 직후는 네트워크도 DB도 다른 서비스도 아직 갖춰지지 않았기 때문에, 「자동」으로 가장 빨리 시작하면 첫 연결에 실패하기 쉽습니다. 지연 시작으로 시차를 둔 다음, 연결 실패는 뒤에서 다루는 ExecuteAsync 안의 재시도로 흡수하는 2단계 구성이 안정적입니다.
하나 더, 시작 타임아웃을 알아 두세요. SCM은 서비스의 시작 완료 보고를 무한히 기다리지 않습니다. 기본 타임아웃(ServicesPipeTimeout, 30초)을 넘으면 이벤트 7000 / 7011이 기록되고 시작 실패로 취급됩니다.7 즉, DB 연결 재시도나 큰 캐시 구축 같은 무거운 초기화를 「시작 처리」 안에서 하면 안 됩니다. 시작은 바로 완료시키고, 무거운 처리는 본체 루프 쪽(ExecuteAsync)에서 하는 것이 철칙입니다.
SCM과 서비스 프로세스의 대화를 한 장으로 모으면 다음 흐름입니다. 시작도 중지도 「요청」과 「완료 보고」의 왕복이며, 그 왕복에 시간 제한이 있는 것이 서비스의 특징입니다.
sequenceDiagram
participant Adm as 관리자 / OS 시작
participant SCM as SCM
participant Svc as 서비스 프로세스
participant BG as BackgroundService
Adm->>SCM: 시작 요청 sc.exe start 또는 자동 시작
SCM->>Svc: 프로세스 시작
Svc-->>SCM: 시작 처리 중이라고 보고 START_PENDING
Note over SCM,Svc: 기본 30초의 ServicesPipeTimeout 안에<br/>실행 중을 보고하지 못하면 이벤트 7000 / 7011
Svc-->>SCM: 실행 중이라고 보고 SERVICE_RUNNING
Svc->>BG: ExecuteAsync 시작
BG-->>BG: 대기·처리 루프
Adm->>SCM: 중지 요청 sc.exe stop 또는 시스템 종료
SCM->>Svc: 중지 요청을 알림
Svc->>BG: stoppingToken을 취소
BG-->>Svc: 루프를 빠져나와 StopAsync가 완료
Note over Svc,BG: 기본 30초의 ShutdownTimeout을 넘으면<br/>뒷정리 완료를 기다리지 않고 중지를 강제
Svc-->>SCM: 중지했다고 보고 SERVICE_STOPPED
이 왕복을 구현하는 것이 본래의 서비스 개발이지만, .NET에서는 AddWindowsService가 넣는 서비스 수명 주기가 대행합니다(제4장). 우리가 쓰는 것은 ExecuteAsync의 내용과 중지 요청에 대한 응답(제7장)뿐입니다.
3.2 세션 0 분리 ── 서비스는 UI를 표시할 수 없다
Windows Vista 이후 서비스는 세션 0이라는 격리된 세션에서 동작하며, 사용자와 직접 대화할 수 없습니다.1 서비스 안에서 MessageBox.Show나 폼 표시를 호출해도, 로그온 중인 사용자 화면에는 아무것도 나오지 않습니다. 그뿐 아니라, 아무도 누를 수 없는 OK 버튼을 기다리며 처리 전체가 멈추는, 고전적인 행(hang)의 원인이 됩니다. 오래된 코드를 서비스로 이식할 때는 오류 표시용으로 남겨 둔 메시지 박스가 남아 있지 않은지 반드시 확인하세요.
세션이 나뉘어 있는 이유, RDP로 연결했을 때 어느 세션에 들어가는지, named object가 세션을 넘을 수 있는지──세션 구조 자체는 「Windows의 세션 분리를 어떻게 이해할 것인가」에서 다룹니다. 이 기사에서 필요한 것은 「서비스는 UI를 표시할 수 없다」는 한 가지뿐이므로, 이하는 그 전제로 읽어 주세요.
UI가 필요할 때의 정답은 서비스(세션 0)와 UI 앱(사용자 세션)을 프로세스로 나누고, 프로세스 간 통신(IPC)으로 대화하는 구성입니다. 이는 Microsoft 자신이 안내하는 설계이며, named pipe 등의 IPC를 쓰고 UI 쪽이 결과를 서비스에 돌려줍니다.1 어느 IPC 수단을 고를지는 같은 날 공개한 자매 기사 「프로세스 간 통신 판단표」에서 정리합니다. UI 앱 쪽도 Generic Host에 올려 두면, 두 프로세스에서 DI·로그·설정의 구조를 맞출 수 있습니다(「Generic Host와 BackgroundService를 데스크톱 앱에서 쓰기」 참조).
4. .NET에서 만드는 법 ── Worker Service와 AddWindowsService
4.1 템플릿과 Program.cs
.NET에서의 서비스 개발은 Worker Service 템플릿에서 시작합니다. SDK에 포함되어 있으므로 dotnet new worker로 뼈대가 만들어지고, 거기에 Microsoft.Extensions.Hosting.WindowsServices 패키지를 추가해 AddWindowsService를 호출하기만 하면 Windows서비스로 동작할 준비가 됩니다.2 토대가 되는 Generic Host의 구조는 「.NET Generic Host란」에서 설명합니다.
using App.MonitorService;
using Microsoft.Extensions.Hosting;
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
// Windows서비스로 시작되었을 때 SCM과 대화하는 수명 주기를 넣습니다.
// 콘솔로 시작되었을 때는 일반 ConsoleLifetime 그대로 동작합니다
builder.Services.AddWindowsService(options =>
{
options.ServiceName = "KsMonitor";
});
builder.Services.AddSingleton<MeasurementQueue>();
builder.Services.AddHostedService<MonitorWorker>();
IHost host = builder.Build();
host.Run();
Host.CreateDefaultBuilder를 쓰는 기존 스타일 프로젝트라면, 대신 IHostBuilder 확장의 UseWindowsService()를 호출합니다. 역할은 같습니다.
하나, 작업 스케줄러 기사의 「시작(옵션)」 문제와 같은 종류의 함정이 있습니다. 서비스로 시작되었을 때의 현재 디렉터리는 C:\Windows\System32입니다. appsettings.json 등을 상대 경로 전제로 읽고 있으면 「콘솔에서는 되는데 서비스에서는 설정을 읽지 못한다」가 됩니다. 설정 파일은 실행 파일 기준으로 해석하거나, 공식 튜토리얼이 안내하는 대로 등록 시 binpath에 --contentRoot를 넘겨 콘텐츠 루트를 명시하세요.2
4.2 ExecuteAsync ── stoppingToken과 예외 설계
본체는 BackgroundService를 상속한 ExecuteAsync에 씁니다. 여기서 정해야 할 것은 「중지 요청에 대한 응답」과 「예외를 어떻게 다룰 것인가」 두 가지에 귀결됩니다.
namespace App.MonitorService;
public sealed class MonitorWorker(
MeasurementQueue queue,
ILogger<MonitorWorker> logger) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
try
{
while (!stoppingToken.IsCancellationRequested)
{
try
{
// 큐에서 1건을 꺼내 처리합니다. 대기에도 반드시 토큰을 넘깁니다
var item = await queue.DequeueAsync(stoppingToken);
await ProcessAsync(item, stoppingToken);
}
catch (Exception ex) when (ex is IOException or TimeoutException)
{
// 계속해도 되는 실패: 기록한 뒤 간격을 두고 재시도합니다
logger.LogError(ex, "처리에 실패했습니다. 30초 후에 재시도합니다.");
await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken);
}
}
}
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
{
// SCM에서의 중지 요청. 정상 흐름이므로 아무것도 하지 않습니다.
// when 절로 stoppingToken 기인으로 한정하는 이유는, 내부 처리의 타임아웃 등이
// 던진 취소를 「정상 중지」로 오인하고, 워커만 조용히
// 멈춘 채 호스트가 살아 있는 상태를 막기 위해서입니다
}
catch (Exception ex)
{
// 예상 밖 실패: 기본값 StopHost에서는 호스트가 「정상적으로」 중지해 버려,
// SCM의 복구 옵션(자동 재시작)이 발동하지 않습니다.
// 0이 아닌 종료 코드로 내려 SCM에 「실패했다」고 전합니다
logger.LogCritical(ex, "복구 불가능한 오류로 서비스를 종료합니다.");
Environment.Exit(1);
}
}
private Task ProcessAsync(Measurement item, CancellationToken token)
=> throw new NotImplementedException();
}
stoppingToken을 모든 대기에 넘깁니다.Task.Delay, I/O, 큐 대기. 한 곳이라도 토큰을 무시한 긴 대기가 있으면, 그곳이 「중지에 응답하지 않는 서비스」의 온상이 됩니다(제7장).- 계속 가능한 실패와 복구 불가능한 실패를 나눕니다. 네트워크 단절이나 일시적인 타임아웃은 루프 안에서 잡아 기록하고 재시도합니다. 삼키고
continue만 하는catch (Exception)은 조용히 공회전하는 서비스를 만들 뿐입니다. 어느 쪽으로 분류할지의 생각은 「예상 밖 예외에서 종료할지 계속할지 판단표」에서 정리합니다. - 처리되지 않은 예외의 기본 동작을 알아 둡니다. .NET 6 이전에는
ExecuteAsync에서 빠져나온 예외가 어둠 속으로 사라지고, 서비스는 동작하는 것처럼 보이면서 아무것도 하지 않는 「좀비」가 되었습니다. .NET 6 이후 기본값이BackgroundServiceExceptionBehavior.StopHost가 되어, 예외가 로그에 기록된 뒤 호스트가 중지합니다.3 다만 이 중지는 「정상 중지」이므로 복구 옵션을 설정해 있어도 재시작되지 않습니다. 복구 옵션으로 복귀시키고 싶은 실패는, 위 코드처럼Environment.Exit(1)로 명시적으로 비정상 종료시키는 것이 공식 튜토리얼대로의 방식입니다.2
4.3 콘솔 그대로 실행할 수 있다 ── 개발이 편해진 가장 큰 이유
AddWindowsService는 Windows서비스로 시작되었을 때만 서비스용 수명 주기로 바뀝니다. 즉 같은 exe가 Visual Studio의 F5나 dotnet run에서는 일반 콘솔 앱으로 동작합니다. 중단점도 걸리고, Ctrl+C를 누르면 중지 요청부터 StopAsync 흐름까지 로컬에서 검증할 수 있습니다.
다만 콘솔에서 동작했다고 서비스에서도 동작한다고는 할 수 없습니다. 실행 계정의 차이(제6장), 세션 0, 현재 디렉터리──이 세 가지만은 서비스로 실제 기기에서 확인이 필요합니다. 「콘솔에서는 되는데 서비스에서는 안 된다」의 원인은 거의 이 세 가지로 모입니다.
5. 등록과 운영 ── sc.exe·복구 옵션·이벤트 로그
5.1 sc.exe create로 등록
게시(dotnet publish, 단일 파일 게시가 다루기 쉽습니다)한 exe를 sc.exe create로 SCM에 등록합니다. 관리자 PowerShell에서 실행합니다.
sc.exe create "KsMonitor" binpath= "C:\Services\KsMonitor\KsMonitor.exe" start= delayed-auto obj= "NT AUTHORITY\LocalService" displayname= "KS 계측 데이터 수집 서비스"
sc.exe description "KsMonitor" "계측 데이터를 수집해 DB에 등록하는 상주 서비스(담당: 정보시스템부)"
고전적인 함정을 먼저 말합니다. binpath=나 start=의 등호 뒤에는 공백이 필요합니다. 등호까지가 옵션 이름이고, 값 사이의 공백이 구문상 필수라는 sc.exe 특유의 사양입니다(생략하면 실패합니다).4 또한 obj=를 생략한 경우의 기본값은 LocalSystem입니다.4 아무 생각 없이 등록하면 최고 권한으로 움직이기 시작하므로, 제6장의 선택을 등록 시점에 마쳐 두세요.
description 설정은 사소해 보여도 중요합니다. 몇 년 뒤 services.msc를 연 누군가가 「이것은 무슨 서비스이고, 멈춰도 되는지, 누구에게 물어야 하는지」를 판단할 수 있는 정보를 남겨 두면, 이후 조사 비용이 사라집니다. 삭제는 중지시킨 뒤 sc.exe delete "KsMonitor"입니다.2
배포처가 여러 대이거나 갱신(파일 교체)이 정기적으로 있으면, sc.exe 수작업이 아니라 설치 프로그램(MSI / WiX의 ServiceInstall 등)으로 등록·갱신·삭제를 세트로 맡기는 구성으로 일찍 옮기세요. 수작업 등록 절차서는 절차를 하나 건너뛴 단말을 반드시 만듭니다.
5.2 복구 옵션 ── 「죽으면 재시작」은 SCM에 맡긴다
서비스화의 큰 이점이 SCM 표준 복구 옵션입니다. 프로세스가 비정상 종료했을 때의 동작을 선언적으로 설정할 수 있습니다.2
sc.exe failure "KsMonitor" reset= 86400 actions= restart/60000/restart/60000/restart/300000
이 예는 「1회째·2회째 실패는 60초 후 재시작, 3회째 이후는 5분 후 재시작, 실패 카운터는 24시간에 재설정」입니다. GUI(services.msc의 속성 →「복구」탭)에서도 같은 설정을 할 수 있습니다. 자체 「감시 작업」이나 감시 스크립트를 만들기 전에, 먼저 이 표준 기능을 쓰세요.
주의는 두 가지입니다. 첫째, 앞 장과 같이 프로세스가 0이 아닌 코드로 종료하지 않으면 발동하지 않습니다. StopHost에 따른 정상 중지는 복구 대상이 아닙니다. 둘째, 시작 직후 반드시 죽는 상태(설정 실수, DB에 연결되지 않음)이면 재시작 루프가 됩니다. 3회째 이후 간격을 길게 두는 것은 그 때문이며, 더불어 「왜 죽었는지」가 이벤트 로그에 남는 상태(다음 절)를 먼저 만들어 둡니다.
5.3 이벤트 로그와 파일 로그의 병용
Host.CreateApplicationBuilder는 Windows에서 EventLog 로거 프로바이더를 자동 추가합니다. 그리고 이벤트 로그에 도달하는 것은 기본값으로 Warning 이상만입니다.2 「서비스로 바꿨더니 Information 로그가 전부 사라졌다」고 혼동하기 쉽지만, 사라진 것이 아니라 이벤트 로그의 기본 필터에 걸린 것입니다. 시작·중지 같은 운영 이벤트도 기록하고 싶으면 appsettings.json에서 EventLog 프로바이더의 레벨을 명시합니다.
{
"Logging": {
"EventLog": {
"SourceName": "KsMonitor",
"LogLevel": {
"Default": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
}
}
}
주의가 한 가지 있습니다. 이벤트 소스는 사전에 등록되어 있지 않으면 쓸 수 없고, 등록에는 관리자 권한이 필요합니다. SourceName을 생략해도 피할 수 없습니다──AddWindowsService의 기본값에서는 애플리케이션 이름이 소스 이름이 되므로2, 어느 쪽이든 「미등록 소스」에서 시작합니다. LocalService처럼 최소 권한 계정은 실행 시 소스를 만들 수 없고, 만들기에 실패하면 이벤트 로그에 아무것도 쓰이지 않은 채 운영이 시작됩니다. 등록은 설치 프로그램(또는 관리자 권한의 셋업 처리) 쪽에서 마쳐 두세요. 작업 스케줄러 기사 6장에서 쓴 「CreateEventSource는 셋업 쪽으로 분리한다」와 같은 이야기입니다.
구분 기준은 이벤트 로그에는 「운영자가 봐야 할 것」만(시작·중지·실패·복구), 처리의 상세 트레이스는 자체 파일 로그입니다. 파일 로그를 직접 둘 때의 최소 요건(로테이션, 쓰기 실패 시의 동작)은 「자체 로거의 최소 요건」에, 프로세스가 통째로 죽었을 때 증거를 남기는 설계는 「크래시 시 로그와 덤프를 남기는 설계」에 정리해 두었습니다. 복구 옵션으로 자동 재시작하는 구성에서는 죽은 순간의 기록이 그 자리에 남아 있는 것이 유일한 단서가 되기 때문입니다.
5.4 등록되었는지 확인한다 ── 명령과 GUI에서 어디를 볼 것인가
등록한 뒤에는 다음 세 곳을 차례로 보며 「의도한 구성으로, 의도한 계정으로 동작하는가」를 확인합니다.
구성 확인(명령). sc.exe qc "KsMonitor"로 등록 내용이 나옵니다. 봐야 할 것은 BINARY_PATH_NAME(게시처 exe를 가리키는가), START_TYPE(자동·지연 자동·수동 중 무엇이 되었는가), SERVICE_START_NAME(실행 계정이 제6장에서 정한 것인가)의 세 줄입니다. 가동 상태는 sc.exe query "KsMonitor"의 STATE, PowerShell이면 Get-Service KsMonitor로 봅니다. 복구 옵션의 현재 값은 sc.exe qfailure "KsMonitor"로 읽어낼 수 있습니다.
구성 확인(GUI). Windows 키 + R →「services.msc」로 「서비스」를 엽니다(「컴퓨터 관리」→「서비스 및 응용 프로그램」→「서비스」여도 같은 화면입니다). 대상 서비스를 오른쪽 클릭 →「속성」에서, 「일반」탭이 시작 유형과 5.1에서 설정한 설명문, 「로그온」탭이 실행 계정, 「복구」탭이 첫 번째·두 번째·그 이후 실패 시의 동작입니다.
이벤트 로그 확인. Windows 키 + R →「eventvwr.msc」로 이벤트 뷰어를 열고, 「Windows 로그」→「시스템」에서 소스 「Service Control Manager」의 이벤트를 봅니다. 시작 완료 보고가 제시간에 되지 않았을 때의 이벤트 7000 / 70117, 서비스가 비정상 종료했을 때의 이벤트 7031(복구 동작을 수반하는 경우)과 7034(복구 동작이 없는 경우)8가 여기에 남습니다. 한편 5.3에서 레벨을 설정한 앱 자신의 로그는 EventLog 프로바이더의 출력처가 기본값으로 「Application」이므로9, 「Windows 로그」→「응용 프로그램」 쪽을 소스 이름(SourceName, 기본값은 애플리케이션 이름2)으로 좁힙니다. 서비스의 생사는 「시스템」, 앱의 내용은 「응용 프로그램」으로 기억해 두면, 장애 때 찾아 헤매지 않아도 됩니다.
6. 실행 계정 ── LocalSystem을 관성에 따라 고르지 않는다
서비스는 지정한 계정의 보안 컨텍스트에서 동작하고, SCM이 시작 시 그 계정으로 로그온해 프로세스에 토큰을 붙입니다.10 작업 스케줄러 기사 제3장에서 쓴 「누구로서 실행되는가」 이야기의 서비스 판입니다. 선택지를 나란히 둡니다.
| 계정 | 권한 | 네트워크상의 신원 | 비밀번호 관리 | 쓰는 곳 |
|---|---|---|---|---|
| LocalSystem | 매우 강함(OS 수준) | 컴퓨터 계정 | 불필요 | 원칙적으로 피함. sc.exe create의 기본값이므로 주의 |
| LocalService | 최소 | 익명(공유에 접근 불가) | 불필요 | 로컬에서 끝나는 처리의 1순위 |
| NetworkService | 최소 | 컴퓨터 계정 | 불필요 | 도메인 안 리소스에 접근하는 가벼운 처리 |
| 가상 계정(NT SERVICE\서비스 이름) | 부여한 만큼만 | 컴퓨터 계정 | 불필요 | 서비스 단위로 ACL을 부여할 수 있음. 로컬 자원에 대한 최소 권한 |
| gMSA | 부여한 만큼만 | gMSA 자신 | DC가 자동 관리 | 도메인에서 공유·DB에 접근하는 서비스의 정석 |
판단 포인트는 세 가지입니다.
- LocalSystem은 「되니까」로 고르지 않습니다. 서비스의 취약점이 그대로 머신 전체의 장악으로 직결됩니다. 관리자 수준 권한이 정말 필요한지는 「관리자 권한이 필요해지는 때는 언제인가」의 정리를 그대로 쓸 수 있습니다. 많은 경우 필요한 것은 「특정 폴더에 쓰기」 정도이고, 그것은 가상 계정(
NT SERVICE\KsMonitor)에 폴더 ACL을 부여하면 충분합니다. 가상 계정은 생성도 비밀번호 관리도 불필요합니다.5 - 네트워크 공유의 함정. LocalService는 네트워크상에서 익명이 되므로
\\server\share접근은 실패합니다. NetworkService·가상 계정·LocalSystem은 컴퓨터 계정(DOMAIN\머신이름$)으로 네트워크에 나가므로5, 공유 쪽에서 컴퓨터 계정에 대해 공유와 NTFS 양쪽 허용이 필요합니다. 「사용자에게는 허용을 줬는데 서비스에서만 읽히지 않는다」의 정체는 대개 이것입니다. 「누구로서 네트워크에 나가는가」를 먼저 정한 다음 공유 쪽 설정을 합니다. - 일반 사용자 계정으로 돌린다면 비밀번호 운영까지 포함해서. 전용 계정을 쓰는 경우에는 「서비스로 로그온」 권한이 필요한 데다가, 비밀번호가 만료되면 로그온에 실패해 서비스를 시작할 수 없게 됩니다.10 「비밀번호 변경으로 작업이 조용히 죽는다」 문제의 서비스 판입니다. 도메인 환경이라면 비밀번호를 도메인 측이 자동 관리하는 gMSA로 이 문제까지 없애는 것이 정답입니다(작업 스케줄러 기사 3.2절과 같은 결론입니다).
7. 안전한 중지 ── ShutdownTimeout과 진행 중 처리
중지 흐름을 짚습니다. services.msc나 sc.exe stop, OS 종료로 SCM이 중지 요청을 내면, .NET의 서비스 수명 주기가 호스트 중지(StopApplication)로 변환하고, stoppingToken이 취소되며, ExecuteAsync가 빠져나오고, 각 서비스의 StopAsync가 호출됩니다. 호스트가 이 일련의 중지 처리를 기다리는 시간이 HostOptions.ShutdownTimeout이며, 기본값은 30초입니다.611 그 안에 끝나지 못하면 뒷정리 완료를 기다리지 않고 중지가 강제됩니다.
진행 중인 1건을 끝까지 기록하는 데 30초로 부족하면, 시간에서 역산해 연장합니다.
builder.Services.Configure<HostOptions>(options =>
{
// 진행 중인 처리를 끝까지 기록하는 데 필요한 최대 시간에서 역산합니다
options.ShutdownTimeout = TimeSpan.FromSeconds(90);
});
그다음, 중지 처리의 설계는 다음 세 가지입니다.
- 중지 요청 뒤에 할 일을 순서로 정해 둡니다. 신규 접수를 그만둔다 → 진행 중인 처리를 완료한다(또는 안전한 끊는 지점에서 중단하고, 재개할 수 있는 형태로 기록한다) → 연결·임시 파일의 뒷정리. 이 순서를 정해 두지 않으면, 토큰을 본 순간에 전부를 내던지거나, 전부를 끝내려다 시간 초과가 나는 양극단이 됩니다.
- 「중지에 응답하지 않는 서비스」를 만들지 않습니다. 토큰을 무시한 긴 루프나 대기가 한 곳만 있어도, SCM상에서 「중지 중」인 채로 굳는 서비스가 됩니다. 이는 Windows Update의 재시작을 가로막고, 서버의 계획 중지마다 수작업 kill을 요구하는, 운영에서 가장 꺼리는 장애입니다. 제4장과 같이, 처리가 끊기는 지점마다 토큰을 확인하고, 긴 I/O에는
CancellationToken을 넘기세요. 콘솔 실행이면 Ctrl+C로 이 중지 흐름을 그대로 시험할 수 있으므로, 「Ctrl+C부터 몇 초에 종료하는가」를 릴리스 전 테스트 항목에 넣어 두는 것을 권합니다. - kill되어도 깨지지 않는 설계를, 중지 처리의 정성보다 우선합니다. 전원 차단이나 강제 종료는 중지 처리를 아무리 다듬어도 일어납니다. 트랜잭션, 원자적 파일 쓰기, 재실행 가능한 처리 단위──「도중에 죽어도 다음 시작 때 정합이 맞는」 구조가 본체이고, 정성 들인 중지 처리는 어디까지나 예의의 문제입니다.
8. 정리
판단은 단순합니다. 정기 처리는 작업 스케줄러, 상시 대기·초 단위 반응·자동 복구가 필요하면 Windows서비스, UI가 본체이면 상주 앱. 서비스로 가기로 했다면, .NET의 Worker Service와 AddWindowsService로 「콘솔로 개발하고, 서비스로 배포하는」 형태가 현재의 표준 경로입니다.
등록 시 설계로 정하는 것은 네 가지──시작 유형(업무 서비스는 지연 시작), 실행 계정(LocalSystem을 피하고, 가상 계정 또는 gMSA), 복구 옵션(sc.exe failure와 Environment.Exit(1)의 세트), 중지(ShutdownTimeout과 진행 중 처리의 뒷정리). 이 네 가지와 stoppingToken을 존중하는 ExecuteAsync 작성법만 잡으면, Windows서비스는 「만드는 것이 특별히 어려운 것」이 아니게 되었습니다. 반대로 이 네 가지를 정하지 않은 채 움직이기 시작한 서비스는, 몇 년 뒤 「멈출 수 없고·죽어도 모르고·권한이 너무 강해 손댈 수 없는」 삼중고로 돌아옵니다. 기존 서비스를 다시 세울 때도 이 네 가지의 점검부터 시작하는 것이 지름길입니다.
관련 기사
- 작업 스케줄러 작업이 실행되지 않거나 0x1로 끝난다 ── 원인 분리와 안전한 운영 설계
- .NET Generic Host란
- 프로세스 간 통신 판단표
- 크래시 시 로그와 덤프를 남기는 설계
관련 상담 영역
合同会社小村ソフト에서는 상주 처리의 설계 리뷰(작업 스케줄러에서의 이전 판단을 포함)나, 「중지에 응답하지 않는다」「어느새 죽어 있다」는 Windows서비스의 조사·재구축 상담을 다룹니다.
참고 링크
-
Microsoft Learn, Interactive Services. Windows Vista 이후 서비스는 사용자와 직접 대화할 수 없다는 점, 서비스는 세션 0에서 실행된다는 점, UI가 필요한 경우 별도 프로세스의 GUI 앱과 IPC로 연계하는 설계가 권장된다는 점에 대해. ↩ ↩2 ↩3
-
Microsoft Learn, Create Windows Service using BackgroundService. Worker Service를 Windows서비스로 만드는 공식 튜토리얼.
AddWindowsService, EventLog 프로바이더의 기본값이 Warning인 점,sc.exe에 의한 등록·복구·삭제, 복구 옵션에는 0이 아닌 종료 코드가 필요하다는 점에 대해. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10 -
Microsoft Learn, .NET 6 breaking change: Exception handling in hosting. .NET 6 이후
BackgroundService.ExecuteAsync의 처리되지 않은 예외가 로그에 기록된 뒤 기본값으로 호스트를 중지시킨다는 점(BackgroundServiceExceptionBehavior.StopHost)에 대해. ↩ ↩2 -
Microsoft Learn, sc.exe create.
start=(auto / demand / delayed-auto 등)나obj=(기본값은 LocalSystem)의 사양과, 옵션 등호 뒤에 공백이 필수라는 점(생략하면 실패한다)에 대해. ↩ ↩2 ↩3 ↩4 -
Microsoft Learn, Service Accounts in Windows Server. 가상 계정(
NT SERVICE\서비스 이름)이 비밀번호 관리 없이 컴퓨터 계정 자격 증명으로 네트워크에 접근한다는 점, gMSA의 비밀번호를 도메인 측이 자동 관리한다는 점에 대해. ↩ ↩2 ↩3 -
Microsoft Learn, .NET Generic Host. 호스트 종료 처리의 흐름과, 중지 시 구성 가능한 종료 타임아웃(기본 30초)으로 기다린다는 점에 대해. ↩ ↩2
-
Microsoft Learn, A service does not start, and events 7000 and 7011 are logged. SCM이
ServicesPipeTimeout으로 지정된 시간만큼 서비스 시작을 기다리고, 초과하면 이벤트 7000 / 7011을 기록한다는 점, 기본 타임아웃 상향 절차에 대해. ↩ ↩2 -
Microsoft Learn(아카이브), Event ID 7031 — Service Stop Operations 및 Event ID 7034 — Service Stop Operations. SCM이 서비스의 예기치 않은 종료와, 복구 동작 뒤에 재시작하지 못했음을 기록한다는 점, 이벤트 7031(심볼 이름
EVENT_SERVICE_CRASH)이 「서비스가 예기치 않게 종료되었습니다. 이것은 N번째입니다. 다음 수정 동작이 M밀리초 후에 실행됩니다」라는 메시지이며, 7034(심볼 이름EVENT_SERVICE_CRASH_NO_ACTION)가 수정 동작 설명을 수반하지 않는다는 점, 둘 다 소스가 Service Control Manager라는 점, 복구 동작은 「서비스」 스냅인의 속성 「복구」 탭에서 변경할 수 있다는 점에 대해. ↩ -
Microsoft Learn, EventLogSettings.LogName Property. EventLog 로거 프로바이더의 출력처 이벤트 로그 이름이, 지정하지 않으면 기본값으로 “Application”이 된다는 점에 대해. ↩
-
Microsoft Learn, Service User Accounts. 서비스가 지정 계정의 보안 컨텍스트에서 실행된다는 점, SCM이 시작 시 로그온을 수행하고 비밀번호가 만료되면 서비스를 시작할 수 없다는 점, LocalService / NetworkService / LocalSystem이라는 특수 계정에 대해. ↩ ↩2
-
Microsoft Learn, HostOptions.ShutdownTimeout Property.
StopAsync의 기본 타임아웃을 제어하는 속성의 정의에 대해. ↩
관련 기사
같은 태그를 공유하는 최신 기사입니다. 더 가까운 주제로 지식을 넓힐 수 있습니다.
업무 앱의 날짜·시각과 타임존 ── DateTime의 함정부터 UTC 저장 원칙, 테스트 설계까지
서버를 옮겼더니 시각이 9시간 어긋난다──날짜·시각 사고의 원인을 DateTime의 Kind와 암묵 변환부터 정리합니다. DateTimeOffset과의 구분, UTC 저장 원칙, TimeZoneInfo와 서머타임, TimeProvider를 이용한...
Windows의 프로세스 간 통신을 어떻게 고를까 ── Named Pipe / TCP / gRPC / 공유 메모리 / COM 판단표
Windows 앱끼리의 연동 수단을 어떻게 고를지 정리합니다. Named Pipe, 로컬 TCP, gRPC, 공유 메모리, 파일 연동, COM의 강점과 함정을 판단표로 정리하고, 정석 구성과 Named Pipe 구현 예까지 설명합니다.
C#에서 SQLite를 업무 앱에 쓰기 ── WAL 모드·배타 제어·손상 대책·EF Core와의 구분
Microsoft.Data.Sqlite로 SQLite를 업무 앱에 넣는 실무 지식을 정리합니다. WAL 모드, SQLITE_BUSY와 쓰기 경로 일원화, 타입 매핑의 함정, VACUUM INTO 백업, EF Core와의 구분까지 설명합니다.
Windows 앱 데이터 저장 위치 고르기 ── SQLite / JSON / 레지스트리 / Access 판단표
Windows 데스크톱 앱의 데이터를 어디에, 무엇으로 저장할지. AppData/ProgramData 구분, SQLite·JSON 파일·레지스트리·Access(.accdb) 각각의 강점과 함정을 판단표와 함께 정리하고, 손상 대책과 비트 수 문제...
Windows 프린터 드라이버 제공 종료 ── 업무 앱의 장표·라벨 인쇄는 어떻게 대비할 것인가
Microsoft는 v3/v4 프린터 드라이버의 제공 종료를 단계적으로 진행하고 있으며, 2026년 7월부터는 IPP 클래스 드라이버가 우선됩니다. Windows protected print mode에서 무엇이 사라지는지, 업무 앱의 장표·라벨 ...
관련 토픽
이 기사와 가까운 토픽 페이지입니다. 기사를 출발점 삼아 관련 서비스와 다른 기사로 이어집니다.
Windows 기술 토픽
Windows 개발, 장애 조사, 기존 자산 활용에 관한 KomuraSoft LLC 기사를 모은 토픽 허브입니다.
Generic Host & 앱 아키텍처
Generic Host, BackgroundService, DI, 구성, 로깅, 앱 수명 설계를 정리한 토픽 페이지입니다.
이 주제와 연결되는 서비스
이 기사는 다음 서비스 페이지로 이어집니다. 가까운 입구부터 확인해 주세요.
Windows 앱 개발
상주 처리, 장비 연동, 운영 로그, 유지 보수 가능한 구조가 필요한 Windows 데스크톱 애플리케이션을 지원합니다.
기술 상담 & 설계 리뷰
설계 방향, 아키텍처 경계, 수명 관리, 기존 Windows 자산 처리 방법을 정리하는 데 도움을 드립니다.
자주 묻는 질문
이 기사 주제에 대해 상담 시 자주 나오는 질문을 모았습니다.
- 작업 스케줄러와 Windows서비스는 어떻게 구분하나요?
- 수 분 이상 간격의 정기 처리이고 처리 사이에 상태를 갖지 않으면 작업 스케줄러로 충분합니다. TCP나 파이프에서 상시 대기, FileSystemWatcher로 하는 폴더 감시, 초 단위 반응, 장애 시 자동 복구가 요구사항에 들어가면 Windows서비스입니다. 「5분마다 작업」으로 잘게 나눠 폴링하기 시작했다면, 상주 프로세스를 열화된 형태로 다시 구현하고 있는 신호입니다. UI가 본체(알림 영역의 조작 등)라면, 로그온 중에만 동작하는 상주 앱이라는 세 번째 선택이 됩니다.
- Windows서비스에서 UI(화면)를 표시할 수 있나요?
- 할 수 없습니다. Windows Vista 이후 서비스는 세션 0이라는 격리된 세션에서 동작하며, 사용자와 직접 대화할 수 없습니다. 서비스 안에서 MessageBox.Show를 호출해도 사용자 화면에는 아무것도 나오지 않고, 아무도 누를 수 없는 OK 버튼을 기다리며 처리가 멈추는 행(hang)의 원인이 됩니다. UI가 필요하면 서비스와 UI 앱을 프로세스로 나누고, named pipe 등의 프로세스 간 통신으로 대화하는 구성이 맞습니다. 오래된 코드를 서비스로 이식할 때는 오류 표시용 메시지 박스가 남아 있지 않은지 반드시 확인하세요.
- .NET에서 Windows서비스를 만들려면 어떻게 하나요?
- Worker Service 템플릿(dotnet new worker)에서 시작해 Microsoft.Extensions.Hosting.WindowsServices 패키지를 추가하고 AddWindowsService를 호출하는 것이 공식 경로입니다. 같은 exe가 Visual Studio의 f5나 dotnet run에서는 일반 콘솔 앱으로 동작하므로 개발·디버그가 훨씬 수월합니다. 등록은 sc.exe create로 하지만, binpath=나 start=의 등호 뒤에 공백이 필수라는 독특한 사양과, obj=를 생략하면 기본값이 LocalSystem(최고 권한)이 된다는 점에 주의가 필요합니다. 서비스 시작 시 현재 디렉터리는 C:\Windows\System32이므로, 설정 파일은 실행 파일 기준으로 해석합니다.
- BackgroundService에서 예외가 나면 서비스는 어떻게 되나요?
- .NET 6 이후 ExecuteAsync에서 빠져나온 처리되지 않은 예외는 기본값(BackgroundServiceExceptionBehavior.StopHost)으로 로그에 기록된 뒤 호스트를 중지시킵니다. 다만 이는 「정상 중지」로 취급되므로 SCM의 복구 옵션(자동 재시작)이 발동하지 않습니다. 복구 옵션으로 재시작하고 싶은 실패는 Environment.Exit(1)처럼 0이 아닌 종료 코드로 프로세스를 명시적으로 비정상 종료해야 합니다. 네트워크 단절처럼 계속 가능한 실패는 루프 안에서 잡아 기록·재시도하고, 복구 불가능한 실패와 구분해 설계합니다.