본문 바로가기

.Net Technology/.NET General

C# nullable 타입의 동작원리

일반적으로 컴퓨터의 언어에서는 데이터의 부재를 “null”이라는 예약어를 이용해서 표현해 왔다. C#에서 또한 null 타입이 존재하지만 닷넷의 데이터 구조상 null을 표현하기에는 다소 모호한 부분이 있다. 그렇기 때문에 이 null 타입에 대한 이슈는 닷넷 1.0 시절부터 상당히 많이 논의되어왔던 내용이다. 그럼 C#에서 null 타입의 모호한 부분이 무엇이며 null타입의 올바른 사용 방법들에 대해서 살펴보도록 하겠다. 

null의 진정한 의미

null에 대한 정의를 내리기 전에 먼저 닷넷의 데이터 타입에 대해서 살펴보도록 하겠다. 닷넷의 CTS(Common Type System)에서는 모든 것이 객체다. 여기에는 우리에게 익숙한 int, char, double 등의 간단한 타입부터 string과 같은 확장된 타입들이 있고 이들은 모두 System.Object라는 클래스에서 파생되었다. 이 타입들은 값 타입(int, float, bool, long, byte 등)과 참조 타입(string, object)으로 나누어 지게 된다. 

 

(Value) 타입

 참조(Reference) 타입

저장되는것

데이터

 데이터가 위치하는 주소

메모리 영역

일반적으로 스택(Stack)

일반적으로 힙(Heap)

대표데이터 형

기본데이터 타입구조체열거형(Enumerator)

클래스배열델리게이트인터페이스

[표] 값 타입과 참조타입의 비교


하지만 여기서 문제가 되는 것은 Value 타입에 null을 대입할 수 없다는 것이다. 그렇다면 왜 값 타입에는 null을 대입할 수 없는 것일까? 이 문제를 쉽게 이해하기 위해서는 먼저 닷넷에서 사용하는 null의 정의를 확실하게 내려야만 한다. 닷넷에서의 null은 “어떤 객체도 참조하지 않음” 이란 의미를 지닌 특별한 값으로 정의를 내릴 수 있다. 그럼 왜 값 타입에는 null을 대입할 수 없는 것일지 이해할 수 있을 것이다. 즉, 참조 타입변수의 값은 참조이고, 값 타입 변수의 값은 실제로 존재하는 어떤 값이기 때문이다.


C#1.0 에서 null을 표현하는 방법

값 타입에 null을 표현한다는 것 자체가 모순이 되는 내용이기도 하지만 값 타입에 null을 이용해야 되는 예외 상황은 존재한다. DB와 데이터 연동을 하다 보면 null을 표현해야만 하는 문제를 만나게 된다. 때문에 C#1.0 에서는 다음과 같은 편법을 이용해서 값 타입에 null을 표현해 왔다. 



먼저 별도의 bool 변수의 선언하는 방법이 있다. 용감 무식한 방법이긴 하지만 모든 값 타입의 변수마다 bool 변수를 만드는 것이다. 이 bool 타입을 만들어 관리하는 방법은 2가지가 있다. 별도의 bool 타입변수를 선언하여 관리하는 간단한 방법과 해당 값 타입과 bool 변수를 클래스로 래핑하여 사용하는 방법이 있을 수 있다.

두 번째 방법은 별도의 참조 타입을 생성해서 필요할 때마다 박싱 언박싱을 해주는 것이다. 즉, 각각의 값 타입은 하나의 인스턴스 변수와 타입 변환 연산자를 가지게 될 것이다. 하지만 여기서 문제는 이 타입은 결국 힙 영역에 생성될 것이고 가비지 컬렉션의 압박을 가하면서 메모리 사용을 늘리는 현상을 초래하게 된다. 

마지막 방법은 임의의 값을 null이라고 지정하는 것이다. 우리는 이 값을 매직값이라고 부른다. 즉, 값의 범위에서 하나의 값을 희생하여 null로 간주하는 방법이다. 예를 들어 DB에 날짜를 넣어야 한다면 AD 1년(1년 1월 1일)의 날짜가 들어있을 경우가 거의 없을 것이다. 그렇기 때문에 우리는 AD 1년의 값을 null로 이용할 수 있다는 것이다. 이렇게 임의의 값을 사용할 경우 메모리의 낭비도 없지만 값을 잘못 선택할 경우 프로그램에서 알 수 없는 버그가 될 수 있는 가능성이 있다. 때문에 최소한 null로 지정한 값을 표현할 상수 정도는 추가로 지정을 해주어야 한다. 


C#2.0 에서 지원하는 Nullable<T>

C#2.0에서는 null을 표현할 수 있는 Nullable<T>라는 지네릭 타입이 추가 되었다. Nullable<T>의 핵심 속성은 HasValue와 Value이다. 먼저 Value는 값이 있다면 실제 값을 반환하고 값이 없다면 InvalidOperation-Exception을 발생시킨다. HasValue는 단순한 bool 값으로 실제 값이 존재하는지의 여부를 반환해준다. 

C#2.0에서는 Nullable<T>구조체를 보다 간결히 사용하기 위해서 “?” 라는 식별자를 지원하고 있다. 즉, 다음 두 선언은 같다고 할 수 있는 것이다.

●    Nullable<int> nullable=5;
●    Int? nullable=5;

 

Nullable 
타입의 박싱/언박싱

박싱과 언박싱에 대한 이야기를 나누어 보기 전에 null 값의 비교에 대해서 살펴보도록 하겠다. Nullable 타입의 비교는 HasValue 속성을 직접 이용할 수도 있고 == 연산자를 이용할 수도 있다. 
 

DateTime? birthday;
If(birthday==null)
If(birthday.HasValue)

 
그리고 Nullable<T> 타입은 다음과 같이 값뿐만 아니라 null을 대입할 수 있다는 것을 볼 수 있다. 
 

DateTime? birthday;
birthday = null;
birthday = new DateTime(1938,03,21);

 
하지만 이 코드를 개념상으로 접근한다면 상당히 혼란스러운 부분일 수 있다. 그래서 내부적인 동작을 IL로 확인해 본 결과 birthday = null 코드는 DateTime? 생성하여 대입하는 것을 볼 수 있었고, birthday = new DateTime(1938,03,21); 코드는 DateTime의 일반 생성자를 호출하고 그 결과를 바로 Nullable<DateTime>으로 다시 감싼 후에 전달하는 것을 볼 수 있었다. 
 
C#에서는 박싱/언박싱 작업이 빈번하게 일어나게 된다. 박싱이란 값 타입을 참조 타입에 대입하는 것을 의미한다. 하지만 값 타입을 어떻게 참조 타입에 넣을 수 있을까? 논리적으로 볼 때 불가능한 부분인 것이다. 하지만 C#의 값 타입은 System.Object 객체로 변환할 수 있게 설계되었기 때문에 자신을 참조타입으로 변환하는 것이 가능하다. 참조타입에 값 타입을 대입하면 스택의 값을 객체에 복사한 후 그 객체는 힙에 저장되게 된다. 언박싱은 그 반대의 작업이 일어난다고 볼 수 있다. 


[그림]


그렇다면 nullable 타입은 값 타입일까? 참조 타입일까? C#에서는 nullable 타입을 값 타입으로 구분하고 있다. 때문에 참조 타입에 대입한다면 박싱, 언박싱이 일어난다는 사실을 알아두어야 한다.
다음은 nullable 타입의 박싱과 언박싱에 대한 예를 보여주고 있다.
 

Int? nullable = 5;
 
//값이 있는 null 박싱
object boxed = nullable;
Console.WriteLine(boxed.GetType());
 
//일반 변수로 언박싱
int normal = (int)boxed;
Console.WriteLine(normal);
 
//null 변수로 언박싱
nullable = (int?)boxed;
Console.WriteLine(nullable);
 
//값이 없는 null 박싱
nullable = new int?();
boxed = nullable;
Console.WriteLine(boxed == null);
 
//null 변수로 언박싱
nullable = (int?)boxed;
Console.WriteLine(nullable.HasValue);

[박싱 언박싱 예제]
 
이 코드를 실행하면 다음과 같은 결과를 볼 수 있다. 


 
첫 번째 줄의 boxed의 타입을 보면 박싱된 값의 타입이 System.Nullable<System.Int32>가 아닌 System.Int32가 출력되는 것을 볼 수 있다. 이것은 GetType() 메서드를 호출하면 내부적으로는 Object 타입으로 박싱되기 때문에 Nullable 타입으로 반환되지 못한다는 것을 알아두자. 나머지 예제들은 nullable 변수로 박싱과 언박싱이 가능하다는 것을 보여주고 있다.