주말 아침 - 주간 닷넷 #47

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


:pushpin: XPath로 Roslyn 구문 트리 쿼리하기

  • 저자: Gérald Barré
  • 태그: #roslyn csharp #staticanalysis

주요 내용

  • Roslyn 구문 트리를 XPathNavigator로 노출해 System.Xml.XPath 엔진으로 쿼리하는 MA0240 규칙의 구현 방식
  • 구문 노드는 SyntaxKind 이름의 XML 엘리먼트로, 토큰은 속성으로 매핑되는 변환 규칙
  • BannedSyntaxes.txt에 XPath 식과 메시지를 작성해 goto·lock·중첩 삼항 연산자·매개변수 5개 초과 등 커스텀 규칙을 ■■라이저 없이 정의
  • semantic 네임스페이스와 operation 네임스페이스로 3단계 쿼리 수준을 제공, syntax() 함수로 operation 트리에서 다시 syntax 트리로 이동하는 혼합 조회

:pushpin: Postgres가 잘못된 인덱스를 선택할 때

  • 저자: Milan Jovanović
  • 태그: #postgresql #database #performance

주요 내용

  • created_at DESC 인덱스를 추가한 후 쿼리가 16.5ms에서 241ms로 오히려 느려진 사례
  • 백만 행 규모 comments 테이블에서 특정 user_id의 1만 건이 12~14개월 전 구간에 몰려 있는 상황을 재현
  • 쿼리 플래너가 ORDER BY + LIMIT에 맞춰 조기 종료를 기대했으나 실제로는 495,944개 행을 버린 뒤에야 결과를 찾는 실행계획을 선택
  • (user_id) 단일 인덱스, (created_at DESC) 단일 인덱스, (user_id, created_at DESC) 복합 인덱스의 실행 시간 비교(16.537ms → 241.354ms → 0.067ms)
  • 복합 인덱스 적용 시 기존 단일 인덱스 중복 여부 검토, 쓰기 성능·저장 공간 오버헤드, 대표성 있는 데이터로 테스트할 것을 권고

:pushpin: Visitor 패턴을 제네릭 공변으로 다시 구현하기 — Visitor.NET

https://medium.com/@steponeit/visitor-pattern-in-c-acyclic-type-safe-no-downcasts-no-runtime-checks-with-visitor-net-6ff213e2f713

  • 저자: StepOne
  • 태그: csharp #designpatterns #sourcegenerators

주요 내용

  • 컴파일러·정적 분석기에서 계층 구조를 반복 순회할 때 발생하는 문제를 침습적 방식 → 클래식 비지터 → 비순환 비지터 순으로 전개
  • 비순환 비지터의 런타임 다운캐스트 문제를 C# 제네릭 분산(공변/반공변)으로 해결한 IVisitable<out TVisitable>, IVisitor<in TVisitable, out TReturn> 인터페이스 설계
  • 원소 계층·기본 비지터 인터페이스·구체 비지터 구현 3단계에 걸친 “triple dispatch” 구조
  • 형변환 방식별(암시적 변환, 명시적 캐스트, as, dynamic) 성능 벤치마크에서 dynamic이 현저히 느리다는 결과
  • [AutoVisitable<Element>] 소스 제너레이터로 보일러플레이트 Accept 오버로드를 자동 생성하는 Visitor.NET 라이브러리 공개

:pushpin: C#로 파이널 스테이트 트랜스듀서를 만든 이유

https://jordansrowles.medium.com/i-built-a-finite-state-transducer-in-c-because-a-dictionary-wasnt-enough-61f4d41365ce

  • 저자: Jordan Rowles
  • 태그: csharp #datastructures #searchengine

주요 내용

  • Dictionary<string,long>은 정확 일치 조회는 되지만 접두사 열거, 와일드카드 순회, Levenshtein 자동자 교집합 같은 검색엔진 기능은 지원하지 못하는 한계
  • Trie로 전환해 공통 접두사를 그래프 간선으로 공유하되 접미사 중복 문제가 남는 지점
  • 동일 출력·동일 도착 상태·동일 수락 여부를 가진 상태를 병합하는 최소 비순환 FST로 발전
  • 빌더는 정렬된 바이트 시퀀스를 받아 미확정 경로(“frontier”)를 유지하다 확정 시 컴파일하고 레지스트리로 동일 상태를 병합
  • {"car":42, "cat":50} 예시로 상태 공유와 출력 분산을 시연, 디스크 포맷도 제시

:pushpin: 하나의 변환, 두 개의 세계 — C#과 IQueryable 매핑

https://medium.com/@smurtdev/one-transformation-two-worlds-mapping-between-c-and-iqueryable-b48c38aeb5f7

  • 저자: Yevhen Cherkes
  • 태그: #efcore #iqueryable #expressiontrees

주요 내용

  • 사람이 읽기 좋은 이름 있는 변환 코드와 IQueryable 프로바이더가 요구하는 투명한 표현식 트리 사이의 충돌
  • 명명된 메서드를 인라인 처리하면 이름이 사라지고, Expression<Func<T,TResult>>로 저장하면 표현식 간 합성이 어려워지는 문제
  • Expression-First(Raffinert.Expressions)와 C#-First(AlephMapper류) 두 소유권 모델을 대조
  • 런타임에 구조가 결정되는 쿼리는 PredicateBuilder/LINQKit 방식의 동적 합성이 필요하다는 지적
  • AutoMapper ProjectTo, Mapperly, EntityFrameworkCore.Projectables, LINQKit을 기존 도구 생태계로 정리하고 “Eliminate, Generate, Derive, Compose” 의사결정 프레임워크 제시

:pushpin: .NET 11이 Duende IdentityServer 개발자에게 바꾸는 것들

  • 저자: AL Rodriguez
  • 태그: #dotnet11 #duende #identityserver

주요 내용

  • .NET 11 앱은 .NET 10 대상으로 빌드된 Duende IdentityServer 패키지를 그대로 참조 가능
  • ASP.NET Core Identity 기본 UI가 SignInOptions 관련 설정을 회원가입 이후 흐름에서 실제로 강제하도록 변경
  • Blazor Interactive Server 컴포넌트가 회로 재연결 없이 ClaimsPrincipal을 갱신하고 AuthenticationStateChanged 이벤트를 발생
  • SignalR 허브가 EnableAuthenticationRefresh 옵션으로 토큰 만료 시 재연결 없이 인증을 갱신
  • Kestrel이 HTTPS Negotiate 인증에 엔드포인트 채널 바인딩 토큰을 제공하며, 배포 간 Data Protection 키 링 보존이 필요함을 명시

:pushpin: ASP.NET wwwroot 파일 접근 보호하기

  • 저자: Rick Strahl
  • 태그: aspnetcore #security #staticfiles

주요 내용

  • wwwroot 폴더의 모든 파일이 기본적으로 인증 없이 공개 정적 자산으로 서빙되는 문제
  • UseStaticFiles() 이전에 커스텀 미들웨어를 추가해 StartsWithSegments()로 경로를 검사하고 미인증 요청은 401로 반환하는 방법
  • UseAuthentication()이 이 미들웨어보다 먼저 실행돼야 하며, MapGet() 라우팅 핸들러는 정적 파일 미들웨어가 먼저 가로채 기존 정적 파일 요청을 안정적으로 가로채지 못하는 한계
  • IIS나 프록시가 특정 확장자를 직접 서빙하도록 설정된 경우 ASP.NET Core 파이프라인 자체를 우회하는 사례와 web.config <location> 설정을 통한 해결책

:pushpin: C#에서 튜플 순회하기

  • 저자: Jason Bock
  • 태그: csharp #tuples #performance

주요 내용

  • ValueTuple<T1..Tn>이 모두 ITuple 인터페이스를 구현하며 Length 프로퍼티와 인덱서를 제공
  • 튜플에 그대로 foreach를 쓰면 CS1579 컴파일 에러가 발생
  • ITuple 확장 메서드로 GetEnumerator를 정의해 인덱스를 yield return하는 방식으로 해결, 8개 초과 요소의 재귀적 TRest 구■■지 이 방법 하나로 처리
  • 4개 요소 튜플 순회는 List<object> 대비 약 10배, 64개 요소는 약 171배 느리고 메모리 할당도 크게 증가

:pushpin: EF Core 리포팅 뷰 — 쿼리 전에 행부터 설계하라

https://www.mitchelsellers.com/blog/article/ef-core-reporting-views-design-the-row-before-writing-the-query

  • 저자: Mitchel Sellers
  • 태그: #efcore #sql #dataaccess

주요 내용

  • 조인부터 하지 말고 “한 행이 무엇을 의미하는지”(grain)를 먼저 정의할 것을 제안
  • 1:N 관계마다 윈도우 함수(ROW_NUMBER() OVER PARTITION BY)로 최신 레코드 선택, 집계, EXISTS 기반 존재 플래그, 스칼라 결합, 별도 상세 쿼리 중 하나를 선택해 사전에 축소
  • 1:N을 그대로 조인하면 3×2×4=24행처럼 카티전 곱이 발생하며 DISTINCT()는 근본 해결책이 아니라는 지적
  • 리포팅 뷰를 entity.HasNoKey()로 키 없는 엔터티로 매핑하되 식별 컬럼은 디버깅·조인을 위해 남겨두는 방법
  • ToQueryString()으로 생성된 SQL을 직접 확인하고 모든 컬렉션에 자식이 여러 개인 케이스로 grain 안정성을 테스트할 것을 조언

:pushpin: TPL Dataflow로 파이프라인 구축하기

  • 저자: Nick Cosentino
  • 태그: #tpldataflow #concurrency dotnet10

주요 내용

  • TransformBlock 2개와 ActionBlock 3단계로 구성된 주문 처리 파이프라인 예제
  • BoundedCapacity, MaxDegreeOfParallelism, EnsureOrdered=true 등 블록별 설정과 근거를 표로 정리
  • SendAsync는 백프레셔에 참여하고 수락 여부를 bool로 반환하는 반면 Post는 즉시 반환만 하는 차이, PropagateCompletion=true 없이는 하위 블록이 완료되지 않는 점
  • TaskCompletionSource<ExceptionDispatchInfo>로 첫 장애를 포착하고 공유 CancellationToken을 취소하는 장애 처리 패턴
  • 파이프라인이 인메모리 구조라 프로세스 종료 시 버퍼가 유실되는 비영속성 한계

:bookmark_tabs: 가벼운 읽을거리

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


Wolverine으로 논리적 메시지 중복 제거하기

  • 오퍼레이터의 이중 클릭, 콘솔 재발행, 스케줄러 재시도 등 서로 다른 경로로 들어온 동일한 "논리적 의도"가 각기 다른 envelope ID를 가져 중복 처리되는 문제
  • DeduplicationId 필드와 wolverine_deduplication 테이블로 설정 가능한 윈도우(기본 24시간) 내 동일 ID 메시지를 조용히 폐기·로깅
  • 핸들러 레벨 [Deduplicated] 특성, 발행 측 DeliveryOptions.DeduplicationId 등 4가지 설정 방식과 옵트인 방식, 메이저 버전에서만 스키마 변경이 이뤄지는 제약

스택 초기화를 막는 작은 C# 특성 — SkipLocalsInit

  • [module: SkipLocalsInit]은 컴파일러가 IL에 initlocals 플래그를 넣지 않게 해 로컬 변수 스택 제로화를 생략
  • C#의 확정 할당 규칙 덕분에 일반 지역 변수는 안전하며, 실제 영향받는 대상은 stackalloc처럼 명시적으로 할당된 미초기화 메모리
  • 버퍼를 완전히 채우지 않고 읽으면 비결정적 버그, 부분 초기화된 버퍼가 노출되면 정보 유출로 이어질 수 있어 코덱·파서·직렬화기 등 핫루프 상황에 한정해 권장

WireMock.Net과 Protobuf로 gRPC 목킹하기

  • 기존 WireMock.Net의 gRPC 목킹은 런타임에 .proto 파일을 로드해야 하고 JSON 기반 바디 매칭으로 여러 스텁이 서로 다른 메시지 타입 역직렬화를 시도해 오류가 발생하는 문제
  • 업스트림에 기여한 EarlyMismatch 기능으로 URL 경로 등 메타데이터로 불일치 스텁을 조기에 걸러 불필요한 역직렬화 방지
  • WireMock.Grpc.Protobuf 패키지가 생성된 protobuf 클래스를 직접 사용해 정확 바이트 매칭과 타입 프레디케이트 매칭을 지원, gRPC 표준 5바이트 프레이밍을 직접 처리

C# DIY 컴파일러 만들기 — HydraScript

  • 대학 졸업논문에서 시작한 TypeScript/Go 영감 언어 인터프리터 HydraScript를 FrontEnd·IR·BackEnd 세 서브도메인으로 분리
  • AST에 흩어진 가상 메서드 대신 Visitor 패턴으로 전환, 단항 표현식 처리 메서드를 약 20줄에서 6줄로 축소
  • 파라미터 기반 주소 지정 대신 O(1) 주소 재계산 자료구조를 직접 설계하고, 공변·반공변 기반 비순환 Visitor 라이브러리(Visitor.NET)로 서브도메인 간 순환 의존성 제거

Result 대안으로서의 유니온 타입 비교

  • OIDC 로그아웃 엔드포인트 사례로 전통적인 Result<T> 래퍼 클래스 방식과 새로 도입된 C# 유니온 타입 문법을 비교
  • Result<T> 방식에서는 처리되지 않은 케이스가 조용히 누락됐으나, 정확히 4개 케이스로 구성된 판별 유니온은 스위치 표현식에서 전수 처리를 강제해 누락 시 빌드 에러 발생
  • catch-all 패턴을 쓰면 유니온에서도 다시 조용한 실패가 재도입될 수 있다는 지적

SSH는 데스크톱이 아니다

  • 이동 중 노트북에 Docker/kubectl/.NET SDK 등이 없어 SSH로 데스크톱에 접속해 Claude Code를 실행하게 된 배경
  • Windows에서 SSH 세션 종료 시 job object teardown으로 모든 프로세스가 종료되는 문제, 비대화형 세션에서 Git Credential Manager 오류로 패스프레이즈 없는 전용 SSH 키를 사용하는 우회
  • Windows Update 재부팅 후 로그인 세션이 필요한 서비스가 SSH로 재기동되지 않는 문제와 브라우저 인증·UAC 등 GUI 종속 작업을 원격 데스크톱 도구로 주기적으로 처리해야 하는 한계

EF Core 10에서 Action Method Selector로 GET 다중 구현하기

  • .NET Core 10으로 컴파일하면 특성 없이는 모든 요청에 파라미터 없는 Get()이 호출되는 브레이킹 체인지
  • ActionMethodSelectorAttribute를 상속한 QueryStringConstraint 클래스로 쿼리스트링 키를 메서드 파라미터명과 비교해 매칭 여부 판정
  • 파라미터 없는 Get()을 포함해 구분이 필요한 모든 GET 메서드에 [QueryStringConstraint]를 붙여야 하는 제약

프로덕션 .NET에서 메모리 누수 추적하기

  • dotnet-counters로 힙 크기와 Gen 2 GC 동작을 모니터링해 “Gen 2 수행 후에도 힙이 줄지 않는” 패턴을 확인
  • dotnet-gcdump 스냅샷 2회 비교와 gcroot 분석으로 static PriceTicker → EventHandler 델리게이트 → CartViewModel 참조 체인을 추적, 약 14,000개 인스턴스 누적을 확인
  • 해결책으로 IDisposable 구현 후 구독 해제, 또는 PriceCache 싱글턴으로 설계를 바꾸는 방법 제시

System.Text.Json 다형성 처리와 Swagger 통합하기

  • Newtonsoft.Json은 런타임 타입 정보로 동적 계약을 구성하지만 System.Text.Json은 명시적 타입 정보 없이는 파생 타입을 빈 객체로 직렬화하는 차이
  • .NET 7 [JsonDerivedType] 특성으로 $type 판별자를 붙여 다형성 직렬화를 구현하고, Swashbuckle이 기본적으로 다형성을 인식하지 못하는 문제를 UseOneOfForPolymorphism·SelectDiscriminatorNameUsing 등으로 해결
  • FluentValidation의 SetInheritanceValidator로 파생 타입별 검증을 연결하고, Refit 클라이언트에서 실제 파생 타입의 BaseType으로 직렬화해야 하는 함정과 해결 코드 제시

마이크로서비스 동기 vs 비동기 통신

  • 동기(HTTP/REST, gRPC+protobuf)와 비동기(RabbitMQ, Azure Service Bus point-to-point / Kafka, Event Hubs pub-sub) 통신 방식을 기술별로 구분
  • HttpClient·gRPC .proto·Microsoft.Extensions.Http.Resilience 기반 재시도·서킷 브레이커 설정과 Azure Service Bus를 이용한 비동기 발행·구독 코드 제공
  • 결합도·지연시간·가용성·확장성·일관성·복잡도·장애 가시성 기준의 비교표와 "경계에서는 동기, 그 뒤에서는 비동기"라는 하이브리드 원칙 정리

.NET에서 System.Text.Json으로 JsonPath 쓰기

  • Meziantou.Framework.JsonPath라는 NuGet 패키지로 System.Text.Json의 JsonNode에 RFC 9535 기반 JSONPath 쿼리를 적용
  • JsonPath.Parse()로 표현식을 한 번 컴파일해 재사용하고 Evaluate()/EvaluateValue()로 정규화된 경로와 값을 반환, 속성명·와일드카드·인덱스·슬라이스·필터 선택자 지원
  • 잘못된 연산 시 null을 반환하는 Lax 모드와 예외를 던지는 Strict 모드 두 가지 평가 모드 제공
3개의 좋아요