주말 아침 - 주간 닷넷 #28


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


:pushpin: .NET 11 Preview 4 출시

https://devblogs.microsoft.com/dotnet/dotnet-11-preview-4/

  • 저자: .NET Team
  • 태그: #dotnet11 #preview #release

주요 내용

  • Process API 대규모 업데이트, Span 기반 압축 인코더/디코더, 부동소수점 16진법 형식 지원
  • 런타임 라이브러리 runtime-async 컴파일로 비동기 작업 성능 향상
  • dotnet watch의 MAUI 모바일 장치 선택, Fish 쉘 완성, OpenTelemetry 기반 CLI 텔레메트리
  • ASP.NET Core OpenAPI HTTP QUERY, Blazor 임시 데이터 처리, SQL Server 2025 벡터 검색
  • Visual Studio 2026 Insiders 또는 VS Code C# Dev Kit 지원

:pushpin: .NET 11의 Process API 개선

https://devblogs.microsoft.com/dotnet/process-api-improvements-in-dotnet-11/

  • 저자: Adam Sitnik
  • 태그: #dotnet11 #process #performance

주요 내용

  • 교착 상태 없이 stdout/stderr 동시 읽기를 위한 RunAndCaptureText, ReadAllLines 등 신규 메서드
  • InheritedHandles 속성으로 자식 프로세스의 핸들 상속 명시적 제어
  • KillOnParentExit, StartDetached로 부모-자식 프로세스 수명 관리
  • Windows에서 병렬 프로세스 시작 처리량 2배, Apple Silicon에서 프로세스 생성 98배 가속
  • SafeProcessHandle 기반 경량 API로 NativeAOT 바이너리 크기 최대 32% 감소

:pushpin: .NET 11에서 .NET MAUI가 CoreCLR로 이동

https://devblogs.microsoft.com/dotnet/dotnet-maui-moves-to-coreclr-in-dotnet-11/

  • 저자: David Ortinau
  • 태그: maui #coreclr #dotnet11

주요 내용

  • .NET 11부터 MAUI Android/iOS/Mac Catalyst 앱이 Mono 대신 CoreCLR에서 실행
  • Tiered JIT, ReadyToRun, Profile-Guided Optimization 활용으로 시작 시간 단축
  • CoreCLR 기반에서 모든 플랫폼의 NativeAOT 사전 컴파일 바이너리 생성
  • <UseMonoRuntime>true</UseMonoRuntime>로 필요 시 Mono 옵트아웃 가능
  • Preview 4 단계에서 Release 모드 빌드로 .NET 10 대비 성능과 패키지 크기 검증 권장

:pushpin: WinUI 3 성능 도약

https://github.com/microsoft/microsoft-ui-xaml/discussions/11096

  • 저자: beth-panx (Microsoft)
  • 태그: winui3 #performance windows

주요 내용

  • File Explorer 런칭 시 할당량 41% 감소, 함수 호출 45% 감소, WinUI 코드 실행 시간 25% 단축
  • winui3/main 브랜치와 WinAppSDK 2.x에 최적화 변경 적용 예정
  • 일부 최적화는 breaking change 포함, 개발자 옵트인 방식
  • 향후 3.0 또는 4.0+에서 옵트아웃 방식으로 전환 가능성
  • 73건 이상의 커뮤니티 응답으로 프레임워크 채택과 배포 복잡성 논의 활발

:pushpin: C# 14의 field 키워드: 적게 쓰고 더 검증하라

https://duendesoftware.com/blog/20260512-the-field-keyword-in-csharp-14

  • 저자: Khalid Abuhakmeh
  • 태그: #csharp14 #field #properties

주요 내용

  • field 컨텍스트 키워드로 명시적 백킹 필드 없이 프로퍼티 검증 로직 작성
  • 설정 클래스, 지연 초기화, 문자열 정리, INotifyPropertyChanged 구현 시나리오
  • 프로퍼티 접근자 본문 내에서만 특별한 의미를 가지며 컴파일러가 자동 백킹 필드 생성
  • 리플렉션/직렬화에서 백킹 필드 이름 제어가 필요한 경우 명시 필드 사용
  • ASP.NET Core 옵션 패턴과 Duende IdentityServer 설정 클래스 활용 사례

:pushpin: C# 레거시 코드를 안전하게 리팩토링하기

https://www.pietschsoft.com/post/2026/05/11/csharp-how-to-refactor-legacy-code-safely

  • 저자: Chris Pietschmann
  • 태그: #refactoring #legacy-code #testing

주요 내용

  • 레거시 코드의 위험성은 나이가 아닌 동작의 불명확성에서 비롯됨
  • 특성화 테스트(characterization test)로 현재 동작을 고정한 후 점진적 개선
  • 순수 로직과 부작용(I/O, 시간, 상태 변경) 분리로 테스트 가능성 확보
  • 배포 위험이 높을 때는 작은 변경 단위와 롤백 경로 확보
  • InvoiceService 사례 기반 의사결정 프레임워크와 우선순위 매트릭스

:pushpin: C# 상속 vs 구성 — 각각 언제 쓰며 왜 AI가 결정할 수 없는가

https://www.pietschsoft.com/post/2026/05/08/csharp-inheritance-vs-composition-when-to-use-each

  • 저자: Chris Pietschmann
  • 태그: #inheritance #composition #design

주요 내용

  • is-a vs has-a 관계 정의와 각 사용 시점 기준
  • 취약한 기본 클래스 문제(Fragile Base Class Problem)를 실 사례로 시연
  • 인터페이스, 의존성 주입, 확장 메서드를 통한 구성 기반 설계
  • 상속에서 구성으로의 리팩토링 예시(할인 전략 패턴)
  • SOLID 원칙과 C# 8+ 기본 인터페이스 메서드까지 포괄

:pushpin: .NET 11에서 BackgroundService 예외가 이제 전파된다

https://steven-giesel.com/blogPost/00fcb870-6bf7-4f97-824f-8eab1b8838be

  • 저자: Steven Giesel
  • 태그: #dotnet11 #backgroundservice #exception

주요 내용

  • 4년 이상 지속된 버그: 첫 await 이후 예외가 무시되고 exit code 0으로 종료되던 동작 수정
  • 동기/비동기 경계의 예외 처리 메커니즘 변경
  • BackgroundServiceExceptionBehavior.Ignore 옵션으로 기존 동작 유지 가능
  • IHostedService와 BackgroundService의 예외 처리 차이
  • GitHub 이슈 #67146 및 PR #124863 참조

:pushpin: .NET 고성능 분산 캐싱 (HybridCache)

https://devblogs.microsoft.com/dotnet/high-performance-distributed-caching-dotnet-postgres-azure/

  • 저자: Jared Meade
  • 태그: #hybridcache #caching #performance

주요 내용

  • 메모리 캐시와 PostgreSQL 데이터베이스 캐시를 활용한 2계층 캐싱 아키텍처
  • 외부 데이터 호출 응답 시간을 수초에서 1밀리초 이하로 단축
  • 메모리 캐시 만료 후에도 데이터베이스 캐시로 성능 유지
  • 두 계층 모두 만료 시에만 원본 소스에서 데이터 갱신
  • HybridCache 구성 및 만료 정책 설정 가이드

:pushpin: 지난번 코드가 저렴해졌을 때 우리가 잃은 것

https://www.poppastring.com/blog/what-we-lost-the-last-time-code-got-cheap

  • 저자: Mark Downie
  • 태그: ai #software-engineering

주요 내용

  • 의료 전산화 회사 경험을 통한 해외 아웃소싱 개발의 이점과 한계 회상
  • AI 코드 생성 도구로 작성 비용은 폭락했으나 이해와 유지보수 비용 증가
  • 과거 아웃소싱 시대와 달리 현재는 의도를 아는 인간이 부재할 수 있다는 구조적 차이
  • 개발자 도구는 생산 속도뿐 아니라 코드 이해도와 문서화 우선 고려 필요
  • Joel Spolsky와 Prediction Machines 인용으로 경제 이론적 맥락 제시

:bookmark_tabs: 가벼운 읽을거리

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


MSIX에는 설치가 없다 - Stage와 Register뿐

  • MSIX 배포 엔진은 Index → Stage → Register의 3단계 구조이며 PackageManager API Add가 이를 순차 실행
  • Stage는 기계 단위, Register는 사용자 단위로 분리되어 다중 사용자 환경의 패키지 격리 실현

가장 최근 파일 10개만 남기는 상수 공간 선형 시간 알고리즘

  • FindFirstFile은 날짜 순서 열거를 지원하지 않으므로 직접 정렬 필요, 나이브 방식은 O(n) 공간/O(n log n) 시간
  • 크기 10의 최소 우선순위 큐로 O(1) 공간·O(n) 시간 달성, std::inplace_vector 활용 시 메모리 할당 회피

C#의 LINQ 필터링: Where, Any, All, Contains, OfType

  • Where의 지연 평가, Any/All의 단락(short-circuit) 의미론, Contains의 HashSet 활용 O(n) 성능, OfType의 강타입 필터링
  • “Count() > 0” 대신 “Any()” 사용, 선택도 높은 조건자 우선 배치 등 안티패턴과 모범 사례

C#의 LINQ 투영: Select, SelectMany, 컬렉션 평탄화

  • Select 일대일 매핑과 .NET 9 Index() 위치 정보, SelectMany 일대다 평탄화와 결과 선택자 오버로드
  • 익명 타입은 로컬 파이프라인, 명명된 레코드는 메서드 경계/DTO 사용 기준

.NET의 Observer 패턴 — 이벤트, 스트림, 인식

  • 단순 알림은 표준 이벤트, 스트림·완료·오류 처리가 필요하면 IObservable로 분리
  • MediatR을 통한 도메인 이벤트 핸들링과 메시지 브로커 기반 Pub/Sub 분산 확장 경로

2026년 모든 개발자가 알아야 할 10가지 .NET 오픈소스 라이브러리

  • Polly, Serilog, FluentValidation, MediatR, Dapper, Spectre.Console 등 검증된 라이브러리
  • TickerQ, TUnit, Facet 같은 소스 생성기 기반 신규 라이브러리와 AI 에이전트용 RazorConsole 트렌드
6개의 좋아요

MSIX에는 설치가 없다 - Stage와 Register뿐

이 글 몰랐던 내용이네요.

NSIS나 MSI나 Wix는 실제 installer가 install을 수행하는 주체인데, MSIX는 지시문만 갖고 있고 Windows 안에 내장된 Install api를 사용하도록 명령만 할 뿐이라 설치 행위의 주체가 installer가 아니라 windows 군요.

msix에 대해 그냥 현대적인 clickonce의 대체품이면서 NSIS처럼 설치 스크립트로 자동화도 못하는 안좋은 설치 방식이라고만 생각했는데, ‘선언형’ 이라고 관점을 바꿔보면 꽤 좋은 느낌이네요.

이 글은 오래된 주제인 "Composition over inheritance"를 다루고 있습니다.

편의를 위해, 예제 코드를 보여드립니다.

public class ReportGenerator
{
    public virtual string GenerateHeader()
    {
        return "REPORT - " + DateTime.Now.ToShortDateString();
    }

    public virtual string Generate()
    {
        var header = GenerateHeader();
        return header + "\n" + "Report body goes here.";
    }
}

public class AuditReportGenerator : ReportGenerator
{
    public override string GenerateHeader()
    {
        return "AUDIT REPORT - " + DateTime.Now.ToString("yyyy-MM-dd HH:mm");
    }
}

그런데, 필자는 상속의 문제점을 밝히는 과정 중, "새로운 개발자가 기반 클래스를 변경하는 경우"를 상정했습니다.

This looks reasonable. But now imagine a new developer modifies the base class:

public class ReportGenerator
{
    public virtual string GenerateHeader()
    {
        return "REPORT - " + DateTime.Now.ToShortDateString();
    }

    public virtual string Generate()
    {
        // Changed: no longer calls GenerateHeader()
        var header = "REPORT - " + DateTime.Now.ToShortDateString();
        return header + "\n" + GenerateBody();
    }

    protected virtual string GenerateBody()
    {
        return "Report body goes here.";
    }
}

Now AuditReportGenerator.GenerateHeader() is never called by Generate(), and the audit report silently produces the wrong header. The base class change was “internal” — but it broke the derived class because AuditReportGenerator depended on the base class’s implementation details, not just its public contract.

This is the fragile base class problem in action. The deeper your hierarchy, the more likely this kind of invisible breakage becomes.

필자는 이 코드의 문제가 "fragile base class problem"이라고 명확히 알고 있는데, 뜬금없이 "상속이 가진 고유의 문제"로 둔갑시키는 듯 합니다.

"fragile base class problem"이 없도록 구현한다면:

public class ReportGenerator
{    
    protected virtual string GenerateHeader() => "REPORT - ";

    public string Generate(ITimeProvider timeProvider) => 
       GenerateHeader() + 
       timeProvider.Now.ToShortDateString() + 
       "\n" +
       GenerateBody();    

    protected virtual string GenerateBody() = "";
}

계약이라는 관점을 보다 명확히 하려면,

public abstract class ReportGenerator
{    
    protected abstract string GenerateHeader();
    protected abstract string GenerateTimeStamp();
    protected abstract string GenerateBody();

    public string Generate() => 
       GenerateHeader() + 
       GenerateTimeStamp() + 
       "\n" +
       GenerateBody();    
}

만약 이것이 "새로운 작성자"의 의도가 아니라면, 코드를 상당 부분 다시 쓰는 재설계를 해야 하고 작성할 코드량도 많은 게 당연합니다.

재설계 시에 상속을 할지 파생을 할지 다시 결정하면 됩니다.

SOLID 와 도메인 문제의 혼동

필자는 예제 코드가 LSP 를 위배하니까, 상속 보다는 합성을 선택하는 게 좋다는 식으로 설명하고 있는데, 반은 맞고 반은 틀립니다.

원래 코드는 LSP를 위배하지 않고 있으니까요.

LSP 는:

파생 객체는 부모 행위를 override 할 때, 입력 값의 조건을 강화해서는 안되고 출력 값의 조건을 완화해서는 안된다

는 것으로 압축할 수 있습니다.

원글의 파생 객체는 파라미터 없이 string 을 출력하기에 LSP를 위배하고 있지 않습니다.
다만 반환되는 string 의 내용이 맘에 안들었던 것 - 도메인 맥락에서 허용되지 않았던 것 뿐입니다.

추상화 부족

LSP를 그러한 도메인 맥락까지 확대하려면, 그에 알맞은 설계가 필요할 뿐인 것이죠.

public record Report(string Body);
public record TitledReport(string Title, string Body) : Report(Body);

public abstract class ReportGenerator
{ 
    public abstract TitledReport Generate();
}
1개의 좋아요