개인 일정으로 월요일 오후에 올리는 주말 아침![]()
한 주 동안 .NET 생태계에서 있었던 주요 이슈와 아티클, 기술 트렌드를 정리해 소개합니다.
.NET ThreadPool과 스레드 고갈
- 저자: Dave Storey
- 태그: threadpool async #performance
주요 내용
async/await는 컴파일러가 생성한 상태 머신으로 동작하며, 진정한 비동기 I/O 대기 중에는 스레드를 점유하지 않고 풀로 반환- await 지점의 실제 흐름: ThreadPool 스레드가 실행 → 미완료면 즉시 풀 반환 → OS 커널이 대기 관리 → 완료 시 다른 스레드가 재개
- ThreadPool의 Hill Climbing 알고리즘은 500ms당 스레드 1개씩만 주입해, 8개에서 100개까지 늘리는 데 약 46초 소요
- 백그라운드 워커의 동기 호출이 Kestrel 요청 스레드를 고갈시키면 헬스체크 실패 → 인스턴스 제거 → 남은 인스턴스 부하 집중으로 이어짐
ThreadPoolMinThreads설정과ThreadPool.SetMinThreads로 조정하되, 스레드당 1MB 콜스택(5,000개면 약 5GB)과 컨텍스트 스위칭 비용이 상한으로 작용
2년간 거짓말한 마이그레이션
- 저자: Roger Keizer
- 태그: #efcore #migrations dotnet10
주요 내용
- .NET 8 → 10 업그레이드 중 EF Core가
EventId를 Users에서 제거하고 UserSignInLogs에 추가하는 마이그레이션을 생성했으나,INFORMATION_SCHEMA.COLUMNS조회 결과 실제 DB는 정반대 상태 - 원인은 2년 전 손상된 ModelSnapshot으로, 프로젝트는 .NET 10인데
dotnet-ef글로벌 CLI가 6.0.12에 머물러 구버전 CLI가 EF 8 모델을 직렬화하며 스냅샷을 손상시킴 - 문제가 드러난 계기는 EF 9에서
PendingModelChangesWarning이 경고에서 오류로 승격된 것이며, 이 경고를 억제하면 실제 드리프트가 가려짐 - EF의 비교 로직은 모델과 스냅샷만 대조할 뿐 실제 데이터베이스와는 대조하지 않으므로, Schema Compare와
SqlPackage /Action:DeployReport로 별도 검증 - 재발 방지책으로 no-op 마이그레이션,
.config/dotnet-tools.json로컬 도구 매니페스트,Directory.Packages.props중앙 패키지 관리 제시
EF Core 마이그레이션 번들 — 무엇이고 왜 중요한가
- 저자: John Ntirintis
- 태그: #efcore #migrations #cicd
주요 내용
- 마이그레이션 번들은 단일
DbContext의 미적용 마이그레이션을 특정 런타임 대상 자립형 실행 파일로 묶은 것으로, 하나의 번들은 하나의 컨텍스트에 대응 - 런타임
MigrateAsync()방식의 문제 세 가지: 다중 인스턴스 동시 스키마 변경 경합, 앱 프로세스가 스키마 변경 권한을 보유, 실행 전 SQL 검토 불가 -rRID는 DB 서버가 아니라 번들이 실행되는 머신 기준이라, Windows DB를 쓰더라도 GitHub Actionsubuntu-latest에서 돌리면linux-x64필요- 실행 파일 옆
appsettings.json에는 연결 문자열 키만 있으면 되고 값은--connection으로 덮어써지며, 파일이 없으면 DbContext 생성 단계에서 실패 - 이미 최신 상태인 DB에 실행하면 붉은 오류 텍스트가 뜨지만 마지막 줄 "Done."으로 정상 종료되고, 번들은 멱등
C#에서 타임아웃 제대로 다루기
- 저자: Dave Storey
- 태그: #httpclient #resilience #polly
주요 내용
- HTTP 타임아웃이 발생하는 네 가지 경로: 네트워크 계층 실패(DNS, TCP/TLS), 업스트림의 HTTP 408 응답,
HttpClient.Timeout만료, 서비스 고갈 - 서비스 고갈은 업스트림이 정상 응답해도 로컬 스레드풀 여유가 없어 async 연속 실행이 밀리며 발생해, 원인이 상대 서비스에 있는 것처럼 보임
HttpClient기본 타임아웃 100초를 프로덕션에 그대로 두지 않고CancellationTokenSource로 호출별 제어권 확보- 예외 구분은 .NET 5 전후로 달라져,
TaskCanceledException·HttpRequestException·InnerException의TimeoutException여부로 타임아웃과 사용자 취소를 판별 - Polly의
ResiliencePipeline/TimeoutStrategyOptions와HttpClientFactory연동도 다루되, 단순 타임아웃 처리에는 외부 의존성을 두지 않는 쪽을 택함
EF Core 페이지네이션이 느리다면 키셋 페이지네이션
- 저자: Dave Grace
- 태그: #efcore #performance #database
주요 내용
- 오프셋 페이지네이션(
Skip/Take)은 DB가 건너뛸 행을 모두 읽고 버리는 구조라 페이지 번호가 커질수록 비용이 선형 증가 - 키셋 페이지네이션은 마지막 레코드의 값과 Id를
WHERE조건으로 넘겨 인덱스 시크로 진입하며, URL 파라미터를lastId·sortBy·sortDirection·pageSize로 구성 ApplyKeysetFilter()가 switch 식으로 정렬 기준별 비교 조건을 만들고,ApplyOrder()가OrderBy+ThenBy(Id)로 동점을 처리{Name, Id},{Price, Id}복합 인덱스를 걸어야 시크가 성립하며, 300만 건·페이지 크기 500·100번째 페이지 기준 키셋 약 8ms, 오프셋 약 57ms- 대가로 페이지 번호 직접 이동과 역방향 이동이 불가하고, 각 페이지의
lastId를 보관해야 하며 정렬 옵션이 늘수록 필터 코드가 복잡해짐
특성 하나로 멱등성 — HTTP API 안전 재시도
https://medium.com/@ivanball76/idempotency-in-one-attribute-safe-retries-for-http-apis-065848fd03f4
- 저자: Ivan Ball-llovera
- 태그: aspnetcore #idempotency #distributedsystems
주요 내용
- 클라이언트 재시도로 POST가 중복 실행되어 주문·결제가 두 번 생성되는 문제를, 액션에
[Idempotent]특성 하나를 붙여 처리 - 클라이언트가
Idempotency-Key헤더를 보내면 최초 실행 시 상태코드와 JSON 본문을 캐시하고, 동일 키 재요청은 액션을 건너뛰고X-Idempotent-Replay: true헤더와 함께 캐시 응답을 재생 - 동시성은 256개
SemaphoreSlim스트라이프 락과 double-check locking으로 처리하며, 캐시 히트는 락 없이 통과 - 캐시 보존 창은 기본 24시간·최대 7일이고 캐시 대상은
ObjectResult응답으로 한정되며, 클라이언트가 재시도 간 키를 유지해야 동작 - 브로커 측 중복에는
IInboxStore와MessageId유니크 인덱스를 가진 인박스 패턴을 병행하며MessageBus:EnableInbox=true로 활성화
핸들러가 아니라 파이프라인에 사는 자가 무효화 캐시
- 저자: Ivan Ball-llovera
- 태그: #caching aspnetcore #architecture
주요 내용
- 캐시 무효화 코드를 커맨드 핸들러마다 흩뿌리는 대신 데코레이터 파이프라인으로 옮기고,
IQueryCacheable과ICacheInvalidating두 마커 인터페이스로 옵트인 ICacheService는GetAsync<T>·SetAsync<T>·RemoveAsync·RemoveByPrefixAsync4개 연산으로 구성되며, 쿼리는"Catalog:Products:{ProductId}"키를, 커맨드는"Catalog:Products"접두사를 선언- 2계층 구성으로 인프로세스는
IMemoryCache에 키 추적용ConcurrentDictionary를 얹고, 분산은 Redis에서 서버 측SCAN으로 접두사를 삭제(기본 TTL 30초) - 엣지 계층
PublicEndpointOutputCachePolicy는 ASP.NET Core 기본 정책을 재구현하되Authorization헤더가 있는 요청을 거부하지 않는 점만 다름 - 쿼리 키와 커맨드 접두사의 정합성은 프레임워크가 검증하지 않으며, Redis 미연결 시
RemoveByPrefixAsync가 no-op이 되어 staleness가 TTL에만 의존
Roslyn 애널라이저와 증분 소스 제너레이터 — 파이프라인 이해하기
- 저자: Nick Cosentino
- 태그: #roslyn #sourcegenerator #analyzer
주요 내용
- Roslyn 컴파일 파이프라인을 파싱 → 바인딩 → 분석 → 소스 생성 → 에미션으로 나누면, 애널라이저는 분석 단계에만 훅되어 읽기 전용이고 제너레이터는 에미션 직전에 코드를 합성
DiagnosticAnalyzer·DiagnosticDescriptor·CodeFixProvider와IIncrementalGenerator·SourceProductionContext.AddSource()·IncrementalValuesProvider<T>의 역할을 대응시켜 정리[MustBeAsync]애널라이저,[GenerateFactory]제너레이터, 그리고 애널라이저와 제너레이터를 짝지어 DI 등록 코드를 생성하는 예제 수록- 증분 캐싱을 유지하려면 파이프라인을 흐르는 객체가
IEquatable<T>를 구현해야 하고,ISymbol/SyntaxNode를 그대로 넘기면 캐싱이 무력화됨 - 제너레이터에서
ReportDiagnostic으로 진단을 내보내는 대신 전용 애널라이저를 짝짓도록 권고하며, .NET 7 / Roslyn 4.3+에서 폐기된ISourceGenerator→IIncrementalGenerator마이그레이션 경로를 다룸
가벼운 읽을거리
후보 항목 중 이슈로 선정되지 않은 가벼운 읽을거리들
ASP.NET Core의 Problem Details와 RFC 9457
- RFC 9457의
application/problem+json미디어 타입과type/title/status/detail/instance필드, 오류 코드와 트레이스 ID를 담는 extension 멤버 구조 409 Conflict하나가 결제 완료된 주문·중복 사용자명·낡은 동시성 토큰을 모두 가리킬 수 있어 서비스마다 독자 오류 포맷이 난립하는 배경AddProblemDetails(),UseExceptionHandler(),UseStatusCodePages(),TypedResults.Problem()조합과, 클라이언트가detail문자열 대신type에 패턴 매칭하는 구성
ByteAether.Ulid v1.4.0 — .NET 데이터 계층의 ULID 통합
- SQL Server
uniqueidentifier는 바이트 10~15를 먼저 비교해 정렬하므로 바이트를 재배치하지 않으면 시간순 정렬이 깨지고, 네이티브 GUID는 PostgreSQL에서는 정상이나 SQLite에서 정렬이 깨짐 - v1.4.0은 EF Core·linq2db·Dapper용 동반 패키지 3종을 추가해
configurationBuilder.RegisterUlid(UlidStorageFormat.Binary)형태의 전역 등록 제공 - 생성 40.57ns(NetUlid 162.20ns),
CompareTo1.36ns(Cysharp ULID 6.78ns)에 할당 0이며, Dapper 전역 타입 핸들러 때문에 앱 수명 동안 단일 저장 형식만 사용 가능
TimeProvider와 테스트 불가능한 DateTime.Now의 종말
- .NET 8의
TimeProvider는System네임스페이스의 추상 기반 클래스로, 팀마다IDateTimeProvider·IClock·ITimeService를 따로 만들던 파편화를 대체 GetUtcNow(),GetTimestamp()/GetElapsedTime(),CreateTimer(), 운영용 싱글턴TimeProvider.System을 제공Microsoft.Extensions.TimeProvider.Testing의FakeTimeProvider로 특정 시점에 고정하거나Advance(TimeSpan)으로 대기 없이 시계를 전진시켜 보류 중인 타이머까지 발화
확장 멤버로 정적 게터 추가하기 (C# 14)
StringComparer를 상속한NaturalSortComparer를OrderBy에 넘길 때 호출부를 자연스럽게 만드는 세 가지 접근 비교- 확장 메서드는
StringComparer.Ordinal.NaturalSort()처럼 어색해지고, 별도 유틸리티 클래스는NaturalSort.Instance같은 중간 헬퍼를 강요 - C# 14 확장 블록으로
extension(StringComparer) { public static StringComparer NaturalSort => new NaturalSortComparer(); }를 선언하면 기존StringComparer.OrdinalIgnoreCase와 같은 형태의 API 구성
텍스트 파일로 DB 만들기 — async만으로는 부족한 이유
NaiveFileStorage가 ReadAllText → 역직렬화 → 리스트 수정 → WriteAllText 순으로 동작할 때, 20개 병렬 쓰기가 모두 같은 빈 리스트를 읽고 되쓰며 20건 중 19건 실패- async는 대기를 논블로킹으로 만들 뿐 공유 상태를 안전하게 만들지 않으며, read-modify-write 시퀀스 자체가 경쟁 조건
- 후속 편에서
SemaphoreSlim동기화,Channel<T>,System.IO.Pipelines를 다룰 예정
연민으로서의 하위 호환성
- 하위 호환성의 수혜 대상을 미션 크리티컬 LOB 운영 기업, 다수 고객사 앱을 유지보수하는 소규모 업체와 프리랜서, 감사 추적이 필요한 규제 산업으로 구분
- 초기 버전 코드를 여전히 실행하는 .NET Framework 4.8, 조용한 삭제 대신 사전 경고를 주는
[Obsolete], .NET Core 전환 시 제공된 호환성 팩과 LTS를 사례로 제시 - 반대편 비용으로 낡은 아키텍처 패턴의 수명 연장, 멀티 타게팅으로 인한 라이브러리 유지보수 복잡도, 더 일찍 은퇴시켰어야 할 기술의 존속을 함께 다룸
WinDev Helper VS Code 확장 v4.0.0 — WinUI 디자이너 개편
- VS Code에서 WinUI 3 / Windows App SDK 개발을 지원하는 확장의 4.0.0 릴리스로, XAML 디자이너를 전면 개편
- 디자이너는 WinUI 컨트롤을 HTML/CSS로 근사한 편집 surface를 Windows·macOS·Linux에서 제공하고, 툴박스 드래그 앤 드롭과 소스 양방향 동기화를 지원하며 Windows에서는 번들 렌더러로 네이티브 미리보기 가능
- F5 디버깅, MSIX 패키징·서명·스토어 게시, UI 트리 검사와 MP4 녹화, 프로젝트 템플릿과 항목 스캐폴딩 포함
NAudio 3 이펙트 프레임워크
IAudioEffect는Span<float>위에서 동작하는Process와Reset, 레이턴시 보고를 규정하고,EffectSampleProvider로 기존ISampleProvider파이프라인에 연결되며EffectChain으로 런타임 조합- 기반 클래스
AudioEffect가 클릭 노이즈 없는 바이패스와 dry/wet 믹스를 제공하고,IParameterized/EffectParameter로 연속·선택·토글 파라미터와 게인 리덕션 미터를 노출 - UI 스레드와 오디오 스레드 간 파라미터 변경을 스레드 안전하게 처리하고
ParameterSmoother로 아티팩트를 방지하며, Delay·Reverb·Compression·EQ·Saturation·Gate를 동봉
