ASP.NET Core용 애플리케이션 마이그레이션: 일반적인 문제를 위한 작은 라이브러리
Source: Dev.to
ASP.NET Core에서 애플리케이션 업데이트 관리하기
You know how it goes. You build an app, you deploy it, everything’s great. Then you need to push an update. Maybe you’re adding a new feature that requires some initial data. Or you need to transform existing records because you changed how something works. Or you just need to create a folder structure on first run.
If you’ve been doing this for a while, you probably have your own hacky solutions:
- a boolean flag in the database that says “did we run the v2 setup?”
- code that checks if a file exists before creating default config
- a bunch of
ifstatements scattered across yourProgram.cs
I’ve written all of these. They work—until they don’t.
The real issue is that we don’t have a proper way to version the application’s state — not the database schema, but the application itself.
EF Core 마이그레이션이 모든 것을 포괄하지는 않는다
EF Core 마이그레이션은 존재하며, 데이터베이스 스키마 변경을 관리하는 데는 훌륭합니다. 새로운 테이블이 필요합니까? 마이그레이션. 새로운 컬럼? 마이그레이션. 인덱스? 마이그레이션.
하지만 EF Core 마이그레이션은 애플리케이션 코드가 실행되기 전에 실행됩니다. 구조를 다룰 뿐, 데이터를 다루지는 않습니다. 다음과 같은 작업은 처리하지 못합니다:
- 스키마 변경 후 초기 데이터 시드
- 기능이 라이브될 때 일회성 알림 전송
- 파일 시스템 구조 생성
- v1에서 v2로 업그레이드할 때 정리 작업 수행
- 저장 방식이 바뀔 때 기존 데이터 변환
원시 SQL을 사용해 이 작업들을 EF 마이그레이션에 억지로 끼워 넣을 수는 있지만, 솔직히 말해 엉망입니다. 의존성 주입이 없고, 서비스에 접근할 수 없으며, 적절한 C# 코드도 없습니다. 그리고 그것을 테스트하는 것도 쉽지 않습니다.
애플리케이션 마이그레이션을 위한 작은 라이브러리
이 작업을 처리하기 위해 라이브러리를 작성했습니다. 아이디어는 간단합니다: 애플리케이션에 데이터베이스 스키마가 이미 가지고 있는 버전 관리 의미론을 그대로 적용합니다.
마이그레이션은 이렇게 보입니다
public class MyMigration : IApplicationMigration
{
public Version Version => new Version(1, 2, 0);
public bool FirstTime { get; set; }
public MyMigration(IMyService service) { … }
public async Task UpAsync(IDictionary cache, CancellationToken ct)
{
// migration logic here
}
public async Task DownAsync(IDictionary cache, CancellationToken ct)
{
// optional rollback (not required)
}
}
몇 가지 주목할 점
- 생성자 주입이 작동합니다 – 마이그레이션은 DI 컨테이너의 완전한 구성원입니다.
- 버전이 명시적입니다 – 날짜 기반 이름이나 매직 문자열이 없습니다.
System.Version만 사용합니다. FirstTime플래그 – 이 버전이 이전에 한 번도 등록된 적이 있는지 알려줍니다.
마지막 항목이 특히 유용합니다. 개발 중에는 현재 버전이 매 시작 시마다 다시 실행되므로 빠르게 반복할 수 있습니다. FirstTime 플래그를 사용하면 실제로 한 번만 수행되어야 하는 작업을 보호할 수 있습니다.
라이프사이클 훅
때때로 더 많은 제어가 필요합니다. 예를 들어 스키마 변경 전에 데이터를 캡처하고 후에 적용해야 하는 복잡한 데이터 변환을 수행하는 경우가 있습니다. 라이브러리는 이를 위한 훅을 제공합니다:
public interface IApplicationMigration
{
Version Version { get; }
bool FirstTime { get; set; }
Task UpAsync(IDictionary cache, CancellationToken ct);
Task DownAsync(IDictionary cache, CancellationToken ct);
Task BeforeAsync(IDictionary cache, CancellationToken ct) => Task.CompletedTask;
Task AfterAsync(IDictionary cache, CancellationToken ct) => Task.CompletedTask;
}
cache 사전은 마이그레이션 간에 공유되므로 UpAsync() 메서드에서 캡처한 데이터를 읽고 필요에 따라 변환할 수 있습니다.
설정하기
설정은 매우 간단합니다:
builder.Services.AddApplicationMigrations()
.AddDbContext(options => …);
DbContext를 구성하면 라이브러리가 애플리케이션 마이그레이션을 실행하기 전에 Database.MigrateAsync()를 자동으로 실행합니다. 따라서 직접 호출할 필요가 없습니다 — EF Core 스키마 변경이 먼저 적용되고, 그 다음에 애플리케이션 마이그레이션이 실행됩니다.
스토리지 부분은 직접 구현해야 합니다 — 어떤 버전이 적용되었는지 추적하려면 어디에 저장하시겠습니까? 대부분은 데이터베이스 테이블을 사용하지만, 파일, Redis 등 설정에 맞는 어떤 것이든 사용할 수 있습니다.
기본 데이터베이스 구현
public class MigrationVersion
{
public int Id { get; set; }
public string Version { get; set; } = default!;
public DateTime AppliedOn { get; set; }
}
다중 서버 배포
앱을 여러 인스턴스(로드 밸런싱, 복제 등)로 실행하고 있다면, 모든 인스턴스가 동시에 마이그레이션을 시도하는 것을 원하지 않을 것입니다. 엔진에는 이를 제어할 수 있는 ShouldRun 속성이 있으며, 이를 재정의하면 됩니다.
일반적인 패턴은 구성 파일을 통해 하나의 인스턴스를 “마스터”로 지정하는 것입니다:
{
"Migrations": {
"IsMaster": true
}
}
그 다음 마이그레이션 실행기에서:
public class MyMigrationRunner : ApplicationMigrationRunner
{
protected override bool ShouldRun => Configuration.GetValue("Migrations:IsMaster");
}
주 인스턴스만 Migrations:IsMaster = true 값을 갖게 됩니다. 나머지 인스턴스는 마이그레이션을 완전히 건너뛰고 정상적으로 시작합니다. 이렇게 하면 분산 락을 사용하지 않아도 경쟁 조건과 중복 실행을 방지할 수 있습니다.
실제 사용 사례
- 데이터 시딩을 포함한 기능 롤아웃 – 새로운 “Categories” 기능을 추가하고 있습니다. EF 마이그레이션이 테이블을 생성하지만 기본 카테고리를 채우고 기존 제품을 할당해야 합니다. 하나의 마이그레이션이 데이터 생성과 할당을 모두 처리합니다.
- 구성 진화 – 앱이 이전에 설정을 한 방식으로 저장했지만 이제는 다르게 저장합니다. 이전 형식을 읽고 새로운 형식으로 쓰는 마이그레이션을 작성합니다.
- 환경 설정 – 새로운 서버에 첫 배포를 진행하시나요? 필요한 디렉터리를 만들고, 기본 구성 파일을 생성하며, 앱이 실행되기 위해 필요한 모든 것을 설정합니다.
- 업그레이드 시 알림 – 주요 버전이 배포될 때 관리자에게 이메일을 보내고 싶나요?
FirstTime으로 보호된 마이그레이션에 넣으세요.
왜 Down() 메서드가 없을까?
Down() 메서드가 롤백을 위해 존재하지 않습니다. 제 경험상 롤백 메서드는 거의 테스트되지 않으며 실제로 필요할 때 종종 깨집니다. 저는 필요한 것을 되돌리는 새로운 순방향 마이그레이션을 작성하는 것이 더 깔끔하다고 생각합니다. 버전 1.1에서 뭔가 문제가 있었나요? 버전 1.2가 이를 해결합니다.
가져오기
패키지는 NuGet에 있습니다:
dotnet add package AreaProg.AspNetCore.Migrations
.NET 6, .NET 8, .NET 9, 그리고 .NET 10을 대상으로 합니다.
코드를 직접 살펴보고 싶다면 GitHub에 소스가 있습니다:
이것은 혁신적인 기능은 아닙니다. 프로젝트마다 반복해서 마주쳤던 빈틈을 메우는 역할을 할 뿐입니다. “첫 배포 시 한 번만 실행”하는 해킹을 해본 적이 있다면, 한 번 시도해 보세요.
질문? 피드백? 다른 분들이 이 문제를 어떻게 해결했는지 듣고 싶습니다.