주말 아침 - 주간 닷넷 #39

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


:pushpin: MSIX 신뢰 사슬과 패키지 무결성

  • 저자: Howard Kapustein
  • 태그: msix windows #security

주요 내용

  • 실행 프로세스 → AppxManifest.xml <Identity> → AppxBlockMap.xml(파일 해시) → AppxSignature.p7x → 개발자 인증서로 이어지는 신뢰 사슬 역추적
  • 패키지 프로세스의 아이덴티티는 토큰의 WIN://SYSAPPID 속성에 있고 읽기만 가능하며 쓰기는 커널만 수행 — 런타임 변조 차단의 근거
  • 설치 시 배포 엔진이 인증서 신뢰, 서명 이후 블록맵 무결성, 매니페스트 해시 일치를 검증하고 실패 시 배포 거부
  • PICE는 Store 서명이 Windows 8.1부터, 매니페스트 opt-in(<uap10:PackageIntegrity>)이 Windows 10 2004부터. pkgdir ACL 제한과 Process Trust Label ACE는 icacls로 확인 가능
  • 보안 모델이 커널 무결성을 전제하며, 커널이 침해되면 모든 보호가 무력화된다는 한계를 명시. 이 런타임 신뢰 모델은 USPTO 특허 US-8990561-B2로 등록

:pushpin: C# Channels와 백프레셔

https://medium.com/@shanto462/mastering-channels-in-c-async-producer-consumer-done-right-with-backpressure-617b3ec552fa

  • 저자: Shanto462
  • 태그: csharp #channels #concurrency

주요 내용

  • ConcurrentQueue<T>에 없는 4가지(async 대기, 백프레셔, 완료 신호, 예외 전파)를 System.Threading.Channels가 채우는 구조
  • BoundedChannelOptionsFullMode 4종별 적용 사례와 Drop 계열의 데이터 손실, SingleReader/SingleWriter는 런타임이 강제하지 않는 계약이라는 점, AllowSynchronousContinuations의 재진입 위험
  • 소비 관용구 2종 — WaitToReadAsync + TryRead 이중 루프(배칭 유리)와 await foreach (ReadAllAsync)
  • Complete()는 일회성이라 중복 호출 시 예외가 나므로 TryComplete() 사용, ReadAllAsync는 원본 예외를 재던지지만 ReadAsyncChannelClosedException으로 감쌈
  • 함정 7가지와, raw 이벤트 채널 → 변환 워커 N개 → 처리 채널 → 싱크로 이어지는 2단 인제스천 파이프라인 예제 약 150줄

:pushpin: C# 비동기 스트림과 IAsyncEnumerable

https://medium.com/@shanto462/mastering-async-streams-in-c-iasyncenumerable-guide-a5d2befcbecc

  • 저자: Shanto462
  • 태그: csharp #iasyncenumerable async

주요 내용

  • Task<IEnumerable<T>>(버퍼드)와 IEnumerable<T>(동기 스트리밍)의 양자택일을 짚고, IAsyncEnumerable<T>가 첫 아이템 지연을 전체 파이프라인에서 첫 페이지로 줄이는 구조
  • await foreach 디슈가링과, [EnumeratorCancellation]을 빠뜨리면 .WithCancellation()으로 넘긴 토큰이 조용히 무시되는 함정
  • EF Core AsAsyncEnumerable()(열거 수명 동안 리더·커넥션 유지), JsonSerializer.DeserializeAsyncEnumerable<T>, HttpCompletionOption.ResponseHeadersRead 조합
  • .NET 10에서 System.Linq.AsyncEnumerable이 런타임 내장. 암시적 파라미터 async 셀렉터가 모호성 오류를 내 (Order o, CancellationToken ct) => 명시가 필요하고, System.Linq.Async 7.0.0 미만과 net10.0 내장 간 충돌 발생
  • pull 기반 async stream과 push 기반 Channel<T>의 백프레셔 모델 차이, 예외가 호출 시점이 아닌 첫 MoveNextAsync()에서 발생하는 등 함정 7가지

:pushpin: C# DateTimeOffset 포맷 — ISO 8601, RFC 3339, JSON, Unix Time

주요 내용

  • 포맷 문자열별 보존 정보 — O(오프셋 + 소수점 7자리 유지), s(오프셋 손실), u(UTC 변환, 밀리초 손실), R(HTTP 헤더용 UTC 전용)
  • RFC 3339는 ISO 8601의 좁은 프로파일이며, 별도 요구가 없으면 명시적 오프셋을 가진 RFC 3339가 전송 포맷의 기본값
  • System.Text.Json 기본 직렬화는 유효한 RFC 3339를 생성하고, DateTimeOffset은 UTC를 +00:00으로 DateTimeKind.UtcDateTimeZ로 직렬화. UTC 전용 계약을 강제하는 커스텀 컨버터 전체 구현 수록
  • Unix 시간의 초/밀리초 혼동은 예외가 아니라 1970년 1월 날짜로 조용히 잘못 해석됨
  • UTC 변환 없이 Z만 붙여 09:11:30이 두 시간 미래로 이동하는 사례, TryParse에서 오프셋 누락 시 머신 로컬 타임존 적용, 브라우저 Date가 오프셋 없는 문자열은 로컬로 날짜만 있는 문자열은 UTC로 반대 해석하는 동작

:pushpin: Rx.NET v7.0 릴리스

  • 저자: Ian Griffiths
  • 태그: #rx #reactive #packaging

주요 내용

  • 세 가지 breaking change — .NET 6/7 타깃 제거(.NET 8+만 지원), OfType 연산자의 nullability 어노테이션 수정, UI 프레임워크 지원의 별도 패키지 분리
  • UI 지원이 System.Reactive.Windows.Forms, System.Reactive.Wpf, System.Reactive.Uwp, System.Reactive.WindowsRuntime으로 분리
  • self-contained 배포 크기 — Windows 타깃 콘솔 앱이 Rx 6.1 추가 시 102MB → 197MB였으나 Rx 7.0에서는 103MB. WPF/WinForms 의존성 강제로 발생하던 약 95MB 증가분 제거
  • 런타임 바이너리 호환성은 유지 — UI 코드는 어셈블리에 남고 참조 어셈블리에서만 숨김. ReactiveUI 등 기존 라이브러리는 재컴파일 없이 동작하고, 새 코드는 애널라이저가 컴파일 오류로 적절한 패키지를 안내
  • 배경으로 Rx 4.0의 “great unification” 이후 .NET Core 3.0에서 Windows 데스크톱 프레임워크가 옵션이 되며 생긴 문제를 회고

:pushpin: IAsyncEnumerable로 메모리 사용량 50배 줄이기

https://medium.com/@cbjpathirana/reduce-c-memory-usage-50x-iasyncenumerable-guide-73efbb012f47

  • 저자: Chathuranga Pathirana
  • 태그: csharp #iasyncenumerable #efcore

주요 내용

  • EF Core에서 ToListAsync()로 전체를 적재하던 집계 엔드포인트를 AsAsyncEnumerable() + await foreach 스트리밍으로 전환
  • 200k행 Postgres 기준 peak retained 메모리 35.2MB → 0.7MB, 800k행에서 121.5MB → 2.7MB. 실행 시간 77.94ms → 58.76ms
  • BenchmarkDotNet의 MemoryDiagnoser가 peak 메모리를 보고하지 않아 GC.GetTotalMemory(forceFullCollection: true)로 별도 샘플링
  • 스트리밍 집계는 입력이 정렬돼 있어야 성립하며(OrderBy 필수) 이는 선택이 아닌 정확성 요건
  • 그룹 단위 누적 후 방출 패턴에 한정된 이득이고, 전체 결과를 한 번에 필요로 하면 해당 없음

:pushpin: .NET 결제 플랫폼 레퍼런스 아키텍처

  • 저자: Rinat Kozin
  • 태그: #payments #ledger #architecture

주요 내용

  • 원장은 append-only posting — posting은 수정하지 않고 취소도 별도 posting으로 처리. 계정에 balance 필드를 두지 않고 SUM(Delta)로 계산하며 스냅샷은 버려도 되는 캐시. 금액은 NUMERIC(38,18)
  • 한 트랜잭션에 posting·outbox row·잔액 스냅샷·멱등성 마커만 넣고 네트워크 I/O는 금지. 교착 방지를 위해 계정 잠금은 항상 id 오름차순
  • 멱등성 2단 — 라우트 레벨 idempotency key로 DB 접근 전 차단, 모델 레벨에서 posting에 embed된 operation ID를 잠금 하에 TOCTOU 가드와 확인
  • Outbox는 평면 SQL 테이블(payload jsonb, processed)로 두고 200건 배치 + 500ms 폴링 후 트랜잭션 ACK
  • 모듈 분할 기준은 "배포 단위 = 장애 단위"로 비즈니스 도메인이 아닌 외부 프로토콜 기준. 클러스터는 Cluster→Group→Node 3계층에 epoch fencing 리더 선출

:pushpin: DI 컨테이너가 지금 숨기고 있는 버그

https://medium.com/@sudipparajuli/your-di-container-is-hiding-a-bug-right-now-49542f649bc2

주요 내용

  • AddSingleton<ReportCache>()AddScoped<AppDbContext>()를 주입받는 captive dependency 구조
  • 수명별 용도 — Transient는 무상태 헬퍼·검증기, Scoped는 DbContext와 그 의존 서비스, Singleton은 설정 객체와 스레드 안전 캐시
  • 싱글턴에서 스코프 서비스가 필요하면 IServiceScopeFactory.CreateScope()GetRequiredService<T>()
  • 스코프 검증이 Development 환경 밖에서는 기본 활성화되지 않아 이 버그가 트래픽이 몰릴 때까지 드러나지 않음
  • 실제 대체 구현이 없는데 인터페이스만 뽑는 것은 테스트성을 개선하지 않으며, DI가 잘못된 도구인 경우도 있다는 정리

:pushpin: 병렬 실행에서 Dictionary가 깨지는 이유

https://medium.com/@ravikumar.makwana/thread-safe-collections-in-c-why-dictionary-can-break-under-parallel-execution-ebbe71d1854e

  • 저자: Ravikumar Makwana
  • 태그: csharp #concurrency #collections

주요 내용

  • Parallel.ForDictionary<TKey,TValue>를 쓰면 내부 버킷·리사이즈 중 상태가 깨져 IndexOutOfRangeException이나 무한 루프, 조용한 데이터 유실이 발생하는 메커니즘
  • ConcurrentDictionary·ConcurrentQueue·ConcurrentStack·ConcurrentBag·BlockingCollection의 용도 구분
  • GetOrAdd()의 팩토리 델리게이트가 경합 시 여러 번 실행될 수 있고 저장되는 결과는 하나뿐이므로, 값 생성이 비싸면 ConcurrentDictionary<int, Lazy<Connection>>로 감싸는 패턴
  • "thread-safe"는 개별 연산 단위 보장이라 ContainsKey()TryAdd() 같은 check-then-act는 여전히 경합. AddOrUpdate() 등 원자적 메서드 사용
  • 단일 스레드에서는 동기화 오버헤드 때문에 일반 Dictionary가 낫고, 한 스레드가 만들고 여럿이 읽기만 하면 불변 컬렉션이 적합

:pushpin: AutoMapper 제거 — 컴파일 타임 DTO 매핑

https://medium.com/@ivanball76/delete-automapper-explicit-compile-time-dto-mapping-that-you-can-actually-test-9c7013cc5d3f

  • 저자: Ivan Ball-llovera
  • 태그: #automapper #mapperly #sourcegenerator

주요 내용

  • 리플렉션 규약 매핑의 실패 모드 — Product.TitleName으로 개명하면 DTO의 Title 매핑이 말없이 중단되는 조용한 오매핑, 프로덕션에서야 발견되는 런타임 프로파일 오류
  • 읽기/쓰기 분리 — IEntityDTOMapper는 DTO를 직접 반환하고, IEntityRequestMapperTask<Result<TEntity>>를 반환. 요청 매핑은 곧 검증이므로 예외가 아닌 Result로 실패 표현
  • Riok.Mapperly 4.3.1 소스 제너레이터가 대입 보일러플레이트를 컴파일 타임에 생성하고 미매핑 프로퍼티를 빌드 에러로 검출
  • 이식 가능한 5원칙 — 읽기/쓰기 분리, 쓰기 매퍼는 result 반환, 리플렉션 대신 소스 제너레이터, 컴파일러가 누락 검출, 시작 시 스캔 후 정적 유지
  • 트레이드오프로 보일러플레이트 증가, Mapperly 학습 곡선, 엔티티마다 매퍼 클래스 필요, 이득이 즉시가 아니라 "6개월 차"에 나타난다는 점을 함께 제시

:bookmark_tabs: 가벼운 읽을거리

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


10년 만의 Visual Studio 복귀와 완성 기능 충돌

  • IntelliSense(언어 인식 멤버 목록), IntelliCode(로컬 트랜스포머 기반 whole-line 예측), Copilot(생성 완성과 다음 편집 위치 예측)의 세 계층이 같은 에디터에서 겹쳐 동작하는 구조와 2023년부터의 충돌 이력을 GitHub 이슈·토론 8건 이상으로 추적
  • Microsoft 대응은 초기 “Tab 두 번” 통합에서 VS 2026의 IntelliSense 활성 중 Copilot 억제 후 재개하는 우선순위 분리로 변경
  • 설정 위치도 VS 2022 Tools > Options > IntelliCode에서 VS 2026 Text Editor > Code Completions로 이동

.NET MAUI에서 Sentry와 로컬 파일 로깅 결합

  • Sentry가 자동 수집하는 디바이스·OS 정보, 앱 버전, breadcrumb을 클라우드 전송 동의와 무관하게 로컬 진단에 재사용하는 구성(Sentry.Maui + NReco.Logging.File)
  • SetBeforeSend() 훅에서 SentryEvent를 가로채 JSON으로 파일에 남기고, ISettingsService의 동의 값으로 클라우드 전송 여부 결정
  • 파일 로거는 FileSystem.Current.CacheDirectory에 최대 5개·각 10MB 롤링. SentryEventLogger가 자기 이벤트를 필터링하지 않으면 StackOverflowException 발생

.NET 현대화 33부 — 조심스럽게 쓴 아키텍처 패턴

  • ASP.NET Framework → ASP.NET Core 마이그레이션 중 DDD·이벤트 소싱·마이크로서비스를 어디까지 도입할지 판단한 기준
  • 플랫폼 이전과 아키텍처 재설계를 동시에 진행하지 않는다는 원칙 — “프레임워크 마이그레이션 + 분산 시스템 분해 = 더 많은 실패 모드와 어려운 진단”
  • 이벤트는 본질적으로 비동기인 작업에만 쓰고 메서드 호출의 범용 대체물로 쓰지 않는다는 지침

하나의 SignalR 허브 위의 1초 미만 라이브 채널

  • 하나의 NotificationHub에 durable용 IPushNotificationSender와 ephemeral용 ILiveChannelPublisher 두 경로를 두고, 기본 등록은 no-op인 NullLiveChannelPublisher
  • 채널 키를 1초 매치 타임아웃의 정규식으로 검증하고, SignalR 자동 재연결 시 그룹 멤버십이 유지되지 않아 클라이언트가 재참여해야 함
  • 서비스 경계를 넘을 때는 2초 데드라인의 gRPC와 미리 직렬화된 JSON 페이로드로 공유 타입 계약 제거. 투표 커밋 후 논블로킹 인큐, 큐 포화 시 드롭

.NET에서 Rune으로 유니코드 문자 이름 얻기

  • Rune에서 유니코드 문자 이름(U+1F600 → “GRINNING FACE”)을 얻는 두 방법
  • DllImport로 OS 제공 ICU의 u_charName을 P/Invoke 호출하고 UCharNameChoice·UErrorCode 열거형을 함께 선언하거나, 관리형 패키지의 GetCharacterInfo(rune).Value.Name 사용
  • ICU는 OS 라이브러리에 의존하는 대신 앱 크기가 작고, 관리형 패키지는 이식성이 좋지만 유니코드 데이터가 함께 배포돼 앱 크기가 커짐

소스 생성 Refit 클라이언트

  • Refit v14가 리플렉션 기반에서 소스 생성으로 전환. 이전에는 인터페이스가 소스 생성돼도 쿼리 문자열 처리와 타입 직렬화에 런타임 리플렉션이 남아 트리밍 경고를 피할 수 없었음
  • PR #2210에서 해당 로직을 코드 생성 시점에 인라인해 런타임 리플렉션을 제거, 전체 AOT 컴파일이 트리밍 경고 없이 가능
  • 마이그레이션은 RestService.For<T>RestService.ForGenerated<T>, AddRefitClientAddRefitGeneratedClient, 쿼리 파라미터에서 [AliasAs] 대신 [JsonPropertyName]

프로덕션의 Hangfire — 구조·실패·상태 추적

  • 프로듀서(API)와 워커를 분리해 공유 스토리지로만 통신 — API에서는 AddHangfireServer()를 호출하지 않고, 워커에서 WorkerCount 조정(기본 CPU × 5)
  • [Queue] 특성과 큐 이름 상수로 우선순위 라우팅. Queues 배열의 순서가 곧 우선순위이며 실행 중인 잡을 선점하지 못해 용량 보장이 필요하면 전용 서버 인스턴스 필요
  • PermanentJobException 기반 클래스 + AutomaticRetryAttribute.ExceptOn 글로벌 필터로 비일시적 오류를 재시도 없이 즉시 실패 처리하고, JobStorage.Current.GetMonitoringApi().JobDetails()로 DB 직접 접근 없이 잡 상태 조회

WebScene — WebView 없이 네이티브에서 웹 컴포넌트 실행

  • 패키징된 React/TypeScript/JS 컴포넌트를 WebView 없이 네이티브 앱 프레임워크에서 실행하는 런타임. V8을 스크립트 엔진으로 쓰고 DOM/CSS/레이아웃 엔진을 자체 구현
  • V8·DOM 상태·레이아웃·입력 디스패치를 UI 스레드 밖에서 실행하고 damage-aware diff만 네이티브 프레젠터로 발행. Avalonia가 레퍼런스 구현이고 Flutter·Uno Platform은 통합 검증 수준, WPF·WinUI는 계획
  • Monaco Editor 0.56.0 번들을 무수정 실행해 텍스트 레이아웃·구문 강조·코드 폴딩 동작 확인. "임의의 공개 웹사이트를 위한 drop-in 브라우저가 아님"을 명시
4 Likes