닷넷의 Main 메서드에서 모자란 부분을 보충해본 코드

개인적으로 닷넷의 Main 메서드에서는 빠진 부분이 많아 아쉬울 때가 많았는데요 (대표적으로 TPL의 CancellationToken, 의존성 주입, 콘솔에서 Ctrl + C 키 처리, 놓친 예외 처리 등) 이런 부분을 보완해줄 수 있는 스타트업 코드를 만들어보고 싶어서 GitHub gist에 샘플 코드를 올려보았습니다.

다만 충분히 검증된 코드는 아니어서, 내용을 첨삭해주시면 좋을 것 같습니다 :pray:

사용 예시:

using Microsoft.Extensions.DependencyInjection;

public sealed class Program : IProgram
{
    static void Main() => IProgram.Start<Program>();

    public async Task<int> MainAsync(
        IEnumerable<string> args,
        IServiceCollection services,
        IServiceProvider provider,
        CancellationToken cancellationToken)
    {
        await Console.Out.WriteLineAsync($"Hello, World!");
        return await Task.FromResult(0);
    }
}
좋아요 10

공유 감사합니다.

  1. 코드를 살펴보니 제가 잘 모르는 부분이 있어 질문을 드려도 될까요?
  2. 댓글과 gist 중 어디로 드리면 될까요?
좋아요 3

넵. 얼마든지 환영합니다. 여기다가 문의주시면 좋을 듯 합니다.

좋아요 3

열심히 고민하셨을 것 같은데 재를 뿌리려는 것은 아니고 궁금해서 여쭤 보는건데요 ㅠㅠ
호스팅 패키지를 이용하는 것과 어떤 차이가 있는걸까요?

internal sealed class Program
{
    private static async Task Main(string[] args)
    {
        await Host.CreateDefaultBuilder(args)
            .ConfigureServices((hostContext, services) =>
            {
                services.AddHostedService<ConsoleHostedService>();
            })
            .RunConsoleAsync();
    }
}

internal sealed class ConsoleHostedService : IHostedService
{
    private readonly ILogger _logger;
    private readonly IHostApplicationLifetime _appLifetime;

    public ConsoleHostedService(
        ILogger<ConsoleHostedService> logger,
        IHostApplicationLifetime appLifetime)
    {
        _logger = logger;
        _appLifetime = appLifetime;
    }

    public Task StartAsync(CancellationToken cancellationToken)
    {
        _logger.LogDebug($"Starting with arguments: {string.Join(" ", Environment.GetCommandLineArgs())}");

        _appLifetime.ApplicationStarted.Register(() =>
        {
            Task.Run(async () =>
            {
                try
                {
                    _logger.LogInformation("Hello World!");

                    // Simulate real work is being done
                    await Task.Delay(1000);
                }
                catch (Exception ex)
                {
                    _logger.LogError(ex, "Unhandled exception!");
                }
                finally
                {
                    // Stop the application once the work is done
                    _appLifetime.StopApplication();
                }
            });
        });

        return Task.CompletedTask;
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        return Task.CompletedTask;
    }
}
좋아요 3

거의 비슷하네요! 말씀해주신 코드에 제가 넣고자 했던 기능을 넣는게 좀 더 좋아보인다는 생각이 들었습니다. :+1:

Apartment Mode 설정이나 Unhandled Exception 처리, Ctrl C 나 signal 처리같은 기능을 자주 쓰다보니 저런 코드를 만들어 보게 되었습니다.

좋아요 4

아! 콘솔에서 자주 쓰이는 기능이 미리 준비 되어 있었네요 :+1:

좋아요 2

코드를 분석하면서 궁금했던 사항을 아래 정리해봤습니다.

1. Apartment state configuration 이 무엇인가요?

코드에 DefaultProgramFactory 메서드에서 스레드와 연관이 있는 것으로 보입니다. 그런데 Windows forms 키워드로 구글 검색을 해도 나오지를 않아서 질의드립니다.

2. Async main methodIProgram 인터페이스로 별도 구현하신 건 호출 순서를 통제하기 위해서 일까요?

일반적으로 아래와 같이 정의하는데 구조가 달라 어떤 부분을 고려하셔서 설계하신 건지 궁금합니다. 코드로 추측하기로는 프로그램 종료 시 대응하기 위한 목적이지 않을까 생각하고 있습니다.

public class Program
{
    static async Task MainAsync(string[] args) => return await Task.FromResult(0);
}

Async main method - C# 7.1 draft specifications | Microsoft Docs

3. AppDomain 클래스가 궁금합니다.

AppDomain Class (System) | Microsoft Docs 이름이나 공식 문서를 봐도 어떤 역할이며 왜 필요한지 잘 이해가 되지 않습니다. 혹시 블로그 글이나 잘 설명된 글을 알고 계신다면 공유 부탁드려도 될까요?

4. StartupExtensions.cs 파일에 있는 Unwrap 메서드는 언제 필요한 걸까요?

StartupExtensions.cs 파일에 있는 Unwrap 메서드는 InnerException를 들춰내기 위한 메서드 같은데, 필요한 타이밍이 언제일지 상상이 잘 안됩니다. 저는 직관적으로는 전체 예외 스택이 있는 것이 더 좋지 않을까 생각되는데 내부 예외 스택만 필요한 경우는 언제인가요?

좋아요 5

좋은 질문들입니다!

  1. 과거 닷넷 프레임워크와 COM의 흔적인데요, Windows Forms 애플리케이션은 폼을 실행하는 스레드가 STA Apartment Thread로 설정되어있어야 하는 제약 조건이 있습니다. 관련 내용

  2. 닷넷 6 이후 버전만을 고려하면 MainAsync로 직접 써도 되긴 하는데, 닷넷 프레임워크에서 쓸 것을 염두에 두고 폭넓게 설계하면서 나온 코드입니다. 그리고 Main이 async/await을 안쓰는 케이스도 같이 커버해보고 싶은 것도 있었습니다. ㅎㅎ

  3. AppDomain도 닷넷 프레임워크 시절의 잔재입니다. 프로세스, 스레드와는 다르게 논리적인 구분 영역이라고 볼 수 있고, 코드를 작성하는 과정에서는 닷넷 어셈블리를 별도의 AppDomain에 불러들였다가 언로딩할 때 AppDomain을 파기해서 언로드하는 방법을 많이 이용했었는데, 닷넷 코어에 와서는 이 부분에만 집중하는 새로운 API인 AssemblyLoadContext가 추가되었고, AppDomain 클래스 자체는 호환성 보전 목적으로만 남겨진 것으로 알고 있습니다. (참고 자료 1, 참고 자료 2)

  4. TPL을 사용하다보면 AggregateException을 만날 일이 자주 있는데, 콜 스택에서 Root Cause 이외에 주변 상황까지 모두 담고 있어서 불편할 때가 많습니다. 그래서 개인적으로는 가장 안에 들어있는 Root Cause를 추출할 목적으로 Unwrapping을 자주 하곤 하는데, Unwrapping이 필요한지 아닌지는 상황에 따라 달라질 수 있을 것 같습니다. 이 외에도 WSDL 호출이나 네트워크 RPC 호출에서도 이런식으로 겹겹이 Exception을 래핑하는 경우가 있는 것으로 알고 있습니다.

좋아요 4

상세한 답변 감사드립니다.

좋아요 3