주말 아침 - 주간 닷넷 #33

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


:pushpin: .NET 11 vs .NET 10 실제 프로덕션 앱 벤치마크 — 업그레이드해야 할까

  • 저자: Randhir Jassal
  • 태그: dotnet10 #dotnet11 #benchmark

주요 내용

  • Mattrx(95K LOC, 11만 MAU, 피크 3,200 req/s SaaS)를 대상으로 .NET 9→10→11 벤치마크
  • BenchmarkDotNet + dotnet-counters + 프로덕션 메트릭으로 측정
  • .NET 9→10: 처리량 +11%, p95 132→120ms, Working Set 415→380MB, AOT 콜드 스타트 84→61ms
  • .NET 10→11(preview): 처리량 +6~9%, p95 -5~7%
  • DATAS GC, 리전 기반 GC, JIT 자동 벡터화, AOT 개선이 주요 성능 드라이버
  • 결론: LTS인 .NET 10은 즉시 업그레이드, .NET 11은 파일럿만

:pushpin: Interval이 본질이다 — .NET에서 범위 타입을 일급 도메인 객체로 모델링하기

https://medium.com/@ricardogro_89299/the-interval-is-the-thing-modelling-range-types-as-first-class-domain-objects-in-net-39e4a4958214

  • 저자: Ricardo Groß
  • 태그: #ddd #efcore #postgresql

주요 내용

  • StartDate/EndDate 두 컬럼 분리의 문제: 경계 의미론(inclusive/exclusive) 암묵적, null 일관성 부재, 겹침 검사 로직 산재
  • PostgreSQL의 daterange/int4range 네이티브 range 타입과 GiST EXCLUDE 제약을 모델링 출발점으로 제시
  • CodoMetis.ValueRanges 라이브러리: EmptyRange/Finite/UnboundedStart/UnboundedEnd/Infinity의 sealed variant 계층
  • Contains/Overlaps/IsAdjacentTo/Intersect/Union/Except 등 메모리 내 interval 대수 + RangeSet 정규화
  • CodoMetis.ValueRanges.EFCore.PostgreSQL로 LINQ → PostgreSQL @> 등 연산자 자동 변환

:pushpin: 프로덕션의 동적 LINQ — 망하지 않는 런타임 쿼리 파싱 구축기

https://medium.com/@marioarce/dynamic-linq-in-production-how-i-built-runtime-query-parsing-that-doesnt-■■■■-79702cc5fbca

  • 저자: Mario Alberto Arce
  • 태그: linq #dynamicquery #performance

주요 내용

  • Dynamic LINQ의 보안 위협(인젝션, 임의 코드 실행) 방지 전략
  • ConcurrentDictionary 기반 컴파일된 표현식 캐싱
  • 프로퍼티 화이트리스트와 위험 토큰(new/typeof/세미콜론) 차단
  • 타입 안전 fluent 쿼리 빌더 패턴
  • 순진한 Dynamic LINQ는 10~20배 느림 → 최적화 시 15% 오버헤드의 정량 비교

:pushpin: Entity Framework Repository Pattern이란 무엇인가

  • 저자: Chris Pietschmann
  • 태그: #efcore #repositorypattern #architecture

주요 내용

  • Repository 패턴의 정의와 EF Core가 이미 제공하는 unit-of-work·repository 추상화 비교
  • 반복 쿼리 중앙화, 비즈니스 인터페이스, 테스트 용이성이 실제 가치
  • 모든 엔티티에 제네릭 저장소를 두는 안티패턴 비판
  • Product 엔티티, IProductRepository, DI 등록, 사용 전후 서비스 코드 비교
  • "추상화는 실제 문제를 해결할 때만 유용하다"는 원칙

:pushpin: 스레드 풀에서 저지연으로 작업을 스케줄링하려면?

  • 저자: Raymond Chen
  • 태그: #win32 #threading #latency

주요 내용

  • 하드웨어 장치 데이터를 20ms 내 처리해야 하지만 표준 스레드 풀에서 100ms 지연 발생 사례
  • “스레드 풀은 throughput용이지 latency용이 아니다” — 데드라인 메커니즘 부재
  • 해결책 1: CreateThreadPool()로 비공개 스레드 풀을 만들어 다른 작업과 격리
  • 해결책 2 (C# 권장): BCL의 커스텀 스레드 풀 미지원 한계로 큐+lock 기반 전용 워커 스레드 패턴
  • 해결책 3 (C++): std::queue + condition variable 패턴

:pushpin: C# 13의 Lock — lock(obj)System.Threading.Lock으로 교체

https://medium.com/@pankaj.ikhar/lock-in-c-13-replacing-lock-obj-with-something-better-4bb0c9631491

  • 저자: Pankaj Ikhar
  • 태그: #csharp13 #threading #lock

주요 내용

  • 기존 lock(object) 패턴의 문제: 타입 모호성, 락 객체 의도치 않은 노출, 의도 불명확
  • System.Threading.Lock 타입은 컴파일러가 전용 Lock 타입만 사용하도록 강제
  • 비경합 경로에서 Monitor 대신 Lock.Enter/Exit를 emit하여 오버헤드 감소
  • EnterScope() API는 IDisposable 스코프 반환으로 예외 시에도 해제 보장
  • async 컨텍스트에서는 await 불가, SemaphoreSlim(1,1)로 대체 필요

:pushpin: ExecuteUpdateAsync — EF Core 최고의 기능 중 하나

https://medium.com/@ravikumar.makwana/why-executeupdateasync-is-one-of-ef-cores-best-features-f6f36a628ed5

  • 저자: Ravikumar Makwana
  • 태그: #efcore #performance #bulkoperations

주요 내용

  • EF Core 7+의 ExecuteUpdateAsync는 엔티티를 메모리에 로드하지 않고 직접 SQL UPDATE 실행
  • 25만 건 비활성 사용자 처리 같은 대량 작업에서 메모리/라운드트립 절감
  • LINQ 람다 식이 SQL 표현식으로 변환되어 DB 측에서 계산 수행
  • ExecuteDeleteAsync도 동일 패턴
  • 사용 금지 케이스: 도메인 이벤트 트리거, 검증 로직 의존, 복잡한 객체 그래프 처리 시

:pushpin: NuGet 버전 복붙 멈춰라 — .NET 중앙 패키지 관리(CPM) 가이드

https://gbemmiey.medium.com/stop-copy-pasting-nuget-versions-a-guide-to-net-central-package-management-cpm-3ddde438b37a

  • 저자: Oluwagbemileke Oyeyoade
  • 태그: #nuget #cpm #packagemanagement

주요 내용

  • 중대형 솔루션의 버전 드리프트 문제와 Directory.Packages.props로 단일 진실 소스화
  • NuGet 6.2 / .NET SDK 6.0.300+, ManagePackageVersionsCentrally 속성으로 활성화
  • GlobalPackageReference, VersionOverride, 추이적 종속성 핀, 계층적 CPM 등 고급 기능
  • 마이그레이션 5단계: props 생성, csproj 감사, 항목 채우기, Version 속성 제거, NU1010/NU1008 해결
  • 알파벳 정렬, XML 주석 그룹핑, VersionOverride를 기술 부채로 추적하는 베스트 프랙티스

:pushpin: Microsoft Entra ID·.NET·Graph로 설정 가능한 토큰 수명 적용

  • 저자: Damien Bowden
  • 태그: #entraid #security #msgraph

주요 내용

  • Microsoft Entra ID의 설정 가능한 토큰 수명 정책을 .NET 콘솔 앱으로 구현
  • 위임된 사용자 자격증명과 애플리케이션 클라이언트 자격증명 두 방식 제시
  • TokenLifetimePolicyService 클래스로 서비스 주체 검색, 정책 정의, 생성/할당 처리
  • 토큰 수명을 분 단위(10~1440)로 설정해 보안 위험 감소
  • Microsoft Graph API를 통한 정책 생성·할당 절차와 GitHub 저장소 제공

:pushpin: 2026년 .NET에서 Vertical Slice Architecture로 프로덕션 앱 구성하기

  • 저자: Anton Martyniuk
  • 태그: #architecture #verticalslice dotnet

주요 내용

  • 기술 레이어가 아닌 기능/유스케이스 단위 프로젝트 구성
  • 슬라이스당 4-파일 패턴(Endpoint/Handler/Mapping/Validators)
  • 마커 인터페이스 기반 엔드포인트/핸들러/밸리데이터 자동 등록
  • 이벤트와 PublicApi 프로젝트를 이용한 슬라이스/모듈 간 통신
  • 예외 대신 Result 패턴으로 비즈니스 에러 처리

:pushpin: EF Core 성능 실수 10가지 — 300~500배 차이를 만드는 안티패턴

  • 저자: Mukesh Murugan
  • 태그: #efcore #performance dotnet10

주요 내용

  • N+1 쿼리, 전체 엔티티 반환, AsNoTracking 미사용 등 10가지 흔한 실수 정리
  • Cartesian explosion 방지를 위한 AsSplitQuery() 활용
  • 대량 작업에서 ExecuteUpdateAsync 사용으로 300~500배 성능 개선 사례
  • Lazy Loading이 JSON 직렬화 중 숨은 쿼리를 트리거하는 패턴 경고
  • 컴파일된 쿼리, 인덱스 부재, 페이지네이션 누락 등 실무 체크리스트와 결정 매트릭스

:bookmark_tabs: 가벼운 읽을거리

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


EF Core BulkMerge Upsert — 50K행 벤치마크 2.5배 개선

  • EF Core 10에도 기본 upsert가 없는 현황과 SQL Server T-SQL MERGE / Entity Framework Extensions의 BulkMerge 방식
  • 50,000행 벤치마크: 수동 2,123ms vs BulkMerge 822ms, ColumnPrimaryKeyExpression 미설정 시 모든 레코드가 신규로 취급되는 함정

IValidateOptions로 시작 시점 설정 검증

  • IOptions<T>가 잘못된 설정을 조용히 기본값으로 받는 문제를 IValidateOptions<T> + ValidateOnStart()로 시작 시점 OptionsValidationException 발생으로 차단
  • 데이터 주석으로 표현 불가능한 크로스 프로퍼티 검증과 명명된 옵션별 다른 검증 규칙 지원

MSIX의 세 번째 규칙 — 패키지 ID 고유성

  • 패키지 ID는 시공간을 통틀어 고유해야 하며 바이너리 내용과 1:1 대응, 비트 변경 시 새 ID 필요
  • 동일 ID + 다른 내용 시도 시 ERROR_PACKAGE_ALREADY_EXISTS (0x80073CFB) 발생, 배포 엔진은 동일 ID 패키지가 있으면 staging 건너뜀

HotChocolate GraphQL에 GROUP BY 추가하기

  • Community.HotChocolate.Data.Grouping 라이브러리로 IQueryable에 [UseGrouping] 적용, 스키마 레벨 타입 안전성 보장
  • key/having/where 절과 avg/sum/min/max 집계, 인메모리·EF Core·MongoDB 등 다중 소스 호환

.NET 11의 KillOnParentExit — 자식 프로세스 자동 종료

  • 블로그 에디터가 Statiq 사이트 빌더를 자식으로 띄우고 종료된 뒤 자식 프로세스가 bin 폴더를 잠그는 문제
  • ProcessStartInfo.KillOnParentExit 한 줄 추가로 부모 종료 시 자식 자동 종료 — 기존 Job Object P/Invoke 대체

Visual Studio 2026 테마 커스터마이징

  • VS 2026 (18.7)에서 Fluent 디자인 기반 테마 색상 토큰을 Tools > Options > Visual Experience에서 실시간 편집
  • JSON 오버라이드를 %LOCALAPPDATA%\Microsoft\VisualStudio\18.0_*\ColorThemes에 배치해 공유, “Blue Steel” 레트로 테마팩 제공

dotnet-cleanup v2 — bin/obj/node_modules 정리 도구

  • 솔루션/프로젝트 파일 해석 대신 glob 패턴(--path, --exclude) 기반으로 재설계
  • 삭제 전 임시 폴더로 이동 후 실제 삭제하는 속도 최적화, preview/--noop/CI용 --yes 옵션 지원
5개의 좋아요

일전에 비슷한 글을 썼다가 지웠던 적이 있습니다.

// int 는 실험적 (테스트를 위한)
public readonly record struct Interval(int Lower, int Size)
{
    public int this[Index index]
    {
        get
        {
            int offset = index.GetOffset(Size);
            if (offset >= Size)
                throw new IndexOutOfRangeException();
            return Lower + offset;
        }
    }

    public int[] this[Range range]
    {
        get
        {
            // inclusive
            var startOffset = range.Start.GetOffset(Size);
            // exclusive
            var endOffset = range.End.GetOffset(Size);

            // 빈 구간인데 전체 범위를 요청한 경우만 허용
            if (Size == 0 && (startOffset, endOffset) == (0, 0))
                return [];

            if (startOffset >= Size)
                throw new IndexOutOfRangeException($"start index: {startOffset}");

            if (endOffset > Size)
                throw new IndexOutOfRangeException($"end index: {endOffset}");

            var count = endOffset - startOffset;
            var result = new int[count];
            var start = Lower + startOffset;
            for (int i = 0; i < count; i++)
            {
                result[i] = start + i;
            }
            return result;
        }
    }
    public int Size { get; init; } = Size < 0 ? 0 : Size;

    public static implicit operator int[](Interval interval) => interval[..];

    public int Upper => Lower + Size;
}
public static class IntervalDefaults
{
    public static bool Overlaps(this Interval @this, Interval other) =>
       !Disjoints(@this, other);

    public static bool Disjoints(this Interval @this, Interval other) =>
        @this.Upper < other.Lower || other.Upper < @this.Lower;
}

이 모델은 필드로 사용하기 위한 것이 아니라, 위 글에서 나타난 Start, End와 같은 데이터 뭉침이 만들어내는 중복 로직을 제거하고 언어의 문법을 도메인 모델에 체화하기 위한 것이었습니다.

public readonly record struct DateSpan(DateOnly From, int Days)
{
    public DateOnly this[Index index] => 
        DateOnly.FromDayNumber(this.AsInterval()[index]);

    public DateOnly[] this[Range range] =>
        [.. this.AsInterval()[range].Select(daynumber => DateOnly.FromDayNumber(daynumber))];

    public int Days { get; init; } = Days < 0 ? 0 : Days;
}
public static class DateSpanDefaults
{
    public static Interval AsInterval(this DateSpan span) =>
        new(span.From.DayNumber, span.Days);

    public static bool Overlaps(this DateSpan @this, DateSpan other) =>
        @this.AsInterval().Overlaps(other.AsInterval());

    public static bool Disjoints(this DateSpan @this, DateSpan other) =>
       @this.AsInterval().Disjoints(other.AsInterval());
}

지웠던 이유는 IQueryable 호환성 문제 해결이 여의치 않았기 때문이었습니다.

링크의 글은 Postgresql 의 데이터 타입과 유사한 구조로 형식을 정의하고 거기에 맞는 인프라를 채택하는 방식을 사용했다는 점이 흥미롭습니다만 여전히 범용성이 떨어지는 문제가 있는 듯 보입니다.

시간 나는 대로 조금 더 연구해서, 결과물을 공유하겠습니다.

5개의 좋아요