주말 아침 - 주간 닷넷 #46

주말 아침 - 주간 닷넷 #46

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


:pushpin: .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로 개선

:pushpin: 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 핸드셰이크의 레코드 구분자 0x1eprotocol·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 테스트를 구축

:pushpin: Postgres 행 수준 보안으로 테넌트 격리하기

  • 저자: Milan Jovanović
  • 태그: #postgresql #efcore #multitenancy

주요 내용

  • ENABLE ROW LEVEL SECURITYCREATE POLICYUSING·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 권한, 테이블 소유자는 정책을 우회

:pushpin: 분산 .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 간격으로 스로틀링

:pushpin: .NET 커스텀 타입에 XPath 적용하기

  • 저자: Gérald Barré
  • 태그: dotnet #xpath #xmlnavigator

주요 내용

  • IXPathNavigableXPathNavigator가 XML 전용이 아니라 임의의 계층 데이터에 노드 뷰를 제공하는 추상화임을 활용
  • NodePosition 연결 리스트로 현재 노드부터 루트까지의 위치를 추적하고 NodeKind 열거형으로 문서 루트·요소·속성을 구분
  • MoveToFirstChild, MoveToNext, MoveToParent, MoveToFirstAttribute 등 탐색 메서드와 Clone, IsSamePosition 등 위치 관리 메서드를 구현
  • XsltContextIXsltContextFunction으로 //File[has-extension('png')] 같은 커스텀 XPath 함수를 정의
  • 표준 XPath 표현식이 /로 시작할 수 있도록 가상 문서 루트 노드를 추가하는 설계 결정을 설명

:pushpin: 클린 아키텍처, 언제 과잉설계가 되는가

  • 저자: Kartik Subramaniam
  • 태그: #cleanarchitecture dotnet #softwaredesign

주요 내용

  • 클린 아키텍처의 목적을 비즈니스 규칙을 DB·HTTP·외부 API 같은 인프라 관심사로부터 분리하는 의존성 관리로 규정
  • 은행·보험·헬스케어처럼 도메인이 복잡하거나 외부 연동이 많거나 팀이 크거나 장수명 애플리케이션일 때 적합하다고 제시
  • EF Core를 감싸기만 하는 리포지토리, 대안 구현체가 없는데도 만드는 인터페이스, 단순 CRUD에 적용한 CQRS, 모든 동작마다 만드는 MediatR 핸들러를 과잉설계 사례로 지적
  • Order.Ship() 같은 도메인 모델의 비즈니스 규칙 보호 예제와 OrdersController에서 ICreateOrderService로 이어지는 단순 구조 예제를 계층 비교 코드로 제시
  • 도입 판단을 돕는 다섯 가지 질문과 대안으로 버티컬 슬라이스 아키텍처를 제시하며 단순한 앱에는 Controller에서 Application Service, EF Core로 이어지는 구조를 권장

:pushpin: 남의 소프트웨어 안에 사는 앱 만들기

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회 처리 플래그로 해결
  • 자동화가 수동 변경과 충돌하면 해당 매물만 자동화를 중단시켜 사용자 개입을 보존하는 설계

:pushpin: FsCheck로 하는 C# 속성 기반 테스트

  • 저자: Davide Bellone
  • 태그: #testing #fscheck csharp

주요 내용

  • 예시 기반 테스트와 달리 속성 기반 테스트는 프레임워크가 무작위 입력을 생성해 불변식을 검증
  • 불변식(Invariant), 왕복(Round-trip), 순서·단조성, 대칭성·교환법칙 네 가지 속성 유형을 제시
  • dotnet add package FsCheck.NUnit 설치 후 [FsCheck.NUnit.Property] 속성을 붙이면 기본 100회 무작위 테스트가 실행됨
  • Arb.From으로 커스텀 생성기(Arbitrary)와 커스텀 축소기(Shrinker)를 작성해 도메인 제약에 맞는 입력을 생성
  • Prop.ForAllQuickCheckThrowOnFailure로 명령형 스타일 통합이 가능하며, 실패 시 최소 재현 입력으로 자동 축소되는 과정을 설명

:pushpin: ASP.NET Core 미들웨어의 네 가지 함정

주요 내용

  • 미들웨어 파이프라인의 동작 원리와 람다 기반, 클래스 기반 두 구현 방식을 비교
  • next 호출을 누락하면 파이프라인이 끊겨 다운스트림 미들웨어와 엔드포인트가 실행되지 않고 빈 응답이 반환됨
  • 미들웨어는 싱글턴이고 InvokeAsync는 요청별 스코프라는 생명주기 차이 때문에 생성자에 Scoped 서비스를 주입하면 “Cannot resolve scoped service” 예외가 발생하며, InvokeAsync 매개변수로 주입해 해결
  • 응답 전송이 시작된 뒤 상태 코드를 변경하면 InvalidOperationException이 발생하므로 context.Response.OnStarting() 콜백을 사용
  • 백그라운드 작업에서 InvokeAsync에 주입된 Scoped 서비스를 참조하면 요청 종료 후 이미 dispose된 서비스를 쓰게 되므로 IServiceScopeFactory로 새 스코프를 생성

:pushpin: C# 인터셉터로 구현하는 덕 타이핑

  • 저자: Steven Giesel
  • 태그: csharp #interceptors #sourcegenerator

주요 내용

  • C#에 구조적 타이핑이 없다는 점에서 출발해 dynamic 없이 무관한 클래스를 인터페이스처럼 사용하는 방법을 모색
  • 실험적 기능인 인터셉터와 소스 제너레이터를 결합해 구현
  • [DuckShape]로 구조적 계약 인터페이스를, [DuckTyped]로 진입점 메서드를 표시하는 커스텀 속성을 설계
  • 소스 제너레이터가 호출부를 분석해 readonly struct 어댑터를 생성하고 해당 타입의 메서드 호출을 그대로 전달
  • 어댑터 구조체 간접 호출로 인한 디스패치 오버헤드가 있으며 컴파일 타임 해석만 가능해 런타임 유연성은 없다는 한계를 명시

:pushpin: C# 인기 ORM이 숨기는 것들 — RepoDB와 SqlSugar 점검

  • 저자: Georgii Tormozov
  • 태그: #staticanalysis #orm #codequality

주요 내용

  • 정적 분석기로 SqlSugar와 RepoDB 오픈소스 저장소를 점검해 잠재적 버그 패턴을 항목별로 제시
  • SqlSugar에서 this.CancellationToken = CancellationToken 자기 대입, 소문자로 변환한 뒤 대문자 부분 문자열 "AS"를 찾아 항상 false가 되는 조건, is shortis long 중복 검사, 미사용 isLeft 파라미터, GetString() 반환값 무시 등 5건을 식별
  • RepoDB에서 앞선 비교가 뒤 조건을 항상 도달 불가능하게 만드는 순차 비교, Fields를 두 번 null 체크하면서 Qualifiers는 검사하지 않는 복붙 오류, fields 파라미터 중복 전달로 qualifiers가 사용되지 않는 문제, null 조건 연산자 결과를 non-null 인자에 그대로 전달하는 문제 등 4건을 식별
  • 각 결함마다 실제 오픈소스 코드를 인용하고 자기 대입·복붙·도달 불가 조건 등 발생 원인을 설명

:pushpin: 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에서 수정됨

:bookmark_tabs: 가벼운 읽을거리

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


내 오픈소스 프로젝트 — 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 내장 미디에이터를 제시
  • AddMediatorIConsumer<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 전환
3개의 좋아요