호스팅 API 친화적인 진입점 만들기 + EF Core 모델이 어떻게 만들어지는지 확인해보기

요즈음 닷넷 코어 기반으로 본격적인 애플리케이션/서비스 개발에 많은 시간을 들이다보니 "최선의 보일러 플레이트"를 찾는데 많은 시간을 투여하고 있습니다.

역사와 전통의 (?) Main 메서드는 여전히 제 기능을 수행하지만, 최근 닷넷 기반 애플리케이션에서 사실상 몰라서는 안될 표준으로 자리잡은 것이 바로 호스팅 API입니다. 그런데 호스팅 API를 그냥 쓰려니 Main 메서드를 쓰던 사상과는 격차가 너무 크게 느껴질때가 많습니다.

그래서 나름대로 찾은 새로운 호스팅 API 친화적인 진입점 만들기 전략으로 아래와 같은 샘플 코드를 사용해보기 시작했습니다.

using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System.Threading.Tasks;
using Microsoft.Data.Sqlite;

// 인메모리 데이터베이스를 계속 유지시키려면 따로 SQLite 연결을 열어둔 상태로 유지해야 합니다.
using var keepAlive = new SqliteConnection("Data Source=:memory:;");
await keepAlive.OpenAsync();

var builder = new HostApplicationBuilder();
builder.Services.AddDbContext<VmManagerDbContext>(options =>
{
	options.UseSqlite(keepAlive);
});
builder.Services.AddHostedService<Application>();

var app = builder.Build();
await app.RunAsync();

public sealed class Application(
	MyDbContext DbContext,
	IHostApplicationLifetime HostAppLifetime
) : BackgroundService
{
	protected override async Task ExecuteAsync(CancellationToken stoppingToken)
	{
		try
		{
			await DbContext.Database.EnsureDeletedAsync(stoppingToken).ConfigureAwait(false);
			await DbContext.Database.EnsureCreatedAsync(stoppingToken).ConfigureAwait(false);
		}
		finally
		{
			HostAppLifetime.StopApplication();
		}
	}
}

위의 코드를 만들기 위해서 사용한 NuGet 패키지는 2개 종류인데, Microsoft.EntityFrameworkCore.SqliteMicrosoft.Extensions.Hosting 패키지입니다. 이외에도 무척 많은 패키지가 필요하지만, 전이적 패키지 (Transitive Package)라는 특성 덕에 연관된 패키지를 일일이 지정하지 않고 대표 패키지만 써줘도 됩니다. (간혹 보안 취약점 때문에 특정 패키지만 버전을 올려줘야하지만 이는 그런 상황이 있을 때에만 선택적으로 챙기면 됩니다.)

위의 코드에서의 포인트는, Main 메서드 대신 BackgroundService를 사용했다는 점이고, BackgroundService에 정의된 ExecuteAsync를 Main 메서드의 대용품으로 볼 수 있다는 점입니다. 물론, BackgroundService는 원하는만큼 더 추가할 수 있고, 이렇게 만들면 여러 엔트리포인트를 병렬로 실행하게 만드는 것도 가능할 것입니다. 그리고 생성자 주입 기능을 사용하도록, 여기서는 Primary Constructor를 사용했지만 원하는 스타일로 생성자 주입 코드를 만들어주시면 좋을 것입니다.

위와 같은 스타일로 애플리케이션 진입점을 설계할 경우 호스팅 API에 좀 더 밀착해서 사용할 수 있어서 관리성이 좋은 코드를 만드는데 도움이 되지 않을까 생각해봤습니다. ㅎㅎ

아울러, 본래 인메모리 DB는 EF Core가 제공하는 InMemory 버전의 프로바이더 패키지가 있지만, 애초에 테스트용이라고 목적을 명시하고 있을뿐 아니라 트랜잭션같은 기능들은 따로 DB 생성 시점에서 옵션까지 지정해줘야 하는 번거로움이 있는데 반해, SQLite에도 인메모리 DB 기능을 자체적으로 제공하고 있어, 위와 같이 설정하고 사용하면 EF Core에서 모델링한 데이터베이스 모델이 실제로 피지컬하게 어떻게 렌더링되는지 "로그"로 확인해볼 수 있어 유용한 접근법이 될 것 같아 소개해봅니다.

SQL 스키마 로그 생성 예시:

포럼 회원 여러분께서는 어떻게 보일러 플레이트를 만들어 사용하시는지 궁금합니다.!

6 Likes

“호스팅 API” 라는 용어를 처음 듣는데, 혹시 Microsoft.Extensions.Hosting 패키지를 가리키는 것인가요?

호스트 기반의 코드 인프라스트럭처를 제공한다는 취지에서 적당한 이름이 생각나는것이 없어 붙인 용어지만, (제네릭) 호스트 친화적이라는 이름이 더 정확한 표현일 수 있겠군요.

그런 의미라면, 닷넷에는 "워커 서비스"라는 프로젝트 템플릿이 있습니다.

VS에서는 "Worker Service"로 표시되고, cli 에서는 아래와 같이 입력하여 보일라 플레이트 코드를 생성합니다.

dotnet new worker

생성된 프로젝트의 Program.cs 는 아래와 같습니다.

// usings

var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddHostedService<Worker>();

var host = builder.Build();
host.Run();

최근에 글을 올리신 것을 보면, 제너릭 호스트의 매력 혹은 서비스 컨테이너의 매력에 빠지신 듯 보입니다. ^^

3 Likes

맞습니다. 제 경우에는 Visual Studio를 켜는 것보다 LINQPad를 사용하는 일이 2:8 정도의 비율로 압도적으로 높다보니, 프로젝트 템플릿은 거의 쓰지 않는 것 같습니다. 그래서 첫 시작에 쓸 보일러플레이트를 면밀하게 들여다보는 일이 많은 것 같습니다. ㅎㅎ

2 Likes

앱 생명주기 제어부분이 추상화되어 너무 편한거 같습니다 ㅎㅎ

2 Likes