C# 15가 .NET 11과 함께 공개됐다. 오래 요청되던 union types와 닫힌 계층(closed hierarchies)이 언어 차원으로 들어오면서, 도메인을 타입으로 표현하는 방식이 바뀐다.
Microsoft는 Dev Blogs를 통해 C# 15의 핵심 신규 기능으로 union types, closed hierarchies, 갱신된 unsafe 모델 프리뷰, 그리고 일상적 보일러플레이트를 줄이는 소소한 개선들을 제시했다. 같은 시기 Microsoft는 자사 OpenJDK 빌드의 8월 크리티컬 패치도 릴리스했다. 이 글은 새 기능이 실제 코드에서 무엇을 바꾸는지, 그리고 지금 도입할지 말지를 판단하는 데 필요한 맥락을 다룬다.
C# 15 .NET 11과 함께 공개된 언어 버전 | union types '여러 타입 중 하나'를 언어로 표현 | closed 닫힌 계층 → 컴파일러 완전성 검사 | unsafe* 갱신된 unsafe 모델은 프리뷰 |
먼저 분명히 해둘 것. 아래 코드 예시 중 union types와 closed의 정확한 문법은 프리뷰 단계에서 다듬어지는 중이라, 확정된 최종 표기와 다를 수 있다. 이 글은 문법의 사소한 표기보다 이 기능들이 해결하는 문제와 개념에 무게를 둔다. 패턴 매칭·switch 식처럼 이미 오래 안정화된 부분은 실제 동작하는 문법 그대로 쓴다.
사건 요약 — 무엇이 공개됐나
Microsoft Dev Blogs에 따르면 C# 15는 .NET 11과 짝을 이뤄 공개됐고, 언어의 무게중심을 '데이터를 어떻게 다루느냐'에서 '데이터가 어떤 모양일 수 있느냐를 타입으로 못 박느냐'로 옮기는 기능들이 중심에 있다. 핵심은 네 갈래다: (1) 여러 타입 중 하나임을 표현하는 union types, (2) 파생 타입 집합을 고정하는 닫힌 계층(closed hierarchies), (3) 프리뷰로 제공되는 갱신된 unsafe 모델, (4) 흔한 보일러플레이트를 덜어내는 여러 소소한 개선.
앞의 두 기능은 함수형 언어(F#, Rust, Kotlin의 sealed class, Swift의 enum) 사용자에게 익숙한 '대수적 데이터 타입(algebraic data type)'을 C#의 객체지향 문법과 조화시키려는 시도다. C#은 그동안 abstract record + sealed record로 이 패턴을 '흉내'낼 수 있었지만, 컴파일러가 계층이 닫혀 있음을 알지 못해 완전성 검사가 불완전했다. C# 15는 이 간극을 언어 수준에서 메우려 한다.
한편 Microsoft는 같은 8월에 자사가 배포하는 OpenJDK 빌드(Microsoft Build of OpenJDK)의 크리티컬 보안 패치도 냈다. .NET 진영의 언어 릴리스와 직접 관련은 없지만, 같은 플랫폼 팀이 JVM 쪽 런타임도 유지·보수한다는 점에서 함께 챙길 릴리스다. 자바 워크로드를 운영 중이라면 해당 패치 적용 여부를 확인하는 편이 좋다.
union types — '여러 타입 중 하나'를 타입으로
현실의 많은 값은 '이것 또는 저것'이다. API 응답은 성공 또는 실패, 결제 결과는 승인·거절·보류 중 하나다. C#에서 이를 표현하는 기존 방법은 셋 다 아쉬웠다. (1) 공통 베이스 클래스/인터페이스 + 다운캐스팅은 런타임 캐스트 실수를 부른다. (2) 모든 필드를 담은 뚱뚱한 클래스에 bool IsSuccess 플래그를 두면 '거절인데 성공 데이터가 채워진' 불가능 상태가 표현 가능해진다. (3) 예외로 실패를 흘리면 제어 흐름이 흩어진다.
union types의 목표는 '불가능한 상태를 표현 불가능하게' 만드는 것이다. 값이 가질 수 있는 형태를 유한한 집합으로 못 박으면, 컴파일러가 모든 경우를 다뤘는지 검사해 준다. 아래는 제안된 형태를 보여주는 개념 예시다(최종 문법은 다를 수 있다).
// 개념 예시 — union으로 '결과'를 세 형태 중 하나로 못 박는다
// (정확한 표기는 프리뷰에서 조정될 수 있음)
public union PaymentResult
{
Approved(string TransactionId, decimal Amount);
Declined(string Reason);
Pending(string PollUrl);
}
// 소비하는 쪽: switch 식으로 모든 경우를 분기
// (switch 식·패턴 매칭은 이미 안정화된 문법)
string Describe(PaymentResult result) => result switch
{
PaymentResult.Approved a => $"승인 {a.TransactionId} / {a.Amount:C}",
PaymentResult.Declined d => $"거절: {d.Reason}",
PaymentResult.Pending p => $"보류, 폴링: {p.PollUrl}",
// 완전성이 보장되면 default 가지가 필요 없어진다
};
핵심은 마지막 주석이다. 컴파일러가 PaymentResult의 형태가 정확히 셋뿐임을 알기 때문에, 세 경우를 모두 처리하면 default 가지 없이도 완전하다. 반대로 나중에 네 번째 형태를 추가하면, 그 타입을 분기하지 않은 모든 switch가 컴파일 경고/오류로 드러난다. 이것이 '타입으로 리팩터링을 강제'하는 대수적 데이터 타입의 실질적 이득이다.
string or int처럼 기존 타입들의 합집합을 그 자리에서 표현하는 '타입 유니온'이다. C# 팀은 두 요구를 모두 논의해 왔으며, 무엇이 어느 범위까지 들어오는지는 릴리스 노트와 후속 프리뷰에서 확인하는 것이 안전하다.닫힌 계층(closed hierarchies) — 완전성의 근거
union이 '새 케이스 묶음'을 선언하는 문법이라면, 닫힌 계층은 기존의 클래스/레코드 상속 계층에 '이 베이스를 상속하는 타입은 여기 나열된 것이 전부'라는 제약을 붙이는 기능이다. Kotlin의 sealed class, Swift·Rust의 enum과 같은 계열의 아이디어다.
C#에는 이미 sealed 키워드가 있지만, 그건 '이 클래스를 더 상속하지 못하게' 하는 것이지 '이 베이스의 파생 집합 전체를 고정'하는 것과는 다르다. 닫힌 계층은 후자를 노린다. 컴파일러가 파생 타입의 전체 목록을 알게 되면, switch에서 그 목록을 다 처리했는지 검사할 수 있다.
// 지금까지의 관용구 — 컴파일러는 계층이 '닫혔는지' 모른다
public abstract record Shape;
public sealed record Circle(double Radius) : Shape;
public sealed record Rect(double W, double H) : Shape;
double Area(Shape s) => s switch
{
Circle c => Math.PI * c.Radius * c.Radius,
Rect r => r.W * r.H,
_ => throw new NotSupportedException() // 방어용 가지가 강제됨
};
위 코드의 _ => throw가 문제의 냄새다. 개발자는 Shape가 두 종류뿐임을 알지만 컴파일러는 모르므로, 도달할 수 없는 방어 가지를 억지로 넣어야 한다. 더 나쁜 건, 나중에 Triangle을 추가해도 이 switch는 조용히 컴파일되고 런타임에야 예외를 던진다는 점이다. 닫힌 계층은 이 계층이 닫혀 있음을 컴파일러에 알려, 새 파생 타입을 더했을 때 그것을 다루지 않은 switch를 컴파일 시점에 잡아낸다.
// 개념 예시 — 베이스를 닫아 완전성 검사를 켠다
// (정확한 키워드/표기는 프리뷰에서 조정될 수 있음)
public closed record Shape;
public sealed record Circle(double Radius) : Shape;
public sealed record Rect(double W, double H) : Shape;
double Area(Shape s) => s switch
{
Circle c => Math.PI * c.Radius * c.Radius,
Rect r => r.W * r.H,
// 닫혀 있으므로 default 없이 완전. Triangle을 추가하면 이 switch가 경고로 드러난다
};
패턴 매칭에서 실제로 달라지는 것
두 기능이 합쳐지면 패턴 매칭의 성격이 '편의 문법'에서 '완전성 도구'로 격상된다. C# 8 이후 switch 식과 프로퍼티/위치 패턴은 강력했지만, 완전성(exhaustiveness)은 늘 개발자 책임이었다. 이제 닫힌 타입 위에서는 컴파일러가 그 책임을 나눠 진다.
| 관심사 | 기존 방식(~C# 14) | C# 15 (union/closed) |
|---|---|---|
| '하나 중 하나' 표현 | 베이스+파생 or 플래그 필드 | union으로 직접 선언 |
| 완전성 검사 | 개발자 책임(default로 방어) | 닫힌 타입 위에서 컴파일러가 검사 |
| 케이스 추가 시 | 런타임 예외로 뒤늦게 발견 | 누락 switch가 컴파일 경고 |
| 불가능 상태 | 플래그 조합으로 표현 가능 | 타입상 표현 불가능 |
| 실패 처리 | 예외로 제어 흐름 분산 | 값으로 반환해 흐름 집중 |
실무 관점의 대표 수혜자는 Result 패턴이다. 예외 대신 성공/실패를 값으로 반환하는 스타일은 이미 많은 팀이 라이브러리(예: 커뮤니티의 Result 타입, OneOf 등)로 흉내 내왔다. union types는 그 패턴을 서드파티 없이 언어와 표준 라이브러리 차원에서 자연스럽게 표현하게 해 준다. 파서, 상태 기계, 표현식 트리, 프로토콜 메시지처럼 '형태가 유한하게 갈라지는' 도메인이 특히 크게 이득을 본다.
갱신된 unsafe 모델(프리뷰)
C# 15에는 갱신된 unsafe 모델이 프리뷰로 포함된다. '프리뷰'라는 꼬리표가 중요하다 — 아직 형태가 굳지 않았고, 프로덕션 의존을 전제로 설계된 상태가 아니라는 뜻이다. 방향은 저수준·고성능 코드(포인터, Span<T> 계열, 상호운용)에서 안전성과 표현력의 균형을 다시 손보는 쪽으로 알려져 있다.
여기서 구체적 문법·의미를 단정하는 것은 성급하다. 정확한 규칙은 프리뷰 문서와 후속 릴리스에서 확정되므로, 지금 단계에서는 'unsafe 관련 개선이 진행 중이며 실험적으로 켜볼 수 있다' 정도로 이해하고, 실제 채택은 안정화 이후로 미루는 편이 안전하다. 프리뷰 기능은 <LangVersion>preview</LangVersion>로 활성화하지만, 이는 어디까지나 실험·피드백 목적이다.
.NET 11 릴리스 맥락
C# 15는 진공에서 나오지 않는다. .NET 11과 함께 묶여 공개됐고, 언어 기능은 런타임·기본 클래스 라이브러리(BCL)·컴파일러(Roslyn)의 진화와 맞물린다. union types 같은 기능이 실용적이려면 BCL이 이를 활용하는 API를 제공하고, JIT/AOT가 새로운 타입 표현을 효율적으로 코드화해야 한다. 언어와 런타임이 한 배로 릴리스되는 이유다.
.NET의 버전 정책도 판단에 영향을 준다. .NET은 짝수 버전이 LTS(장기 지원), 홀수 버전이 STS(표준 기간 지원)로 번갈아 릴리스돼 왔다. 이 정책을 따른다면 .NET 11은 STS 성격에 해당하며, 지원 기간이 LTS보다 짧다. 새 언어 기능을 빨리 맛보려는 팀과, 지원 수명을 길게 가져가려는 팀의 선택이 갈리는 지점이다. 정확한 지원 기간·정책은 .NET 공식 지원 정책 문서로 확인하는 것이 좋다.
- 언어-런타임 동반 릴리스: C# 15의 신기능은 .NET 11의 컴파일러·BCL 지원을 전제로 한다.
- 지원 정책 확인: LTS/STS 여부에 따라 업그레이드 주기 계획이 달라진다.
- 프리뷰 vs 정식: union/closed는 정식 기능 방향, 갱신된 unsafe 모델은 프리뷰로 성격이 다르다.
마이그레이션·도입 시 고려
새 언어 기능은 '가능하다'와 '지금 도입한다'가 다르다. 실무 체크리스트는 다음과 같다.
1) LangVersion과 타깃 프레임워크. C# 15 문법은 프로젝트가 .NET 11(및 대응 SDK/Roslyn)을 타깃해야 온전히 쓸 수 있다. 프리뷰 기능은 <LangVersion>preview</LangVersion>로만 켜지며, CI/배포 파이프라인의 SDK 버전을 함께 올려야 빌드가 재현된다.
2) 기존 관용구와의 공존. abstract record+sealed record로 이미 대수적 데이터 타입을 흉내 낸 코드베이스라면, 그 계층을 닫힌 타입으로 점진 전환할 수 있다. 전면 재작성보다, 완전성 검사가 실수를 자주 부르던 도메인 모델부터 골라 적용하고 switch의 default 방어 가지를 걷어내는 순서가 안전하다.
3) 라이브러리 API 설계 주의. 공개 라이브러리의 베이스 타입을 닫으면, 소비자가 그 계층을 확장할 수 없게 된다. 이는 의도된 제약일 수도, 파괴적 변경일 수도 있다. 내부 도메인엔 닫힌 계층이, 확장 지점엔 열린 다형성이 맞다는 원칙을 API 경계마다 재확인해야 한다.
4) 직렬화·상호운용. union 타입 값을 JSON·gRPC·DB로 오갈 때, 어떤 케이스인지 판별하는 태그를 어떻게 표현할지 정해야 한다. 직렬화 라이브러리(System.Text.Json 등)의 union 지원 성숙도와, 다른 언어 클라이언트와의 계약을 함께 검토하라.
개발자에게 무슨 의미인가
요약하면, C# 15는 타입 시스템으로 도메인의 규칙을 더 강하게 못 박게 해 준다. 그동안 규율과 코드 리뷰로 지키던 '불가능한 상태를 만들지 말자', '모든 경우를 처리했는지 확인하자' 같은 약속을 컴파일러가 강제하도록 옮길 수 있다. 함수형 스타일과 객체지향 스타일 사이에서 C#은 다시 한 걸음 실용적 중간지대를 넓혔다.
다만 냉정하게, 이 기능들이 모든 코드를 바꾸지는 않는다. 데이터 형태가 유한하고 내가 통제하는 도메인 모델·프로토콜·상태 기계에서 크게 빛나고, 확장성이 본질인 경계에서는 오히려 쓰지 말아야 한다. 갱신된 unsafe 모델은 프리뷰이니 프로덕션 의존은 이르다. 새 도구가 늘 그렇듯, 가치는 '어디에 쓰느냐'에서 갈린다. 정확한 문법과 지원 정책은 Microsoft Dev Blogs와 .NET 공식 문서로 확인하고, 팀의 도메인 중 완전성 실수가 잦던 곳부터 작게 시작하기를 권한다.
자주 묻는 질문
C#의 union types는 F#의 discriminated union과 같은 건가요?
같은 계열의 아이디어(대수적 데이터 타입)입니다. F#·Rust·Swift가 오래전부터 제공해 온 '값이 유한한 형태 중 하나'라는 개념을 C#의 객체지향 문법과 조화시키려는 시도입니다. 다만 C#은 기존 클래스/레코드 생태계와의 상호운용, 직렬화, 널 처리 등을 함께 고려해야 해서 세부 문법과 의미는 F#과 다를 수 있습니다. 정확한 표기는 프리뷰에서 다듬어지므로 릴리스 노트를 확인하세요.
기존 abstract/sealed record 패턴을 지금 다 바꿔야 하나요?
아니요. 기존 관용구는 계속 유효합니다. 이득이 가장 큰 곳은 switch의 default 방어 가지가 실수를 자주 부르던 도메인 모델입니다. 그런 지점부터 닫힌 계층으로 점진 전환해 완전성 검사를 켜고, 안정적으로 동작하던 코드는 서두르지 않아도 됩니다. 전면 재작성은 위험 대비 이득이 낮습니다.
닫힌 계층을 쓰면 무조건 default 가지를 지워도 되나요?
컴파일러가 계층이 닫혀 있고 모든 케이스를 처리했다고 판단할 때만입니다. 리플렉션·소스 생성으로 런타임에 타입이 만들어지거나 외부에 상속을 열어둔 경우엔 완전성 보장이 약해집니다. 또한 방어적 코딩 차원에서 예상치 못한 값에 대한 처리를 남겨두는 편이 나은 상황도 있으니, 도메인 특성에 맞춰 판단하세요.
갱신된 unsafe 모델을 프로덕션에 써도 되나요?
권장하지 않습니다. C# 15에서 이 기능은 프리뷰이며, 문법·의미가 확정 전이라 향후 변경될 수 있습니다. 실험과 피드백 목적으로 LangVersion을 preview로 켜 시험해 볼 수 있지만, 프로덕션 코드가 프리뷰 동작에 의존하게 만드는 것은 위험합니다. 안정화 릴리스를 기다리는 편이 안전합니다.
.NET 11로 올리면 지원 기간은 어떻게 되나요?
.NET은 짝수 버전 LTS, 홀수 버전 STS로 번갈아 릴리스돼 왔고, 이 정책대로면 .NET 11은 STS로 LTS보다 지원 기간이 짧습니다. 새 언어 기능을 빨리 도입하려는 팀엔 매력적이지만, 지원 수명을 길게 가져가야 하는 서비스라면 업그레이드 주기를 함께 계획해야 합니다. 정확한 기간·정책은 .NET 공식 지원 정책 문서로 확인하세요.
C# 15 소식에 왜 OpenJDK 패치가 함께 언급되나요?
직접적 기술 연관은 없습니다. 다만 Microsoft가 같은 8월에 자사 OpenJDK 빌드의 크리티컬 보안 패치도 릴리스했기 때문에, 플랫폼 운영자 입장에서 함께 챙길 릴리스로 언급됩니다. .NET과 자바 워크로드를 동시에 운영한다면 해당 OpenJDK 패치 적용 여부를 별도로 확인하는 것이 좋습니다.

댓글 0