본문 바로가기

C#

C#의 readonly 키워드와 불변성에 대해

readonly vs const

처음 readonly를 접하면, 자연스럽게 const와 비교하게 된다. 언뜻 보기에는 둘다 변하지 않는 값을 선언하는 문법으로, 큰 차이를 느낄 수 없다. 다른 점을 찾아봐도 주로 const는 컴파일 시간 상수고, readonly는 런타임 상수라는 정도, 혹은 readonly는 생성자에서 값을 변경할 수 있다 라는 정도로 이해하는 경우가 대다수이다.

 

그러나 이는 완전한 오해이다. const와 readonly는 의미적으로나 실제로나 완전히 다르다.

근본적인 차이점은 const는 상수이고 readonly는 변수라는 것이다. 

 

const는 일종의 별명과도 같다. const int a = 1; 이라는 문장이 있을 때, 이 것은 1의 별명을 a로 하겠다는 선언이다. 이는 재정의하거나 변경할 수 없으며 무조건 참인 진리이다. a는 곧 1이다. 그것이 상수constant의 의미이기도 하다. 따라서 인스턴스와 같은 개념 또한 당연히 없다. 

 

하지만 readonly는 이름에서 매우 직관적으로 알 수 있듯이, 쓸 수는 없고 읽을 수만 있는 변수이다. 여기서 중요한 차이가 발생한다. readonly는 상수가 아닌 단순한 변수이다. 단지 쓸 수는 없고 읽을 수만 있을 뿐이다. 좀 더 정확하게 표현하자면, 할당할 수 있고 읽을 수도 있지만 변경할 수는 없다. 이는 이 변수가 불변immutable임을 나타낸다. 단순한 변수일 뿐이므로, 원하는 만큼의 인스턴스를 선언할 수 있다. 변경을 제외한 모든 동작은 보통의 변수와 똑같다.

 

정리하면, readonly는 불변하는 값을 선언하는 문법이다. (변경할 수 없으므로, 변수라는 표현보다는 단순한 값value이라는 표현이 더 적절해 보인다.)

 

 


불변성

불편하게 왜 변수의 값을 변경하는 것을 막아버리는 기능을 굳이 만들었을까 의문이 들 것이다. 

 

최근에 개발되는 언어는 대부분 어떤 형식으로든 불변성을 지원한다. C#도 readonly에 이어 record등을 추가해서 불변성 지원을 강화중이다. Java에선 final 키워드로 불변성을 획득할 수 있다. Rust는 기본적으로 모든 변수가 불변값이다. 변경할 수 없는 값이란 개념을 도입하는 것이 어떤 이득이 있을까?

 

  1. 상태state의 수를 줄인다.
    • 변하는 값 대신 변하지 않는 값을 사용하면 당연하게도 그 값의 변화에 대해 신경을 쓸 필요가 없다. 
    • 어떤 프로그램/클래스/모듈 등을 읽을때의 복잡도는 일반적으로 상태의 수에 비례한다. 상태를 어디서 누가 어떻게 바꾸며, 그 상태가 바뀌었을때 어떤 효과가 발생하는지를 모두 기억해야 동작을 올바르게 이해할 수 있기 때문이다.
    • 곧, 불변하는 값을 사용하면 머릿속의 watch창에서 항목 하나를 지워도 되고, 읽기 쉬워진다는 의미가 된다.
  2. 디버깅에 도움이 된다.
    • 상태가 예상 밖의 값으로 바뀌거나, 실행 중 특정 상태들 간의 조합이 생겼을때 버그가 발생하는 일이 많다. 불변값을 사용하면 값이 예상하지 못하게 바뀌지 않는다고 가정할 수 있어서 디버깅이 보다 쉬운 경우가 많으며 재현에도 유리하다.
  3. 병렬 처리에 유리하다.
    • 일반적으로 병렬처리에서 문제가 발생하는 부분은 하나의 변수를 두 군데 이상에서 동시에 수정하려고 하는 부분이다. 변수의 수정이라는 동작 자체가 없으므로 스레드 간 동기화도 생각 할 필요가 없다.

 

여러 가지 장점을 찾을 수 있지만, 대표적으로는 위와 같은 이점들을 얻을 수 있다. 크게 보면 결국 2,3도 1의 부수적인 효과라고 볼 수도 있으므로 상태를 줄여서 순수함수를 늘린다는 점이 가장 큰 이점이라고 볼 수 있다.

 

그동안 readonly를 사용한 적이 없었다면 작성한 코드를 잘 살펴보자. 객체의 멤버 중 초기화 시점에 한번만 할당하며 객체의 생명주기 내내 값이 전혀 변하지 않는 변수를 생각보다 많이 찾을 수 있을 것이다. 이런 변수들이 readonly로 선언하기 적합한 변수들이다.


struct에서의 readonly

지금까지 쓴 readonly는 주로 클래스의 필드에 붙는 readonly에 관한 이야기였다. struct에서는 어떨까? struct에서의 readonly는 "이 메소드/필드/속성이 struct를 변경하지 않음" 이라는 의미를 가진다.

 

public struct S
{
    public readonly int A;  // A는 변경될 수 없다.
    public int B;           // B는 변경될 수 있다.

    public int IncA() => ++A;               // 컴파일 에러. A는 변경할수 없다.
    public readonly int Sum() => A + B;     // Sum메소드는 구조체의 상태를 변경하지 않는다.
    public readonly int IncB() => ++B;      // 컴파일 에러. 
    // IncB메소드는 구조체의 상태를 변경하지 않아야 하지만, 변경하고있다.	
}

 

struct의 모든 멤버가 readonly인 경우. struct 자체를 readonly struct로 선언할 수 있는데, 그렇게 해야만 비로소 struct를 온전히 불변하는 값으로 간주할 수 있다.


readonly 참조 타입

당연한 이야기일수도 있지만, C#에서 참조타입을 readonly로 선언했다고 참조하는 객체의 내부 상태가 바뀌는 것까지 막아주지는 못한다.

using System;
using System.Collections.Generic;
					
public class Program
{
    private static readonly List<int> _list = new List<int>() { 0 };
	
    public static void Main()
    {
        Console.WriteLine(_list.Count);     // 1
        _list.Add(1);
        Console.WriteLine(_list.Count);     // 2
    }
}

분명히 _list 변수 자체는 불변이지만, 리스트 안에 추가도, 삭제도 자유롭게 할 수 있다.

 

참조타입을 readonly로 쓸 때는 이런 부분에 충분한 주의를 기울여야만 한다.

일반적으로 참조타입을 readonly로 쓸 경우, 불변값으로 인한 이점은 누릴 수 없으며, 단순히 해당 변수에 새로운 인스턴스를 할당하지 않는다는 선언 정도의 의미로 사용하여야 한다.