프로그래밍/C#

[C#] 불변성 처리

AlgorFati 2024. 3. 5. 20:39

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# 개발자들은 불변성의 힘을 활용하여 동시성 문제와 현대 소프트웨어 개발의 복잡성을 견딜 수 있는 코드를 만들 수 있다.