System.CommandLine과 Dependency Injection: 완전한 솔루션

발행: (2026년 1월 12일 오전 10:38 GMT+9)
9 min read
원문: Dev.to

Source: Dev.to

위에 제공된 텍스트만으로는 번역할 내용이 없습니다. 번역하고 싶은 전체 텍스트를 제공해 주시면, 요청하신 대로 마크다운 형식과 코드 블록, URL은 그대로 유지하면서 한국어로 번역해 드리겠습니다.

문제

System.CommandLine 프로젝트에 의존성 주입을 추가하려고 할 때 일반적으로 발생하는 상황은 다음과 같습니다:

  • IServiceCollection을 수동으로 연결해야 합니다.
  • 명령 핸들러에서 서비스를 어떻게 해결할지 파악해야 합니다.
  • 취소 토큰을 사용하는 비동기 핸들러는 추가 작업이 필요합니다.
  • 명령 간에 코드를 공유하는 것이(예: 공통 옵션) 반복적이 됩니다.
  • 외부 서비스(데이터베이스, API)와 입력을 사전 검증하는 것이 번거롭습니다.

Albatross.CommandLine이 각각을 어떻게 해결하는지 살펴보겠습니다.

Getting Started: DI Out of the Box

Install the package

dotnet add package Albatross.CommandLine

A complete, working CLI application with full DI support

using Albatross.CommandLine;
using Albatross.CommandLine.Annotations;
using Microsoft.Extensions.DependencyInjection;
using System.CommandLine;

// Entry point
await using var host = new CommandHost("MyApp")
    .RegisterServices(RegisterServices)
    .AddCommands()               // Generated by source generator
    .Parse(args)
    .Build();

return await host.InvokeAsync();

static void RegisterServices(ParseResult result, IServiceCollection services)
{
    services.RegisterCommands(); // Generated by source generator
    services.AddSingleton();
}

그게 전부입니다. CommandHost는 DI 컨테이너 수명 주기를 관리하고, 소스 제너레이터가 모든 연결 코드를 생성합니다.

명령 정의: 특성 + 소스 생성

명령은 간단한 클래스와 특성을 사용하여 정의됩니다:

[Verb("greet", Description = "Greet a user")]
public class GreetParams
{
    [Option(Description = "Name of the person to greet")]
    public required string Name { get; init; }

    [Option("f", Description = "Use formal greeting")]
    public bool Formal { get; init; }
}

public class GreetHandler : BaseHandler
{
    private readonly IMyService _service;

    public GreetHandler(ParseResult result, GreetParams parameters, IMyService service)
        : base(result, parameters)
    {
        _service = service; // Fully injected!
    }

    public override Task InvokeAsync(CancellationToken cancellationToken)
    {
        var greeting = parameters.Formal ? "Good day" : "Hey";
        Writer.WriteLine($"{greeting}, {parameters.Name}!");
        return Task.FromResult(0);
    }
}

소스 생성기는 GreetCommand 클래스를 생성하고, 모든 것을 DI에 등록하며, 옵션 파싱을 연결합니다. 여러분은 비즈니스 로직만 작성하면 됩니다.

Beyond Basics: 재사용 가능한 매개변수

실제 CLI 애플리케이션에서는 여러 명령에 걸쳐 공통 옵션이 나타납니다: 입력 디렉터리, 출력 형식, API 키, 상세 로그 플래그 등. 이러한 옵션을 중복해서 작성하는 대신 재사용 가능한 매개변수 클래스를 만들 수 있습니다.

재사용 가능한 옵션 정의

[DefaultNameAliases("--input", "-i")]
public class InputFileOption : Option
{
    public InputFileOption(string name, params string[] aliases) : base(name, aliases)
    {
        Description = "Input file path";
        this.AddValidator(result =>
        {
            var file = result.GetValueForOption(this);
            if (file != null && !file.Exists)
            {
                result.ErrorMessage = $"File not found: {file.FullName}";
            }
        });
    }
}

재사용 가능한 옵션 사용

[Verb("process")]
public class ProcessParams
{
    [UseOption]
    public required FileInfo Input { get; init; }

    [Option(Description = "Output directory")]
    public DirectoryInfo? Output { get; init; }
}

검증 로직, 설명, 별칭이 캡슐화되어 재사용이 가능합니다. 라이브러리에는 InputDirectoryOption, OutputFileOption, FormatExpressionOption와 같은 일반적인 옵션도 Albatross.CommandLine.Inputs 패키지에 포함되어 제공됩니다.

고급: DI를 활용한 사전 처리

때때로 명령이 실행되기 전에 외부 서비스에 대해 입력을 검증해야 할 때가 있습니다—예를 들어, 데이터베이스에 악기 ID가 존재하는지 확인하는 경우가 그렇습니다. 이때 옵션 핸들러가 빛을 발합니다.

핸들러와 함께 옵션 정의하기

[DefaultNameAliases("--instrument", "-i")]
[OptionHandler]
public class InstrumentOption : Option
{
    public InstrumentOption(string name, params string[] aliases) : base(name, aliases)
    {
        Description = "Instrument Id";
    }
}

핸들러는 전체 DI 지원을 제공합니다

public class VerifyInstrumentId : IAsyncOptionHandler
{
    private readonly IInstrumentService _service;
    private readonly ICommandContext _context;

    public VerifyInstrumentId(IInstrumentService service, ICommandContext context)
    {
        _service = service;
        _context = context;
    }

    public async Task InvokeAsync(InstrumentOption option, ParseResult result, CancellationToken token)
    {
        var id = result.GetValue(option);
        if (id != 0)
        {
            var valid = await _service.IsValidInstrument(id, token);
            if (!valid)
            {
                // Short‑circuit: command handler won't execute
                _context.SetInputActionStatus(/* ... */);
                // You can also set an error message or exit code here
            }
        }
    }
}

핸들러는 명령보다 먼저 실행됩니다

검증에 실패하면 명령이 전혀 실행되지 않습니다. 이렇게 하면 명령 핸들러가 비즈니스 로직에 집중하고 입력 검증은 옵션 핸들러가 담당하게 됩니다.

TL;DR

  • Albatross.CommandLine은 거의 보일러플레이트 없이 DI‑준비된 System.CommandLine 앱을 제공합니다.
  • 소스 제너레이터가 명령 클래스, 옵션 파서 및 DI 등록을 자동으로 생성합니다.
  • 재사용 가능한 옵션 클래스는 코드를 DRY하고 유지 보수하기 쉽게 합니다.
  • 옵션 핸들러를 사용하면 전체 DI 지원과 함께 사전 검증 로직을 실행할 수 있습니다.
new OptionHandlerStatus(option.Name, false, $"Instrument {id} not found"));
            }
        }
    }
}

마법: 입력 변환

가장 강력한 패턴을 소개합니다. 명령 핸들러가 int 형식의 악기 ID를 받는 대신 전체 InstrumentSummary 객체를 받게 하고 싶다면 어떻게 할까요?

// Add a third generic argument: the output type
[DefaultNameAliases("--instrument", "-i")]
[OptionHandler]
public class InstrumentOption : Option {
    public InstrumentOption(string name, params string[] aliases) : base(name, aliases) {
        Description = "Instrument identifier";
    }
}

// The transformer fetches and returns the full object
public class InstrumentTransformer : IAsyncOptionHandler {
    private readonly IInstrumentService _service;

    public InstrumentTransformer(IInstrumentService service) {
        _service = service;
    }

    public async Task InvokeAsync(
        InstrumentOption option, ParseResult result, CancellationToken token) {
        var identifier = result.GetValue(option);
        if (string.IsNullOrEmpty(identifier)) {
            return new OptionHandlerResult();
        }
        var summary = await _service.GetSummary(identifier, token);
        return new OptionHandlerResult(summary);
    }
}

이제 params 클래스가 변환된 타입을 받습니다:

[Verb("price")]
public class GetPriceParams {
    // Property type is InstrumentSummary, not string!
    [UseOption]
    public required InstrumentSummary Instrument { get; init; }
}

public class GetPriceHandler : BaseHandler {
    public override Task InvokeAsync(CancellationToken token) {
        // Direct access to the full object
        Writer.WriteLine($"Price for {parameters.Instrument.Name}: ${parameters.Instrument.Price}");
        return Task.FromResult(0);
    }
}

사용자는 --instrument AAPL 과 같이 입력하지만, 핸들러는 완전히 채워진 InstrumentSummary 객체를 받게 됩니다. 변환 과정은 완전히 투명하게 이루어집니다.

왜 이것이 중요한가

계층책임
Reusable Options검증 규칙, 설명, 별칭
Option Handlers외부 검증, 데이터 가져오기
Command Handlers순수 비즈니스 로직

명령 핸들러는 단순하고 테스트하기 쉬워집니다. 인프라스트럭처가 복잡한 부분을 처리합니다.

요약

.NET으로 CLI 애플리케이션을 만들면서 다음이 필요하다면:

  • 일급 의존성 주입
  • 취소를 지원하는 비동기 핸들러
  • 재사용 가능한 검증된 매개변수
  • 외부 서비스와의 사전 처리
  • 입력 변환

**Albatross.CommandLine**을 확인해 보세요. System.CommandLineMicrosoft.Extensions.Hosting을 기반으로 하여 두 생태계의 전체 기능을 보일러플레이트 없이 활용할 수 있습니다.

링크

Back to Blog

관련 글

더 보기 »

WSL-UI 구축: Mock Mode와 Fake Distros

아키텍처 결정: WSL‑UI의 Mock Mode WSL‑UI와 함께 내린 최초의 아키텍처 결정 중 하나는 완전한 Mock Mode를 구축하는 것이었습니다. 이는 단지 ...