C# 2.0의 새 기능 Nullable Types의 DCR

얼마전에 Microsoft의 Developer Division 대빵인 Somasegar가 Nulls not missing anymore라는 글로 Nullable Types의 DCR(Design Change Request)을 통한 큰 변화에 관해서 블로깅을 했었다. 이에 관한 이해를 위해서 관련된 여러 이야기를 풀어보고자 합니다. (이것저것을 이야기하고 있으니 원하는 토픽만 읽으셔도 되겠습니다.)

- 프로그래밍 언어의 null

조금은 경험이 생긴 프로그래머라면 아마도 null이라는 애매모호한 것을 처리하는 것이 때때로 얼마나 귀찮은 것인가를 느끼고 있는 분들이 있을겁니다. "數價가 없음"을 처리하기 위해서 값을 매겨야한다니, 아이러니하지 않을 수 없습니다. "無"라는 것을 표현하기 위해서 "無"라는 글자를 써야하는 것과 같은 이치라고 생각하고 그 철학적인 아이러니는 빼고 생각하도록 합시다. 프로그램이라는 것은 어떠한 경우라도 표현이 되어야하는 개체들이 모여서 만들어진 개체입니다. 국어와 같은 언어가 아니라 프로그래밍 언어라도 마찬가지로 "無"라는 상태를 표현해야하며, 소프트웨어의 세계에서는 이를 "null"이라는 용어를 사용합니다. 상황에 따라서는 "NUL(Dos의 3글자 디바이스 표현 한계에서 기인)"이나 "Nil(Not in list, https://en.wikipedia.org/wiki/Null)"이나 "NaN(Not a Number, https://en.wikipedia.org/wiki/NaN)"등을 사용하지만, 일반적으로 "null(https://en.wikipedia.org/wiki/Null)"이 최근의 가장 널리 사용되는 표현입니다.

"표현"이라는 사람이 이해할 수 있는 방식과 컴퓨터 언어가 처리할 수 있는 "상태"는 구분됩니다. 컴퓨터는 "null"이라는 것을 이해하지 못합니다. "0 또는 1이냐"지 "0 또는 1 또는 없냐"가 아닙니다. 따라서, null이라는 사람을 위한 표현은 컴퓨터가 이해(처리)할 수 있는 0과 1의 조합(값)으로 바꾸어야 합니다. 어떻게 생각하면 null은 이런 컴퓨터 자체의 구현(Implementation) 속에서 태어난 하나의 꼼수입니다. 컴퓨터의 저장공간(메모리/HD등)에 아무것도 없다는 논리는 있을 수 없습니다. 프로그램은 동작하는 동안 저장공간의 일부를 특정 용도라 사용하겠다고 가정을 하게 됩니다(refenence type). 이 과정을 거치면 해당하는 부분에 있던 기존의 값은 의미를 잃게 되고 이를 "기존의 값이 무슨 의미인지 알 수 없는 상태"라고 표현하는 것 보다는 그냥 "null"이라고 표현하는 것이 간단하고 편리하다는 것을 알게 된 것이죠.

그렇다면 이 공간을 항상 초기화하도록 하면 되지 않느냐고 반문할 수도 있습니다. 때에 따라서는(언어에 따라서는) 맞을 수도 있지만, 이는 무조건 가능한 가정이 아닙니다. 초기화는 로직이고 존재를 나타내는 값과는 다릅니다. 로직은 없을 수도 있고, 바뀌기도 하고, 생략되기도 하고, 어떤때는 틀리게 마련입니다. 여기서 이런 초기화되지 않은 상태와 초기화된 상태를 나누어야 되는 역사적인 허점이 발생하게 되는 것입니다. 0과 1로 표현되어야할 숫자에 제 3의 상태가 필요하게 되는 상황이 생기는 것이죠. 이런 이유로 컴퓨터가 이해하는 입장에서 꼼수라고 표현했습니다만 언어에 있어서는 위에서 설명한 이유로 존재할 수 밖에 없습니다(언어가 이런 존재를 value type만을 사용하여 사용자의 편의를 위해서 감출 수는 있습니다).

그렇다면, 0과 1을 사용해서 null을 대표하는 "값"을 도대체 무엇으로 해야할까요? 기계어나 어셈블리어 혹은 이에 가까운 C언어는 이 값을 통일하기가 꽤 어렵습니다. 0과 1에 대응되는 더이상 숫자 이상으로 추상화되지 않은 기본 형식(primitive type, 숫자/bool값등)를 중심으로 하기 때문에 null은 이 숫자를 사용한 값을 가져야 되고, 어떤 숫자를 사용하면 그 숫자의 원래 값과의 애매모호성(ambiguity)를 피할 수가 없게 됩니다. 간단히 integer라는 형식(type)의 null값을 0이라는 숫자로 표현하면, 0이라는 값을 사용할 경우에 이것이 0이라는 숫자인지 아니면 정말 null이라는 상태인지를 알 수 없게 됩니다. "100+null"이 "100"이라는 결과를 바라는지 아니면 "잘못된 연산"이라는 결과를 바라는 것인지가 구분이 되지도 않습니다. 형검사(type checking)을 하지 않기 때문에 형식들 사이에 마구 변환(casting)을 하게 되면 그 값의 의미는 의도하지 않게 다른 문맥으로 변환되어 엉뚱한 의미를 가지게 될 수도 있습니다.

이와는 다르게 고급언어는 이를 추상화해서 프로그래머로부터 이 골치아픈 문제를 감추고 독립된 null 값을 사용하게 할 수 있는 기회를 가지고 있습니다. 좀 더 세련된(sophisticated) 형시스템(type system)을 가지고 있어서, 위에서 이야기한 "로직"이라는 것을 형(type)에 넣을 수 있기 때문입니다. 언어 자체의 스펙 레벨에서 정의할 수도 있고, 언어의 런타임이나 컴파일러에서 지원할 수도 있습니다. 혹은, 이를 추상화한 라이브러리를 사용하게 할 수도 있습니다. 하지만, 어떤 경우라도 그냥 null이라는 대표값 하나만의 지원으로는 부족합니다. 왜냐하면, 위에서 설명한 기본 형식(primitive type)과 그 외의 언어 나름대로 추상화한 표현방식을 "모두" 만족시켜주어야 하기 때문입니다. 어설픈 조화(inconsistency)는 프로그래머에게 피곤함을 안겨주게 되지요.

- C#에서의 null

C#은 결과와는 다르게 본래 Java처럼 C에서 포인터의 사용으로 인한 복잡성(?)을 줄이기 위한 의도도 가지고 있었습니다. 하지만, C#이 C# 언어만의 문제가 아니라 CLI라는 표준에 대비되는 종속성과 함께 기존의 Win32 인프라와 조화를 이뤄야하는 당위성도 가지고 있었습니다. 무슨 이야기인고 하니 .NET이라는 플랫폼이 생기면서 기존의 API들과 통신 방식들(RPC등)을 모두 커버/대체할 수는 없었고, 이를 사용하기 위해서는 직접적으로 포인터는 아니더라도 이에 대응되는 참조(reference)라는 개념은 없을 수가 없습니다. 물론 이런 이유가 절대적인 이유는 아니지만, 이해가 간편한 예로 설명한 것입니다.

언어의 사용자 입장에서, 다수의 타언어들과 비슷하게 C#의 null은 0도 아니고 -32767도 아닌 그냥 null 자체로 하나의 의미를 가지는 키워드(keyword)입니다. 현재 사용되는 C#의 1.0버젼에서는 참조 형식(reference type)에서만 null을 사용할 수 있습니다. 참조 형식(reference type)에서의 null은 참조하고 있는 위치 정보가 없다는 뜻입니다. (위치에 값이 없다고 편하게 이야기할 수도 있겠지만, 실제로는 위 단락에서 설명한 것처럼 값이 없을 수는 없습니다.) 참조 형식만이 null일 수 있다는 이야기는 값 형식(value type)에 null이라는 값을 넣게 되면 문제가 발생한다는 것을 의미합니다.

포인터라는 개념의 존재가 완벽하게 없다면, null은 필요가 없는 값일까요? 위에서 설명했듯이 메모리 위치에 값이 존재하지 않을 수는 없지만, 그 값이 의미가 없는 혹은 아직 초기화되지 않은 값일 경우 이를 null이라고 할 수는 있을 것입니다. 값 형식(value type)은 포인터와는 상관없이 참조하는 위치는 없지만 이처럼 초기화되지 않은 상태로서의 null이 필요할 수 있습니다. 연산에서 값 형식(value type)과 참조 형식(reference type)이 섞인 경우에는 더더욱 필요할 수 있을 것입니다. C# 2.0의 널러블 형식(nullable type)에서 해결하고자 한 것은 참조 형식(reference type)에서만 사용되던 null을 값 형(value type)에서도 사용할 수 있도록 통일하여 일관성을 가지도록 하기 위한 것입니다. C# 2.0의 스펙(specification)에 nullable type을 값 형식(value type)으로 분류합니다.

- 언어의 자체 기능

C#은 수많은 논란 속에서 "foreach"등을 언어 자체의 기능으로 넣었습니다. 이는 자주 사용되는 상용구를 언어의 자체 스펙(spec)에 keyword나 문법으로 넣은 것으로 많은 논란이 있었습니다. 자주 사용되는 방식들을 라이브러리가 아니라 언어의 기능으로 넣는 것에는 많은 사람들이 반대를 하기도 합니다. 물론 언어의 순수성(?)이라는 것을 해친다는 이유도 있고, 자주 사용되긴 하지만 잘못된 practice인 경우에는 문제가 될 수도 있습니다. 또한 그 구현을 알아야 제대로 사용할 수 있다는 어려움도 존재합니다.

그냥 사용하면 되는 것인데, 뭣하러 그 구현방식까지 이해해야하는냐고 생각할 수도 있겠습니다. 이는 많은 양의 코드를 짧게 줄여주는 겉으로 보기에는 간편함을 추구하는 것으로 보이지만, 실상으로는 코드를 감추고 있기 때문에 버그의 위험을 안고 있을 수 있습니다. 퍼포먼스 상의 버그, 논리 상의 버그, 이를 지원하지 않는 IDE에서는 디버깅의 불편함을 겪을 수도 있습니다. 이런 이유에서입니다.

foreach는 자주 사용되는 컬렉션(collection) 내의 모든 아이템들을 방문하는 루프(loop) 모양의 일반적인 코드를 언어의 기능으로 넣어서 매번 작성시에 반복되는 코드들을 foreach라는 줄여진 문법 구문으로 생략할 수 있도록 한 예입니다. (가변 형식(dynamic type) 시스템을 사용하는 언어에서는 흔하지만, 그렇지 못한 C#에서는 GetEnumerator라는 멤버함수를 지원해야만 사용이 가능하도록 구현합니다.) 위에서 이야기한 단점도 있지만, 에러처리를 하는 코드등을 생략하는 등의 코딩 실수도 줄일 수 있을 뿐만 아니라, 코드량이 줄고 깔끔해져서 가독성도 높일 수 있는 등의 장점을 가집니다. 다양한 관점에서 논란의 여지가 있는 것이죠.

비슷하게 C# 2.0의 Nullable Types는 "?"를 언어에 넣어 구현했습니다. 스펙(specification)에 의하면 T?(T는 형type)는 System.Nullable<T>의 줄인 형태이며 두 형태는 서로 치환가능하다고 되어있습니다. foreach처럼 길다란 Nullable<T>대신 "?"를 언어에 넣은 것이죠. 아직도 이에 관해서는 의견이 분분하지만, 아직은 C# 2.0이 발표되어 구현된 정식 컴파일러와 런타임이 나온 것이 아니기 때문에 이에 관한 유용성이 아직 정확히 입증되지는 않은 상태라고 할 수도 있겠습니다만 개발중인 컴파일러를 공개하여 출시 이전에 되도록 많은 사람들의 의견을 받아서 유용할 수 있는 노력을 하고 있고, 이에 부응해서 DCR이 나온 것이라고 할 수 있겠습니다.

- DCR(Design Change Request)란?

스펙(언어 명세?, Specification)이 작성되고 이를 코딩하여 구현하게 되고, 이를 테스팅할 방법이 마련되고 문서도 작성되고 다른 팀들이 이와 관련성이 있다면 사용되기도 합니다. 이런 시점에서 그냥 스물스물 스펙의 디자인을 바꾸게 되면 수많은 팀들이 알 수 없는 오류로 인해서 이를 알아내는 시간을 허비하게 됩니다. 사용자에게도 어떤 영향을 미치게 되는지도 중요한 문제이고, 만일 사용자에게 오히려 마이너스적인 요소라면 이런 변화는 하느니만 못할 것입니다. 명백하고 영향이 없는 작은 변화라면 모를까 그렇지 않다면 이를 모든 팀에 공표를 하고 허가를 받아야 합니다. 이 허가를 받기 위해서 만드는 문서가 바로 DCR(Design Change Request)라는 것입니다. 바꾸기 전에 DCR을 작성하여 무엇에 어떤 변화가 생기는지를 명확히 알리게 됩니다.

DCR의 R이 나타내듯이 이는 요청(Request)입니다. 이 요청을 가지고 이에 대한 가부 결정 권한이 있는 사람들이 전쟁터(war room)에 모여서 전쟁(war)을 합니다. 전쟁을 하는데 있어서 해당 시점의 가부 결정을 위한 통과요건(bar)이라는 가이드라인이 있습니다. 이 bar라는 최소 요건을 갖춰야 일단 DCR을 받아들일 수 있는 기본적인 요건을 갖추게 되는 것입니다. 영향이 있는 팀들이 있다면 이 팀들에게 알리고 의논을 하며 논쟁을 합니다. bar에 있어서 "해당 시점"은 중요합니다. 현재 전체 개발 주기의 어느 부분에 위치하고 있느냐에 따라서 얼마나 많은 영향(Impact)을 가지게 되는지가 결정되기 때문입니다. 스케줄에 지장을 줄만한 DCR이라면, 이 DCR이 통과요건을 갖췄더라도 더 윗선에서 스케줄 재조정이라는 커다란 장벽을 거쳐야합니다.

DCR은 반려되더라도 다시 요청할 수도 있습니다. war 팀에서는 DCR이 현 시점에서 통과되어야 하는 정당한 이유가 있음에도 이를 캐치하지 못했을 수도 있고, 이를 정확히 표명하지 않아서 반려되었을 수도 있습니다. 이런 이유로 DCR은 정확해야하고 때에 따라서는 꽤 큰 책임을 수반합니다. 개발 주기의 후반으로 가게되면 bar가 매일 높아집니다. 따라서 DCR을 제출하는 시점도 굉장히 중요합니다. 반려된 뒤에 다시 제출하고자 하면 bar가 더 높아져버려서 소용이 없어질 수도 있는 상황도 존재합니다. 한 회사의 프로젝트에는 일정한 비용/시간/인력이 들어가게 되는데, DCR은 이를 늘리고자하는 요청일 수 있습니다. 따라서 아무리 중요한 DCR이라고 하더라도 이런 상업적인 요소를 고려하게 되면 포기할 수 밖에 없는 상황이 발생할 수도 있는 것이지요. 그만큼 DCR은 스펙 자체만큼이나 중요한 위상을 차지합니다.

참고: https://blogs.msdn.com/release_team/archive/2004/11/12/256772.aspx, https://blogs.msdn.com/mikhailarkhipov/archive/2004/09/13/229209.aspx

- 이번 DCR의 내용

C#에서의 값 형식(value type)은 항상 boxing/unboxing이라는 기법을 동반합니다. 값 형식(value type)과 참조 형식(reference type)과의 근본적인 차이는 (참조가 없을 경우)전자는 스택에 할당되는 것이고 후자는 힙에 할당되는 것이라는 사실입니다. 스택에 존재하는 경우에는 참조없이 바로 사용할 수 있어 더 빠르게 사용할 수 있습니다. 반면 객체로 사용할 수 없습니다. C#에서는 이 두마리의 토끼를 동시에 잡을 수 있는 방법으로 값 형식(value type)을 객체(System.Object 형식)으로 변환할 수 있는 방법을 마련했는데, 이것을 boxing/unboxing이라고 합니다. boxing을 하게되면 객체를 힙에 할당하고 스택의 값을 객체에 복사를 하고, unboxing은 그 반대 과정을 하게 됩니다.

Nullable types 또한 값 형식(value type)이라는 이야기를 했었습니다. 그렇다면 이 또한 boxing을 하게 되는데, 이 과정에서 애매모호함이 발생했습니다. 기존의 구현은 Nullable<T>를 사용하는 것으로 충분할 것으로 생각하고 이루어졌지만 본래의 형식 T와 Nullable<T>간에는 런타임을 고치지 않고는 해결할 수 없는 일치하지않는 내용들이 있었습니다. 이 내용 중에서 Somasegar의 설명에 있는 예는 값 형식(value type) T?형 변수가 null일 경우에 boxing되면 Nullable<T>형의 object가 할당되어버려 null이 아닌 다른 값을 가진다는 예입니다. 이 예가 가장 큰 영향(impact)를 가져서 대표적인 예로 설명되었지만, 이외에도 boxing된 T가 Nullable<T>로 unboxing될 수 없다거나, Nullable<T>는 T가 상속한 인터페이스를 상속하지 않기 때문에 다른 결과를 돌려주는 등 여러가지 문제들이 기존 구현으로는 발생할 수 밖에 없었습니다. 쉽게 설명을 하면 실제 Nullable Type은 Nullable<T>에 투명하고 T인 척을 해야하는데, 그 역할을 하지 못하고 Nullable<T>인 티를 팍팍 낸다는 것이었습니다.

이런 문제는 런타임때 인식을 해서 예외적으로 올바른 결과를 돌려주도록 해야하는 부분들이었고, 이를 해결하고자 한 것이 이 DCR의 내용이었습니다.

- Nullable Types DCR이 문제가 된 이유

사실 이번 DCR은 고칠 수 밖에 없는 불가결한 내용이었습니다. 하지만, 문제는 그 시점이 출시에 가까운 모드였기 때문에 발생한 것입니다. Microsoft에서 뿐만 아니라 대부분의 일반적인 제품들은 위에 DCR을 설명하면서 언급한 것처럼 그 기능과 스케줄에서 현실적인 타협점을 찾아야 합니다. 문제가 있는 제품이 출시되는 것도 이런 맥락에서이죠. 직접적으로 이야기를 하면, 나온다는 약속을 어겨서 욕을 먹는 것과 나왔는데 문제가 있어서 나중에 고친다는 이유로 욕을 먹느냐의 두가지를 두고 고민을 하게 되는 상황입니다. 기능이 10가지 100가지인 제품도 이런 기로에서 헤매는데 하물며 VS와 같이 대규모의 제품에서는 더더욱 고민을 하게될 요소가 많은 것이 사실입니다.

Nullable Types의 DCR은 해당하는 몇가지 코드를 고친다고해서 해결될 일이 아니라 근 한달에 가까운 시간이 제품 개발에 관련된 모든 부분에서 소요되는 것으로 추산되었습니다. 그 영향(impact)이 큰 것이죠. 제품 출시 딜레이가 없는 것으로 결정된 상황에서 이 DCR을 받아들인다는 결정은 그만큼 모험(risky)이라는 것이 분명한 것이고 모든 관계자들이 더 고생해야함을 의미하는 것이고 자칫하면 출시일이 결정과는 상관없이 DCR을 처리하는 과정에서 부득이하게 발생한 문제로 연기될 수도 있다는 의미이기도 합니다.

이렇게 큰 결정을 고객을 위해서 내렸다는 것은 회사의 입장에서는 Somasegar레벨(저의 7~8단계 위 상사^^)에서 블로깅할만한 요소였던 것 같습니다. (애초부터 구현을 제대로 했다면 문제가 없었을 것이 아니냐는 철없는 이야기로 반박할 수도 있겠습니다만, 이는 제품 개발이라는 것을 잘 이해한다면 수긍을 할 수 있는 부분이라고 생각합니다.)

- 기타 가쉽