주말 아침 - 주간 닷넷 #51


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


:pushpin: .NET에서의 CBOR(Compact Binary Object Notation) 입문

  • 저자: Andrew Lock
  • 태그: dotnet #cbor #serialization

주요 내용

  • CBOR는 JSON과 같은 데이터 타입에 바이너리 지원을 더한 IETF 표준 포맷. JSON 예제를 hex 덤프와 주석으로 풀어 헤더 바이트 + 본문 구조 해설. JSON의 base64 인코딩(약 33% 증가) 대비 바이너리를 그대로 담을 수 있음
  • Protobuf(스키마 사전 공유 필요), MessagePack(공식 표준 없음, 스트리밍 차이)과 비교
  • System.Formats.Cbor(.NET 10 공식 지원, .NET Framework와 .NET Standard 2.0 지원). CborWriter/CborReader는 프리미티브만 읽고 쓰는 저수준 API이며 객체 자동 변환 없음. 맵/배열 길이를 미리 지정하지 않으면 indefinite-length로 기록
  • CborConformanceMode 4종(Lax, Strict, Canonical, Ctap2Canonical). Canonical 모드에서는 맵 키가 인코딩 순서로 정렬되어 같은 데이터의 출력 바이트가 달라짐
  • 알 수 없는 키는 ReadEncodedValue로 건너뛰는 전방 호환 처리, PeekState로 범용 디코딩. .NET 11 ASP.NET Core의 DBSC에서 CborWriter(allowMultipleRootLevelValues: true)로 claimUid·sessionId를 인코딩해 Data Protection 암호화 후 쿠키에 담는 코드

:pushpin: DuckDB.NET 1.5.6.1: 데이터 읽기 최대 12배 빨라지다

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

주요 내용

  • 같은 DuckDB 1.5.6 엔진에서 .NET 코드만 바꿔 비교한 결과, 타입 지정 읽기 3.5~6배, 정수 컬럼을 double로 읽기 12배, DateTime Append 26배(할당 2.8GB에서 93KB), 스칼라 함수 5~7.7배
  • DOTNET_JitDisasm으로 GetInt32의 Tier1 코드를 확인: 실제 로드는 mov 2개뿐이고 제네릭 가상 메서드 조회(CORINFO_HELP_VIRTUAL_FUNC_PTR) 2회가 오버헤드. 일반 가상 호출 0.6~1.5ns, 제네릭 가상 호출 5~9ns
  • DuckDBDataReader를 sealed로 변경(1.5배), StorageType으로 변환 없는 빠른 경로 추가(4배), 숫자 타입별 NumericVectorDataReader로 switch 제거, ReadInt64 등 일반 가상 메서드 추가, 표현식 트리 기반 nullable 델리게이트를 일반 클래스로 교체
  • int를 double로 읽을 때 object 반환 switch와 Convert.ChangeType으로 값당 24바이트 박스 2개(8M 값에 366MB)가 생기던 문제를 NumericConverter.Convert<TStorage, T>로 제거(18KB)
  • BenchmarkDotNet, .NET 10, 4 프로세스·워밍업 5회·측정 8회. 1.5.6 끼리 비교로 노이즈 약 15%를 먼저 측정했고, DuckDB가 처리하는 SUM 등 대조군은 3% 이내. Zen 5에서 28자리 decimal Append가 0.94배로 느려진 사례도 공개

:pushpin: 생일은 타임스탬프가 아니다

https://medium.com/@skcode/a-birthday-is-not-a-timestamp-7f443bac14e0

  • 저자: Subhankar Karmakar
  • 태그: dotnet #angular datetime #dateonly

주요 내용

  • 사용자가 선택한 10월 15일이 저장 후 10월 14일로 바뀌는 문제를 Asia/Kolkata(UTC+05:30) 환경에서 재현: new Date(1998, 9, 15) → JSON.stringify가 "1998-10-14T18:30:00.000Z" 전송 → ASP.NET Core의 DateTime이 Kind=Utc, 날짜 부분 10월 14일로 수신 → SQL Server date 컬럼에 14일 저장
  • 원인은 달력 날짜를 시점(instant)으로 바꾼 첫 변환이며 이후 계층은 정확히 전달했을 뿐이라는 분석. JS는 날짜만 있는 ISO 문자열('1998-10-15')을 UTC 자정으로, 시간이 있는 오프셋 없는 문자열은 로컬로 파싱
  • 흔한 우회책이 실패하는 사례: 하루 더하기, 타임존 강제 지정, toISOString().split('T')[0], 전부 UTC로 저장
  • 수정 방향: 프런트는 <input type="date">의 yyyy-MM-dd 문자열을 그대로 전송, API는 DateOnly(.NET 7부터 System.Text.Json 내장 지원), DB는 date와 datetime2 용도 구분, 표시는 UTC 기준 포맷 또는 로컬 부분 값으로 Date 생성
  • Kolkata, Los Angeles, UTC, Auckland, American Samoa 시간대 검증과 Samoa 2011-12-30 건너뜀 사례, 코드 리뷰 질문 6개

:pushpin: C# 15의 새 기능: 개요

  • 저자: Dmitrii Kharitonov
  • 태그: #csharp15 #uniontypes #language

주요 내용

  • union 타입: public union ApiResult(Success, ValidationError)처럼 상속 없이 타입 집합을 선언하며, switch가 모든 타입을 다루지 않으면 컴파일러가 경고하고 default 분기가 필요 없음
  • closed 계층: public closed record는 다른 어셈블리에서 상속할 수 없고 항상 abstract이며, 자손은 자동으로 closed가 되지 않고 struct·interface에는 사용 불가
  • 레이블 break/continue: break outer; 형태로 중첩 루프·switch를 한 번에 빠져나가 플래그 변수를 없앰 (for, foreach, while, do, switch에 레이블 적용)
  • 확장 인덱서: extension<T>(IReadOnlyList<T> list) { public IReadOnlyList<T> this[Range range] ... }로 list[..2] 구문 제공. 우선순위는 명시적 타입 인덱서, 암시적 타입 인덱서, 명시적 확장 인덱서, 암시적 확장 인덱서 순
  • 컬렉션 식 인자 [with(capacity), ..items]는 첫 요소 위치에서만 허용되고, 메모리 안전성 규칙은 포인터 사용만으로 unsafe가 되지 않고 역참조 지점에 unsafe 블록을 두며 safe 키워드가 추가됨(별도 활성화 필요)

:pushpin: 메서드 등록이 없는 JSON-RPC 라이브러리: 호출 가능한 메서드는 어떻게 결정되는가?

https://medium.com/@jeff377/a-json-rpc-library-with-no-method-registration-how-does-it-decide-what-can-be-called-f489835890ae

  • 저자: Jeff (Medium 표기명)
  • 태그: dotnet #jsonrpc aspnetcore #systemtextjson

주요 내용

  • Polhem.JsonRpc: System.Text.Json만 사용하는 JSON-RPC 2.0 패키지(서버 디스패처, ASP.NET Core MapJsonRpc, 클라이언트 JsonRpcConnector, 선택적 Payload 패키지)이며 net10.0 대상. 클라이언트·공유·페이로드 패키지는 Native AOT 호환, 서버는 리플렉션 기반이라 AOT 미지원
  • 호출 허용 검사 순서: ProgId.Action 이름 형식(각 64자 이하) → IJsonRpcObjectFactory.CreateObject가 객체 결정 → public·비제네릭·파라미터 1개 인스턴스 메서드 → 파라미터 {Action}Request, 반환 {Action}Response 명명 규칙 → 메서드 정책이 필터보다 먼저 실행. 규칙 위반은 -32601 Method not found
  • 서버 필터(IJsonRpcFilter)와 클라이언트 인터셉터, 배치·알림 지원. 일반 예외는 -32603으로 응답하며 메시지는 클라이언트에 노출하지 않음
  • 페이로드 암호화: AES-256-CBC + HMAC-SHA256(encrypt-then-MAC), HMAC은 메서드명과 방향을 포함하고 상수 시간으로 비교, 타임스탬프·시퀀스 번호로 재전송 방지
  • StreamJsonRpc 2.25.29, Tochka.JsonRpc 7.4.1과 비교하고, LSP·MCP처럼 외부 정의 프로토콜과 서버→클라이언트 호출(풀 듀플렉스)은 지원하지 않는다고 명시

:pushpin: PowerShell과 C#의 n-gram 필터링 성능

https://medium.com/@tomas.madajevas/performance-of-n-gram-filtering-with-powershell-and-c-b46a406dce5f

주요 내용

  • 9만 줄 이상의 로그에서 유사 줄을 제거하려고 n-gram + Jaccard 유사도 필터를 PowerShell로 작성했으나 7분 이상 소요
  • 거의 그대로 옮긴 C# cmdlet(HashSet)은 약 4초로 120x 개선(BenchmarkDotNet 187x, 할당 112x 감소). 무작위 데이터셋이라 시드에 따라 결과가 오차 범위 밖으로 달라진다고 단서를 붙임
  • Span 스택 할당과 해시 비교에서 충돌률이 높아 System.IO.Hashing의 XxHash32.HashToUInt32로 전환, 약 5% 개선
  • ZLinq의 AsValueEnumerable()로 약 4%와 할당 3분의 1 이상 감소, HashSet ObjectPool로 시간 0~2%와 할당 대폭 감소. 실제 파일 기준 128x
  • PowerShell이 느린 이유는 프로파일러로 규명하지 못하고 인터프리터와 파이프라인 가설만 제시(Get-NGram을 for 루프로 바꾸면 처리 시간 4분의 1 감소)

:pushpin: 벤치마크에서 가장 빨랐던 C# 코드가 실제 API 테스트에서는 졌다

  • 저자: Muhammad Rizwan
  • 태그: csharp #performance #benchmarkdotnet #hashset

주요 내용

  • 승인 고객 ID 목록으로 거래 합계를 구하는 메서드에서 List<int>.Contains, 매번 새로 만드는 HashSet, 재사용 HashSet 세 방식을 BenchmarkDotNet으로 비교(i5-13400F, .NET 10.0.11, BDN 0.15.2)
  • 10,000건 거래 기준 ID 16개에서는 리스트 36.48µs가 재사용 셋 69.62µs보다 빠르고, ID 4,096개에서는 리스트 2,455.96µs 대비 재사용 셋 72.92µs(약 33.7배). 100건/4,096 ID에서는 신규 셋 15,449.9ns가 리스트 14,938.4ns보다 느리며 신규 셋 할당은 432B(16 ID)와 77,936B(4,096 ID)
  • struct 배열 대 class 배열 1,000개 생성: 1.020µs·15.65KB 대 5.959µs·39.09KB. NoInlining 메서드 호출의 JIT 디스어셈블리(tail jmp)와 dotnet-counters 사용 안내
  • k6 10 VU 약 60초 HTTP 부하 시험(SQLite 10,000행 조회 포함): 평균 지연 리스트 14.19ms, 재사용 셋 10.96ms, 신규 셋 5.66ms로 마이크로벤치마크와 순위가 반대. 전략별 순차 실행이라 통제가 안 됐다고 명시
  • 프로덕션 변경 전 체크리스트(문제 정의, 기준선, 가설 분리, 메모리·프로파일링, 정확성과 캐시 무효화 전략)

:pushpin: Microsoft.UI.Reactor: C# 우선 방식의 WinUI 앱 개발

  • 저자: Alexandre Zollinger Chohfi
  • 태그: #winui #mvu #reactor csharp

주요 내용

  • XAML 파일·코드 비하인드·ViewModel 없이 MVU(Model-View-Update) 패턴으로 WinUI 컨트롤을 렌더링하는 실험적 C# 프레임워크 (새 렌더러 없음, 실제 WinUI 컨트롤 사용)
  • Render()가 불변 Element 트리를 반환하고 이전 결과와 diff해 바뀐 부분만 실제 컨트롤에 반영하는 방식, UseState 훅 사용
  • 같은 카운터 앱을 XAML + MVVM Toolkit(MainWindow.xaml, 코드 비하인드, CounterViewModel, [ObservableProperty], x:Bind) 구성과 .NET 10 파일 기반 앱(#:package, winapp run) 단일 .cs 파일로 비교
  • Meta Yoga 엔진 C# 포트 기반 flexbox 레이아웃, 60개 이상의 Reactor 전용 Roslyn 분석기(훅 규칙, 접근성, 테마 등), DataGrid·도킹 시스템, XAML과 양방향 상호운용
  • MIT 라이선스 오픈소스(microsoft/microsoft-ui-reactor), 실험 단계로 지원 정책·API 보장 없음, winapp new -t reactor 템플릿 4종

:bookmark_tabs: 가벼운 읽을거리

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


NAudio 3 타깃 프레임워크 단순화

  • NAudio.Core는 크로스 플랫폼, NAudio.Wasapi·NAudio.WinMM은 Windows 전용으로 분리했고, 새로 NAudio.Alsa, NAudio.SoundFile, 프리뷰 NAudio.MacOS 추가. 최소 런타임은 .NET 9(.NET 8의 ComWrappers 종료자 버그 때문)
  • net9.0-windows10.0.19041.0로 지정하면 NuGet이 이를 Windows 7로 해석해 netX.0-windows 소비자가 Windows 기능을 받지 못했고, net9.0-windows로 바꿔도 plain netX.0 콘솔 앱은 AudioFileReader 등을 받지 못함
  • NAudio 3.3.0부터 메타패키지는 net9.0 단일 TFM에 Core, Midi, WinMM, Wasapi를 참조하고 Windows 제한은 [SupportedOSPlatform]으로 표시. WinForms(-windows TFM 필요), Dmo(레거시), Asio는 메타패키지에서 제외

WinUI Chart 컨트롤 소개: WinUI 3 앱용 네이티브 차트

  • Windows App SDK 2.5.4-experimental에 포함된 1st-party Chart 컨트롤(Microsoft.UI.Xaml.Controls.Charts 네임스페이스). 선·영역·막대 시리즈, 범주/날짜시간 축, 값 축, 범례, 데이터 레이블, 마커 지원
  • 시리즈마다 X/Y 데이터 Slot을 두고 Samples(ItemsSource)로 값을 공급. X축은 CategoryAxis 또는 DateTimeAxis, Y축은 LinearAxis만 사용 가능. 축은 Chart.Axes에 먼저 추가한 뒤 시리즈에 지정
  • DataLabelOverrides / DataMarkerOverrides로 0부터 시작하는 인덱스 기준 개별 포인트에 레이블·마커 지정. 마커 모양은 9종(원, 삼각형, 다이아몬드, 대시 등)과 None

ASP.NET Core 웹 애플리케이션에서 여러 인증 스킴 사용하기

  • 대규모 애플리케이션을 새 ID 공급자로 이전하며 OIDC 서버를 하나로 줄이는 과정에서, 단일 쿠키 기본 스킴으로 합치는 대신 여러 스킴을 지원하는 방식을 선택한 사례
  • 쿠키 스킴 4개(SchemeOne, SchemeTwo 등)를 두고, AddPolicyScheme의 ForwardDefaultSelector가 요청 쿠키(__Host-Http-one, __Host-Http-two) 존재 여부로 스킴을 동적으로 선택하며 없으면 SchemeOne으로 대체
  • Duende IdentityServer 기반 OpenID Connect 설정: 코드 플로우 + PKCE, PAR(PushedAuthorizationBehavior.Require), client assertion, MapInboundClaims=false, UserInfo 엔드포인트 클레임 사용

WPF 안의 Blazor Hybrid: 라이브러리 없이 라이브 차트가 있는 다크 대시보드 만들기

  • csproj의 Sdk를 Microsoft.NET.Sdk.Razor로 변경, Microsoft.AspNetCore.Components.WebView.Wpf 8.0.* 패키지, BlazorWebView의 HostPage와 RootComponent Selector="#app"이 index.html의 id와 일치해야 렌더링됨
  • CSS 변수(:root)로 다크 테마 색상을 한 파일에 정의, color-scheme: dark 지정, 불변 DashboardSnapshot 레코드를 틱마다 교체해 락 없이 갱신
  • PeriodicTimer(1.5초) 백그라운드 스레드에서 갱신하고 InvokeAsync(StateHasChanged)로 Blazor 렌더러 컨텍스트에 복귀

백그라운드 작업 재시도: 비즈니스 연산 중복 실행 방지

  • 작업 시도(attempt)와 비즈니스 연산의 식별자를 분리하고, 요청 수락 시점에 연산 ID를 저장해 재시도 때 재사용
  • 완료 플래그 방식은 외부 호출 커밋과 로컬 갱신 사이의 크래시, 두 워커의 동시 읽기를 막지 못함. 고유 제약과 원자적 claim은 로컬 워커만 조율
  • 복구 계약 템플릿: 연산 ID, 페이로드 지문, 로컬 승인, 공급자 멱등성, 결과 불명 시 원본 ID로 조회 후 재제출, 해결 불가 시 자동 제출 중단

Sizoscope - Native AOT 바이너리 크기를 분석하는 .NET 도구

  • Native AOT(.NET 7 이상) 빌드가 생성하는 MSTAT·DGML 파일(IlcGenerateMstatFile, IlcGenerateDgmlFile)로 어셈블리·네임스페이스·타입·메서드별 크기 기여를 시각화
  • 전후 스냅샷 diff 뷰와, '왜 포함되었는가’를 보여주는 루트 분석(.NET 8 이상) 제공
  • dotnet tool install sizoscope --global(GUI), sizoscope-cli(비대화형 diff, 예: System.Private.CoreLib +129.0 kB) 설치 지원

dotnet-inspect - docker inspect, kubectl describe처럼 .NET 자산을 검사하는 도구

  • 검사 대상 코드를 로드하지 않고 라이브러리·NuGet 패키지·복원된 프로젝트·플랫폼 어셈블리의 메타데이터, API, 의존성, 소스, 버전 차이를 조회
  • dotnet tool install -g dotnet-inspect 또는 dnx dotnet-inspect -y -- package System.Text.Json으로 설치 없이 실행
  • member, depends 명령과 demo ... --mermaid 호출 그래프 예제(Options 허브, JSON 숫자 파서, Aspire PostgreSQL 등록), 브라우저용 Inspect Web 제공
1개의 좋아요

"날짜"와 “순간” 을 구분한 이후, 날짜는 DateOnly를 쓰고, 순간은 자체적으로 UTC 기반인 DateTimeOffset 을 사용하게 된 것 같습니다.

그리고, 대부분의 DateTimeOffset 데이터는 DB 에만 있고, 도메인 모델에는 나타나지 않습니다.

저장은 EF Core 가 Shadow property 로 자동 처리하거나, DbContext.SaveChangesAsync의 통제를 벗어난 메서드(ExecuteUpdateAsync) 나 RAW SQL을 사용하는 경우, 수동으로 처리하곤 합니다.

또한 저장 혹은 직렬화할 때는 DateTimeOffset.ToUnixTimeMilliseconds() 을 호출하여 UnixEpoch 로 변환하는데, DB 제품이나 직렬화 도구 변경에 지장이 없도록 하기 위함입니다.
(주의: 1970 년 이전의 순간을 저장해야 한다면 적절하지 않은 방법입니다.)

이런 스타일로 바꾼 이후, DateTime의 생성, 변환, 직렬화 전반에 걸쳐서 나타나는 골치 아픈 케이스들이 싹 다 사라졌습니다.

4개의 좋아요