주말 아침 - 주간 닷넷 #53


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


:pushpin: Runtime async 벤치마크: .NET 10 vs .NET 11

  • 저자: Gérald Barré
  • 태그: #dotnet11 #runtimeasync #benchmark

주요 내용

  • 같은 소스를 세 프로젝트로 컴파일해 비교: Net10(net10.0), Net11Off(net11.0, UseRuntimeAsync=false), Net11On(net11.0, Features=runtime-async=on). Net11Off가 "새 런타임의 차이"와 "runtime async의 차이"를 분리
  • 예외 없이 완료되는 20단계 중첩 async 체인: .NET 10 155 ns·1512 B, runtime async 44 ns·72 B. 레이어마다 Task를 할당하던 구조가 최외곽 Task 1개로 줄고, 깊이 1과 20의 할당량이 같아짐
  • 20단계 suspend 체인은 할당 1980 B에서 1472 B(약 26% 감소), 시간 2146 ns에서 1862 ns(약 13% 단축). 얕은 suspend는 할당이 늘어남(깊이 1: 206 B에서 256 B, 지역 변수 보존: 216 B에서 288 B)
  • 이미 준비된 항목 1000개를 도는 IAsyncEnumerable/await foreach는 6043 ns에서 4786 ns(약 20% 단축), 할당 동일
  • 한계: RC3, Apple M5 Pro(arm64) 단일 장비, 마이크로벤치마크. Net11Off가 동기 경로에서 .NET 10보다 약 30% 느린 원인은 미규명. Preview 7에서는 suspend 벤치마크가 약 4배 느렸으나 RC3에서 해소

:pushpin: Lazy 스레드 안전 모드 이해하기

  • 저자: Andrew Lock
  • 태그: dotnet #lazy #threadsafety #concurrency

주요 내용

  • LazyThreadSafetyMode의 세 모드: None(스레드 안전 없음, 사실상 ??=와 동일), PublicationOnly(여러 스레드가 팩토리를 동시 실행하고 먼저 끝난 값만 게시), ExecutionAndPublication(한 스레드만 팩토리 실행, 나머지는 대기)
  • 예외 캐싱은 None·ExecutionAndPublication만 해당하고 PublicationOnly는 캐시하지 않음. 캐시된 예외가 발생하면 Lazy가 영구히 “깨진” 상태가 됨
  • PublicationOnly가 유리한 조건: 초기화 중 동시 접근이 많고, 팩토리가 빠르고 중복 실행이 안전하며, 중복 할당 비용이 작고, 락 경합이 큰 경우
  • 예외 캐싱 여부를 분리하는 cacheExceptions 플래그 제안이 승인되어 .NET 12에서 구현될 가능성
  • LazyInitializer.EnsureInitialized로 Lazy 객체 할당 없이 PublicationOnly형·ExecutionAndPublication형 동작을 구현하는 방법(예외는 캐시하지 않음)

:pushpin: DuckDB.NET 1.5.6.1: 날짜 추가(Append)가 최대 27배 빨라진 과정

  • 저자: Giorgi Dalakishvili
  • 태그: #duckdb #performance #benchmark #jit

주요 내용

  • 같은 DuckDB 1.5.6 엔진에서 DuckDB.NET 1.5.6과 1.5.6.1을 BenchmarkDotNet(.NET 10, AMD EPYC 9V45)으로 비교. DateTime 4컬럼 100만 행 추가가 772.0ms에서 29.4ms로, 할당은 2.8GB에서 93KB로 감소
  • 원인: DuckDBTimestamp가 IEquatable을 구현하지 않아 Equals가 ValueType.Equals로 연결되고, 패딩 때문에 memcmp 경로를 못 타 박싱과 리플렉션(FieldInfo) 할당 발생. IEquatable 구현으로 약 7배 개선
  • 이후 DateTime을 연·월·일·시·분·초로 쪼개 네이티브로 변환하던 방식을 산술 변환으로 교체해 추가 개선
  • 제네릭 가상 메서드(AppendNumeric)의 CORINFO_HELP_VIRTUAL_FUNC_PTR 조회를 타입별 가상 메서드로 대체해 JIT의 클래스 추측 가능, GetCurrentWriter 인라인화로 이중 경계 검사와 CheckColumnAccess 호출 제거
  • 숫자 추가 1.2~1.4배, 매핑 appender 최대 3.9배, enum 2.5배, list 1.6~2.6배, 짧은 문자열 1.3~2.5배. 변경 없는 경로를 대조군(1.0배)으로 포함

:pushpin: 실시간 SignalR 게임에서 비밀 역할을 서버에 보관하기

https://medium.com/@mrcdprm/keeping-a-secret-role-on-the-server-in-a-real-time-signalr-game-4b4b760b6570

  • 저자: Miraç Deprem
  • 태그: #signalr #security aspnetcore #testing

주요 내용

  • 구버전 Casus의 누수 두 건: 페이지 소스에 직렬화된 SpyParticipantId, 방 전체 그룹에 역할과 비밀 단어를 보내고 인증도 없는 허브 메서드 PlayerRoleAssigned
  • 플레이어마다 {code}:{playerId} 형식의 SignalR 그룹을 두고, 방 전체로 가는 메시지를 없앰
  • 연결 시점에 세션 쿠키로 신원을 확정해 Context.Items에 저장하고, 세션이 없거나 해당 방 참여자가 아니면 Context.Abort(). 허브 메서드는 플레이어 id를 인자로 받지 않음
  • 방 상태가 바뀔 때마다 RoomView.For(room, playerId, now)로 플레이어별 뷰를 만들어 해당 그룹에만 전송. 스파이 카드의 장소는 null
  • WebApplicationFactory로 실제 SignalR 연결 4개를 열어 스파이 연결이 받은 원시 JSON에 wordIndex, spyIds, "place":{가 없는지 검사하고, 고의로 누수를 넣어 테스트가 깨지는 것을 확인. 세션 없는 연결, 타 사이트 쿠키 연결(403), 비호스트 시작 시도, 20회/10초 호출 제한 테스트도 포함

:pushpin: 1 CPU 컨테이너에서 .NET 병렬 루프가 직렬이 될 수 있다

  • 저자: Guilherme Appel
  • 태그: #parallel #container #concurrency

주요 내용

  • 200건 레코드 처리 시간이 CPU 제한 2개에서 42.1초, 1개에서 83.6초로 약 2배(각 5회 중앙값)
  • Parallel.ForEachAsync는 ParallelOptions에 취소 토큰만 지정해도 MaxDegreeOfParallelism이 -1이라 Environment.ProcessorCount를 사용. 컨테이너 CPU 제한(0.5도 1로 올림)이 ProcessorCount를 결정하고 프로세스 수명 동안 고정
  • Little의 법칙으로 필요한 in-flight 수 추정, 동시성 제한(SemaphoreSlim)과 속도 제한(rate limiter)을 구분
  • 의존성별 SemaphoreSlim 예산(DependencyBudgets) 코드와 4개 전략 비교(C1 기본값, C2 32 동시, C3 공용 30 퍼밋, C4 enrichment 24 + 쓰기 6). 쓰기 150ms 지연 시 503 거절이 C2 64, C3 57, C4 0
  • 정상 상태에서 C4 처리량이 C3 대비 약 11~14% 낮음, 호출 타임아웃은 퍼밋 획득 후 시작, 퍼밋 대기 마감 별도

:pushpin: 고성능 자료구조 설계와 GC 최적화

http://ayende.com/204260-a/high-performance-data-structures-design-optimizing-for-the-gc

  • 저자: Oren Eini (Ayende Rahien)
  • 태그: #ravendb #gc #datastructures #performance

주요 내용

  • ImmutableDictionary를 대체하기 위해 O(1) 조회, 저할당, 단순한 GC 구조, 스냅샷 격리라는 4가지 요구사항으로 ScratchPagesTable을 설계
  • 선형 탐색과 Fibonacci 해싱을 쓰는 오픈 어드레싱 해시 테이블이며 _keys, _heads, _entries 배열에는 포인터가 없고 파일 참조는 작은 _refs 배열에만 모음
  • 단일 작성 스레드가 스냅샷(배열 참조 4개, 항목 수, 시퀀스 번호)을 게시하고, OlderIndex와 Seq 필드로 연결 리스트를 따라 스냅샷 시점의 항목을 조회
  • 가장 오래된 활성 트랜잭션의 시퀀스로 오래된 항목을 가지치기하고, 제거한 항목은 모든 열린 트랜잭션이 이후에 시작될 때까지 보관(parking)한 뒤 재사용
  • 백업 같은 장기 읽기 트랜잭션으로 체인이 깊어지면 현재 커밋 버전과 가장 오래된 읽기 사이의 중간 버전을 폐기하며, 전체 RavenDB 성능이 20% 이상 개선

:pushpin: LINQ를 바꾸기 전에, 그것이 무엇으로 변환되는지 읽어라

  • 저자: Daniel Marbach
  • 태그: linq #performance #allocation

주요 내용

  • Azure Service Bus AMQP 수신기의 lock token 처리 코드에서 Any 술어가 인스턴스 필드를 캡처해 호출마다 Func<Guid, bool>을 새로 할당함을 디컴파일 결과로 확인(Select 델리게이트는 ??= 로 캐시됨)
  • 술어를 foreach 루프로 교체하자 단일 요소 배열이 129ns/240B에서 93ns/144B로 개선. 배열·리스트·집합·지연 시퀀스 모든 입력에서 빨라짐
  • IReadOnlyCollection이면 그대로 쓰고 아니면 ToArray하는 2차 변경에서 4요소 HashSet은 225ns/280B에서 80ns/128B로 개선, 빈 지연 시퀀스는 20ns에서 31ns로 악화, 4요소 배열도 느려짐
  • .NET 10에서는 OrderBy(…).Contains 같은 체인이 정렬 없이 처리되는 등 LINQ 특수화가 늘었으므로 대상 프레임워크에서 재측정할 것
  • 소규모 개선 목록: Array.Empty, 용량 지정, TryGetNonEnumeratedCount, 컬렉션 생성 지연, 호출자 분포를 근거로 한 판단

:pushpin: 재시도가 서비스를 무너뜨렸다: .NET의 Polly, 타임아웃, 재시도 폭풍

https://medium.com/techtrends-digest/our-retries-took-down-the-service-polly-timeouts-and-retry-storms-in-net-6fa5a803e674

  • 저자: Hemanth Kumar
  • 태그: #polly #resilience #httpclient

주요 내용

  • 장애 사례: 외부 청구 중개 API가 1초에서 약 20초로 느려졌을 때 게이트웨이 3회 x 서비스 4회 재시도 = 클릭당 최대 12회 호출. 고정 2초 대기로 재시도 동기화, HttpClient 기본 타임아웃 100초, 취소 토큰 미전달로 고아 재시도 지속
  • 수정 1: 재시도는 한 계층에서만, 멱등 호출만, DelayBackoffType.Exponential + UseJitter. HttpRetryStrategyOptions는 Retry-After 헤더를 기본 반영
  • 수정 2: 시도 3 s < 총 8 s < 호출자 10 s 순으로 타임아웃을 중첩하고 CancellationToken을 끝까지 전달. 전략 추가 순서가 감싸는 순서를 결정
  • 수정 3: AddCircuitBreaker(FailureRatio 0.5, MinimumThroughput 20, 샘플링·차단 30 s)를 재시도 안쪽에 두고, AddConcurrencyLimiter(50, 0)로 격벽 구성
  • 수정 4: BrokenCircuitException·TimeoutRejectedException·RateLimiterRejectedException 등을 잡아 마지막 성공 결과를 "재검증 대기"로 반환하는 폴백. AddStandardResilienceHandler 기본값(타임아웃 30 s/10 s, 재시도 3회, 차단기 10%/100건)과 POST 재시도 주의, WireMock.Net 지연 응답 테스트와 Polly 카오스 전략 소개

:pushpin: .NET MAUI에서 iPhone Duo API 사용하기

  • 저자: Pieter Nijs (본문 바이라인 없음, 사이트 링크 기준)
  • 태그: #dotnetmaui #ios #uikit

주요 내용

  • DuoExtensions.Maui(NuGet 0.1.0-preview.1) 실험 라이브러리: UseDuoExtensions()가 커스텀 Shell 렌더러와 IHingeService 등록. 앱 iOS 타깃은 net10.0-ios27.1, 배포 최소 버전은 iOS 15.0 유지 가능
  • 툴바 API: 항목별 ToolbarItemAxisBehavior.AxisBehavior(Automatic/HorizontalOnly/VerticalPreferred), Shell 전체 ToolbarLayout.SetVerticalBehavior(Disabled 지정), 페이지별 CompressionBehavior(PrefersToolbarItems/PrefersTabBar)
  • 구현 메커니즘: AutomationId를 네이티브 AccessibilityIdentifier와 대조해 Shell의 UIBarButtonItem을 찾아 AxisBehavior 설정, PreferredVerticalBarBehavior 오버라이드와 ShellFlyoutRenderer 하위 클래스로 UIKit 조회 경로 연결, 아이콘 항목의 Text가 오버플로 메뉴에 안 보이는 문제는 nativeItem.Title로 복사
  • 시스템 선호 가장자리는 TraitCollection.VerticalBarEdge를 읽는 읽기 전용 첨부 속성(Leading/Trailing/Unspecified), 힌지는 창별 UIHingeInteraction으로 상태(Unknown/Closed/PartiallyOpen/FullyOpen)와 각도(라디안을 도로 변환)를 IHingeObserver로 제공, ConditionalWeakTable과 약한 참조로 수명 관리
  • 한계: iOS 27.1 이상 필요, 기존 커스텀 Shell 렌더러와 병합 필요, 축 동작은 초기값을 명시해야 추적됨, 툴바 항목이 수직 막대 하단에 놓이는 동작은 SwiftUI 네이티브 앱에서도 동일하게 재현

:pushpin: INotifyPropertyChanged 없는 WPF 바인딩: 소스가 메모리에 남는 이유

  • 저자: Iguchi (s-iguchi09, 성명 확인 못 함)
  • 태그: wpf #databinding #memoryleak dotnet10

주요 내용

  • .NET 10(10.0.13)에서 창을 100회 열고 닫은 뒤 GC 후 살아 있는 객체를 세어, 남는 것은 창이나 BindingExpression이 아니라 바인딩 소스임을 확인
  • INPC를 구현하지 않은 객체의 속성에 OneWay/TwoWay로 바인딩하면 해당 속성의 PropertyDescriptor 내부 _valueChangedHandlers(ConcurrentDictionary, 강참조 키)와 ValueChangedRecord._source가 소스를 붙잡음
  • 읽기 전용(get-only) 속성도 감시 대상이며, {Binding Rows.Count}의 List<T>처럼 경로 중간 객체도 키가 됨. INPC 뷰모델, Mode=OneTime, ObservableCollection<T>는 남지 않음
  • 등록은 GC 대기로는 제거되지 않고, 이전 BindingExpression이 수집된 뒤 다른 OneWay 바인딩 창을 열 때 제거됨
  • 대응: 뷰모델에 INPC 구현, 목록 개수는 ObservableCollection<T> 또는 RowCount 속성으로 노출, 변하지 않는 DTO는 Mode=OneTime

:bookmark_tabs: 가벼운 읽을거리

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


runtime async로 마이그레이션하기

  • 동작은 그대로: 첫 suspend까지 동기 실행, 예외는 반환된 Task에 기록, AsyncLocal 흐름, 취소 시 TaskStatus.Canceled 구분, 비동기 스트림·await using 동일
  • 바뀌는 5가지: 라이브 스택 트레이스 단축, [AsyncStateMachine] 소멸, 할당 프로파일의 타입 이름 변경, 커스텀 [AsyncMethodBuilder] 메서드는 상태 머신을 계속 생성하며 기능에서 제외(PoolingAsyncValueTaskMethodBuilder 포함), IL 수준 제약(catch·finally 안의 suspend 불가 등)
  • [AsyncStateMachine] 특성이 없는지로 런타임 async 컴파일 여부를 판별하는 코드 예시

IsValid 안에서 데이터베이스를 블로킹하지 마세요: .NET 11의 비동기 검증

  • 기존 ValidationAttribute.IsValid 안에서 GetAwaiter().GetResult()로 DB를 조회하면 스레드 풀 스레드를 쿼리 시간 동안 점유
  • .NET 11에서 추가된 AsyncValidationAttribute(IsValidAsync), IAsyncValidatableObject(IAsyncEnumerable 반환 ValidateAsync), Validator.TryValidateObjectAsync/ValidateObjectAsync/ValidatePropertyAsync/ValidateValueAsync
  • 직접 실행 측정: 200ms·600ms 조회 두 속성이 병렬로 검증되어 전체 608ms. results에 null을 넘기면 첫 실패에서 중단되어 200~214ms이며 600ms 조회는 토큰 취소를 받음

WinUI 3 + Native AOT 앱을 Microsoft Store에 출시하며 겪은 문제들

  • CsWin32가 생성한 ICompositorDesktopInterop, ICompositorInterop, CreateDispatcherQueueController 바인딩이 Native AOT에서 IL2057 경고와 빌드 실패를 일으킴. IID로 인터페이스를 조회해 vtable slot으로 호출하는 수작업 interop으로 대체
  • WinUI 스레드에서 new Compositor()가 0x80070005를 던지는 원인은 Windows.System 디스패처 큐 부재. DispatcherQueue.GetForCurrentThread().EnsureSystemDispatcherQueue() 호출로 해결
  • Microsoft Store Developer CLI(msstore)는 msstore reconfigure로 Partner Center의 Microsoft Entra 애플리케이션 인증을 먼저 해야 이후 명령이 동작

웹에서 돌아가는 .NET MAUI - Shiny Controls 대부분이 그대로 동작

  • BlazorWebView 방식이 아니라 MAUI 앱을 변경 없이 브라우저에서 실행하는 WebAssembly용 MAUI 백엔드 Platform.Maui.Web 소개(0.4.0, NuGet 공개, 실험 단계)
  • 컨트롤을 실제 DOM 요소(button, input, dialog)로 렌더링하고 도형·테두리는 SVG, GraphicsView는 2D canvas 사용. 레이아웃은 MAUI 자체 엔진이 계산
  • Shiny Controls 갤러리의 Linux(GTK4) 헤드를 웹 헤드로 교체. MauiProgram은 UseMauiAppLinuxGtk4를 UseMauiAppWeb으로 바꾸는 한 줄 변경

.NET 10을 위해 다시 쓴 MediatR 기반 CQRS

  • 2024년 .NET 8 글을 패치하지 않고 .NET 10과 최신 MediatR로 처■■터 재작성. 최소 API 엔드포인트와 ISender/IPublisher 주입, Results.Created/NotFound 사용
  • MediatR 라이선스: 12.5.0이 마지막 Apache 2.0, 13.0부터 RPL-1.5/상용 듀얼. 연매출 500만 달러 미만 Community 무료, 키는 MEDIATR_LICENSE_KEY, 설정 cfg.LicenseKey. 키가 없으면 로그만 출력
  • 기존 placeholder 예외 behavior를 ValidationBehavior(FluentValidation) + IExceptionHandler(400 변환)로 교체. next(cancellationToken) 호출 시 MediatR이 default 토큰을 원 요청 토큰으로 치환하는 소스를 인용하고 테스트로 확인

AutoMapper 16.3.0 및 15.1.4 보안 수정 릴리스

  • MaxDepth가 적용되지 않는 경로에서 StackOverflowException으로 프로세스가 종료되던 DoS 문제 2건 수정: 생성자 매개변수(GHSA-6vhm-j9h7-f9gh, positional record 등), 기타 재귀 경로(GHSA-gpwp-5797-mjff)
  • 기타 경로: 파생 맵(Include/IncludeBase)의 MaxDepth 미상속, object·인터페이스 멤버의 기본 한도 미적용, Dictionary<string, object>/ExpandoObject 깊이 미추적, 재귀 ConvertUsing의 MaxDepth 무시
  • 동작 변경: MaxDepth로 막을 수 없는 재귀는 InsufficientExecutionStackException을 던지며, 다형 그래프는 기본 맵의 MaxDepth(기본 64)에서 잘림. 더 깊으면 MaxDepth를 명시해야 함

침해된 eMudhra 인증서: JetBrains IDE, ReSharper, .NET 도구, Toolbox App, Gateway 즉시 업데이트 권고

  • 2026년 10월 eMudhra 중간 인증서 8개의 개인 키가 유출되어, 네트워크 공격자가 JetBrains 도구가 접속하는 사이트·서비스를 사칭할 수 있음. 악용 사례는 확인되지 않았다고 명시
  • 영향 대상: 번들 JetBrains Runtime(JBR)의 신뢰 저장소를 쓰는 IDE, Toolbox App, Gateway, ReSharper(OS 업데이트로는 막을 수 없음). IDE 내장 브라우저(JCEF)와 백그라운드 서비스 jetbrainsd(내장 인증서 목록)는 별도 수정
  • 수정 버전: Rider 2026.2.4 / 2026.1.5.3 / 2025.3.5.3 / 2026.3 EAP 9, ReSharper 동일 계열, Toolbox App 3.9.1·3.8.2, jetbrainsd 0.9.9728. 2025.1 이하와 Toolbox 3.7 이하는 수정 제공 안 함

WinApp VS Code 확장의 XAML IntelliSense 지원 발표

  • WinApp 확장 v0.4에서 WinUI XAML용 언어 서비스를 실험적 프리뷰로 제공: 요소·속성·이벤트·첨부 속성·열거형/Boolean 값·마크업 확장·리소스 키 자동완성, hover 정보, 사용자 정의 컨트롤 사용 시 xmlns 선언 자동 추가
  • 입력 중 XAML 구문 오류와 프로젝트 해석 타입 기반 의미 오류(미해결 타입, 잘못된 x:Bind 경로)를 에디터에서 표시하고 Ctrl+.로 수정 제안 제공
  • F12/Ctrl+Click으로 이벤트 핸들러·x:Bind 멤버의 C# 선언, x:Name·리소스 키 선언으로 이동하고 Find All References·Rename 지원(컴파일된 메타데이터만 있는 프레임워크/NuGet 타입은 hover만 가능)
3개의 좋아요