C# 불변성 처리
소프트웨어 개발이 발전하는 과정 속에서, 불변성의 원칙은 견고하고 thread-safe한 애플리케이션을 만드는 기초로 자리잡았다. 불변성은 객체가 생성된 후 변경되지 않는 능력을 의미한다. 이 개념은 비록 간단해 보일지라도, 특히 동시성과 멀티 스레드 프로그래밍의 영역에서 많은 이점을 가져다 준다. 이 글에서는 불변성을 위한 C#의 새로운 기능들, 그리고 readonly 및 const와 같은 다른 C# 구문과의 차이점을 알아볼 것이다.
불변성의 장점
불변성의 주된 매력은 그 단순성과 예측 가능성에 있다. 한 번 초기화되면, 불변 객체는 그 상태가 변경되지 않을 것이라는 보장을 제공하여, 특히 여러 스레드가 동시에 데이터에 접근하는 환경에서 상태 관리와 관련된 다양한 버그의 가능성을 없앤다. 이러한 안정성 보장은 불변 객체를 본질적으로 thread-safe 하게 만들어, 복잡한 lock 메커니즘의 필요성을 줄이고 race condition의 위험을 크게 줄인다.
C#의 불변성을 위한 기능
C#은 불변성 패러다임을 받아들이며, 불변 객체의 생성과 관리를 용이하게 하는 다양한 기능과 도구를 제공한다.
Record Types
C# 9에서 소개된 레코드 타입은 불변성을 구현하고자 하는 개발자들에게 큰 도움이 된다. 데이터 모델링과 값 의미론을 염두에 두고 설계된 레코드는 불변 데이터 타입 선언을 단순화하며, 값 기반의 동등성 검사와 불변 객체 생성을 위한 간단한 문법을 자동으로 제공한다.
다음은 record type의 정의이다. 일반적인 객체와 달리 한 줄로 정의할 수 있다.
public record Person(string FirstName, string LastName, DateTime DateOfBirth);
다음은 record type의 사용 예제이다.
static void Main(string[] args)
{
// Creating instances of the Person record
var person1 = new Person("John", "Doe", new DateTime(1980, 1, 1));
var person2 = new Person("Jane", "Doe", new DateTime(1985, 5, 23));
// Displaying the person records using the built-in ToString implementation
Console.WriteLine(person1); // Output: Person { FirstName = John, LastName = Doe, DateOfBirth = 1/1/1980 12:00:00 AM }
Console.WriteLine(person2); // Output: Person { FirstName = Jane, LastName = Doe, DateOfBirth = 5/23/1985 12:00:00 AM }
// Demonstrating value-based equality
var person3 = new Person("John", "Doe", new DateTime(1980, 1, 1));
Console.WriteLine(person1 == person3); // Output: True, because person1 and person3 have the same values for all properties
// Creating a modified copy using with-expression
var person4 = person1 with { FirstName = "Jake" };
Console.WriteLine(person4); // Output: Person { FirstName = Jake, LastName = Doe, DateOfBirth = 1/1/1980 12:00:00 AM }
}
Immutable Collections
System.Collections.Immutable 네임스페이스는 ImmutableList, ImmutableDictionary 등과 같은 불변 컬렉션 클래스를 제공한다. 이 컬렉션들은 처음부터 불변으로 설계되었으며, 수정을 시도할 경우, 원본을 그대로 둔 채 새 컬렉션을 반환한다.
다음은 ImmutableList에 대한 예제이다.
using System;
using System.Collections.Immutable;
class Program
{
static void Main()
{
// Create an immutable list with some initial elements.
ImmutableList<int> immutableList = ImmutableList.Create(1, 2, 3);
// Display the initial list
Console.WriteLine("Original ImmutableList:");
foreach (var item in immutableList)
{
Console.WriteLine(item);
}
// Attempt to add a new element. This does not modify the original list but returns a new one.
ImmutableList<int> newList = immutableList.Add(4);
// Display the original list to show it has not changed
Console.WriteLine("\nOriginal ImmutableList after attempting to add:");
foreach (var item in immutableList)
{
Console.WriteLine(item);
}
// Display the new list to show the addition
Console.WriteLine("\nNew ImmutableList with the added element:");
foreach (var item in newList)
{
Console.WriteLine(item);
}
}
}
Init-Only Setters
C# 9에서도 init-only 설정자가 도입되었다.
init 키워드로 표시된 이 설정자들은 객체 초기화 중에 속성을 설정할 수 있게 하지만, 이후에는 불변성을 유지하게 한다.
이는 객체 생성 중에 유연성과 그 후의 불변성 사이의 균형을 이루게 한다.
C# 9에는 init 키워드로 표시된 init 전용 setter도 도입되었다.
이를 통해 초기화 중에 개체 속성을 설정할 수 있지만 이후에는 변경할 수 없게 하여, 개체 생성 중 유연성과 그에 따른 불변성 사이의 균형을 유지한다.
다음은 init only setters를 이용하여 불변타입을 초기화하는 예제이다.
public record Description(string Desc);
public class Library
{
public string Name { get; init; }
public ImmutableArray<string> Books { get; init; }
public Description Description { get; init; }
}
// how to instantiate
var library = new Library()
{
Name = "Downtown Library",
Books = ImmutableArray.Create(new[] { "Book1", "Book2", "Book3" }),
Description = new Description("blabla")
};
readonly 및 const와 비교
C#의 readonly 및 const 키워드는 불변성으로 가는 경로처럼 보일 수 있지만, 더 구체적이고 제한된 목적을 가지고 있다.
예를 들어, readonly 키워드는 선언 시 또는 같은 클래스의 생성자 내에서만 필드에 할당할 수 있도록 하지만, 참조하는 객체가 수정될 수 있는지는 보장하지 않는다.
다음은 readonly를 이용하는 경우에도 불변성을 갖추지 못할 수 있다는 것을 보여주는 예제이다.
public class ExampleClass
{
public readonly List<int> Numbers = new List<int>();
public ExampleClass()
{
// Allowed: Modifying the content of the readonly field.
Numbers.Add(1);
}
public void AddNumber(int number)
{
// Allowed: Because we're not changing the reference of the readonly field, just the object's state.
Numbers.Add(number);
}
}
위의 예에서 Numbers는 readonly이고 생성 후 다른 List<int> 인스턴스에 재할당될 수 없지만, List의 내용은 계속 수정할 수 있다. readonly로는 참조 타입에 대한 불변성을 갖출 수 없다.
const는 어떨까? const 키워드는 컴파일 시점 상수를 위해 사용되며, 컴파일 타입에 값이 결정된 후에 더 이상 값을 변경할 수 없다. 이렇게 보면 불변성을 갖출 수 있을 것 같지만, const 키워드는 해당 변수를 인스턴스에 속한 게 아닌 타입 그 자체에 속하도록 만들기 때문에 사용성에 문제가 생길 수 있다. 또한 컴파일 타입에 값이 고정되기 때문에 이들은 기본 타입들에 대해서만 적용이 가능하고, 오브젝트 타입과 같이 런타입에 생성되는 상황에는 적용이 불가능하다.
그에 반해, 불면성은 객체 전체와 그 필드의 상태가 생성 후 변경되지 않도록 하는 더 포괄적인 접근 방식이다. 이 차이점은 불변성이 더 넓은 범위를 가지며, 데이터 무결성과 스레드 안전성이 최우선인 복잡한 실제 애플리케이션에 적합함을 강조한다.
결론
C#에서 불변성은 안전하고 효율적이며 신뢰할 수 있는 애플리케이션을 구축하기 위해 개발자에게 필요한 도구를 제공하는 데 목표를 두고 있다. 레코드 타입, 불변 컬렉션 및 init-only 설정자와 같은 기능을 활용함으로써, C# 개발자들은 불변성의 힘을 활용하여 동시성 문제와 현대 소프트웨어 개발의 복잡성을 견딜 수 있는 코드를 만들 수 있다.
'프로그래밍 > C#' 카테고리의 다른 글
[C#] Nullable 타입 (0) | 2024.03.07 |
---|---|
[C#] 비동기 프로그래밍 (1) | 2024.03.04 |