[C# 기초강좌] 13. C# 상수 선언, const 와 readonly

Posted in SW개발 // Posted at 2023. 11. 6. 13:54
728x90
이 글은 제가 과거에 운영했던 사이트인 http://dotnet.mkexdev.net 의 글을 옮겨온 것입니다. 원본 글은 2010년에 작성되었습니다.

그 전에 운영했었던 사이트(mkex.pe.kr)은 흔적도 없이 사라 졌습니다. 그속의 글들도 모두... 그래서 이 사이트도 사라지기 전에 옮기고 싶은 글을 조금씩 이 블로그로 이동시키려 합니다.
(원본글) http://dotnet.mkexdev.net/Article/Content.aspx?parentCategoryID=1&categoryID=5&ID=677

이 글은 닷넷 기초 지식을 전파하기 위해 2010경에 작성되었으며, 당시 윤성우의 프로그래밍 스터디그룹 네이버 카페에도 필진으로 참여하여 연재했던 글이기도 합니다. 현재 시점에서 조금 달라진 부분이 있을 수 있으나 기본 원리와 언어 기초에 해당하는 부분은 크게 변하지 않았을 것으로 생각하며 이런 부분들을 감안해서 봐 주시기 바랍니다.

 

상수 선언, const  readonly”

 

안녕하세요. 박종명입니다. 닷넷 열세 번째 강좌를 진행하도록 하겠습니다.

이번 강좌는 C#에서 상수를 선언하는 키워드인 const  readonly 에 대해 알아 보겠습니다.

상수 선언에 있어 왜 두 가지 키워드를 제공해 주는 걸까요?

두 키워드의 설명과 사용법 그리고 차이점에 대해 하나씩 살펴보도록 하겠습니다


 

변하는 수 혹은 변경 가능한 수를 변수라 합니다. 프로그램을 개발할 때 필수로 사용되는 한 요소이죠.

상수는 그 반대 입니다. 상수는 변하지 않는 수’, ‘변경할 수 없는 수를 말합니다.

 

영어로 변함없는 이라는 뜻을 가진 constant 로 해석되며 한자로는 항상 상()자를 이용하여 상수(常數) 라 합니다.

 

어떤 수를 한번 정의 해 두면 더 이상 변경할 필요가 없는 것을 상수로 선언해 사용하게 되는데요.

대표적으로 이러한 류의 데이터 예로 원주율인 π(파이) 를 들 수 있겠습니다.

 

private const PI = 3.14;

 

원주율 값이 정해져 있기 때문에 한번 그 값이 설정되면 프로그램 수행 도중 다른 곳에서 수정할 이유가 없는 것이죠.

따라서 상수로 사용하는 좋은 예입니다

 

이처럼 변하지 않는 원주율 값을 상수로 선언해 두면 이 값을 참조하기 위해 어느 곳에서든 PI 를 이용할 수 있습니다.

물론 이 경우 상수로 선언하지 않아도 원주율이 필요한 곳에서 3.14 란 값을 직접 사용할 수도 있습니다.

그러나 프로그램의 직관성 및 효율성을 위해 의미 있는 이름의 상수 선언을 권장합니다.

 

 

const

영어로 상수란 뜻을 가진 단어인 constant의 표현 그대로가 사용된 대표적인 상수 선언 키워드 입니다.

간단히 아래와 같이 상수 값을 선언할 수 있습니다

 

class MyClass{

    public const int X = 1;       

}

 

const 상수의 하나씩 살펴 보도로 하겠습니다.

 

 

const 상수는 반드시 선언 시 그 값을 할당해야 한다

일반적인 멤버 변수는 선언 시 값을 할당하지 않아도 자료형에 따른 기본 값이 자동 할당됩니다.

반면 const 상수는 선언 시 반드시 그 값을 할당(초기화)해야 합니다.

 

 

const 상수는 한번 값이 할당되면 이후 변경 불가능하다

당연한 예기죠. 상수니깐요. 당연한 것을 굳이 설명하는 이유는 이후 알아볼 readonly의 차이점 때문입니다.

 

 

const 상수는 자동으로 static 이다

const 로 선언한 상수는 자동으로 static 변수가 됩니다. 즉 정적 상수의 역할을 하게 되는 것이죠.

다음과 같이 const 상수와 일반 변수를 선언하고 IL코드를 보면 const 상수는 자동으로 static 키워드가 추가된 것을 확인할 수 있습니다

 

class MyClass{       

    public const int X = 1;

    public int Y = 1;

}

 

 

자동으로 static 이기 때문에 코드에서 명시적으로 static 를 지정할 수 없습니다.

public static const int X = 1; //컴파일 오류 발생

 

 

참조 형식 상수 선언

참조 타입은 const 일 수 없습니다. 다만 string 객체는 가능합니다.

엄밀히 말하면 참조 타입이 const일 수 없다기 보다는 const 로 선언된 참조 타입의 값은 null 일수 밖에 없다는 것입니다.

 

동적으로 메모리에 할당되는 객체와 같은 참조타입을 const 로 선언하여 초기화 할 수 없습니다

예를 들어 다음과 같이 배열(참조타입) const로 선언하여 초기화 할 수 없습니다

 

public const int[] array = new int[] { 1, 2 }; //컴파일 오류 발생

 

이는 const 상수의 값은 반드시 컴파일 시점에 결정되어야 하는, 다시 말해 컴파일 타임에 완전하게 계산될 수 있는 값이어야 하기 때문입니니다. 컴파일러는 const 상수를 컴파일 시 그 값을 어셈블리의 메타데이터에 바로 기록하도록 합니다.

 

그러나 한가지 예외가 있는데요. 바로 문자열 객체입니다.

문자열(string) 역시 참조타입 이지만 const 로 선언할 수 있습니다. 문자열은 컴파일 시 그 값을 메타데이터에 정확히 기록할 수 있기 때문에 상수로 사용이 가능합니다

 

public const string S = "MKEX"; //사용 가능함

 

더불어 참조 형식을 const 로 선언할 경우 null로 초기화는 가능합니다.

public const MyClass myClass = null; //사용 가능함

 

그러나 상수는 이후에 변경이 불가능하기 때문에 이러한 코드는 무의미 할 수 있습니다. 따라서 참조타입 상수화는 문자열을 제외하고는 잘 사용하지 않습니다.

 

 

const 상수는 참조형태로 전달할 수 없습니다

앞의 내용과 연관되는 부분인데요.

const 상수는 ref  out 키워드를 사용하여 참조형태로 값을 전달할 수 없습니다.

 

앞서 설명한대로 const 상수는 컴파일 시 메타데이터에 그 값이 직접 기록되기 때문에 런타임 시에 이 상수에 대한 메모리 할당은 이루어지지 않습니다. 따라서 주소 값을 기반으로 하는 ref와 같은 참조형태로 값을 전달할 수 없는 것입니다.

 

상수를 선언하고 그 상수를 사용할 경우 IL 코드를 보겠습니다

 

다음과 같이 const 상수 X 10으로 초기화 되어 있고

class MyClass{

    public const int X = 10;

}

 

이 상수 X를 사용하는 다음 어셈블리의 IL코드를 보면 아래 그림과 같습니다.

class Program { 

    static void Main(string[] args){

        Console.WriteLine(MyClass.X);                                                

    }

}

 

 

Main 메서드의 메타데이터인데요,

상수 X의 실제 값인 10 이 메타데이터로 바로 기록된 것을 확인할 수 있습니다.

 

 

상수 선언에 다른 상수를 사용할 수 있습니다.

다음 코드와 같이 상수를 선언할 때 이미 정의된 다른 상수를 대입하여 상수식을 만들 수 있습니다.

 

class MyClass{

    public const int X = 1;

    public const int Y = X * 2;                       

}

 

 

readonly

닷넷은 상수 선언을 위해 const 외에 readonly 라는 키워드도 추가로 제공합니다.

상수면 상수지 왜 두 가지 버전의 상수를 선언하도록 하는 것일까요?

 

그것은 바로 상수 값의 초기화 방법에 대한 확장을 주기 위함입니다.

앞서 const 는 선언과 동시에 값을 할당, 즉 초기화 해야 한다고 했습니다.

 

그러나 readonly는 여기에 더불어 생성자에서 한번 더 값을 할당할 수 있는 확장성을 제공합니다.

 readonly 상수는 선언할 때 값을 할당할 수도 있으며 생성자에도 할당할 수 있습니다.

 

다음코드처럼 readonly 상수인 X는 선언 시 초기화 하고 더불어 생성자에서도 그 값을 변경할 수 있습니다.

 

class MyClass{       

    public readonly int X = 1;

       

    public MyClass(int i){

        X = i; //생성자를 통해 상수 읽기전용 X의 값을 할당한다

    }                                 

}

 

결과적으로 readonly 상수의 값을 생성자에 따라 다르게 구현할 수 있는 확장성이 제공되는 것입니다.

그래서 const 를 컴파일 타임 상수라고 하며 readonly 를 런타임 상수라고 부르기도 합니다.

 

readonly 상수의 특징을 하나씩 살펴 보도로 하겠습니다.

 

 

readonly 상수는 선언 시 값을 할당하지 않아도 됩니다.

const 상수는 선언과 동시에 값을 초기화해야 하지만 readonly 상수는 선언 시 초기화가 의무는 아닙니다.

 readonly 상수를 선언할 때 값을 할당하지 않으면 클래스 멤버변수와 같이 타입에 맞는 기본 값이 자동 할당됩니다.

 

 

readonly 상수는 생성자에서 한번 더 그 값을 변경할 수 있습니다.

const 는 선언 시 할당된 값은 이후 변경이 불가능한 반면 readonly 상수는 선언 시 값을 초기화 했다고 하더라도 생성자에서 한번 더 그 값을 변경할 수 있습니다. 이것이 const 와의 핵심적인 차이라 하겠습니다.

 

 

readonly 상수는 static 이 아닙니다

const 는 자동으로 static 이지만 readonly 는 그렇지 않습니다const 가 클래스 상수라면 readonly 는 객체 상수라고 할 수 있습니다.

 readonly 상수는 클래스의 인스턴스로 생성된 객체를 통해서 접근할 수 있습니다.

 

아래 IL코드를 보면 readonly 로 선언된 상수 X static 아 아님을 알 수 있습니다.

 

다만 readonly 상수를 정적 상수로 만들고 싶으면 static 키워드를 명시해 주면 됩니다. 

만일 readonly  static키워드와 같이 사용하여 정적 상수로 만들 경우에는 일반 생성자가 아닌 정적 생성자를 통해서만 값을 변경할 수 있습니다

 

다음 코드를 보면 X 가 정적 상수로 선언되어 있습니다. 그리고 정적 생성자를 통해 그 값을 한번 더 변경하도록 합니다.

 

class MyClass{

    public static readonly int X = 1;

 

    static MyClass()

    {

        X = 2; //static로 선언된 readonly상수는 static 생성자를 통해 값을 변경할 수 있다다

    }

}

 

 

참조 형식 상수 선언

const 에서는 문자열(string) null을 제외하고는 참조 타입을 초기화 할 수 없는 반면 readonly는 참조타입의 객체를 생성할 수 있습니다. 즉 다음과 같은 선언이 가능합니다.

 

public readonly int[] array = new int[] { 1, 2 };

 

따라서 readonly는 상수라는 표현보다는 읽기전용 필드라고 표현하는 것이 일반적입니다.

 

한가지 예외, 리플렉션

readonly 로 선언된 읽기전용 필드는 선언 시 한번 그리고 생성자에서 한번 해서 총 두 지점에서만 값의 초기화가 가능하다고 하였습니다.

 

그러나 리플렉션 기법을 이용하면 실행 중에도 이 값을 변경할 수 있습니다.이것은 예외 적인 상황이라 할 수 있는데요. 사실 리플렉션은 일반적인 클래스 사용 패턴이 아니기 때문에 굳이 예외적이라기 보다는 특수한 형태라는 표현이 더 맞겠네요. 리플렉션에 관해서는 다음 기회에 알아 보도록 하겠습니다.

 

 

 

const 보다는 readonly를 사용하라???

닷넷 개발자들이 자주 하는 말입니다. ‘상수를 사용할 거면 const 보다는 readonly를 사용하라 입니다.

이것은 다중 어셈블리환경, 어셈블리 간 참조로 구성된 프로그램일 경우 어셈블리 버전 문제에 해당되는 내용입니다.

 

만일 A라는 어셈블리에 const 상수가 존재하고 B라는 어셈블리에서 A를 참조해서 상수를 사용하는 경우에 상수 값이 변경되어 A 어셈블리를 다시 컴파일 해서 배포할 경우 B 어셈블리에서는 이 내용이 갱신되지 않는 문제가 발생합니다. 즉 버전 변경에 따른 예기치 않은 결과가 나타나는 것이죠.

 

이 경우 A 어셈블리뿐만 아니라 이를 참조하는 B 어셈블리도 같이 재 컴파일 해야만 문제를 해결할 수 있습니다.

 

이것은 앞서 설명한대로 const 상수는 이를 사용하는 어셈블리의 메타데이터로 그 값이 직접 기록되기 때문에 발생하는 문제입니다.  A 어셈블리는 외부어셈블리이지만 B 어셈블리의 메타데이터로 상수 값 자체가 기록되어 버리기 때문에 A 어셈블리의 버전 업이 B어셈블리의 메타데이터를 변경하지는 못하는 것입니다.

 

만일 A 어셈블리의 상수가 readonly 필드로 정의되어 있다면 이러한 문제로부터 자유로워 집니다.

 readonly 필드의 값은 B 어셈블리의 메타데이터에 바로 기록되는 것이 아니라 외부 어셈블리를 참조하도록 기록되기 때문에 A어셈블리의 재 컴파일 만으로도 변경된 값이 적용되는 것입니다.

 

이런 이유로 해서 ‘const 보다는 readonly를 사용하라 라고 하는 것입니다.

만일 이런 상황이 아니라면 굳이 readonly 가 더 좋을 이유는 없습니다.

 

자세한 내용이 궁금하시면 아래 글을 참고해 보기 바랍니다

http://mkexdev.net/Article/Content.aspx?parentCategoryID=1&categoryID=5&ID=515

 

그럼 이것으로 강좌를 마치도록 하겠습니다

 

좋은 하루 되세요~~