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는 기본적으로 모든 변수가 불변값이다. 변경할 수 없는 값이란 개념을 도입하는 것이 어떤 이득이 있을까?
- 상태state의 수를 줄인다.
- 변하는 값 대신 변하지 않는 값을 사용하면 당연하게도 그 값의 변화에 대해 신경을 쓸 필요가 없다.
- 어떤 프로그램/클래스/모듈 등을 읽을때의 복잡도는 일반적으로 상태의 수에 비례한다. 상태를 어디서 누가 어떻게 바꾸며, 그 상태가 바뀌었을때 어떤 효과가 발생하는지를 모두 기억해야 동작을 올바르게 이해할 수 있기 때문이다.
- 곧, 불변하는 값을 사용하면 머릿속의 watch창에서 항목 하나를 지워도 되고, 읽기 쉬워진다는 의미가 된다.
- 디버깅에 도움이 된다.
- 상태가 예상 밖의 값으로 바뀌거나, 실행 중 특정 상태들 간의 조합이 생겼을때 버그가 발생하는 일이 많다. 불변값을 사용하면 값이 예상하지 못하게 바뀌지 않는다고 가정할 수 있어서 디버깅이 보다 쉬운 경우가 많으며 재현에도 유리하다.
- 병렬 처리에 유리하다.
- 일반적으로 병렬처리에서 문제가 발생하는 부분은 하나의 변수를 두 군데 이상에서 동시에 수정하려고 하는 부분이다. 변수의 수정이라는 동작 자체가 없으므로 스레드 간 동기화도 생각 할 필요가 없다.
여러 가지 장점을 찾을 수 있지만, 대표적으로는 위와 같은 이점들을 얻을 수 있다. 크게 보면 결국 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로 쓸 경우, 불변값으로 인한 이점은 누릴 수 없으며, 단순히 해당 변수에 새로운 인스턴스를 할당하지 않는다는 선언 정도의 의미로 사용하여야 한다.
'C#' 카테고리의 다른 글
LINQ(2) - 일반적인 LINQ 코드 작성 원칙 및 유의점 (0) | 2024.03.21 |
---|---|
LINQ(1) - 함수형 프로그래밍에 대해서 (0) | 2024.03.09 |
C#의 프로퍼티와 필드는 뭐가 다를까? (0) | 2024.03.07 |
C#의 확장 메서드는 언제 사용하면 좋을까? (0) | 2024.02.24 |
IEnumerable, IEnumerator를 반환타입으로 가지는 메서드 파고들기 (0) | 2023.09.08 |