주말 아침 - 주간 닷넷 #38

개인 일정으로 월요일 오후에 올리는 주말 아침:folded_hands:

한 주 동안 .NET 생태계에서 있었던 주요 이슈와 아티클, 기술 트렌드를 정리해 소개합니다.


:pushpin: .NET ThreadPool과 스레드 고갈

주요 내용

  • async/await는 컴파일러가 생성한 상태 머신으로 동작하며, 진정한 비동기 I/O 대기 중에는 스레드를 점유하지 않고 풀로 반환
  • await 지점의 실제 흐름: ThreadPool 스레드가 실행 → 미완료면 즉시 풀 반환 → OS 커널이 대기 관리 → 완료 시 다른 스레드가 재개
  • ThreadPool의 Hill Climbing 알고리즘은 500ms당 스레드 1개씩만 주입해, 8개에서 100개까지 늘리는 데 약 46초 소요
  • 백그라운드 워커의 동기 호출이 Kestrel 요청 스레드를 고갈시키면 헬스체크 실패 → 인스턴스 제거 → 남은 인스턴스 부하 집중으로 이어짐
  • ThreadPoolMinThreads 설정과 ThreadPool.SetMinThreads로 조정하되, 스레드당 1MB 콜스택(5,000개면 약 5GB)과 컨텍스트 스위칭 비용이 상한으로 작용

:pushpin: 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 중앙 패키지 관리 제시

:pushpin: EF Core 마이그레이션 번들 — 무엇이고 왜 중요한가

https://medium.com/@JohnNtirintis/ef-core-migration-bundles-what-they-are-why-they-matter-635e2dcc8fe6

  • 저자: John Ntirintis
  • 태그: #efcore #migrations #cicd

주요 내용

  • 마이그레이션 번들은 단일 DbContext의 미적용 마이그레이션을 특정 런타임 대상 자립형 실행 파일로 묶은 것으로, 하나의 번들은 하나의 컨텍스트에 대응
  • 런타임 MigrateAsync() 방식의 문제 세 가지: 다중 인스턴스 동시 스키마 변경 경합, 앱 프로세스가 스키마 변경 권한을 보유, 실행 전 SQL 검토 불가
  • -r RID는 DB 서버가 아니라 번들이 실행되는 머신 기준이라, Windows DB를 쓰더라도 GitHub Actions ubuntu-latest에서 돌리면 linux-x64 필요
  • 실행 파일 옆 appsettings.json에는 연결 문자열 키만 있으면 되고 값은 --connection으로 덮어써지며, 파일이 없으면 DbContext 생성 단계에서 실패
  • 이미 최신 상태인 DB에 실행하면 붉은 오류 텍스트가 뜨지만 마지막 줄 "Done."으로 정상 종료되고, 번들은 멱등

:pushpin: C#에서 타임아웃 제대로 다루기

  • 저자: Dave Storey
  • 태그: #httpclient #resilience #polly

주요 내용

  • HTTP 타임아웃이 발생하는 네 가지 경로: 네트워크 계층 실패(DNS, TCP/TLS), 업스트림의 HTTP 408 응답, HttpClient.Timeout 만료, 서비스 고갈
  • 서비스 고갈은 업스트림이 정상 응답해도 로컬 스레드풀 여유가 없어 async 연속 실행이 밀리며 발생해, 원인이 상대 서비스에 있는 것처럼 보임
  • HttpClient 기본 타임아웃 100초를 프로덕션에 그대로 두지 않고 CancellationTokenSource로 호출별 제어권 확보
  • 예외 구분은 .NET 5 전후로 달라져, TaskCanceledException·HttpRequestException·InnerExceptionTimeoutException 여부로 타임아웃과 사용자 취소를 판별
  • Polly의 ResiliencePipeline/TimeoutStrategyOptionsHttpClientFactory 연동도 다루되, 단순 타임아웃 처리에는 외부 의존성을 두지 않는 쪽을 택함

:pushpin: 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를 보관해야 하며 정렬 옵션이 늘수록 필터 코드가 복잡해짐

:pushpin: 특성 하나로 멱등성 — 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 응답으로 한정되며, 클라이언트가 재시도 간 키를 유지해야 동작
  • 브로커 측 중복에는 IInboxStoreMessageId 유니크 인덱스를 가진 인박스 패턴을 병행하며 MessageBus:EnableInbox=true로 활성화

:pushpin: 핸들러가 아니라 파이프라인에 사는 자가 무효화 캐시

https://medium.com/@ivanball76/the-self-invalidating-cache-that-lives-in-the-pipeline-not-your-handlers-e11548062d2f

  • 저자: Ivan Ball-llovera
  • 태그: #caching aspnetcore #architecture

주요 내용

  • 캐시 무효화 코드를 커맨드 핸들러마다 흩뿌리는 대신 데코레이터 파이프라인으로 옮기고, IQueryCacheableICacheInvalidating 두 마커 인터페이스로 옵트인
  • ICacheServiceGetAsync<T>·SetAsync<T>·RemoveAsync·RemoveByPrefixAsync 4개 연산으로 구성되며, 쿼리는 "Catalog:Products:{ProductId}" 키를, 커맨드는 "Catalog:Products" 접두사를 선언
  • 2계층 구성으로 인프로세스는 IMemoryCache에 키 추적용 ConcurrentDictionary를 얹고, 분산은 Redis에서 서버 측 SCAN으로 접두사를 삭제(기본 TTL 30초)
  • 엣지 계층 PublicEndpointOutputCachePolicy는 ASP.NET Core 기본 정책을 재구현하되 Authorization 헤더가 있는 요청을 거부하지 않는 점만 다름
  • 쿼리 키와 커맨드 접두사의 정합성은 프레임워크가 검증하지 않으며, Redis 미연결 시 RemoveByPrefixAsync가 no-op이 되어 staleness가 TTL에만 의존

:pushpin: Roslyn 애널라이저와 증분 소스 제너레이터 — 파이프라인 이해하기

  • 저자: Nick Cosentino
  • 태그: #roslyn #sourcegenerator #analyzer

주요 내용

  • Roslyn 컴파일 파이프라인을 파싱 → 바인딩 → 분석 → 소스 생성 → 에미션으로 나누면, 애널라이저는 분석 단계에만 훅되어 읽기 전용이고 제너레이터는 에미션 직전에 코드를 합성
  • DiagnosticAnalyzer·DiagnosticDescriptor·CodeFixProviderIIncrementalGenerator·SourceProductionContext.AddSource()·IncrementalValuesProvider<T>의 역할을 대응시켜 정리
  • [MustBeAsync] 애널라이저, [GenerateFactory] 제너레이터, 그리고 애널라이저와 제너레이터를 짝지어 DI 등록 코드를 생성하는 예제 수록
  • 증분 캐싱을 유지하려면 파이프라인을 흐르는 객체가 IEquatable<T>를 구현해야 하고, ISymbol/SyntaxNode를 그대로 넘기면 캐싱이 무력화됨
  • 제너레이터에서 ReportDiagnostic으로 진단을 내보내는 대신 전용 애널라이저를 짝짓도록 권고하며, .NET 7 / Roslyn 4.3+에서 폐기된 ISourceGeneratorIIncrementalGenerator 마이그레이션 경로를 다룸

:bookmark_tabs: 가벼운 읽을거리

후보 항목 중 이슈로 선정되지 않은 가벼운 읽을거리들


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), CompareTo 1.36ns(Cysharp ULID 6.78ns)에 할당 0이며, Dapper 전역 타입 핸들러 때문에 앱 수명 동안 단일 저장 형식만 사용 가능

TimeProvider와 테스트 불가능한 DateTime.Now의 종말

  • .NET 8의 TimeProviderSystem 네임스페이스의 추상 기반 클래스로, 팀마다 IDateTimeProvider·IClock·ITimeService를 따로 만들던 파편화를 대체
  • GetUtcNow(), GetTimestamp()/GetElapsedTime(), CreateTimer(), 운영용 싱글턴 TimeProvider.System을 제공
  • Microsoft.Extensions.TimeProvider.TestingFakeTimeProvider로 특정 시점에 고정하거나 Advance(TimeSpan)으로 대기 없이 시계를 전진시켜 보류 중인 타이머까지 발화

확장 멤버로 정적 게터 추가하기 (C# 14)

  • StringComparer를 상속한 NaturalSortComparerOrderBy에 넘길 때 호출부를 자연스럽게 만드는 세 가지 접근 비교
  • 확장 메서드는 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 이펙트 프레임워크

  • IAudioEffectSpan<float> 위에서 동작하는 ProcessReset, 레이턴시 보고를 규정하고, EffectSampleProvider로 기존 ISampleProvider 파이프라인에 연결되며 EffectChain으로 런타임 조합
  • 기반 클래스 AudioEffect가 클릭 노이즈 없는 바이패스와 dry/wet 믹스를 제공하고, IParameterized/EffectParameter로 연속·선택·토글 파라미터와 게인 리덕션 미터를 노출
  • UI 스레드와 오디오 스레드 간 파라미터 변경을 스레드 안전하게 처리하고 ParameterSmoother로 아티팩트를 방지하며, Delay·Reverb·Compression·EQ·Saturation·Gate를 동봉
6개의 좋아요