주말 아침 - 주간 닷넷 #46
한 주 동안 .NET 생태계에서 있었던 주요 이슈와 아티클, 기술 트렌드를 정리해 소개합니다.
.NET 11의 성능 개선 — Runtime Async와 JIT 이스케이프 분석
- 저자: Stephen Toub
- 태그: #dotnet11 #performance #jit
주요 내용
- JIT의 이스케이프 분석이 guarded devirtualization, nullable 박싱 확장, conditional escape analysis로 강화되어 nullable 박싱 벤치마크가 9.2ns에서 4.1ns로, 할당이 24바이트에서 0바이트로 감소
- 배열·Span 경계 검사 제거 로직으로
!= constant범위 ■히기,(uint)i < span.Length하한 추론,span.Slice길이 관계 추적, 루프 클로닝이 추가되어 Base64 유사 인덱싱 코드의 어셈블리가 95바이트에서 79바이트로 축소 - Runtime Async는 C# 컴파일러가 담당하던 async lowering을 JIT로 이관하며
<Features>runtime-async=on</Features>로 옵트인, 샘플 앱 바이너리가 10,752바이트에서 5,632바이트로 52% 감소 - Runtime Async 벤치마크에서 동기 완료 2단계 체인이 21.2ns에서 6.2ns로 3.2배 빨라지고 할당이 100% 감소, 10단계 예외 처리는 3.3~4.8배 빠르고 할당이 82~93% 감소
- 델리게이트 표현 단순화로 64비트에서 객체당 8바이트가 절감되고 equality·hash-code 연산이 각각 3.3ns에서 2.2ns, 5.6ns에서 3.7ns로 개선
SignalR Inspector — SignalR 전용 DevTools를 만들다
https://medium.com/@grzywaczewski.jakub/signalr-is-not-just-a-websocket-frame-3c380c3baee7
- 저자: Jakub Grzywaczewski
- 태그: #signalr aspnetcore #devtools
주요 내용
- Chrome Network 패널은 전송 계층 WebSocket 프레임만 보여주고 허브 메서드나 호출 결과 같은 프로토콜 수준 정보는 보여주지 않는다는 문제에서 출발
- URL 패턴 대신 SignalR 핸드셰이크의 레코드 구분자
0x1e와protocol·version필드를 파싱해 프로토콜 기반으로 탐지 - Page World, Content Script, Service Worker, DevTools Panel 4계층 신뢰 경계를 두어 페이지 스크립트가 보낸 메시지를 검증
- WebSocket은 생성자 래핑으로, SSE와 Long Polling은 네트워크 API 관찰로 각각 다르게 계측하고 MessagePack 디코더는 VarInt 길이 프리픽스와 허브 메시지 타입 1~9를 처리
- 32KiB 수신 한도 근접, 30초 이상 미완료 호출, 비정상 keep-alive 간격 등 4가지 경고 규칙과 500개 로그 상한, Playwright 기반 E2E 테스트를 구축
Postgres 행 수준 보안으로 테넌트 격리하기
- 저자: Milan Jovanović
- 태그: #postgresql #efcore #multitenancy
주요 내용
ENABLE ROW LEVEL SECURITY와CREATE POLICY의USING·WITH CHECK절로 테넌트 격리 정책을 구현NULLIF(current_setting('app.tenant_id', true), '')::uuid패턴으로 설정이 누락됐을 때 전체 데이터가 노출되는 것을 방지- EF Core 커넥션 풀링이 세션 상태를 리셋하므로
DbConnectionInterceptor.ConnectionOpenedAsync에서 매 연결마다set_config를 호출해야 함 - 인터셉터 오버헤드는 요청당 약 0.4ms로 측정
IgnoreQueryFilters(), 필터 없는ExecuteUpdateAsync, attached entity 등 EF 쿼리 필터가 놓치는 지점을 RLS가 보완하지만 superuser와 BYPASSRLS 권한, 테이블 소유자는 정책을 우회
분산 .NET 앱의 숨은 지연시간 찾기 — 범인은 Task.Delay(50)
- 저자: Justin Yoo
- 태그: #dotnetaspire #performance #profiling
주요 내용
- .NET Aspire 기반 앱에서 응답 시간이 1.5~2분까지 늘어나는 현상을 Visual Studio Performance Profiler의 CPU Usage와 Call Tree로 추적
- 별도 launchSettings 프로필을 만들어 WebUI 프로세스만 독립적으로 프로파일링
- 스트리밍 응답 처리 루프에서 매 업데이트마다 걸려 있던
Task.Delay(50)을 원인으로 특정 - 5회 테스트에서 업데이트가 147~190회 발생해 누적 지연이 9.03~11.71초였고, 제거 후 중앙값이 84.77초에서 77.26초로 약 7.51초 단축
- CPU Usage 프로파일러는
Task.Delay대기 시간을 포착하지 못하므로 Stopwatch 직접 계측을 병행했고, 해결책으로 스트림은 그대로 소비하되StateHasChanged()호출만 50ms 간격으로 스로틀링
.NET 커스텀 타입에 XPath 적용하기
- 저자: Gérald Barré
- 태그: dotnet #xpath #xmlnavigator
주요 내용
IXPathNavigable과XPathNavigator가 XML 전용이 아니라 임의의 계층 데이터에 노드 뷰를 제공하는 추상화임을 활용NodePosition연결 리스트로 현재 노드부터 루트까지의 위치를 추적하고NodeKind열거형으로 문서 루트·요소·속성을 구분MoveToFirstChild,MoveToNext,MoveToParent,MoveToFirstAttribute등 탐색 메서드와Clone,IsSamePosition등 위치 관리 메서드를 구현XsltContext와IXsltContextFunction으로//File[has-extension('png')]같은 커스텀 XPath 함수를 정의- 표준 XPath 표현식이
/로 시작할 수 있도록 가상 문서 루트 노드를 추가하는 설계 결정을 설명
클린 아키텍처, 언제 과잉설계가 되는가
- 저자: Kartik Subramaniam
- 태그: #cleanarchitecture dotnet #softwaredesign
주요 내용
- 클린 아키텍처의 목적을 비즈니스 규칙을 DB·HTTP·외부 API 같은 인프라 관심사로부터 분리하는 의존성 관리로 규정
- 은행·보험·헬스케어처럼 도메인이 복잡하거나 외부 연동이 많거나 팀이 크거나 장수명 애플리케이션일 때 적합하다고 제시
- EF Core를 감싸기만 하는 리포지토리, 대안 구현체가 없는데도 만드는 인터페이스, 단순 CRUD에 적용한 CQRS, 모든 동작마다 만드는 MediatR 핸들러를 과잉설계 사례로 지적
Order.Ship()같은 도메인 모델의 비즈니스 규칙 보호 예제와OrdersController에서ICreateOrderService로 이어지는 단순 구조 예제를 계층 비교 코드로 제시- 도입 판단을 돕는 다섯 가지 질문과 대안으로 버티컬 슬라이스 아키텍처를 제시하며 단순한 앱에는 Controller에서 Application Service, EF Core로 이어지는 구조를 권장
남의 소프트웨어 안에 사는 앱 만들기
https://medium.com/@karen_90962/building-apps-that-live-inside-someone-elses-software-3f5327cd354b
- 저자: Craig Vincent
- 태그: #embeddedapps #masstransit #eventdriven
주요 내용
- 부동산 CRM Reapit AppMarket에 내장되는 React 앱을 .NET과 MassTransit, RabbitMQ 백엔드로 구축한 사례
- 웹훅 이벤트를 at-least-once이고 순서가 보장되지 않는 것으로 취급해 이벤트 ID 기반 중복 제거와 속성별 파티셔닝을 적용, 동일 속성은 순차 처리하고 다른 속성은 병렬 처리
- 임대 매물 상태를 단일 선형 흐름 대신 단계와 노출 가능성의 2축 모델로 재설계해
arrangingTenancy가 신규 임대인지 재임대인지 구분되지 않던 모호성을 해소 - OAuth 리다이렉트 과정에서 딥링크 파라미터가 소실되는 문제를 세션당 1회 처리 플래그로 해결
- 자동화가 수동 변경과 충돌하면 해당 매물만 자동화를 중단시켜 사용자 개입을 보존하는 설계
FsCheck로 하는 C# 속성 기반 테스트
- 저자: Davide Bellone
- 태그: #testing #fscheck csharp
주요 내용
- 예시 기반 테스트와 달리 속성 기반 테스트는 프레임워크가 무작위 입력을 생성해 불변식을 검증
- 불변식(Invariant), 왕복(Round-trip), 순서·단조성, 대칭성·교환법칙 네 가지 속성 유형을 제시
dotnet add package FsCheck.NUnit설치 후[FsCheck.NUnit.Property]속성을 붙이면 기본 100회 무작위 테스트가 실행됨Arb.From으로 커스텀 생성기(Arbitrary)와 커스텀 축소기(Shrinker)를 작성해 도메인 제약에 맞는 입력을 생성Prop.ForAll과QuickCheckThrowOnFailure로 명령형 스타일 통합이 가능하며, 실패 시 최소 재현 입력으로 자동 축소되는 과정을 설명
ASP.NET Core 미들웨어의 네 가지 함정
- 저자: David Grace
- 태그: aspnetcore #middleware dotnet
주요 내용
- 미들웨어 파이프라인의 동작 원리와 람다 기반, 클래스 기반 두 구현 방식을 비교
next호출을 누락하면 파이프라인이 끊겨 다운스트림 미들웨어와 엔드포인트가 실행되지 않고 빈 응답이 반환됨- 미들웨어는 싱글턴이고
InvokeAsync는 요청별 스코프라는 생명주기 차이 때문에 생성자에 Scoped 서비스를 주입하면 “Cannot resolve scoped service” 예외가 발생하며,InvokeAsync매개변수로 주입해 해결 - 응답 전송이 시작된 뒤 상태 코드를 변경하면
InvalidOperationException이 발생하므로context.Response.OnStarting()콜백을 사용 - 백그라운드 작업에서
InvokeAsync에 주입된 Scoped 서비스를 참조하면 요청 종료 후 이미 dispose된 서비스를 쓰게 되므로IServiceScopeFactory로 새 스코프를 생성
C# 인터셉터로 구현하는 덕 타이핑
- 저자: Steven Giesel
- 태그: csharp #interceptors #sourcegenerator
주요 내용
- C#에 구조적 타이핑이 없다는 점에서 출발해
dynamic없이 무관한 클래스를 인터페이스처럼 사용하는 방법을 모색 - 실험적 기능인 인터셉터와 소스 제너레이터를 결합해 구현
[DuckShape]로 구조적 계약 인터페이스를,[DuckTyped]로 진입점 메서드를 표시하는 커스텀 속성을 설계- 소스 제너레이터가 호출부를 분석해
readonly struct어댑터를 생성하고 해당 타입의 메서드 호출을 그대로 전달 - 어댑터 구조체 간접 호출로 인한 디스패치 오버헤드가 있으며 컴파일 타임 해석만 가능해 런타임 유연성은 없다는 한계를 명시
C# 인기 ORM이 숨기는 것들 — RepoDB와 SqlSugar 점검
- 저자: Georgii Tormozov
- 태그: #staticanalysis #orm #codequality
주요 내용
- 정적 분석기로 SqlSugar와 RepoDB 오픈소스 저장소를 점검해 잠재적 버그 패턴을 항목별로 제시
- SqlSugar에서
this.CancellationToken = CancellationToken자기 대입, 소문자로 변환한 뒤 대문자 부분 문자열 "AS"를 찾아 항상 false가 되는 조건,is short와is long중복 검사, 미사용isLeft파라미터,GetString()반환값 무시 등 5건을 식별 - RepoDB에서 앞선 비교가 뒤 조건을 항상 도달 불가능하게 만드는 순차 비교,
Fields를 두 번 null 체크하면서Qualifiers는 검사하지 않는 복붙 오류,fields파라미터 중복 전달로qualifiers가 사용되지 않는 문제, null 조건 연산자 결과를 non-null 인자에 그대로 전달하는 문제 등 4건을 식별 - 각 결함마다 실제 오픈소스 코드를 인용하고 자기 대입·복붙·도달 불가 조건 등 발생 원인을 설명
EF Core에서 대량 리스트 필터링 — 2,100 파라미터 한계
- 저자: Mukesh Murugan
- 태그: #efcore #sqlserver #performance
주요 내용
- SQL Server의 파라미터 한도는 2,100개이고 sp_executesql이 2개를 소모해 실사용 한도는 2,098개이며, EF Core 10.0.2에서 이 계산이 수정됨
- EF Core 10은 2,098개 미만이면
IN (...)에 파라미터를 각각 바인딩하고 초과하면 단일 JSON 파라미터와OPENJSON으로 자동 전환 - BenchmarkDotNet 0.15.8과 SQL Server 2025 LocalDB, 100만 행 테이블 기준으로 2,098건과 2,099건 경계에서 기본 Contains가 34.6ms에서 4.0ms로 약 8배 차이
- 1,000~2,098건 구간에는
EF.Parameter(), 안정적인 소량에는EF.Constant(), 복합키에는 임시 테이블과 INNER JOIN 또는WhereBulkContains를 권장 - 복합키를 OR 체인으로 구성하면 490쌍에서 StackOverflow로 프로세스가 종료되며 관련 이슈는 EF Core 10.0.1과 10.0.2에서 수정됨
가벼운 읽을거리
후보 항목 중 이슈로 선정되지 않은 가벼운 읽을거리들
내 오픈소스 프로젝트 — AngleSharp 12년사
- 2013년 크로스플랫폼 GUI 툴킷 개발 중 기존 파서의 한계를 발견하고 HTML5 스펙을 직접 들고 파서를 작성하기 시작한 배경
- 스펙의 해피패스는 20%이고 나머지 80%는 에러 처리와 malformed 마크업 복구에 있다는 개발 경험
- 핵심 패키지 3억 1천만 다운로드, HtmlSanitizer·bUnit·Bitwarden·OrchardCore 등 채택처와 함께 시각 렌더링 엔진 부재, Jint 기반 JS 지원 한계를 명시
내 오픈소스 프로젝트 — Mages 수식 파서
- 의존성 없는 경량 수식 파서·인터프리터로 변수·함수·클로저·객체·리스트·복소수·JSX 템플릿을 지원
- 리플렉션 기반이던 전신 YAMP의 속도 한계를 넘기 위해 AST 런타임 순회 대신 스택 기반 VM 명령어로 컴파일하는 구조를 채택하고 파서와 실행 백엔드를 분리
- Microsoft PowerToys, Flow Launcher, OMICRON Lab 계측기 등에 채택됐으며 LSP·디버거 부재와 완전 동적 타입을 한계로 명시
<메서드명>d__X는 대체 뭘까 — async/await 상태 머신 해부
- 스택 트레이스에 나타나는
ClassName+<MethodName>d__12.MoveNext()가 컴파일러가 생성한 상태 머신 클래스임을 설명 - 상태 머신 클래스의
State·Builder·awaiter세 필드와MoveNext()의 재진입 로직을 의사코드로 제시 Builder.AwaitUnsafeOnCompleted(ref awaiter, ref this)한 줄이 완료 시MoveNext()재호출을 등록하는 핵심임을 설명
추측 없이 프로파일러 트레이스 읽기
- 호출 스택을 막대 너비로 표현하는 플레임 그래프와 같은 데이터를 트리로 보여주는 프로파일 트리를 비교하고, 기본 활성화된 Hot Path가 가장 큰 리프 노드로 바로 이동
- CPU time, AWAIT_TIME, BLOCKED_TIME, 락 경합(
clr!JITutil_MonContention), JIT 컴파일([COLD]) 등 병목 라벨을 표로 정리 - PDB 심벌이 배포되지 않으면 메서드 이름이 raw 메모리 주소로 표시된다는 실무 주의점
Signal Shingle — 멀티 위젯 대시보드 아키텍처
- 위젯별 병렬 팬아웃 패턴이 동시 사용자 50명에서 약 500건의 경쟁 호출을 유발해 p95 지연이 7초를 초과한 사례
- 증분 윈도우 버킷 데이터 프로젝션, 수화된 페이지 모델 캐시, 버전 관리된 OOB 위젯 HTML 캐시로 구성된 2단계 딜리버리 캐시를 제안하고 SignalR은 데이터가 아닌 dirty beacon만 전파
- 갱신 비용이 예산을 초과하면 주기를 늘리고 안정되면 줄이는 적응형 리프레시 스케줄링
MSIX 패키지 유형 5가지
- Main, Framework, Resource, Optional, Bundle 5가지 유형의 목적과 매니페스트 선언 방식을 구분
- Framework 패키지는 여러 버전이 공존 가능하고, Resource 패키지는 기본적으로 실행되지 않으며
<uap6:AllowExecution>로 예외 처리 - Optional은
<uap3:mainpackagedependency>로 특정 Main 패키지 하나에 종속되는 반면 Framework는 여러 패밀리에서 재사용 가능
MassTransit으로 구현하는 미디에이터 패턴
- MediatR이 2025년 상용 라이선스로 전환된 이후의 대안으로 MassTransit 내장 미디에이터를 제시
AddMediator와IConsumer<T>기반 컨슈머로 인프로세스 디스패치를 구성하고RespondAsync()와 request client로 요청·응답 패턴을 지원- 재시도와 인메모리 아웃박스, 사가는 그대로 동작하지만 라우팅 슬립 액티비티는 미디에이터 경로에서 지원되지 않음
EF Core 10 이름 있는 기본 제약 조건
UseNamedDefaultConstraints()가DF__Jobs__Status__...형태의 자동 생성 이름을DF_Jobs_Status로 바꾸며, 기존 스키마에 적용하면 모든 기본 제약이 이름 변경 대상이 됨dotnet ef migrations script로 DB 연결 없이 마이그레이션 SQL을 미리 생성해 제약 삭제와 재생성 순서를 확인- 대규모 스키마에서는 전역 컨벤션 대신 속성 단위 이름 지정을 고려하라는 권고
redb 4.0 메이저 릴리스
- redb.Core/Route/Tsak/Identity 4개 제품을 아우르는 76개 NuGet 패키지와 7개 컨테이너 이미지 규모의 통합 릴리스
.route.xml기반 라우트 선언과 TestKit, 스텁 객체를 지연 로드하는 lazy reference,[RedbUnique]기반 DB 강제 고유 키,IRedbSaveInterceptor쓰기 인터셉터를 추가- 레거시 연동용 WS-Trust SOAP 파사드와 RFC 9068 audience 지원, HTTP 헤더 위장·동의 화면 악용 취약점 수정과 기본 해시 bcrypt 전환
