.NET Generic Host란 - DI·설정·로그의 토대

· 업데이트: · · C#, .NET, Generic Host, Worker, 설계

수정 이력(7건, 최종 수정 2026년 09월 03일)

이 글에 적용한 변경 사항의 기록입니다. 보관해 둔 수정 전 버전은 DOI가 부여된 고정 URL에서 읽을 수 있습니다.

permalink·저자 표기·지식 맵 래퍼·깨진 내부 링크 등 CI가 지적한 표시용 수정을 반영했습니다. 본문의 기술적인 주장은 바꾸지 않았습니다.
일본어 원문의 완역으로 다시 번역했습니다. 기존 한국어판은 원문의 일부만 옮긴 축약본이라 절, 표, Mermaid 그림, 그림 설명, FAQ가 빠져 있었습니다. 일본어 원문에 맞춰 이들을 모두 복원했고, 기술적인 주장은 일본어판과 같습니다. 수정 전 버전 보기 (DOI: 10.5281/zenodo.21635129)
Builder와 Host의 경계, 입구가 여러 개인 과정, 정상 종료의 순서, 짧은 수명 작업과 상주 작업을 나누는 법 등을 그림으로도 따라갈 수 있도록 Mermaid 그림을 16개 추가했습니다(본문 500~750자당 1그림 규약에 맞춘 것입니다). 본문 문장은 바꾸지 않았습니다.
글 맨 앞에 "이 글의 지식 맵" 절을 추가했습니다. 본문에서 다루는 개념과 그 관계를 요약·그림·상세 페이지 링크로 모은 것입니다. 본문의 주장은 바꾸지 않았습니다.
외부 리뷰(1283건)에 대한 대응으로 본문을 업데이트했습니다. 개별 변경 내용은 아래 이력을 참고하면 됩니다.
DI(의존성 주입)를 포함한 용어를 앞에서 정의하고, 최소 예 바로 뒤에 기본 콘솔 로거의 출력과 `Production`이 기본이 되는 이유를 추가했습니다. `appsettings.json`의 함정에 대해서는 Generic Host가 이 파일을 생략 가능한 파일로 읽기 때문에, 실제 증상은 예외가 아니라 "값이 읽히지 않는다"는 점임을 고쳤습니다. Web Host에서 Generic Host로 이어진 과정 절과, 채택 여부 기준표를 추가했습니다.
본문의 관련 글 링크 문구가 링크 대상의 현재 제목과 어긋나 있던 것을 실제 제목에 맞췄습니다. 본문 내용은 바꾸지 않았습니다.
최초 공개
이 글을 인용하기(DOI: 10.5281/zenodo.21635128)

이 글은 Zenodo에 보관되어 있습니다. 항상 최신 버전으로 연결되는 DOI와 지금 보고 있는 버전에 고정된 DOI를 아래에 함께 제시합니다.

Go Komura (2026). 「.NET Generic Host란 - DI·설정·로그의 토대」. 합동회사 코무라소프트. https://doi.org/10.5281/zenodo.21635128 https://comcomponent.com/ko/blog/dotnet-generic-host-what-is/

DOI(최신 버전)
10.5281/zenodo.21635128
DOI(이 버전)
10.5281/zenodo.22217432

.NET에서 콘솔 앱이나 worker를 쓰기 시작하면, 처음에는 Main에 처리를 조금 넣는 것만으로 충분합니다. 다만 조금 커지면 대체로 이런 것들이 늘어납니다.

  • appsettings.json을 읽고 싶다
  • 환경 변수로 덮어쓰고 싶다
  • ILogger로 로그를 내고 싶다
  • 서비스 생성을 new 투성이로 하고 싶지 않다
  • 백그라운드에서 루프를 돌리고 싶다
  • Ctrl+C나 서비스 정지로 깔끔하게 끝내고 싶다

여기서 나오는 것이 Generic Host입니다. 다만 이 이름도 조금 섞이기 쉽습니다.

  • Host.CreateApplicationBuilderHost.CreateDefaultBuilder는 무엇이 다른가
  • IHost는 DI 컨테이너와 같은가
  • BackgroundService와 무엇이 연결되어 있는가
  • ASP.NET Core의 WebApplicationBuilder와 별개인가
  • 콘솔 앱에서도 쓸 가치가 있는가

이 부근이 섞이면 Generic Host가 “Web 앱 전용처럼 보이는 것”으로 보이거나, 반대로 “무엇이든 host에 넣어야 하는 것”으로 보이기도 합니다. 어느 쪽도 조금 거칠습니다.

이 글에서는 주로 .NET 6 이후의 현재 실무 감각을 전제로, 다음 네 가지를 먼저 정리합니다.

  • Generic Host의 정체
  • 무엇을 한꺼번에 맡아 주는가
  • Host.CreateApplicationBuilder / Host.CreateDefaultBuilder / WebApplication.CreateBuilder의 관계
  • 어디서부터 들어가면 무난한가

목차

  1. 먼저 결론(한 마디로)
    • 1.1. 용어를 먼저 정해 둔다
  2. 먼저 보는 정리표
    • 2.1. Generic Host가 담고 있는 것
    • 2.2. builder의 차이
    • 2.3. 입구가 여러 개인 이유
  3. Generic Host의 전체 그림
  4. Generic Host에서 무엇이 좋은가
    • 4.1. 시작 처리를 한곳에 모을 수 있다
    • 4.2. DI / 설정 / 로그가 처음부터 이어진다
    • 4.3. 정상 종료와 상주 실행을 다루기 쉽다
  5. 최소 구성
    • 5.1. 콘솔 앱에서 쓰는 최소 예
    • 5.2. appsettings.json
    • 5.3. BackgroundService를 추가한다
  6. 전형 패턴
    • 6.1. 짧은 수명의 콘솔 도구
    • 6.2. worker / 백그라운드 서비스
    • 6.3. ASP.NET Core 아래에도 있다
  7. 맞는 경우
  8. 맞지 않는 / 과한 경우
  9. 빠지기 쉬운 곳
  10. 정리
  11. 참고 자료

이 글의 지식 맵

.NET Generic Host는 의존성 주입(DI)·설정·로깅·IHostedService나 BackgroundService에 의한 상주 처리·Ctrl+C나 SIGTERM에 대응하는 수명 관리를 하나의 토대로 묶는 구조로, 새로운 비 Web 애플리케이션에서는 Host.CreateApplicationBuilder로 조립하는 것이 자연스럽다고 여겨집니다. 기존 코드용으로는 Host.CreateDefaultBuilder라는 경로도 남아 있고, ASP.NET Core의 WebApplication.CreateBuilder는 예전에 별도로 존재했던 Web Host를 대신해 이 사고방식을 Web용으로 넓힌 입구가 되어 있습니다. BackgroundService에는 기본 scope가 없기 때문에 scoped 서비스를 쓰려면 IServiceScopeFactory로 명시적으로 scope를 만들어야 하며, 대표적으로 빠지기 쉬운 곳이 되고 있습니다.

.NET Generic Host의 지식 맵Generic Host가 DI·설정·로그·IHostedService와 BackgroundService·수명 관리를 하나로 묶고, Host.CreateApplicationBuilder나 WebApplication.CreateBuilder와 어떻게 연결되는지 보여주는 그림이용한다이용한다이용한다이용한다이용한다구현을 담당한다에서 구성할 수 있다에서 구성할 수 있다의 후속이용한다전제로 한다이용한다이용한다전제로 한다전제로 한다이용한다Generic Host의존성 주입(DI).NET의 구성 시스템(IConfiguration)Microsoft.Extensions.Logging(ILogger)IHostedServiceHost의 수명 관리(IHostApplicationLifetime)BackgroundServiceHost.CreateApplicationBuilderHost.CreateDefaultBuilderWebApplication.CreateBuilderWeb Host(IWebHostBuilder)IServiceScopeFactory옵션 패턴Windows 서비스.NET(Core 이후)HostApplicationBuilder

그림의 실선은 항상 성립하는 관계, 점선은 조건이 붙는 관계입니다(성립 조건은 상세 페이지의 관계별 설명에 적혀 있습니다). 관계 전체 목록(총 16건, 근거와 확신도 포함)과 주요 개념의 정의는 지식 맵 상세 페이지에 정리되어 있습니다(일본어). 데이터: JSON-LD / Turtle

1. 먼저 결론(한 마디로)

  • Generic Host는 .NET 앱의 시작과 수명을 한꺼번에 다루는 토대입니다.
  • 그 안에 DI, 설정, 로그, IHostedService / BackgroundService, 앱의 정지 처리가 들어갑니다.
  • 새로운 비 Web 앱에서는 먼저 Host.CreateApplicationBuilder(args)로 들어가는 것이 자연스럽습니다.
  • ASP.NET Core의 WebApplicationBuilder도 별세계가 아니라, 같은 host의 사고방식을 Web용으로 넓힌 창구입니다.
  • 즉 Generic Host는 DI 컨테이너만의 이야기가 아니라, 앱의 조립 지점과 수명 관리를 한데 모으는 구조입니다.

요컨대 앱이 “인수를 읽어 한 번 표시하고 끝”을 조금 넘긴 무렵부터 Generic Host는 꽤 효과가 납니다. 반대로 거기까지 자라지 않은 작은 도구에까지 매번 반드시 가져올 것은 아닙니다.

Generic Host가 효과를 내기 시작하는 범위한 번 표시하고 끝나는 작은 도구에는 매번 가져오지 않고, 그것을 조금 넘긴 무렵부터 Generic Host가 효과를 낸다는 기준을 나타내는 도.매번은 가져오지 않는다꽤 효과가 난다한 번 표시하고 끝나는 도구Generic Host그것을 조금 넘긴 앱

그림 1: “한 번 표시하고 끝”을 조금 넘긴 무렵부터 Generic Host는 효과를 내기 시작한다.

1.1. 용어를 먼저 정해 둔다

이 글은 이어서 비유를 많이 쓰므로, 정확한 표현을 먼저 둡니다.

용어 정확히 말하면 이 글에서 쓰는 비유
DI(의존성 주입 / Dependency Injection) 클래스가 필요한 상대를 스스로 new하지 않고 바깥에서 넘겨받는 방식입니다. 넘길 상대를 한꺼번에 등록해 두는 자리가 DI 컨테이너(IServiceProvider)이고, Generic Host는 이것을 처음부터 갖고 있습니다 배선
Builder(HostApplicationBuilder) host를 조립하기 위한 객체입니다. Services, Configuration, Logging 같은 속성을 갖고, 여기에 등록해 갑니다. Build()를 호출하기 전까지 앱은 움직이지 않습니다 조립대
Host(IHost) Build()의 결과로 얻는 조립이 끝난 앱 본체입니다. DI 컨테이너, 구성, 로그, hosted service를 포함하고 Run() / RunAsync()로 시작한 뒤 정지까지 맡습니다 토대
Hosted service(IHostedService / BackgroundService) host의 시작·정지에 맞춰 움직이는 처리를 담는 곳입니다. host가 시작되면 StartAsync가 호출되고, BackgroundService라면 ExecuteAsync가 돌아갑니다 상주 작업
Lifetime 앱의 시작부터 정지까지의 관리입니다. Ctrl+C, SIGTERM, 서비스 정지 같은 신호를 받아 멈추는 방식을 맞춥니다 수명

가장 섞이기 쉬운 것은 Builder와 Host입니다. Builder는 조립하는 쪽, Host는 조립된 결과이고, Build()가 그 경계입니다. 여기만 잡아 두면, 이후에 나오는 “토대” “상자” “창구” “입구” 같은 표현도 무엇을 가리키는지 헤매지 않고 읽을 수 있습니다.

Builder와 Host의 경계Builder는 조립하는 쪽, IHost는 조립된 결과이며 Build 호출이 그 경계가 됨을 나타내는 도.Build()Builder(조립하는 쪽)IHost(조립된 결과)Run / RunAsync로 시작부터 정지까지

그림 2: Builder는 조립하는 쪽, IHost는 조립된 결과이며, Build()가 그 경계가 된다.

DI가 처음이라면 이렇게 생각하면 빗나가지 않습니다. new의 연쇄를 스스로 쓰는 대신 “이 타입이 필요해지면 이 구현을 넘겨 주세요”라고 시작 시점에 등록해 두고, 받는 쪽은 생성자 인수로 받기만 합니다. 그 등록처가 builder.Services입니다.

2. 먼저 보는 정리표

2.1. Generic Host가 담고 있는 것

처음에 이 상자의 내용을 나눠 두면 꽤 편합니다.

요소 Generic Host가 맡는 것 무엇이 좋은가
DI IServiceCollection에서 서비스를 조립한다 new의 연쇄를 줄이기 쉽다
Configuration appsettings.json, 환경 변수, 명령줄 인수 등을 모은다 환경마다의 차이를 다루기 쉽다
Logging ILogger<T>를 쓰기 위한 기반을 만든다 로그 출력처를 나중에 갈아끼우기 쉽다
Hosted service IHostedService / BackgroundService의 시작과 정지를 다룬다 상주 처리를 앱 본체와 나누기 쉽다
Lifetime IHostApplicationLifetime, IHostEnvironment 등을 통해 시작·정지를 다룬다 Ctrl+C, SIGTERM, 서비스 정지로 끝나는 방식을 맞추기 쉽다

여기서 중요한 것은 Generic Host가 “편리한 DI 래퍼 하나”가 아니라는 점입니다. 실제로는 앱 입구 주변을 한꺼번에 배선하는 상자, 정도로 보는 편이 가장 빗나가지 않습니다.

2.2. builder의 차이

여기도 처음에 한 장으로 보는 편이 빠릅니다.

입구 주된 용도 작성 스타일 우선 선택
Host.CreateApplicationBuilder(args) 콘솔 / worker 등 신규 비 Web 앱 builder.Services / builder.Configuration / builder.Logging에 직접 쓴다 신규라면 이것
Host.CreateDefaultBuilder(args) 기존 코드나 오래된 확장 메서드 중심 구성 ConfigureServices 등을 체인한다 기존 자산이 있으면 이것
WebApplication.CreateBuilder(args) ASP.NET Core Web 앱 / API Generic Host에 Web용 사정을 더한 입구 Web이라면 이것

CreateApplicationBuilderCreateDefaultBuilder는 한쪽이 신기능이고 다른 쪽이 별개라는 이야기가 아닙니다.

둘 다 같은 핵심 기능과 기본 동작을 갖고 있습니다. 다른 점은 주로 작성 방식입니다.

새로운 비 Web 앱이라면 지금은 Host.CreateApplicationBuilder(args)로 들어가는 것이 자연스럽습니다. WebApplication.CreateBuilder(args)는 그 흐름을 Web용으로 넓힌 입구라고 보면 정리하기 쉽습니다.

세 입구의 관계CreateApplicationBuilder와 CreateDefaultBuilder는 같은 핵심 기능과 기본 동작을 갖고 작성 방식만 다를 뿐이며, WebApplication.CreateBuilder는 그 흐름을 Web용으로 넓힌 입구임을 나타내는 도.Web용으로 넓힌 입구CreateApplicationBuilder같은 핵심 기능과 기본 동작CreateDefaultBuilder직접 쓰는 방식체인하는 방식WebApplication.CreateBuilder

그림 3: 두 builder는 같은 핵심 기능과 기본 동작 위에서 작성 방식만 다를 뿐이고, Web용으로는 그것을 넓힌 입구가 있다.

2.3. 입구가 여러 개인 이유

입구가 여러 개인 것은 Web 쪽과 비 Web 쪽이 따로 자란 뒤 합류했다는 경위가 있기 때문입니다.

  • 원래 ASP.NET Core에는 Web 전용 Web Host(IWebHostBuilder)가 있고, 비 Web 앱용 Generic Host(IHostBuilder)는 따로 마련되어 있었습니다.
  • 그 후 ASP.NET Core 쪽이 Generic Host로 모아졌고, Web도 비 Web도 같은 host의 사고방식 위에 실리게 되었습니다.
  • 나아가 콜백을 이어 가는 쓰는 방식(ConfigureServices 등)에 더해, 속성에 직접 쓰는 방식(builder.Services 등)의 입구가 늘었습니다. Host.CreateApplicationBuilderWebApplication.CreateBuilder는 이쪽입니다.

현재 공식 문서에서는 Host.CreateApplicationBuilder 계열(IHostApplicationBuilder)은 신규 프로젝트용이며 현재 템플릿의 기본, Host.CreateDefaultBuilder 계열(IHostBuilder)은 기존 코드와의 호환을 위해 남아 있는 종래 방식으로 정리되어 있습니다. 둘 다 같은 핵심 기능과 기본 동작을 가진다는 설명도 명시되어 있습니다.

.NET Framework나 .NET Core 3.1 시대 코드에서 오면 “왜 쓰는 방식이 두 가지나 있나”라고 느끼지만, 옛 방식과 새 방식이 따로 있는 것이 아니라 합류 과정에서 입구가 늘었다고 보면 납득하기 쉽습니다. 기존 자산에 맞출 사정이 없다면 신규는 Host.CreateApplicationBuilder로 충분합니다.

입구가 여러 개인 경위Web 전용 Web Host와 비 Web용 Generic Host가 따로 있고, ASP.NET Core 쪽이 Generic Host로 모아져 합류한 뒤 속성에 직접 쓰는 입구가 늘었다는 경위를 나타내는 도.Web Host(IWebHostBuilder)ASP.NET Core가 Generic Host로 합류Generic Host(IHostBuilder)속성에 직접 쓰는 입구가 추가CreateApplicationBuilder와 WebApplication.CreateBuilder

그림 4: 따로 자란 Web Host와 Generic Host가 합류하고, 그 과정에서 직접 쓰는 스타일의 입구가 늘었다.

3. Generic Host의 전체 그림

전체 그림을 대략 그리면 이렇습니다.

args / 환경 변수 / appsettings.jsonHost.CreateApplicationBuilder(args)builder.Configurationbuilder.Servicesbuilder.LoggingIHostedService / BackgroundServicebuilder.Build()IHostRun / RunAsync시작·정지·Ctrl+C·SIGTERM

그림 5: builder에 설정·서비스·로그를 등록하고, Build()로 얻은 IHost를 Run/RunAsync로 돌리면 lifetime이 hosted service의 시작·정지까지 이어진다.

보통은 Program.cs에서 builder를 만들고, builder.Services에 서비스를 더하고, builder.Configuration이나 builder.Logging을 필요에 따라 조정한 뒤, 마지막으로 Build()해서 IHost를 얻고 Run() / RunAsync()로 돌립니다.

은근히 큰 점은 Host.CreateApplicationBuilder(args) 시점에 이미 꽤 많은 것이 올라가 있다는 것입니다. 기본으로는 예를 들어 다음이 들어갑니다.

  • 콘텐츠 루트는 현재 디렉터리
  • 호스트 구성은 DOTNET_ 접두사 붙은 환경 변수와 명령줄 인수
  • 앱 구성은 appsettings.json, appsettings.{Environment}.json, Development의 user secrets, 환경 변수, 명령줄 인수
  • 로그는 Console / Debug / EventSource / EventLog(Windows만)
  • Development 환경에서는 scope 검증과 의존 관계 검증

즉 아무 생각 없이 처음부터 배선하는 것이 아니라, 처음부터 “보통 쓰는 분량에는 꽤 충분한 토대”가 놓여 있습니다.

처음부터 올라가 있는 기본값Host.CreateApplicationBuilder 시점에 호스트 구성·앱 구성·기본 로그 등이 올라가 있어, 보통 쓰는 분량에는 충분한 토대가 처음부터 놓여 있음을 나타내는 도.CreateApplicationBuilder(args)호스트 구성(DOTNET_ 계열과 인수)앱 구성(appsettings.json 등)기본 로그(Console 등)보통 쓰는 분량에는 충분한 토대

그림 6: builder를 만든 시점에 구성·로그의 기본값이 올라가 있으며, 처음부터 배선하는 것은 아니다.

4. Generic Host에서 무엇이 좋은가

4.1. 시작 처리를 한곳에 모을 수 있다

Generic Host의 가장 눈에 잘 띄지 않지만 큰 효과는 앱의 입구가 흩어지기 어려워진다는 점입니다.

앱이 조금 커지면 Main 주변에 늘어나는 것이 이 부근입니다.

  • 설정 파일 읽기
  • 환경마다의 갈아끼우기
  • 로거 초기화
  • HttpClient나 repository나 service의 조립
  • 백그라운드 처리의 시작
  • 종료 신호 때의 뒷정리

이것을 host 없이 전부 손으로 이어 가면, 처음에는 가벼워도 나중에 점점 입구가 끈적해집니다.

Generic Host를 쓰면 Program.cs가 “의존 관계를 한꺼번에 조립하는 장소”로 분명해집니다. 이 정리만으로 코드 리뷰하기가 꽤 달라집니다.

시작 처리가 한곳에 모인다host 없이 전부 손으로 이으면 나중에 입구가 끈적해지는 반면, Generic Host를 쓰면 Program.cs가 의존 관계를 한꺼번에 조립하는 장소로 분명해짐을 나타내는 도.host 없이 손으로 잇는다나중에 입구가 끈적해진다Generic Host를 쓴다Program.cs가 조립 장소가 된다코드 리뷰하기 쉽다

그림 7: 직접 연결하면 입구가 점점 끈적해지지만, host로 모으면 Program.cs가 조립 장소로 분명해진다.

4.2. DI / 설정 / 로그가 처음부터 이어진다

Generic Host를 쓰면 DI, 설정, 로그가 처음부터 같은 토대에 실립니다.

예를 들어 클래스 쪽에서는 이런 것을 보통 받을 수 있습니다.

  • ILogger<T>
  • IConfiguration
  • IHostEnvironment
  • IOptions<T>

여기서 효과가 나는 것은 설정을 읽는 방식과 서비스를 만드는 방식이 따로 갈라지기 어렵다는 점입니다.

설정이 하나나 둘이면 IConfiguration["Section:Key"]를 직접 읽기만 해도 동작합니다. 다만 실무에서 설정이 늘어나면 IOptions<T>로 section마다 클래스로 묶는 편이 안전합니다. 기준은 키 문자열이 5개를 넘긴 무렵입니다. 이 규모가 되면 오타가 실행 때까지 알아채지 못하는 실패로 나오기 시작하고, 어느 키가 어디서 읽히는지도 따라가기 어려워집니다.

마찬가지로 로그도 ILoggerFactory를 여기저기에서 손으로 만들기보다, 필요한 클래스에 ILogger<T>를 주입하는 편이 한눈에 보기 좋아집니다.

Generic Host가 편리한 것은 이들을 따로 이야기하지 않고 앱 전체의 토대로 함께 다룰 수 있다는 점입니다.

설정 읽기 방식을 키우는 법설정이 하나나 둘이면 IConfiguration을 직접 읽기만 해도 동작하지만, 키 문자열이 5개를 넘긴 무렵부터 IOptions로 section마다 클래스로 묶는 편이 안전해짐을 나타내는 도.설정이 1~2개IConfiguration을 직접 읽는다키 문자열이 5개 초과IOptions로 클래스로 묶는다오타를 실행 때까지 알아채지 못한다

그림 8: 설정이 적을 때는 직접 읽기로 충분하지만, 키가 5개를 넘긴 무렵부터 IOptions로 묶는 편이 안전해진다.

4.3. 정상 종료와 상주 실행을 다루기 쉽다

Generic Host는 “어떻게 시작할지”뿐 아니라 “어떻게 멈출지”도 맡습니다.

host가 시작되면 등록된 각 IHostedServiceStartAsync가 호출됩니다. worker 서비스에서는 BackgroundService를 포함한 hosted service의 ExecuteAsync가 돌아갑니다.

여기서 말하는 “정상 종료”는 처리를 갑자기 끊는 것이 아니라,

  • 정지 신호를 보낸다
  • 루프나 대기를 빠져나온다
  • 연결과 리소스를 정리한다

라는 순서를 밟고 끝나는 것입니다.

오래 움직이는 앱에서는 여기가 꽤 중요합니다. Ctrl+C, SIGTERM, 서비스 정지 같은 이벤트로 앱 전체의 멈추는 방식을 맞추기 쉬워집니다.

또한 종료를 앱 쪽에서 요청하고 싶을 때는 IHostApplicationLifetime.StopApplication()을 쓸 수 있습니다. “이미 일은 끝났으니 깔끔하게 내려가 달라”는 신호를 host의 맥락에서 낼 수 있습니다.

정상 종료의 순서Ctrl+C나 SIGTERM이나 서비스 정지 이벤트를 받아 정지 신호를 보내고, 루프나 대기를 빠져나오며, 연결과 리소스를 정리하는 순서로 끝남을 나타내는 도.StopApplication()Ctrl+C / SIGTERM / 서비스 정지정지 신호를 보낸다루프나 대기를 빠져나온다연결과 리소스를 정리한다앱 쪽의 종료 요청

그림 9: 정상 종료는 정지 신호·루프 탈출·뒷정리의 순서를 밟고, 앱 쪽에서는 StopApplication()으로 같은 흐름에 신호를 낼 수 있다.

5. 최소 구성

5.1. 콘솔 앱에서 쓰는 최소 예

먼저 중요한 것은 Generic Host를 쓴다고 해서 반드시 BackgroundService를 만들 필요는 없다는 점입니다.

한 번만 실행되는 콘솔 도구에서도 DI, 설정, 로그가 필요하다면 Generic Host는 충분히 쓸 수 있습니다.

보통의 console 프로젝트에 나중에 올릴 때는 우선 Microsoft.Extensions.Hosting을 참조합니다.

dotnet add package Microsoft.Extensions.Hosting

Program.cs의 최소 예는 예를 들어 이렇습니다.

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.Services.AddSingleton<JobRunner>();

using IHost host = builder.Build();

try
{
    JobRunner runner = host.Services.GetRequiredService<JobRunner>();
    await runner.RunAsync();
    return 0;
}
catch (Exception ex)
{
    ILogger logger = host.Services
        .GetRequiredService<ILoggerFactory>()
        .CreateLogger("Program");

    logger.LogError(ex, "Unhandled exception occurred during job execution.");
    return 1;
}

internal sealed class JobRunner(
    ILogger<JobRunner> logger,
    IConfiguration configuration,
    IHostEnvironment hostEnvironment)
{
    public Task RunAsync()
    {
        string message = configuration["Sample:Message"] ?? "(no message)";

        logger.LogInformation("Environment: {EnvironmentName}", hostEnvironment.EnvironmentName);
        logger.LogInformation("Message: {Message}", message);

        return Task.CompletedTask;
    }
}

dotnet run하면 콘솔에는 이렇게 나옵니다(Message의 값은 다음 5.2에서 둘 appsettings.json에서 옵니다).

info: JobRunner[0]
      Environment: Production
info: JobRunner[0]
      Message: hello from Generic Host

info: 오른쪽에 있는 것은 로그 카테고리(여기서는 ILogger<JobRunner>이므로 타입 이름)와 이벤트 ID입니다. 기본 콘솔 로거는 이 “첫째 줄에 카테고리, 둘째 줄에 본문” 형태로 냅니다. EnvironmentProduction인 것은 환경 변수 DOTNET_ENVIRONMENTASPNETCORE_ENVIRONMENT도 설정하지 않았을 때의 기본값이기 때문입니다. 개발 시 바꾸려면 DOTNET_ENVIRONMENT=Development를 설정하고 실행합니다.

오래 상주하지 않는다면 RunAsync()까지 가지 않아도 됩니다. Build()해서 필요한 서비스를 가져와 일을 마치면 그대로 종료합니다. 그래도 Generic Host의 이점은 충분히 쓸 수 있습니다.

여기는 의외로 중요합니다. 짧은 수명의 작업에까지 매번 Worker 템플릿을 가져올 필요는 없습니다.

짧은 수명 작업에서의 쓰는 법오래 상주하지 않는다면 RunAsync까지 가지 않고, Build해서 필요한 서비스를 가져와 일을 마치면 그대로 종료하는 형태여도 Generic Host의 이점은 얻는다는 것을 나타내는 도.Build()한다필요한 서비스를 가져온다일을 한다그대로 종료RunAsync()까지 가지 않아도 된다

그림 10: 짧은 수명 작업이라면 Build()해서 서비스를 가져와 실행하고 끝나기만 해도 host의 이점은 얻는다.

5.2. appsettings.json

위 예라면 설정 파일은 이런 최소 형태로 충분합니다.

{
  "Sample": {
    "Message": "hello from Generic Host"
  }
}

한 가지만, 흔히 빠지는 함정이 있습니다. 콘솔 프로젝트에서는 appsettings.json을 추가하기만 해서는 출력 폴더로 복사되지 않습니다. 프로젝트 속성에서 “출력 디렉터리에 복사”를 “새 내용만 복사”로 하거나, csproj에 다음을 씁니다.

<ItemGroup>
  <Content Include="appsettings.json" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>

잊었을 때의 증상도 알아 두면 빠릅니다. Generic Host는 appsettings.json생략 가능한 파일로 읽으므로, 없어도 예외는 나지 않습니다. 값이 읽히지 않을 뿐입니다. 위 최소 예라면 Message: (no message)로 표시됩니다. “오류는 안 나는데 설정이 안 먹는다”일 때는 먼저 출력 폴더에 appsettings.json이 있는지 확인합니다.

appsettings.json이 없을 때의 증상appsettings.json이 출력 폴더로 복사되어 있지 않아도 생략 가능한 파일로 읽히므로 예외는 나지 않고, 값이 읽히지 않을 뿐이라는 증상의 흐름을 나타내는 도.출력 폴더에 파일이 없다생략 가능한 파일로 읽는다예외는 나지 않는다값이 읽히지 않을 뿐이다먼저 출력 폴더를 확인한다

그림 11: appsettings.json이 없어도 예외는 나지 않고 “값이 읽히지 않는다”뿐이라서, 먼저 출력 폴더를 확인한다.

이 예에서는 직접 configuration["Sample:Message"]를 읽고 있습니다. 값을 하나나 둘만 본다면 이것으로 충분합니다.

다만 실무에서 설정이 늘어나면,

  • section마다 클래스로 나눈다
  • IOptions<T>로 주입한다
  • 시작 시점에 검증한다

는 형태로 모으는 편이 키 문자열을 흩뿌리는 일을 피하기 쉽습니다.

또한 Generic Host의 기본값에서는 appsettings.json뿐 아니라 appsettings.{Environment}.json, 환경 변수, 명령줄 인수도 이어지므로 “개발 때만 갈아끼운다” “프로덕션에서는 환경 변수로 덮어쓴다”가 꽤 자연스럽게 됩니다.

5.3. BackgroundService를 추가한다

오래 움직이는 처리라면 BackgroundService를 쓰는 것이 꽤 자연스럽습니다.

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.Services.AddScoped<PollingJob>();
builder.Services.AddHostedService<PollingWorker>();

using IHost host = builder.Build();
await host.RunAsync();

internal sealed class PollingWorker(
    IServiceScopeFactory scopeFactory,
    ILogger<PollingWorker> logger) : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        using PeriodicTimer timer = new(TimeSpan.FromSeconds(30));

        while (await timer.WaitForNextTickAsync(stoppingToken))
        {
            using IServiceScope scope = scopeFactory.CreateScope();
            PollingJob job = scope.ServiceProvider.GetRequiredService<PollingJob>();

            await job.RunAsync(stoppingToken);
            logger.LogInformation("Polling completed.");
        }
    }
}

internal sealed class PollingJob(ILogger<PollingJob> logger)
{
    public Task RunAsync(CancellationToken cancellationToken)
    {
        logger.LogInformation("Do work here.");
        return Task.CompletedTask;
    }
}

이 예에서 봐 둘 점은 두 가지입니다.

  1. BackgroundService의 본체는 ExecuteAsync
  2. scoped 의존 관계가 필요하면 IServiceScopeFactory로 scope를 만든다

BackgroundService 자체에는 기본 scope가 없습니다. 예를 들어 DbContext처럼 scoped 서비스를 쓰고 싶다면 위처럼 job 쪽을 scope 안에서 가져오는 형태가 안전합니다.

BackgroundService에서 scoped를 쓰는 형태BackgroundService 자체에는 기본 scope가 없으므로 IServiceScopeFactory를 주입하고, ExecuteAsync 안에서 scope를 만든 뒤 그 안에서 job 쪽 서비스를 가져오는 형태가 안전함을 나타내는 도.BackgroundService(기본 scope 없음)IServiceScopeFactory를 주입ExecuteAsync 안에서 scope를 만든다scope 안에서 job을 가져온다

그림 12: 기본 scope가 없는 BackgroundService에서는 IServiceScopeFactory로 scope를 만들고, 그 안에서 job을 가져온다.

참고로 정기 실행 도구 자체의 고르는 법은 다른 주제지만, async 기반으로 쓴다면 PeriodicTimer는 꽤 무난합니다. 이 부근은 관련 글의 타이머 글과도 이어집니다.

6. 전형 패턴

6.1. 짧은 수명의 콘솔 도구

배치, 변환 도구, 유지 관리 명령처럼 한 번만 일하고 끝나는 앱에서도 Generic Host는 보통 쓸 수 있습니다.

맞는 것은 이런 장면입니다.

  • 설정 파일을 읽고 싶다
  • 로그를 내고 싶다
  • HttpClient나 repository를 주입하고 싶다
  • 종료 코드를 반환하고 싶다

이런 앱에서 갑자기 BackgroundServiceRunAsync()를 가져오면 조금 무거운 데 비해 host의 수명 관리를 과하게 쓰게 됩니다.

짧은 수명 작업이라면 앞의 최소 예처럼 JobRunner를 가져와 실행하기만 하면 충분합니다.

짧은 수명 콘솔 도구에서의 고르는 법한 번만 일하고 끝나는 앱에서는 BackgroundService와 RunAsync를 가져오면 수명 관리를 과하게 쓰게 되고, 서비스를 가져와 실행하기만 하면 충분함을 나타내는 도.충분과해지기 쉽다한 번만 일하고 끝나는 앱JobRunner를 가져와 실행BackgroundService와 RunAsync()

그림 13: 한 번뿐인 앱에 BackgroundService를 가져오는 것은 과하고, 서비스를 가져와 실행하기만 하면 충분하다.

6.2. worker / 백그라운드 서비스

상주 worker, 폴링, 큐 소비, 감시, 정기 실행 같은 처리에서는 Generic Host와 BackgroundService의 조합이 꽤 자연스럽습니다.

특히 좋은 것은 이 부근입니다.

  • 시작과 정지의 흐름이 host 쪽에서 맞춰진다
  • 로그, 설정, DI가 처음부터 쓸 수 있다
  • Ctrl+C나 정지 신호로 취소를 전달하기 쉽다
  • 상주 처리의 본체를 Program.cs에서 분리하기 쉽다

나아가 Windows Service나 컨테이너의 맥락과도 잇기 쉽습니다. 상주 앱으로 키운다면 Generic Host는 꽤 자연스러운 토대입니다.

Windows Service로 만드는 장면에서는 현재 디렉터리 전제로 파일을 찾기보다 IHostEnvironment.ContentRootPath를 기점으로 생각하는 편이 실수가 적습니다. host의 맥락에서 “앱의 기준 경로”가 정해지기 때문입니다.

상주 worker의 토대상주 worker나 정기 실행에서는 Generic Host와 BackgroundService의 조합이 자연스럽고, Windows Service나 컨테이너의 맥락과도 잇기 쉬움을 나타내는 도.상주 worker / 정기 실행Generic Host와 BackgroundServiceWindows Service컨테이너 상주ContentRootPath를 기점으로 찾는다

그림 14: 상주 처리는 host와 BackgroundService의 조합이 자연스럽고, Windows Service나 컨테이너로도 키우기 쉽다.

6.3. ASP.NET Core 아래에도 있다

Web 앱 / API에서는 WebApplication.CreateBuilder(args)를 쓰므로 얼핏 보면 Generic Host와 별세계처럼 보일 수 있습니다.

그래도 감각으로는 꽤 이어져 있습니다.

  • builder.Services
  • builder.Configuration
  • builder.Logging

의 작성 스타일이 비슷한 것은 그 때문입니다.

ASP.NET Core에서는 HTTP 서버의 시작도 host의 lifetime 안에 들어 있습니다. 즉 Web 쪽 Program.cs를 읽을 때 “왜 여기서 DI나 설정이나 로그를 만지는가”가 이해하기 쉬워진다는 의미에서도 Generic Host의 이해는 효과가 납니다.

Web도 같은 host 위ASP.NET Core 앱도 WebApplication.CreateBuilder를 통해 같은 host의 사고방식 위에 있고, HTTP 서버의 시작도 host의 lifetime 안에 들어 있음을 나타내는 도.ASP.NET Core 앱WebApplication.CreateBuilder같은 host의 사고방식 위HTTP 서버의 시작도 lifetime 안

그림 15: Web 쪽 builder도 같은 host의 사고방식 위에 있고, HTTP 서버의 시작도 lifetime에 포함된다.

7. 맞는 경우

Generic Host가 잘 들어맞는 경우를 들어 봅니다.

  • 설정, 로그, DI를 쓰는 콘솔 앱
  • queue consumer, poller, watchdog, scheduler 같은 worker
  • Ctrl+C나 SIGTERM으로 뒷정리하고 싶은 장시간 실행 앱
  • 나중에 Windows Service / 컨테이너 상주로 자랄 가능성이 있는 앱
  • ASP.NET Core와 같은 확장 집합 방식에 맞추고 싶은 앱

공통점은 “앱의 입구와 수명 관리를 대충 하고 싶지 않다”는 것입니다.

그렇다고 이것만으로는 선을 긋기 어려우니 기준도 둡니다. 다음 중 2개 이상 해당하면 처음부터 Generic Host에 실어 두는 편이 나중에 편한 경우가 많습니다.

채택 판단의 기준기준 표에 2개 이상 해당하면 처음부터 Generic Host에 싣고, 하나도 해당하지 않으면 불필요하다고 보아도 된다는 판단의 흐름을 나타내는 도.2개 이상하나도 없다기준에 해당하는 수를 센다처음부터 Generic Host에 싣는다Generic Host는 불필요하다고 보아도 된다

그림 16: 기준에 2개 이상 해당하면 처음부터 싣고, 하나도 해당하지 않으면 가져오지 않아도 된다.

기준 구체적인 선
설정의 수 환경마다 바뀌는 설정이 3개 이상 있다(연결처, 임계값, 출력처 등)
로그 파일이나 Event Log에 남겨야 한다. 표준 출력에 쓰고 끝이 아니다
실행의 형태 상주한다. 또는 하루 1회 이상, 정해진 간격으로 움직인다
의존 관계 생성자로 받고 싶은 상대가 3개 이상 있다. 테스트에서 갈아끼우고 싶은 상대가 있다
수명 Ctrl+C나 서비스 정지 때 중간의 뒷정리가 필요하다
향후 Windows Service나 컨테이너에서 돌릴 가능성이 있다

8. 맞지 않는 / 과한 경우

반대로 처음부터 Generic Host를 주역으로 두지 않아도 되는 장면도 있습니다.

  • 인수를 한 번 읽고 한 번 출력하고 끝나는 작은 도구
  • 몇십 분만 쓰는 대충의 검증 코드
  • 라이브러리 프로젝트
  • 설정을 하나만 읽을 뿐이고, DI나 로그나 수명 관리까지는 필요 없는 경우

여기서는 host를 세우기보다 Main에 직접 쓰는 편이 읽는 양도 파일 수도 적게 끝납니다. 기준으로는 7장의 표에 하나도 해당하지 않으면 Generic Host는 불필요하다고 보아도 괜찮습니다.

중요한 것은 Generic Host가 강하다고 해서 모든 executable에 필수는 아니다 는 점입니다.

9. 빠지기 쉬운 곳

마지막으로 Generic Host를 처음 쓸 때 밟기 쉬운 점을 정리합니다.

  • Generic Host를 DI 컨테이너로만 본다
    • 실제로는 시작·정지·설정·로그·hosted service를 포함하는 토대입니다.
  • 신규 앱인데 관성으로 Host.CreateDefaultBuilder부터 시작한다
    • 기존 코드에 맞출 사정이 없다면 우선 Host.CreateApplicationBuilder가 자연스럽습니다.
  • BackgroundService에 scoped 서비스를 직접 넣는다
    • hosted service에는 기본 scope가 없습니다. IServiceScopeFactory로 scope를 만드는 편이 안전합니다.
  • 한 번만 끝나는 worker인데 정지를 host에 알리지 않는다
    • Worker 템플릿으로 “run once”를 한다면 일이 끝난 시점에 IHostApplicationLifetime.StopApplication()을 호출하지 않으면 host는 그대로 계속 돌아갑니다.
  • 정상 종료하고 싶은데 Environment.Exit로 끊는다
    • host를 쓰고 있다면 깔끔히 멈추고 싶은 장면에서는 StopApplication()이 더 맞습니다.
  • Windows Service에서 current directory를 전제로 한다
    • 파일 탐색은 IHostEnvironment.ContentRootPath 기점으로 생각하는 편이 안정적입니다.
  • 짧은 수명의 CLI인데 처음부터 BackgroundService로 감싼다
    • 한 번뿐인 일이라면 보통의 서비스 클래스를 가져와 실행하기만 하면 충분합니다.
  • BackgroundService의 정기 실행에 callback 타이머를 대충 넣는다
    • async 흐름으로 쓴다면 PeriodicTimer가 읽기 쉽고 흐트러지기 어려운 경우가 많습니다.

Generic Host에서는 “짧은 수명 작업인지, 상주 작업인지”를 처음에 나누는 것만으로 꽤 헤매기 어려워집니다.

처음에 나누는 물음짧은 수명 작업인지 상주 작업인지를 처음에 나누고, 짧으면 보통의 서비스 클래스를 가져와 실행하기만 하고, 상주라면 BackgroundService와 host의 lifetime 관리를 쓴다는 정리를 나타내는 도.짧다상주짧은 수명 작업인가, 상주 작업인가서비스를 가져와 실행하기만 한다BackgroundService와 lifetime 관리

그림 17: 짧은 수명인지 상주인지를 처음에 나누기만 해도, host의 도구를 어디까지 쓸지 헤매기 어려워진다.

10. 정리

Generic Host를 한 마디로 말하면 .NET 앱의 입구와 수명 관리를 모으는 토대입니다.

봐 둘 포인트를 되짚습니다.

  1. Generic Host는 DI뿐 아니라 설정, 로그, 정지 처리, hosted service를 포함한다
  2. 새로운 비 Web 앱이라면 먼저 Host.CreateApplicationBuilder(args)가 자연스럽다
  3. 짧은 수명 작업이라면 BackgroundService를 쓰지 않고 build해서 실행하기만 해도 된다
  4. 상주 처리라면 BackgroundService와 host의 lifetime 관리가 꽤 효과가 난다
  5. BackgroundService에는 기본 scope가 없으므로 scoped 서비스는 명시적으로 scope를 만든다
  6. ASP.NET Core의 WebApplicationBuilder도 사고방식으로는 같은 흐름 위에 있다

Generic Host는 무거운 의식을 위한 도구가 아닙니다. 설정, 로그, 의존 관계, 시작, 종료가 조금이라도 늘어났다면 그것들을 벽 안으로 흘려보내지 않고 입구에 모아 두기 위한 도구입니다.

반대로 아직 거기까지 필요 없는 작은 도구라면 가져오지 않아도 됩니다. 이 구분이 되면 Generic Host는 “왠지 넣는 것”이 아니라 쓸 곳이 분명한 실무의 토대가 됩니다.

11. 참고 자료

같은 태그를 공유하는 최신 기사입니다. 더 가까운 주제로 지식을 넓힐 수 있습니다.

이 기사와 가까운 토픽 페이지입니다. 기사를 출발점 삼아 관련 서비스와 다른 기사로 이어집니다.

이 기사는 다음 서비스 페이지로 이어집니다. 가까운 입구부터 확인해 주세요.

자주 묻는 질문

이 기사 주제에 대해 상담 시 자주 나오는 질문을 모았습니다.

Generic Host란 무엇인가요?
.NET 앱의 시작과 수명을 한꺼번에 다루는 토대입니다. 그 안에 DI, 설정(Configuration), 로그, IHostedService / BackgroundService, 앱의 정지 처리가 들어갑니다. 단순한 DI 컨테이너 래퍼가 아니라, 앱의 조립 지점과 수명 관리를 한데 모으는 구조로 보면 빗나가기 어렵습니다. 설정, 로그, 의존 관계, 시작, 종료가 조금이라도 늘어난 앱에서 효과를 냅니다.
Host.CreateApplicationBuilder와 Host.CreateDefaultBuilder는 어느 쪽을 써야 하나요?
새로운 비 Web 앱이라면 Host.CreateApplicationBuilder(args)로 들어가는 것이 자연스럽습니다. 둘 다 같은 핵심 기능과 기본 동작을 가지며, 한쪽이 신기능이고 다른 쪽이 별개인 관계는 아닙니다. 다른 점은 주로 작성 방식입니다. CreateApplicationBuilder는 builder.Services 등에 직접 쓰는 스타일, CreateDefaultBuilder는 ConfigureServices 등을 체인하는 스타일입니다. 기존 코드나 오래된 확장 메서드 중심 구성에 맞춰야 한다면 CreateDefaultBuilder를 고릅니다.
콘솔 앱에서도 Generic Host를 쓸 가치가 있나요?
DI, 설정, 로그가 필요하다면 한 번만 실행되는 콘솔 도구에서도 충분히 쓸 수 있습니다. 반드시 BackgroundService를 만들 필요는 없고, Build()한 뒤 필요한 서비스를 가져와 일을 마치면 그대로 종료하는 형태여도 Generic Host의 이점은 얻습니다. 반대로 인수를 한 번 읽고 한 번 출력하고 끝나는 작은 도구나 대충 쓰는 검증 코드에는 과하므로, 매번 반드시 가져올 것은 아닙니다.
BackgroundService에서 scoped 서비스를 쓰려면 어떻게 하나요?
BackgroundService에는 기본 scope가 없으므로, scoped 서비스를 생성자에 직접 주입하는 것은 안전하지 않습니다. IServiceScopeFactory를 주입하고, ExecuteAsync 안에서 명시적으로 scope를 만든 뒤 그 안에서 작업 쪽 서비스를 가져오는 형태가 안전합니다. DbContext처럼 scoped 서비스를 쓰고 싶을 때는 특히 이 형태를 의식해야 합니다.

저자 프로필

기사 저자의 프로필 페이지입니다.

Go Komura

합동회사 코무라소프트 대표

Windows 소프트웨어 개발, 기술 상담, 장애 조사를 중심으로 재현이 어려운 장애 조사와 기존 자산이 남아 있는 프로젝트에 강점이 있습니다.

블로그 목록으로 돌아가기