System.CommandLine과 Dependency Injection: 완전한 솔루션
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.CommandLine과 Microsoft.Extensions.Hosting을 기반으로 하여 두 생태계의 전체 기능을 보일러플레이트 없이 활용할 수 있습니다.