By 한동훈
.NET Framework Essentials(9월 국내서 출간 예정)
C# 에센스
닷넷 프레임워크(.NET Framework)를 이루고 있는 기반이며, 각기 다른 언어로 코딩한 것들이 닷넷 기반에서 호환성을 유지하며, 상호 운영성을 보장하며 실행될 수 있도록 만드는 것이 CTS(공통 형 시스템)과 CLS(공통 언어 명세)다.
Common Type System(CTS, 공통 형 시스템)
닷넷에서는 모든 언어를 같은 것으로 취급하기 때문에, C#에서 작성한 클래스나, VB.NET에서 작성한 클래스나 같은 것으로 취급한다. 즉, 닷넷에서는 어떠한 언어로 작성해도 차이가 없다. 실제로 이러한 언어들을 하나로 통합해 내고 상호 운영성을 보장하기 위해서 모든 언어들은 CTS를 따라야 한다. CTS는 값 타입(value type), 레퍼런스 타입(reference type), 클래스, 인터페이스, 위임, 인터페이스, 포인터, 열거형과 같은 다양한 형을 지원한다.
CTS는 값 타입과 레퍼런스 타입으로 나눌 수 있다. 계층도는 밑에 있으니 참고하며 이에 대해 더 논의해 보도록 하자.
C++는 완전한 객체 지향 언어가 아니다. C 언어에 객체 지향적인 특징을 추가했지만, 모든 것을 객체로 다루지 않았다. 많은 객체 지향 언어들은 기본 데이터형(primitive)과 객체형(class)로 나뉘어지고 있다. C++에서 변수는 객체가 아니며 프로그래머에게는 비밀스러운 존재로 남아 있었다.
스몰토크(smalltalk)에서는 모든 것을 객체로 다루고 있다. 변수까지 객체로 다룸으로써 완전한 객체 지향을 이룰 수 있었지만, 성능 문제 때문에 널리 쓰이지 못하게 되었다. 어떻게 하면 데이터 형도 객체로 다루면서 성능 상의 이점을 가져올 것인가라는 고민을 하게 되었을 것이고, 결국 전체 데이터형을 두 범주로 나누었다. 그것은 바로 값 타입과 레퍼런스 타입이다. 기본 데이터형은 값 타입으로 선언되지만, C#에서는 모든 데이터형을 객체로 다루고 있으며, 기본 데이터형도 객체로 다루고 있다. 이와 같은 것을 가능하게 하기 위해 값 타입을 레퍼런스 타입으로 변환하는 복싱(boxing)과 레퍼런스 타입을 값 타입으로 변환하는 언복싱(unboxing)이라는 개념을 C#에서는 소개하고 있다. 이에 대해서는 다음 절에서 자세히 알아보도록 하자.
C++에서는 전체 클래스 계층에서 데이터 형은 그 어디에도 속해 있지 않았으며, 객체도 아니었으나 C#에서는 데이터 형이 클래스 계층도에 속해 있게 되었다. 이것이 가지는 이점은 모든 것을 객체로 다룰 수 있게 됨과 동시에, 데이터 형이 클래스 계층도에 속해 있게 되므로, C#에서는 모든 클래스에 대한 공통된 기반 클래스를 갖게 되었다는 것이다. C#에서 모든 데이터 형은 System.Object에서 파생된 System.Type에서 상속된다(사용자 정의 데이터 형을 만들기 위해 System.Type을 상속할 수 있다는 말이다).
따라서 C#에서 모든 데이터형은 System.Object의 공통된 메소드, 즉 GetHashCode, ToString, Equals, Finalize를 갖게 된다.
CTS가 주는 이점중에 마지막은 형 안정성(type safety)이라는 것이다. 필자에게는 별로 와 닿지도 않는 개념이고, 그것이 무엇인지 개념이 잡히지 않았다(왜냐하면 필자가 사용했던 언어들에서 형 안정성이라는 괴상망측한 용어를 접할 기회도 없었으며, 자바 프로그래머가 아니었기 때문에 그러한 용어를 접했을 때 조차도 흘려 듣고 말았기 때문이다).
그래서 필자와 같은 사람들을 위해서 형 안정성을 설명하면, 데이터 형이 데이터 형 그 자체이며, 정해진 형에 대한 적절한 연산만을 수행할 수 있음을 보장해 준다는 것이다.
이는 C#에서 적절한 이름 짓기(naming convetion)가 필요하다는 것을 의미한다. Visual C++에서 쓰였던 헝가리안 표기법과 같은 것이 더 이상 필요 없음을 의미한다.
모든 객체에 대한 참조는 데이터 형에 명시되어 있고, 객체를 참조하는 객체에 대한 데이터 형 또한 명시되어 있다. 다시 말하면, CTS는 언제나 정해진 객체만을 참조할 수 있도록 정해주기 때문에, 잘못된 객체를 참조하는 것은 CTS에서 인정하지 않는 단 말이다. 즉, 객체가 참조하는 것이 어떤 데이터 형인지를 알 수 있도록 헝가리안 표기법을 쓰면서 객체가 참조하는 데이터 형에 대해 고민하지 않아도 된다는 말이다.
CTS가 모든 데이터 형을 감시하므로, 다른 데이터 형이라고 재정의할 수 없다(이것은 C#에서 클래스 A를 정의하고, A의 상속 클래스 B를 정의하는 경우, A에서 B 클래스로의 다운 캐스트는 행해지나, B에서 A로의 업 캐스트는 행해지지 않으며 예외를 발생시키게 되는 근본적인 이유이다).
각각의 데이터형은 접근 제한자를 통해서 접근을 제한하도록 되어 있다. public, protected, private, internal와 같은 접근 제한자를 사용할 의무가 있다.
마지막으로 CTS에 대한 내용을 요약하면 다음과 같을 것이다. 닷넷 프레임워크에서 언어간의 상호 운영성을 보장하고, 하나로 통합해내며, 형 안정성을 지원하기 위해 모든 언어는 CTS를 따르도록 되어 있다.
CTS로 인해, 모든 언어는 통일된 클래스 계층에 속해있게 되며, 공통된 메소드를 갖게 된다. 데이터 형의 선언, 클래스 선언뿐만 아니라 각 객체 간의 상호작용을 보장해 준다.
이제 CTS의 주요 형들에 대해서 알아보도록 하자.
값 타입(value type)]
Value type(값 타입)은 기존의 언어들에서 말하는 기본 데이터형(primitive)을 말한다.
여기서 String과 Object를 제외한 나머지는 값 타입이다.
각자가 사용하는 언어의 데이터 형에 대한 키워드는 적절한 CTS 클래스 이름으로 매핑(mapping)된다.
필자가 C#에서 int형을 사용하면 그것은 CTS의 System.Int32로 매핑된다. 마찬가지로 VB.NET에서 short, int를 사용하면 그것은 CTS의 System.Int16, System.Int32로 각각 매핑된다. 또한 C#에서는 선언문에 CTS의 클래스 명을 그대로 사용해도 된다. 즉, C#에서 각각의 데이터 형에 대한 키워드는 적절한 CTS형으로 alias만 되어 있다는 것을 알 수 있다.
int age = 21;
System.Int32 realAge = 21;
|
C#에서 위 두 코드는 모두 잘 수행된다(왜 trax가 21살 이라는 데 안 믿는건가... T.T).
Value type(값 타입)은 선언시에 데이터를 스택에 저장한다. 값 타입은 널(null)이 될 수 없고, 항상 값을 가져야 한다. 함수나, 객체에 이들 데이터 형을 넘기면 이들 데이터 형에 대한 복사본이 넘겨지며, 원본에 있는 값은 바뀌지 않는다(예제를 보여주고 싶으나 귀찮다..! 배 째...).
기본 데이터형 뿐만 아니라 구조체(struct)와 열거형(enum) 또한 값 타입이다.
마지막으로 C#의 모든 데이터 형은 System.Object의 파생 클래스 System.Type에서 상속되며, 값 타입은 System.Type을 상속받은 System.ValueType에 속한다. 더 많은 값 타입에 대해서 알아보려면 SDK에서 System.ValueType 클래스를 참고하면 된다.
레퍼런스 형(Reference Type)
레퍼런스 형은 값이 저장된 위치에 대한 참조를 저장한다. 레퍼런스 형은 스택이 아닌 힙에 저장되며, 널 값을 가질 수 있다. 레퍼런스 형은 함수나 객체에 값에 대한 참조를 넘기며, 변경된 값이 그대로 반영된다. 레퍼런스 형은 System.Type에서 상속되며 값 타입과 같이 System.ValueType과 같은 클래스를 갖지 않는다.
Reference Type에 속하는 것으로는 배열, 클래스, 복싱된 값 타입(boxed value type)이 있다(복싱에 대해서는 다음 절에서 다룰 것이다).
레퍼런스 타입은 포인터, 인터페이스와 자기 서술형(Self Describing Type)이 있다. 자기 서술형은 배열, 클래스가 있다(왜 자기 서술형이냐 하면 배열, 클래스는 모두 GetType과 같은 메소드를 갖고 있고, 이들을 통해서 이들이 어떤 녀석들인지 알아낼 수 있다. 이것은 CTS가 보장하는 형 안정성과 중요한 관련이 있다).
클래스는 다시 사용자 정의 클래스와 복싱된 값 타입(boxed value type), 위임이 있다.
쉽게 정의하면 레퍼런스 형은 힙에 정의되고, 우리는 메모리에 있는 주소를 갖고 참조만 할 수 있는 데이터 형이다. 객체 지향 언어에서는 모든 것이 힙에 올라가야 한다. 데이터 형 또한 힙에 객체로서 올라가야 한다. 이렇게 되면, 모든 것을 객체로 할 수 있으나 성능상으로는 과다한 메모리 사용량과 불필요한 부하가 걸린다는 것을 어렵지 않게 추측할 수 있을 것이다.
그래서 CTS에서는 모든 데이터 형을 객체로 다루면서 성능상의 이점을 찾기 위한 타협점을 찾아 냈는데, 기본 데이터 형은 값 타입으로 선언하고, 필요에 따라 이것을 레퍼런스 타입으로 변환(boxing)하고 다시 필요가 없을 때 이것을 다시 값 타입으로 변환(unboxing)하기로 한 것이다.
물론 이와 같은 변환과정(boxing, unboxing)은 상당한 부하를 야기하지만, 모든 데이터 형을 객체로 사용하여 발생하는 부하에 비하면 훨씬 적다고 할 수 있다.
데이터 형 변환 과정(boxing, unboxing)
값 타입을 레퍼런스 타입으로 변환하는 과정을 복싱이라고 한다.
int i = 21;
object obj = i;
|
두번째 라인은 레퍼런스 타입에 값 타입의 값을 대입하고 있다. 이 경우에 i는 레퍼런스 타입으로 변환되고, i의 값이 아닌 주소가 obj에 저장된다.
이와 같이 하면 레퍼런스 타입인 obj가 값 타입으로 변환된 다음에 realAge에 값을 복사한다. 즉, 메모리의 주소를 복사하는 것이 아니라는 것이다.
즉, 이것을 보면 C 언어에서 보았던 형 변환(type casting)과 비슷하다는 것을 알 수 있다. 왜 그럴까? ^_^
C 언어에서의 형 변환(type casting)이 스택에 있는 변수들의 형을 변환하거나 힙에 있는 객체들간의 변환을 하는 것에 비해, C#에서는 힙 영역에 있는 레퍼런스 타입 객체와 스택 영역에 있는 값 타입 사이를 자유롭게 변환한다.
즉, 스택과 힙을 자유롭게 넘나들도록 언어를 설계함으로써 모든 데이터 형을 객체로 다룰 수 있게 되었고, 공통된 클래스에 기반할 수 있다.
이로 인해 값 타입이나 레퍼런스 타입에 관계없이 CTS에 기반한 언어들은 모두 자기 서술형의 성격을 갖게 되었다. 즉, 자신이 어떠한 데이터 형인지, 어떠한 클래스인지를 설명할 수 있다(GetType 메소드 구현을 모두 갖고 있다).
이로 인해, 프로그래머는 프로그래밍을 할 때, 넘겨 받은 인자가 어떤 데이터 형을 갖고 있는지 검사할 필요가 없어졌다. 반드시 올바른 데이터 형이 넘어온다는 것을 CTS가 보장하기 때문이다. 그리고 이러한 타입은 강제로 변경될 수 없다.
공통 언어 명세(CLS: Common Language Specification)
CTS에 대해서는 꽤 장황하게 설명한 느낌이 있다. CLS는 여러 언어를 하나로 통합하고, 상호 운영성을 지원하기 위한 제안이다. 이 제안서는 지켜야 하는 최소한의 규칙(rule)만을 정의하고 있다. 이 말의 의미는 매우 중요한데, 썬 등에서는 반드시 지켜야 하는 것을 정의하지만, MS에서는 지켜야 하는 최소한의 규칙만을 구현하며, 그 이외의 확장에 대해서는 관여하지 않는다는 의미를 갖고 있다. 이것에 대해서는 오라일리의 존 오스본과 MS의 앤더스 헤즐스버그의
인터뷰를 참고하기 바란다.
CLS는 컴파일러 개발자나 애플리케이션 개발자 모두 읽어볼 필요가 있다. CLS에는 CIL, CLI, CLS, CTS에 대해 자세하게 서술되어 있다.
마치며
부족한 글을 쓰는데도 시간이 많이 걸렸다. 예제를 작성하거나 적당한 예제를 찾아서 옮기고 싶었지만 시간이 너무 많이 걸릴 것 같아서 그만뒀다(아직도 모르는 게 많음을 절실히 느꼈다).
이 문서는 감히 General Shovel License를 따른다고 말하고 싶으나 정해진 게 아무것도 없는 듯 하고, 아무렇게나 가져다 써도 되고, 신경도 안쓴다. 다만 "한 글자"라도 바뀌게 되면(오타라도) 반드시 알려주길 바란다.
관련 링크