02 7월 2025
이 기사에서는 C# 11과 12에 도입된 몇 가지 새로운 기능을 살펴보며, 이를 통해 코드가 간소화되고 개발이 더 원활해지는 방법을 알아보겠습니다. 이러한 업데이트는 혁신적이지 않을 수 있지만, 불필요한 복잡성을 줄여 시간을 절약할 수 있도록 설계된 실용적인 기능들입니다. 일상적인 코딩 작업에서 작은 변화가 어떻게 더 깔끔하고 효율적인 솔루션으로 이어지는지 확인해 보겠습니다.
C#에서 복잡한 내용을 포함한 문자열을 작성하는 것은 역사적으로 어려운 과제였습니다. 개발자들은 종종 특수 문자, 개행 문자, 따옴표를 이스케이프 처리해야 했으며, 이로 인해 코드가 장황하고 읽기 어려워졌습니다. 특히 JSON, XML, 정규 표현식과 같은 형식을 소스 파일에 직접 포함할 때 이 과정이 더욱 번거로워졌습니다.
C# 11은 이러한 문제를 직접 해결하기 위해 원시 문자열 리터럴을 도입했습니다. 이 기능은 문자열이 여러 줄에 걸쳐 있을 수 있고, 이스케이프 시퀀스 없이 따옴표나 백슬래시를 포함한 거의 모든 문자를 포함할 수 있게 합니다. 원시 문자열 리터럴은 최소 세 개의 큰따옴표("""
)로 시작하고 끝납니다.
C# 11 이전:
string oldJson = "{\r\n \"name\": \"Alice\",\r\n \"age\": 30\r\n}";
Console.WriteLine(oldJson);
C# 11 이후:
string newJson = """
{
"name": "Alice",
"age": 30
}
""";
Console.WriteLine(newJson);
종료 따옴표 앞의 모든 공백은 문자열의 최소 들여쓰기를 정의하며, 컴파일러는 최종 출력에서 이를 제거합니다. 원시 문자열 리터럴은 문자열 가독성을 크게 향상시키고 구문 오류 가능성을 줄여줍니다.
C#의 패턴 매칭은 크게 발전해 왔으며, C# 11에서는 배열이나 리스트 내에서 시퀀스 매칭을 가능하게 하는 리스트 패턴이 도입되었습니다. 이 개선은 개발자가 컬렉션의 구조와 내용을 간결하고 표현력 있게 검사할 수 있도록 합니다.
이전에는 컬렉션 구조를 검증하려면 길이와 개별 인덱스를 수동으로 확인해야 했고, 이는 코드가 장황하고 유지보수가 어려워졌습니다. 리스트 패턴은 상수, 타입, 속성, 관계 패턴과 같은 하위 패턴을 지원하여 이를 해결합니다. 주요 기능으로는 단일 요소를 매칭하는 버림 패턴(_
)과 0개 이상의 요소 시퀀스를 매칭하는 범위 패턴(..
)이 있습니다.
C# 11 이전:
int[] numbers = { 1, 2, 3 };
if (numbers != null && numbers.Length == 3 &&
numbers[0] == 1 && numbers[1] == 2 && numbers[2] == 3)
{
Console.WriteLine("배열이 정확히 1, 2, 3을 포함합니다.");
}
if (numbers != null && numbers.Length >= 2 && numbers[1] == 2)
{
Console.WriteLine("배열의 두 번째 요소가 2입니다.");
}
C# 11 이후:
int[] numbers = { 1, 2, 3 };
if (numbers is [1, 2, 3])
{
Console.WriteLine("배열이 정확히 1, 2, 3을 포함합니다.");
}
if (numbers is [_, 2, ..])
{
Console.WriteLine("배열의 두 번째 요소가 2입니다.");
}
리스트 패턴은 시퀀스 검증을 간결하고 읽기 쉬운 형태로 간소화하여 이러한 작업에 필요한 코드 라인을 크게 줄여줍니다.
객체 초기화는 때때로 필수 속성이나 필드가 할당되지 않은 바람직하지 않은 상태로 이어질 수 있습니다. 전통적으로 개발자들은 모든 필수 매개변수를 받는 생성자를 통해 또는 메서드 내에서 방어적 검사를 추가하여 필수 초기화를 강제했습니다.
C# 11은 속성과 필드에 대한 required
수정자를 도입하여 컴파일 타임에 강제하는 메커니즘을 제공합니다. 멤버가 required
로 표시되면 컴파일러는 객체 생성 중 생성자나 객체 초기화자를 통해 값이 할당되도록 보장합니다. 이는 타입의 인스턴스가 항상 유효하고 완전히 초기화된 상태임을 보장하여 데이터 누락과 관련된 일반적인 버그를 방지합니다.
C# 11 이전:
public class OldPerson
{
public string FirstName { get; set; }
public string LastName { get; set; }
public void DisplayName() => Console.WriteLine($"이름: {FirstName} {LastName}");
}
// 사용 예:
var person = new OldPerson(); // 컴파일 타임 오류 없음, 하지만 잠재적으로 유효하지 않은 객체 생성
person.DisplayName();
C# 11 이후:
public class NewPerson
{
public required string FirstName { get; init; }
public required string LastName { get; init; }
public void DisplayName() => Console.WriteLine($"이름: {FirstName} {LastName}");
}
// 사용 예:
// var person = new NewPerson(); // 컴파일 오류 - 필수 속성 누락
// var person = new NewPerson { FirstName = "John" }; // 컴파일 오류 - LastName 누락
var person = new NewPerson { FirstName = "Jane", LastName = "Doe" }; // 정상
person.DisplayName();
필수 멤버는 컴파일 타임에 초기화를 강제하여 런타임 문제를 제거하고 수동 검사의 필요성을 줄입니다. 이 기능은 방어적 코딩을 줄이고 코드 신뢰성을 높여 개발자가 검증보다 기능에 집중할 수 있도록 합니다.
C# 12는 모든 클래스와 구조체에 기본 생성자를 도입하며, 이전에는 레코드 타입에만 국한되었던 기능을 확장합니다. 이를 통해 생성자 매개변수를 타입 정의에 직접 선언할 수 있으며, 이는 자동으로 전체 클래스에 걸쳐 필드나 속성으로 범위가 지정됩니다. 전통적인 생성자와 달리 이 접근 방식은 명시적 필드 선언과 수동 할당을 건너뜁니다.
여기서 해결된 주요 문제는 객체 초기화의 반복적인 보일러플레이트 코드입니다. 이전에는 개발자가 비공개 필드를 정의하고 생성자 인수를 명시적으로 매핑해야 했으며, 이는 불필요하게 코드 크기를 늘렸습니다. 기본 생성자는 이를 간소화하여 초기화 논리를 타입 시그니처에 직접 포함합니다.
C# 12 이전:
public class OldProduct
{
private readonly int _productId;
private readonly string _productName;
public OldProduct(int productId, string productName)
{
_productId = productId;
_productName = productName;
}
public string PrintDetails() => $"제품 ID: {_productId}, 이름: {_productName}";
}
// 사용 예:
OldProduct oldProd = new OldProduct(101, "노트북");
oldProd.PrintDetails();
C# 12 이후:
public class NewProduct(int productId, string productName)
{
public string PrintDetails() => $"제품 ID: {productId}, 이름: {productName}";
}
// 사용 예:
NewProduct newProd = new NewProduct(102, "키보드");
newProd.PrintDetails();
기본 생성자는 데이터 중심 타입의 정의를 매우 간결하게 만듭니다. 이는 필수 구성 매개변수를 타입 이름 바로 옆에 배치하여 가독성을 높이고 클래스나 구조체의 종속성을 한눈에 명확히 보여줍니다.
C#에서 컬렉션 초기화는 리스트의 경우 new List<T> { ... }
, 배열의 경우 new T[] { ... }
와 같이 컬렉션 타입에 따라 다양한 구문을 사용해야 했습니다. 기존 컬렉션을 새로운 컬렉션으로 결합하거나 병합하려면 반복 루프나 Concat()
과 같은 LINQ 메서드가 필요했으며, 이는 오버헤드와 장황함을 추가했습니다.
C# 12는 다양한 컬렉션 타입을 생성하고 초기화하는 통합되고 간결한 구문인 컬렉션 표현식을 도입합니다. 간단한 [...]
구문을 사용하여 개발자는 배열, 리스트, Span<T>
및 기타 컬렉션 유사 타입을 만들 수 있습니다. 새로운 확산 요소(..
)는 기존 컬렉션의 요소를 새로운 컬렉션 표현식에 직접 인라인으로 추가할 수 있게 하여 수동 연결의 필요성을 없앱니다.
C# 12 이전:
// 다양한 컬렉션 초기화
int[] initialNumbers = new int[] { 1, 2, 3 };
List<int> moreNumbers = new List<int> { 4, 5 };
// 컬렉션 결합
List<int> allNumbers = new List<int>();
allNumbers.AddRange(initialNumbers);
allNumbers.AddRange(moreNumbers);
allNumbers.Add(6);
allNumbers.Add(7);
Console.WriteLine(string.Join(", ", allNumbers));
C# 12 이후:
// 다양한 컬렉션 초기화
int[] initialNumbers = [1, 2, 3];
List<int> moreNumbers = [4, 5];
// 확산 연산자를 사용한 컬렉션 결합
List<int> allNumbers = [..initialNumbers, ..moreNumbers, 6, 7];
Console.WriteLine(string.Join(", ", allNumbers));
컬렉션 표현식은 컬렉션 초기화 및 결합의 장황함을 줄여 더 깔끔하고 직관적인 구문을 제공합니다. 이 효율성은 코딩을 가속화하고 가독성을 높이며, 적은 코드로 더 큰 영향을 줄 수 있는 원칙을 지원합니다.
C#에서 함수형 프로그래밍의 초석인 람다 표현식은 역사적으로 매개변수에 기본값을 정의할 수 없었습니다. 람다가 선택적 인수를 처리하거나 대체 값을 제공해야 하는 경우, 개발자는 람다 본문 내에서 조건부 논리를 사용하거나 여러 오버로드를 정의해야 했지만, 람다는 직접 오버로드를 지원하지 않습니다.
C# 12는 람다 표현식의 매개변수에 기본값을 허용함으로써 이 간극을 메웁니다. 구문과 동작은 메서드나 로컬 함수 매개변수의 것과 유사하여 유연한 람다 함수를 정의하는 더 유창하고 간결한 방법을 제공합니다.
C# 12 이전:
// 기본 매개변수가 없는 람다.
// 'y'에 기본값이 필요할 경우 종종 래퍼나 조건부 논리가 필요했습니다:
Func<int, int, int> addOld = (x, y) => x + y;
Func<int, int> addWithDefaultOld = x => addOld(x, 10); // 일반적인 해결 방법
Console.WriteLine(addOld(5, 3));
Console.WriteLine(addWithDefaultOld(5));
C# 12 이후:
// 기본 매개변수가 있는 람다
Func<int, int, int> addNew = (x, y = 10) => x + y;
Console.WriteLine(addNew(5, 3)); // y는 3
Console.WriteLine(addNew(5)); // y는 기본값 10
람다에 기본 매개변수를 도입함으로써 유연성과 표현력이 크게 향상됩니다. 이는 중복 람다 정의나 내부 조건부 논리의 필요성을 줄여줍니다.
C# 11과 12는 "적게 쓰고 더 많이 하자"라는 약속을 충실히 이행하는 매력적인 기능들을 제공합니다. C# 11의 원시 문자열 리터럴과 리스트 패턴�부터 C# 12의 기본 생성자와 컬렉션 표현식에 이르기까지, 이러한 발전은 일상 코딩에서 겪는 실제 좌절감을 해결합니다. 이는 불필요한 구문을 제거하고 가독성을 높이며 더 안전한 패턴을 강제하여 소프트웨어 개발 및 코드 변환 프로젝트의 워크플로우를 직접적으로 향상시킵니다. 필수 멤버를 강제하거나 컬렉션 설정을 단순화하는 등의 각 혁신은 키 입력을 줄이면서 명확성을 극대화하고 오류 위험을 최소화합니다.