아이템 2: const보다 readonly를 사용하라

C#은 컴파일타임 상수와 런타임 상수 두 유형의 상수를 가진다.

이 둘은 서로 다르게 동작하기 때문에 적절하지 않은 상수 타입을 사용하면 상응하는 대가가 따른다.

컴파일타임 상수보다는 런타임 상수를 사용하라..!

컴파일타임 상수가 약간 더 빠르긴 하지만 런타임 상수에 비해 유연성이 상당히 떨어진다.

컴파일타임 상수는 성능이 매우 중요하고 상수의 값이 절대로 바뀌지 않는 경우에만 제한적으로 사용하는 것이 좋다.

컴파일타임 상수와 런타임 상수

런타임 상수는 readonly 키워드를 사용하여 선언하고, 컴파일타임 상수는 const 키워드를 사용한다.

// 컴파일타임 상수
public const int Millennium = 2000;

// 런타임 상수
public static readonly int ThisYear = 2004;

컴파일타임 상수는 내부에서 선언할 수 있지만, 런타임 상수는 메서드 내에서는 선언할 수 없다.

런타임 상수와 컴파일타임 상수가 서로 다르게 동작하는 이유는 값에 접근하는 방법이 서로 다르기 때문이다.

컴파일타임 상수는 컴파일타임에 변수가 값으로 대체된다.

if (myDataTime.Year == Millennium)

if (myDataTime.Year == 2000)

이 코드는 다음 코드와 정확히 동일한 IL(Intermediate Language)을 생성한다.

반면, 런타임 상수는 런타임에 값이 평가된다.

컴파일타임 상수와 런타임 상수의 차이

readonly 키워드를 이용하여 선언된 런타임 상수는 컴파일타임 상수처럼 컴파일타임에 값으로 대체되지 않고, 상수에 대한 참조로 컴파일된다.

이러한 차이로 인해 각각의 상수형은 서로 다른 한계를 가진다.

컴파일타임 상수는 내장된 숫자형, enum, 문자열, null에 대해서만 사용될 수 있다.

이는 내장 자료형이어야만 컴파일타임에 상수를 리터럴로 대체할 수 있기 때문이다.

다음 코드는 컴파일타임 상수를 초기화하려 했으나 내장 자료형이 아닌 DataTime 타입을 이용했기 때문에 컴파일 오류가 발생한다.

// 컴파일되지 않는다. 대신 readonly를 사용해야 한다.

private const DataTime classCreation = new DataTime(2000, 1, 1, 0, 0, 0);

런타임 상수는 생성자에서 초기화될 수 있으며 그 이후에는 수정될 수 없다.

또한 그 값이 런타임에 할당된다는 면에서 컴파일타임 상수와는 다르다.

이런 동작 방식의 차이로 인해 런타임 상수는 컴파일타임 상수보다 더 유연하게 될 수 있다.

먼저 런타임 상수는 어떤 타입과도 함께 사용될 수 있다.

또한 런타임 상수는 멤버 초기화 구문뿐 아니라 생성자를 통해서도 초기화 할 수 있다.

클래스 내에서 런타임 상수를 정의하는 경우라면 동일 클래스의 인스턴스라 하더라도 인스턴스별로 서로 다른 값을 가질 수 있다.

이에 반해 컴파일타임 상수에 따라 정적 상수이므로 모든 인스턴스가 동일한 값을 가진다.

런타임 상수를 참조하는 코드를 컴파일하면 컴파일타임 상수처럼 코드를 값으로 대체하지 않고, readonly 변수에 대한 참조 코드를 생성한다.

이러한 차이는 응용프로그램을 유지보수할 때 상당한 영향을 미친다.

컴파일타임 상수는 다른 어셈블리의 참조 여부와 상관없이 항상 숫자나 문자열 등을 직접 사용한 것과 동일한 IL코드를 생성한다.

컴파일타임 상수와 런타임 상수의 이러한 차이로 인해 간혹 호환성 문제가 발생하곤 한다.

어셈블리의 측면

public class UseFulValues
{
    public static readonly int StartValue = 5;
    public const int EndValue = 10;
}

다른 어셈블리에서 이 값들을 다음과 같이 사용한다고 해보자

for (int i = UseFulValues.StartValue; i < UseFulValues.EndValue; i++)
{
    Console.WriteLine("Value is {0}", i);
}
Value is 5
Value is 6

Value is 9

이제 Infrastrueture 어셈블리를 다음과 같이 수정하자

public class UseFulValues
{
    public static readonly int StartValue = 105;
    public const int EndValue = 120;
}

Infrastructure 어셈블리만 수정했으므로 프로그램 전체를 리빌드하지 않고 Infrastructure파일만 배포할 수도 있다.

하지만 실제로는 아무런 결과가 출력되지 않는다..!

시작 값의 startValue가 5에서 105로 변경되었지만, 종료조건의 EndValue는 여전히 10이다.

반복하지만 C#은 컴파일러는 const를 사용하는 컴파일타임 상수에 대해서는 참조 코드를 생성하지 않고 값으로 대체해버린다.

따라서 변경된 EndValue의 값을 참조하지 않고 앞서 컴파일 시점에 대체되었던 10으로 그 값이 유지된다.

반면 StartValue는 readonly를 사용하는 런타임 상수이기 때문에 컴파일 시에 StartValue에 대한 참조 코드가 생성되고 런타임에 비로소 그 값을 평가하게 된다.

이러한 이유로 응용프로그램을 완전히 리빌드하지 않더라도 관련 어셈블리의 변경 사항을 올바르게 반영할 수 있다.

정리

정리하자면 readonly대신 const를 사용했을 때 얻는 이점은 성능이 빠르다는 것이다.

하지만 기대값 만큼의 성능개선 효과가 크지 않고 무엇보다 유연성을 해치는 담정이 있다.

하지만 컴파일 단계에서 명확한 const는 사용할 필요가 있으며 ex) PI값..

이러한 몇가지 예외를 제외하면 대부분 const보다는 readonly를 사용하는 것이 좋다.

댓글남기기