데이터 Persistence 관리
Lino의 data persistence는 Clean Architecture boundary를 중심으로 생성됩니다. domain model은 Entity Framework Core와 독립적으로 유지되고, database concern은 Infrastructure.Persistence에 위치합니다. 이 구조는 entity를 business rule에 집중하게 하고, mapping, provider configuration, migrations, repositories, transactions, initialization을 infrastructure layer에 둡니다.
lino service new로 service를 만들면 Lino는 service architecture와 data provider 같은 중요한 persistence 결정을 기록합니다. 현재 provider option은 SqlServer와 PostgreSql입니다. traditional service에서는 persistence가 service 바로 아래에 생성됩니다. modular service에서는 각 module이 자체 Infrastructure.Persistence project와 자체 ApplicationDbContext를 받으며, database는 service에서 공유됩니다.
traditional service와 modular service 모두 database는 service당 하나입니다. modular service에서는 각 module이 같은 database 안의 서로 다른 schema에 mapping됩니다. 이를 통해 각 bounded context는 기능적 isolation, 더 명확한 versioning, 더 정리된 migrations를 갖춘 별도의 logical namespace를 유지할 수 있습니다.
이 페이지는 Lino가 Entity Framework Core configurations, DbContext registration, repositories, IUnitOfWork, transactions, Transactional Outbox flow, 그리고 CLI가 노출하는 migration cycle을 어떻게 구성하는지 설명합니다.
중요: 생성된 persistence code는 production-oriented scaffolding입니다. shared database나 production database에 변경을 적용하기 전에 mapping, constraints, indexes, delete behavior, transactions, migration scripts를 항상 검토하십시오.
Entity Type Configurations
Lino는 Persistence-Ignorant 원칙을 따릅니다. domain entity는 infrastructure 세부 사항을 알지 못합니다. 모든 ORM mapping은 IEntityTypeConfiguration<TEntity>를 구현하는 클래스에 두며, 위치는 Infrastructure.Persistence/Configurations입니다. 따라서 entity는 business behavior를 표현하고, infrastructure는 entity가 어떻게 저장될지 정의합니다.
configuration 파일은 CLI에서 선택한 결정에 따라 생성됩니다: primary keys, strongly typed IDs, 필수 field, string size, Value Objects, relationships, enumerations, audit fields, tenant fields, child entities입니다. 생성된 결과는 강한 시작점으로 다루어야 하며, 검토가 전혀 필요 없는 산출물로 보면 안 됩니다.
Configuration이 보통 정의하는 것
- Table 및 schema: 현재 service 또는 module의 table name과 schema입니다.
- Primary keys: entity identifiers, strongly typed IDs conversion, key generation behavior입니다.
- Properties: required fields, max length, column types, enum persistence, Value Objects의 owned structures, file metadata columns입니다.
- Relationships: one-to-one, one-to-many, many-to-many, child collections, foreign keys, delete behavior입니다.
- Indexes 및 constraints: uniqueness, lookup performance, tenant-aware uniqueness, database level consistency입니다.
Global conventions
반복되는 convention은 ModelConfiguration 또는 동등한 helper로 중앙화할 수 있습니다. 예를 들어 decimal precision, collation, DateTime conversion, naming convention, global filters, auditable properties, multi-tenant field의 공통 rule이 여기에 해당합니다.
Configuration 적용 방식
생성된 ApplicationDbContext는 OnModelCreating에서 configuration을 적용합니다. 기본적으로 Lino는 Source Generators를 사용해 Reflection 없이 더 나은 성능으로 configuration을 등록합니다. assembly scanning을 사용할 때 configuration은 다음과 같이 보일 수 있습니다:
modelBuilder.ApplyConfigurationsFromAssembly(typeof(ApplicationDbContext).Assembly);
messaging이 활성화된 경우 Lino는 OutboxMessage configuration도 적용합니다. 이를 통해 integration events를 worker가 비동기로 publish하기 전에 같은 database transaction에 persist할 수 있습니다.
Review checklist
- table name, schema, convention이 module boundary를 따르는지 확인합니다.
- migration을 생성하기 전에 string size, decimal precision, nullable columns, enum persistence를 검토합니다.
- relationship과 delete behavior를 신중하게 검토합니다. 특히 aggregate roots와 child entities에서 중요합니다.
- grid, filter, integration, background job에서 사용하는 query를 위해 index를 추가하거나 조정합니다.
- migration을 만들기 전에 build를 실행하여 EF Core가 일관된 model을 볼 수 있는지 확인합니다.
DbContexts
생성된 ApplicationDbContext는 service 또는 module의 persistence boundary를 위한 EF Core context입니다. mapping된 entity에 대한 DbSet<TEntity> property를 노출하고 IApplicationDbContext를 구현하므로, query handler와 application service는 concrete infrastructure class가 아니라 application abstraction에 의존할 수 있습니다.
Traditional services 및 modular services
- Traditional service: persistence는
src/Services/<ServiceName>/Infrastructure.Persistence에 생성되며, service API가 migration의 startup project로 사용됩니다. - Modular service: 각 module은 자체
src/Services/<ServiceName>/Modules/<ModuleName>/Infrastructure.Persistenceproject와 자체ApplicationDbContext를 가집니다. service host가 migration의 startup project로 사용됩니다.
modular service에서는 이 분리가 각 bounded context를 code에서 명확하게 드러냅니다. 선택한 architecture에서 같은 service database를 사용하더라도, module은 자신의 persistence model을 독립적으로 발전시킬 수 있습니다.
Provider 등록 및 구성
Lino는 IHostApplicationBuilder extension을 통해 persistence registration을 생성합니다. 생성된 code는 Unit of Work, repositories, domain services, ApplicationDbContext의 pooled factory, scoped ApplicationDbContext, 그리고 IApplicationDbContext abstraction을 등록합니다.
provider-specific configuration은 service에 선택한 database에 따라 생성됩니다:
- SQL Server service에는
UseSqlServer(...)를 사용합니다. - PostgreSQL service에는
UseNpgsql(...).UseSnakeCaseNamingConvention()를 사용합니다. MigrationsHistoryTable(Constants.Database.MigrationsHistoryTable, Constants.Database.Schema)는 migration history를 예상한 table과 schema에 저장합니다.- provider-specific constraint validator는 database violation을 일관된 application error로 변환하도록 등록됩니다.
Tenant-aware contexts
module에 tenant-aware entity가 포함되면 생성된 context는 tenant state와 global query filter를 포함할 수 있습니다. 이 경우 scoped factory는 현재 scope에 맞게 설정된 context를 생성하여, handler가 모든 read/write 지점에서 tenant filter를 수동으로 반복하지 않게 합니다.
Repositories
Lino는 domain layer에 repository interface를 생성하고 Infrastructure.Persistence/Repositories에 concrete implementation을 생성합니다. 이는 dependency direction을 보존합니다. domain은 자신에게 필요한 persistence contract를 정의하고, infrastructure는 Entity Framework Core로 implementation을 제공합니다.
service 구조에 따라 concrete repository는 <ModuleName>/Infrastructure.Persistence.Repositories에 나타나고 <ModuleName>/Domain.Repositories의 interface를 구현할 수도 있습니다. 중요한 점은 application과 domain이 EF Core의 concrete detail에 직접 의존하지 않는다는 것입니다.
repository는 주로 aggregate를 persist해야 하는 command handler와 domain-oriented flow에서 사용됩니다. Query Handler는 read에 최적화된 projection, filter, pagination, DTO 형식 result가 필요할 때 IApplicationDbContext를 직접 사용할 수 있습니다.
Repository responsibilities
- command handler에 필요한 aggregate roots와 child data를 load합니다.
- persistence model에 따라 aggregate를 add, update, remove합니다.
- aggregate behavior 또는 write orchestration의 일부인 persistence query를 encapsulate합니다.
- EF Core-specific operation을 domain project 밖에 유지합니다.
- complex query가 write flow에 속할 때 LINQ,
FromSql, auxiliary projection을 포함해 encapsulate합니다.
Practical guidance
- aggregate consistency를 보존해야 하는 write use case에는 repository를 사용합니다.
- DTO를 만들기 위해 full aggregate를 load하는 대신 read에는 query projection을 사용합니다.
- repository method는 명시적으로 유지합니다. application에 임의의 persistence operation을 노출하는 generic method는 피하십시오.
- handler가 aggregate, child collection, many-to-many relationship을 수정할 때 include와 tracking을 검토합니다.
- domain과 application에 필요한 method만 노출하여 repository를 aggregate-root-centric하게 유지합니다.
Unit of Work
IUnitOfWork는 Lino가 생성하는 transactional boundary입니다. EF Core, transaction control, domain event publishing, integration event를 위한 Outbox persistence를 조정합니다. handler는 application의 모든 지점에서 DbContext.SaveChangesAsync를 직접 호출하는 대신, 이 abstraction을 사용해 변경을 명시적으로 confirm합니다.
단순한 scenario에서는 DbContext 자체가 unit of work로 충분한 경우가 많습니다. 그래도 Lino는 transaction, event, Outbox integration을 일관되게 제어하기 위해 전용 UnitOfWork implementation을 생성합니다.
Generated operations
SaveChangesAsync(cancellationToken): 변경을 저장하고, 설정된 경우 domain event를 publish합니다.SaveChangesAsync(publishDomainEvents, cancellationToken): domain event publish 여부를 선택해 저장할 수 있습니다.SaveChangesInTransactionAsync(cancellationToken): transaction을 열고, 변경을 저장하고, commit합니다.BeginTransactionAsync,CommitAsync,RollbackAsync: 명시적인 transaction control을 제공합니다.CommitOrRollbackAsync:Result에 따라 transaction을 commit하거나 rollback합니다.
Domain events 및 Outbox
domain event가 존재하면 Lino는 event를 publish하기 전에 열린 transaction을 요구합니다. 이는 의도된 동작입니다. domain event가 integration event를 생성하면 해당 event는 Outbox에 기록되고 같은 transaction에 persist됩니다. 이후 worker가 Outbox record를 처리하고 configured messaging infrastructure를 통해 message를 publish할 수 있습니다.
이 flow는 consistency를 보호합니다. command가 event를 발생시키는 entity를 생성하고 project가 Outbox를 사용한다면 handler는 예를 들어 SaveChangesInTransactionAsync로 transaction 안에서 저장하거나 transaction을 명시적으로 열고 commit해야 합니다. 이 transaction이 없으면 infrastructure는 신뢰할 수 없는 event flow를 허용하는 대신 실패해야 합니다.
각 save style을 사용할 때
- event와 Outbox 사이의 consistency 요구가 없는 직접 persistence에는
SaveChangesAsync를 사용합니다. - domain event와 Outbox persistence가 같은 commit의 일부여야 할 때
SaveChangesInTransactionAsync를 사용합니다. - handler가 여러 단계를 가지고
Result에 따라 최종 결과를 결정해야 할 때BeginTransactionAsync,CommitAsync,RollbackAsync를 사용합니다.
Practical rule: write operation이 integration event를 만들 수 있는 domain event를 publish한다면 database change와 Outbox record를 같은 transaction에 유지하십시오.
Managing Migrations
Lino는 service, module, architecture, provider, DbContext, startup project, version file, script output location을 알고 있는 CLI command로 Entity Framework Core migration cycle을 감쌉니다. 이를 통해 수동 command 오류를 줄이고 migration metadata를 Lino project model에 맞게 추적합니다.
migration은 entity, Value Objects, relationships, indexes, Entity Framework configuration 변경에서 발생한 database evolution을 기록합니다. migration을 만들기 전에 solution 또는 최소한 영향받는 service를 compile하여 EF Core가 현재 model을 load할 수 있게 하십시오.
Migration 생성
lino database migrations add --service <ServiceName> --module <ModuleName>
command는 service, module, src/Services/<ServiceName>/version.txt의 현재 service version, migration description을 요청합니다. Lino는 version과 sequence를 사용해 name을 만들고, 올바른 persistence project와 startup project에 대해 dotnet ef migrations add를 실행합니다.
Lino는 dotnet ef migrations script를 사용해 migration SQL script도 생성합니다. script는 영향받은 persistence project 아래의 versioned folder에 다음 pattern으로 기록됩니다:
Infrastructure.Persistence/Scripts/<Version>/<Sequence>_<Description>.sql Infrastructure.Persistence/Scripts/v1.2.3/001_AddCustomerIsActive.sql
참고: Entity Framework가 생성하는 .cs 파일 외에도 Lino는 DDL statement가 포함된 대응 .sql script를 생성합니다. 이는 infrastructure team, DBA, controlled deploy process에서 review하기 쉽게 합니다.
Apply, list, revert, remove
lino database migrations add --service <ServiceName> --module <ModuleName> lino database migrations list --service <ServiceName> --module <ModuleName> lino database migrations apply --service <ServiceName> --module <ModuleName> lino database migrations revert --service <ServiceName> --module <ModuleName> lino database migrations remove --service <ServiceName> --module <ModuleName>
add: model의 pending change에 대한 migration을 생성합니다.list: 선택한 context에 대해 알려진 migration을 나열합니다.apply: configured database에 migration을 적용하도록dotnet ef database update를 실행합니다.revert: flow가 허용하는 경우 마지막으로 성공한 migration path를 되돌립니다.remove: 적용 가능한 경우 마지막으로 생성된 migration record와 생성된 EF file을 제거합니다.
canonical group은 database migrations입니다. db and migration 같은 alias가 productivity를 위해 존재할 수 있지만, documentation은 full form을 우선해야 합니다.
Lino가 대신 선택하는 것
- traditional service에서 EF project는
src/Services/<ServiceName>/Infrastructure.Persistence이고 startup project는src/Services/<ServiceName>/Api입니다. - modular service에서 EF project는
src/Services/<ServiceName>/Modules/<ModuleName>/Infrastructure.Persistence이고 startup project는src/Services/<ServiceName>/Host입니다. - context는 선택한 service 또는 module에 대해 생성된
ApplicationDbContext입니다. - EF migration history table은
Constants.Database.MigrationsHistoryTable및Constants.Database.Schema로 구성됩니다.
Recommended workflow
- entity, Value Objects, relationships, indexes, persistence configuration을 model하거나 변경합니다.
- migration을 생성하기 전에 build를 실행하고 compile error를 수정합니다.
- 올바른 service와 module에 대해
lino database migrations add를 실행합니다. - 적용하기 전에 생성된 C# migration과 SQL script를 검토합니다.
- local 또는 development environment에 migration을 적용하고 database schema를 확인합니다.
- 변경이 필요했던 domain 또는 application change와 함께 migration file 및 SQL script를 commit합니다.
Checklist: publish하기 전에 migration diff를 검토하고, local database에 적용하고, build를 실행하고, 영향받은 flow를 test합니다.
