Microsoft는 “Visual Studio for Yukon” Version의 C# 언어로 여러 가지 개발 및 산업 언어의 다양한 기능을 통합 하려는 계획을 세우고 있습니다. 이 계획의 일부로 개발자의 생산성을 향상시켜 주는 실용적인 언어 구문으로 Generics, Iterator, Anonymous, Partial 등의 기능이 포함 되었습니다.
여기서 간단히 다룰 자료형은 Generics입니다. Generic을 우리말로 ‘일반적, 포괄적이다’ 라고 번역할 수 있습니다. 우리는 앞에서 자료형은 값 형식(Value Type)과 참조 형식(Reference Type)으로 구분 되어지고, 값 형식(Value Type)은 Stack에 참조 형식(Reference Type)은 Heap에 저장된다고 배웠습니다. 그러나 이 두 가지 자료형을 모두 고려한 메소드를 만들고자 하면 System.Object 형으로 변환하여 저장하고, 다시 값 형식(Value Type)이나 참조 형식(Reference Type)으로 변환하여 사용해야 하는 불편함 및 많은 문제점을 야기 시켰습니다.
이를 대체하기 위해 미리 Type을 지정하지 않고 실제 자료형이 저장되는 시점에 자료형이 결정 되어지는 일반적인 자료형을 지원하기 위해 Generics 자료형이 추가된 것입니다.
1. Generics의 필요성 제시(System.Object 사용에 따른 문제 정의)
예를 들어 범용 Stack 데이터 구조를 개발한다고 가정해봅시다. Stack의 지원 메서드로는 Pop() 및 Push() 메서드가 있습니다. 이를 사용하여 다양한 형식의 인스턴스를 저장할 수 있습니다. 즉, Stack에 사용되는 내부 데이터 형식은 무정형 개체이며, stack 메서드는 개체와 상호 작용합니다.
.Net 기반 형식으로 기본 제공되는 개체 기반 stack을 사용하여 모든 형식의 항목을 보관할 수 있습니다. 그러나 개체 기반 솔루션에서 다음과 같은 문제점이 있습니다.
public class Stack { readonly int m_Size; int m_StackPointer = 0; object[] m_Items; public Stack():this(100) {} public Stack(int size) { m_Size = size; m_Items = new object[m_Size]; } public void Push(object item) { if(m_StackPointer >= m_Size) throw new StackOverflowException(); m_Items[m_StackPointer] = item; m_StackPointer++; } public object Pop() { m_StackPointer--; if(m_StackPointer >= 0) return m_Items[m_StackPointer]; else { m_StackPointer = 0; throw new InvalidOperationException("스택이 비워 있어 Pop할 자료가 없습니다."); } } } Stack vstack = new Stack(); vstack.Push(1); int number = (int) vstack.Pop(); Stack rstack = new Stack(); rstack.Push(new Customer()); Customer c = (Customer) rstack.Pop(); |
<문제점 제시>
1) 성능에 문제가 있습니다.
값 형식(Value Type)을 사용할 경우 값을 넣으려면(Push) 박싱(Boxing) 처리하고, 받아 내려면(Pop) 언박싱(UnBoxing) 처리를 해야 하는데 그 자체만으로도 성능이 크게 저하되며, 의 부담도 늘어 나므로 성능이 좋지 않습니다.
참조 형식(Reference Type)을 사용하는 경우에도 개체에서 상호 작용하고 있는 실제 형식으로 형 변환 해야 하고 형 변환에 따른 추가 작업이 필요하므로 성능이 여전히 저하됩니다.
Stack vstack = new Stack(); vstack.Push(1); int number = (int) vstack.Pop(); |
2) 형식의 안전성에 문제가 있습니다.
컴파일러(Compiler)에서는 모든 내용을 Object 타입으로 형 변환 하거나, Object 타입에서 다른 타입으로 형 변환할 수 있으므로 컴파일(Compile)시 형식의 안전성이 떨어 집니다.
타입별로 안전한 스택을 만들어 박싱 및 형 변환에 따른 성능 저하 및 컴파일(Compile)시 잘못된 형 변환에 따른 형식의 안전성 문제를 해결할 수 있을 것입니다. 하지만, 이에 못지 않은 문제점이 발생 합니다.
Stack rstack = new Stack(); rstack.Push(new Customer()); Customer c = (Customer) rstack.Pop(); |
3) 작업 생산성이 낮아지는 상황이 발생합니다.
타입별 데이터 구조를 작성하는 작업은 더디고, 반복적이며 오류가 발생하기 쉽습니다. 데이터 구조에서 결함을 수정할 경우 동일한 데이터 구조를 타입별로 중복 항목이 있는 곳마다 결함을 해결해야 합니다. 또한 알 수 없거나 아직 정의되지 않은 미래의 형식이 사용될지도 모르므로 개체 기반 데이터 구조도 유지해야 합니다.
public class Stack { int[] m_Items; public void Push(int item) {...} public int Pop() {...} } |
2. Generics의 정의
C# Generics는 새로운 주요 개발 방법으로서 적절하고 읽기 쉬운 구분을 사용하여 성능, 형식 안정성 및 품질을 향상 시키고, 반복적인 프로그래밍 작업을 줄이, 전체 프로그래밍 모델을 단순화 하는 새롭게 추가된 자료형입니다. IL(Intermediate Language) 및 CLR(Common Language Runtime) 자체에서 기본적으로 지원하며, generic Class 코드를 컴파일하면 다른 모든 형식과 마찬 가지로 IL로 컴파일합니다. 단, IL에는 실제 특정 형식의 매개 변수나 자리 표시자만 들어 있습니다. 또한 Generic Class의 메타 데이터에는 Generic 정보만 들어 있습니다. 다음은 C# Generics에 대해 쉽게 표현하고 있습니다.
3. Generics의 구현 (적용)
IL(Intermediate Language) 및 CLR(Common Language Runtime)에서 generics를 기본적으로 지원하므로 CLS(Common Language Specification) 규약 및 CTS(Common Data System) 데이터 형식이 적용된 대부분의 CLR 호환 언어는 generic 형식을 활용할 수 있습니다. 다음은 앞에서 개체 기반 stack을 Generics를 적용하여 C#으로 구현해 보겠습니다.
public class Stack<T> { readonly int m_Size; int m_StackPointer = 0; T[] m_Items; public Stack():this(100) {} public Stack(int size) { m_Size = size; m_Items = new T[m_Size]; } public void Push(T item) { if(m_StackPointer >= m_Size) throw new StackOverflowException(); m_Items[m_StackPointer] = item; m_StackPointer++; } public T Pop() { m_StackPointer--; if(m_StackPointer >= 0) { return m_Items[m_StackPointer]; } else { m_StackPointer = 0; throw new InvalidOperationException("스택이 비워 있어 Pop할 자료가 없습니다."); } } } Stack<int> vstack = new Stack<int>(); vstack.Push(1); int number = vstack.Pop(); Stack<Customer> rstack = new Stack<Customer>(); rstack.Push(new Customer()); Customer c = rstack.Pop(); |
Stack 클래스 선언 시 클래스명 뒤에 꺾쇠 괄호 안에 Generic 형식 매개 변수를 지정하여 Stack Generic 클래스 선언합니다.
public class Stack<T> |
Object 간에 데이터 변환하는 대신 Generic 클래스의 Instance는 해당 Instance가 만들어진 데이터 형식을 받아 들이며, 해당 형식의 데이터를 변환 없이 저장합니다. Generic 형식 매개 변수 T는 지정된 실제 형식이 사용될 때까지 자리 표시자 역할을 합니다. T는 내부 항목 배열의 요소 형식(T[] m_Items;)으로, Push 메서드에 대한 매개 변수 형식(public void Push(T item))으로, Pop 메서드에 대한 반환 형식(public T Pop())으로 사용됩니다.
Generic Class의 Instance 생성시 꺾쇠 괄호 표기법을 사용하여 기본 정수 형식을 매개 변수로 지정하여 Stack Class에서 정수 형식을 사용하도록 합니다.
Stack<int> vstack = new Stack<int>(); vstack.Push(1); int number = vstack.Pop(); |
Customer Class(Reference Type)를 매개 변수로 지정하여 Stack Class에서 Customer Class 형식을 사용하도록 합니다.
Stack<Customer> rstack = new Stack<Customer>(); rstack.Push(new Customer()); Customer c = rstack.Pop(); |
Generic 형식 매개 변수는 형식의 기본값을 반환하는 default라는 속성을 지원합니다.
Stack의 Pop() 메서드를 다음과 같이 수정하여 Exception으로 처리하지 않고 default 값을 Return하도록 합니다(참조 형식(Reference Type)의 기본값은 null이고, 정수 또는 열거 및 구조체와 같은 값 형식(Value Type)일 경우 기본값은 ‘0’입니다).
public T Pop() { m_StackPointer--; if(m_StackPointer >= 0) { return m_Items[m_StackPointer]; } else { m_StackPointer = 0; return T.default; //throw new InvalidOperationException("스택이 비워 있어 Pop할 자료가 없습니다."); } } |
위와 같이 클래스에서 Generics을 사용했듯이 구조체에서도 Generics을 사용할 수 있습니다. 예제를 통해 유용한 Generic point 구조체를 만들어 봅시다.
001 using System; 002 using System.Collections.Generic; 003 using System.Text; 004 005 namespace DataGenericsType 006 { 007 public struct Point<T> 008 { 009 public T X; 010 public T Y; 011 } 012 013 class Program 014 { 015 static void 016 { 017 // 일반정수좌표 018 Point<int> intPoint; 019 intPoint.X = 1; 020 intPoint.Y = 2; 021 Console.WriteLine(" 022 Console.WriteLine(""); 023 024 // 부동소수점 정밀도가 필요한 차트좌표 025 Point<double> doublePoint; 026 doublePoint.X = 1.2; 027 doublePoint.Y = 3.4; 028 Console.WriteLine("부동 소수점 Point 좌표: X({0}), Y좌표({1})", doublePoint.X, doublePoint.Y); 029 Console.WriteLine(""); 030 } 031 } 032 } |
[Generics형 변수 실행결과]
4. C# Generics와 다른 언어에서 지원되는 Generics의 차이점
C++의 Template (Generics 지원)
C++에서는 Template라는 형태로 Generics를 제공해 왔습니다. 패턴적인 측면에서는 C#의 Generics와 크게 다르지 않으나, 결정적인 차이가 있다면 C#에서는 CLR이 지원한다는 것입니다. 일종의 매크로(템플릿에 제공된 각 형식 매개 변수에 대한 특수화된 형식을 생성)일 뿐인 C++의 Template과는 달리 Generics는 Runtime시에 JIT 컴파일러에 의해 적절히 해석되고 Native Code로 컴파일된다는 것입니다.
C++와 같은 후기 바인딩 방식에서는 단지 클래스 또는 인터페이스의 상속을 통한 바인딩이었기에 Logic의 확장이 어려웠고, 형식의 안정성에서의 문제점도 꼼꼼히 따져야만 했습니다. 적어도 전달되는 클래스는 같은 클래스에서 상속을 받거나 같은 인터페이스를 구현해야 했습니다. 물론, COM의 IUnknown 같이 자기기술자[Self-Descriptor]를 가진 클래스/인터페이스를 만들면 해결할 수 있겠지만 그 노력은 만만치 않습니다. C#의 Generics를 적용하면 ‘Logic’과 ‘Logic에 의해 다뤄지는 객체’를 명확하게 구분할 수 있습니다.
결국 Domain/Business Logic을 구현이라는 점에서는 크게 다를 바가 없지만 그 유연성에 있어서는 상당히 선택의 폭이 넓어졌다고 할 수 있다.
JAVA의 Generics (Parametric polymorphism)
Java의 Generics는 Pizza라는 프로젝트로부터 시작되었으며, 이후 GJ라고 이름이 붙여졌고, JSR로 바뀐 것입니다. Sun은 Java Virtual Machine을 수정할 필요가 없는 구현을 선택했습니다. 따라서 Sun은 수정되지 않은 가상 시스템에서 generics를 구현함으로 해서 많은 단점들을 가지게 되었습니다.
Java Generics는 어떤 실행 능률도 높여 주지 못합니다. Java Compiler는 List<Integer>라는 Parameterized Type을 만들면 Integer를 Object로 모두 변환하고 필요한 곳에 (Integer) 형 변환을 실행합니다. 이는 수정되지 Java Virtual Machine이 값 형식(Value Type)에 대해 Generics를 지원하지 못하기 때문입니다. 따라서 Java의 Generics에서는 실행 효율성을 얻을 수 없습니다.
Java의 Generics는 erasure of the type parameter(매개변수 타입의 말소)라는 것에 의존하기 때문에 Runtime시에 List<Integer>는 List라는 클래스와 동일한 클래스로 표현됩니다. 이것은 어떤 클래스가 List<Integer>인지 List<String>인지 Runtime시에는 알 수 없다는 것입니다. 이는 Runtime시에 Generics 형식의 Instance에 대한 형식 매개 변수를 확인할 수 방법이 없으며 다른 리플렉션(Reflection) 사용이 엄격하게 제한된다는 의미입니다.
[출처] C#2.0 에 추가된 자료형 Generics|작성자 독선생