.NET 9는 많은 변화와 개선 사항을 제공하며, 곧 출시를 앞두고 있습니다. 이 글에서는 .NET 9와 C# 13에서 가장 영향을 많이 미치고 널리 적용 가능한 주요 기능들을 살펴보겠습니다.
1. 새로운 Lock 객체
C# 13에서는 System.Threading.Lock라는 새로운 타입이 도입되어 상호 배제를 처리합니다. 기존에는 object
타입을 사용해 잠금을 구현했지만, 이제는 전용 Lock
타입이 제공되어 앞으로 대부분의 잠금 작업에 표준으로 자리 잡을 것으로 기대됩니다.
1 | // 기존 방식 (Before) |
주요 장점
- 더 깔끔하고 안전한 코드: 코드가 더욱 읽기 쉽고 예측 가능해집니다. 또한,
Lock
인스턴스를 일반object
로 잘못 사용하면 컴파일러가 경고를 제공합니다. - 성능 향상: Microsoft에 따르면, 임의의
object
인스턴스를 잠금에 사용하는 것보다 더 효율적일 수 있습니다. - 새로운 잠금 메커니즘:
EnterScope
가 내부적으로Monitor
클래스를 대체합니다. 이 메커니즘은Dispose
패턴을 따르는ref struct
를 반환하므로using
문과 매끄럽게 결합됩니다. - 비동기 작업의 제한:
lock
블록 내에서는 여전히async
호출이 허용되지 않습니다. 이는 잠금과 비동기 코드가 상호 작용하는 방식에 내재된 한계 때문입니다. 기존의SemaphoreSlim
접근 방식이 여전히 대안으로 사용됩니다.
1 | public class LockExample |
2. Task.WhenEach
다양한 시간 간격으로 완료되는 작업(Task) 리스트가 있다고 가정해봅시다. 작업이 모두 끝날 때까지 기다리는 WaitAll()
방식은 이 경우 적합하지 않습니다. 각각의 작업이 완료되는 즉시 처리하고 싶다면 Task.WaitAny()
를 사용하여 대안적으로 구현할 수 있습니다. 그러나 C# 13에서는 이를 더 우아하고 효율적으로 처리할 수 있는 Task.WhenEach
기능이 도입되었습니다.
1 | // 랜덤한 간격으로 완료되는 5개의 작업 리스트 생성 |
Task.WhenEach
는 IAsyncEnumerable<Task<TResult>>
를 반환하며, await foreach를
사용해 작업이 완료되는 즉시 쉽게 반복(iterate) 처리할 수 있도록 해줍니다.👌
3. params Collections
C# 13부터 params
매개변수로 컬렉션 표현식에 지원되는 모든 타입을 사용할 수 있게 되었습니다.
1 | // 기존 방식 (Before) |
C# 13 이후, params
매개변수는 다양한 컬렉션 타입을 지원합니다.
1 | // .NET 9 |
- 더 깔끔한 코드:
.ToArray()
,.ToList()
호출 횟수를 크게 줄일 수 있습니다. - 성능 향상:
.ToArray()
,.ToList()
같은 호출은 자체적으로 추가적인 리소스 오버헤드를 발생시킵니다. 이제Span<>
과IEnumerable<>
를 지원함으로써 더 효율적인 메모리 사용과 지연 실행(lazy execution)을 활용할 수 있습니다. 결과적으로, 유연성과 성능이 요구되는 시나리오에서 더 나은 성능을 제공합니다.
4. Semi-Auto Properties (반자동 속성)
C#에서 public int Number { get; set; }
와 같은 자동 구현 속성을 선언하면, 컴파일러가 자동으로 백업 필드(예: _number
)와 내부 getter/setter 메서드(void set_Number(int number)
, int get_Number()
)를 생성합니다.
하지만 속성의 getter나 setter에서 유효성 검사, 기본값 설정, 계산, 지연 로딩(lazy loading) 등의 커스텀 로직이 필요할 경우, 클래스에서 백업 필드를 직접 정의해야 했습니다.
C# 13에서는 field
키워드를 도입하여, 백업 필드를 직접 정의하지 않고도 바로 사용할 수 있도록 간소화했습니다.
1 | // 기존 방식 |
1 | // .NET 9 방식 |
- 보일러플레이트 코드 감소: 백업 필드를 수동으로 정의할 필요가 없어져 코드가 더 깔끔하고 간결해집니다.
- 가독성 향상:
field
키워드를 표준으로 사용하면서, 커스텀 백업 필드 이름을 관리할 필요가 없어 코드의 명확성이 높아집니다. - 속성 범위 내 필드 제한: 백업 필드는 속성 내부로 제한되어 클래스의 다른 부분에서 의도치 않게 사용되는 일을 방지하며, 캡슐화를 강화합니다.
- 🚨 잠재적 호환성 문제: 클래스에 이미
field
라는 이름의 속성이 있다면 새 키워드보다 우선 적용되어 예기치 않은 동작이 발생할 수 있습니다. 이는 이 기능이 2016년 최초 제안 이후 지연된 이유 중 하나로 보입니다.
5. Hybrid Cache
새로운 HybridCache API는 기존의 IDistributedCache
와 IMemoryCache
API에서 발생하는 문제를 해결하며, 새로운 기능과 성능을 제공해 .NET에서 캐싱을 더 유연하고 효율적으로 만듭니다. 특히, 스탬피드 문제와 같은 캐싱의 한계를 개선하며 대부분의 IDistributedCache
및 IMemoryCache
시나리오에 드롭인(dop-in) 방식으로 대체할 수 있도록 설계되었습니다.
1 | public record Post(int UserId, int Id, string Title, string Body); |
- 두 가지 장점의 결합 (Best of Both Worlds):
HybridCache
는 단일 API로 데이터를 메모리 캐시(L1) 또는 분산 캐시(L2)에 저장할 수 있는 유연성을 제공합니다. L1 캐시는 자주 사용되는 데이터를 빠르게 로컬에서 액세스할 수 있도록 하고, L2 캐시는 대규모 및 덜 자주 접근되는 데이터를 처리할 수 있는 확장성을 제공합니다. 이 동작은 HybridCacheEntryFlags로 제어할 수 있습니다. - 스탬피드 보호 (Stampede Protection):
IMemoryCache
와IDistributedCache
모두 스탬피드 문제를 겪지만,HybridCache
는 동일한 키에 대해 하나의 호출만 값 생성을 수행하고, 다른 호출은 결과를 대기하도록 처리해 불필요한 캐시 재생성을 방지합니다. - 추가 기능:
HybridCache
는 태깅(Tagging),.WithSerializer(...)
및.WithSerializerFactory(...)
메서드를 통한 설정 가능한 직렬화,[ImmutableObject(true)]
어노테이션을 활용한 캐시 인스턴스 재사용과 같은 추가 기능을 제공합니다.
1 | // 분산 캐시 (Redis) 설정 |
HybridCache는 메모리 캐시와 분산 캐시의 장점을 결합하여 빠른 액세스와 확장성을 동시에 제공합니다. 스탬피드 문제를 해결하며, 다양한 설정 및 추가 기능으로 유연하고 강력한 캐싱 솔루션을 제공합니다. 🚀
6. 내장 OpenAPI 문서 생성
.NET 5부터 Web API 템플릿은 Swashbuckle.AspNetCore
패키지를 통해 OpenAPI 지원을 기본으로 제공해왔습니다.
.NET 9에서는 Microsoft가 자체적으로 개발한 Microsoft.AspNetCore.OpenApi
패키지를 통해 OpenAPI 사양을 지원하며, 이는 기존의 Swashbuckle.AspNetCore를 대체합니다.
1 | // 기존 방식 (Before) |
.NET 9에서는 더 간단한 방식으로 OpenAPI 문서를 설정할 수 있습니다.
1 | // .NET 9 방식 |
앱을 실행한 후 /openapi/v1.json으로 이동하면 생성된 OpenAPI 문서를 확인할 수 있습니다.
-
Swagger UI: 문법이 더 짧아지고 처음 보기에 더 "네이티브"하게 보이지만, 기본적으로는 상호작용 가능한 API 문서(Swagger UI)는 제공되지 않고 OpenAPI 문서만 생성됩니다. 😢 Swagger UI 같은 상호작용 가능한 API 문서가 필요하다면 Scalar와 같은 서드파티 도구를 통합해야 합니다. 자세한 가이드는 Scalar .NET API Reference Integration에서 확인할 수 있습니다.
-
Build-Time Generation:
Microsoft.Extensions.ApiDescription.Server
패키지를 사용해 빌드 시점에 OpenAPI 문서를 생성할 수도 있습니다.
7. SearchValues 개선 사항
SearchValues는 .NET 8에서 도입된 불변(immutable) 및 읽기 전용 값 집합으로, 기존의 ICollection.Contains보다 훨씬 더 효율적인 검색을 제공합니다. 처음에는 문자(char)나 바이트(byte) 집합만 지원했지만, .NET 9에서는 문자열(string)도 지원하도록 확장되었습니다.
1 | var text = "Exploring new capabilities of SearchValues!".AsSpan(); |
.NET 9에서는 StringComparison
매개변수를 사용해 비교 방식을 지정할 수 있습니다.
이제 문자열도 지원하며, 대소문자 무시 등의 비교 옵션을 지정할 수 있는 기능이 추가되었습니다. 앞으로 이 기능은 문서 파싱, 입력 필터링, 스팸 감지, 데이터 편집, 검색 등 광범위한 텍스트 처리 애플리케이션에서 필수적인 도구가 될 것입니다. 🚀
8. 새로운 LINQ 메서드
.NET 9에서는 CountBy
, AggregateBy
, Index
라는 세 가지 새로운 LINQ 메서드가 추가되었습니다. 이 메서드들은 일반적인 데이터 조작 작업에서 성능과 간결성을 향상시키도록 설계되었습니다. 아래는 각 메서드의 예시와 설명입니다.
CountBy
특정 키로 그룹화하고 각 그룹의 항목 수를 계산합니다.
1 | (string firstName, string lastName)[] people = |
AggregateBy
그룹화된 데이터에서 값들을 집계합니다.
1 | (string name, string department, int vacationDaysLeft)[] employees = |
Index
컬렉션의 각 항목에 인덱스를 매핑합니다.
1 | var managers = new[] |
가장 좋은 함수는 Index()
입니다. foreach에서 인덱스가 없는 점은 항상 골칫거리였고, 종종 더 복잡한 우회 방법을 사용하게 만들었기 때문입니다.
9. 내장 UUID v7 생성
.NET 초기부터 Guid.NewGuid()
를 사용해 UUID를 생성해왔습니다. 이 방식은 UUID v4를 생성합니다. 그러나 UUID 사양은 지속적으로 발전해 현재의 안정된 버전은 UUID v7입니다.
UUID v7의 주요 특징 중 하나는 UUID에 포함된 타임스탬프(timestamp)입니다. 구조는 다음과 같습니다:
1 | +------------------+---------------+----------------------+ |
이 타임스탬프 덕분에 UUID를 생성 시간에 따라 정렬할 수 있습니다. 이는 데이터베이스에서 더욱 적합하며, 분산 환경에서 더 나은 고유성을 보장합니다.
이제 .NET에서는 외부 라이브러리(예: UUIDNext
)를 사용하지 않고도 UUID v7을 생성할 수 있습니다. 새로운 Guid.CreateVersion7()
메서드가 이를 지원하며, 특정 타임스탬프를 받아 UUID를 생성할 수도 있습니다. 이는 테스트 목적이나 정렬된 시퀀스에 특정 위치에 항목을 삽입할 때 유용합니다.
1 | var guid = Guid.NewGuid(); // v4 UUID |
Guid.CreateVersion7()
는 내부적으로NewGuid()
를 사용하며, 48비트 타임스탬프를 추가하고 UUID v7 표준에 맞게 올바른 버전 및 변형 비트를 설정합니다.- 이로 인해
NewGuid()
보다 약간 느릴 수 있지만, 수백만 개의 UUID를 생성해야 하는 경우가 아니라면 성능 차이는 거의 느껴지지 않습니다.
10. 기타 기능
아래는 흥미로운 변경 사항들의 목록으로, 특정한 사용 사례에 적합하며 널리 채택되기보다는 특정 상황에서 유용하게 사용될 수 있는 기능들입니다.