본문 바로가기
프로그래밍/Effective C#

Effective C# Item 2 : const보다는 readonly가 좋다

by bantomak 2023. 10. 24.

const보다는 readonly가 좋다

C#은 컴파일타임 상수와 런타임 상수 두 유형의 상수를 지원한다. 이 둘은 서로 다르게 동작하기 때문에 적절하지 않은 상수 타입을 사용하면 상응하는 대가가 따른다.

 

컴파일타임 상수보다는 런타임 상수를 사용하라. 컴파일타임 상수가 약간 더 빠르긴 하지만 런타임 상수에 비해 유연성이 상당히 떨어진다. 컴파일타임 상수는 성능이 매우 중요하고 상수의 값이 절대로 바뀌지 않는 경우에만 제한적으로 사용하는 것이 좋다.

 

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

 

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

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

 

앞의 코드는 클래스나 구조체 내에서 사용할 수 있는 두 가지 상수 선언의 예를 보여준다. 컴파일타임 상수는 메서드 내부에서도 선언할 수 있지만, 런타임 상수는 메서드 내에서는 선언할 수 없다.

 

런타임 상수와 컴파일타임 상수가 서로 다르게 동작하는 이유는 값에 접근하는 방법이 서로 다르기 때문이다. 컴파일타임 상수는 컴파일타임에 변수가 값으로 대체된다. 다음을 보자.

 

if (myDateTime.Year == Millennium)

 

이 코드는 다음 코드와 정확히 동일한 IL(Intermediate Language) 코드로 컴파일된다.

 

if (myDateTime.Year == 2000)

 

반면, 런타임 상수는 런타임에 값이 평가된다. readonly 키워드를 이용하여 선언된 런타임 상수는 컴파일타임 상수처럼 컴파일타임에 값으로 대체되지 않고 상수에 대한 참조로 컴파일된다. 이러한 차이로 인해 각각의 상수형은 서로 다른 한계를 가진다. 컴파일타임 상수는 내장된 숫자형, enum, 문자열, null에 대해서만 사용될 수 있다. 이는 내장 자료형이어야만 컴파일타임에 상수를 리터럴로 대체할 수 있기 때문이다. 다음 코드는 컴파일타임 상수를 초기화하려 했으나 내장 자료형이 아닌 DateTime 타입을 이용했기 때문에 컴파일 오류가 발생한다.

 

// 컴파일이 되지 않는다. 대신 readonly를 사용해야 한다.
private const Datetime classCreation = new DateTime(2000, 1, 1, 0, 0, 0);

 

런타임 상수는 생성자에서 초기화될 수 있으며 그 이후에는 수정될 수 없다. 또한 그 값이 런타임에 할당되다는 면에서 컴파일타임 상수와는 다르다. 이런 동작 방식의 차이로 인해 런타임상수는 컴파일타임 상수보다 더 유연하게 활용될 수 있다. 

 

  • 런타임 상수는 어떤 타입과도 함께 사용될 수 있다. 예를 들어 DateTime 구조체는 readonly로 선언할 수 있지만 const로는 선언할 수 없다. 또한 런타임 상수는 멤버 초기화 구문뿐 아니라 생성자를 통해서도 초기화할 수 있다.
  • 클래스 내에서 런타임 상수를 정의하는 경우라면 동일 클래스의 인스턴스라 하더라도 인스턴스별로 서로 다른 값을 가질 수 있다. 반면 컴파일타임 상수는 정의에 따라 정적 상수이므로 모든 인스턴스가 동일한 값을 가질 수밖에 없다.
  • 런타임 상수는 상수의 값이 런타임에 평가된다. 런타임 상수를 참조하는 코드를 컴파일하면 컴파일 타임 상수처럼 코드를 값으로 대체하지 않고, readonly 변수에 대한 참조 코드를 생성한다.

이러한 차이는 응용프로그램을 유지보수할 때 상당한 영향을 미친다. 컴파일타임 상수는 다른 어셈블리의 참조 여부와 상관없이 항상 숫자나 문자열 등을 직접 사용한 것과 완전히 동일한 IL 코드를 생성한다.

 

컴파일타임 상수와 런타임 상수의 이러한 차이로 인해 간혹 호환성 문제가 발생하곤 한다. Infrasructure라는 어셈블리 내에 const와 readonly 필드를 다음과 같이 정의했다고 하자.

 

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

 

이제 다른 어셈블리에서 이 값들을 다음과 같이 사용한다고 가정하자.

 

for (int i = UseFfulValues.StartValue; i < UsefulValues.EndValue; i++)
{
   Console.WriteLine("value is {0}", i);
}

 

이 코드를 수행해 보면 다음 결과가 출력될 것이다.

 

Value is 5

Value is 6

...

Value is 9

 

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

 

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

 

Infrastructure 어셈블리만 수정했으므로 응용프로그램 전체를 리빌드 하지 않고 리빌드 한 Infrastructure 파일만 배포할 수도 있다. 아마 다음 결과를 기대했을지 모르겠다.

 

Value is 105

Value is 106

...

Value is 119

 

하지만 실제로 수행해 보면 아무런 결과도 출력되지 않는다. for 루프는 시작 값으로는 수정된 StartValue의 값인 105가 사용되지만, 종료 조건에서 사용한 EndValue 값은 수정하기 이전 값인 10을 계속해서 사용하기 때문이다. 반복해서 이야기하지만 C# 컴파일러는 const를 사용하는 컴파일타임 상수에 대해서는 참조 코드를 생성하지 않고 값으로 대체해 버린다.

따라서 변경된 EndValue의 값을 참조하지 않고 앞서 컴파일 시점에 대체되었던 10으로 그 값이 유지된다. 반면 StartValue는 readonly를 사용하는 런타임 상수이기 때문에 컴파일 시에 StartValue에 대한 참조 코드가 생성되고 런타임에 비로소 그 값을 평가하게 된다. 이런 이유로 응용프로그램을 완전히 리빌드 하지 않더라도 관련 어셈블리의 변경 사항을 올바르게 반영할 수 있다. 즉 수정된 Infrastructure 어셈블리만을 배포하는 것만으로도 전체 응용프로그램이 수정된 값을 사용하게 된다. public으로 선언된 컴파일타임 상수의 값을 수정할 때는 타입의 인터페이스를 변경하는 것만큼이나 신중해야 하며 해상 상수를 참조하는 모든 코드를 반드시 재컴파일해야 한다. 하지만 런타임 상수를 사용하는 경우에는 값을 변경하는 것만으로 족하며 기존 코드와의 이진 호환성도 그대로 유지된다.

 

readonly 대신 const를 사용했을 때 얻을 수 있는 장점은 성능이 빠르다는 것이다. 상숫값으로 코드를 대체하면 readonly 변수를 통해 값을 참조하는 것보다 빠를 수밖에 없다. 하지만 이를 통해 얻을 수 있는 성능 개선 효과가 크지 않고 무엇보다 유연성을 해치는 단점이 있다. 유연성을 포기하기 이전에 어느 정도 성능 향상이 이루어질 수 있을지 반드시 성능을 측정해 볼 것을 권한다.

 

컴파일할 때 사용되는 상숫값을 정의할 때는 반드시 const를 사용해야 하는 경우가 있다. 특성(attribute)의 매개변수,  switch/case 문의 레이블. enum 정의 시 사용하는 상수 등은 컴파일 시에 사용돼야 하므로 반드시 const를 통해 초기화돼야 한다. 이러한 용도로 활용되는 상숫값들은 거의 수정되지 않는다. 몇 가지 예외적인 상황을 제외한다면 대부분의 경우 const 보다 readonly를 사용하는 것이 좋다.

 

 

출처

 

이펙티브 C# - 예스24

더 나은 C# 코드를 작성하는 새로운 방법 50가지 C#은 전통적인 .NET 기반 개발에서 유니티 게임 엔진으로 개발 영역을 확대하면서 더욱 주목받고 있다. 또한 자마린으로 다양한 모바일 플랫폼에

댓글