제네릭이란 무엇인가?
 
C# 1.x 프로그래밍의 불만이란 무엇일까.
 
구체적으로는 여러 가지가 있다고 생각하지만, 아마 아래의 내용에 대해서는, 대부분 C# 1.x 프로그래머가 불만이라고 느끼고 있을 것이다.
 
★ 컬렉션의 요소를 엑세스 할 때, 캐스트가 요구된다.
 
구체적으로 말하면, 다음과 같다.

using System;
using System.Collections;
 
class Program
{
 static void Main(string[] args)
 {
    ArrayList list = new ArrayList();
    list.Add("Hello!");
 
    Console.WriteLine(((string)list[0]).ToUpper()); // 캐스트필요
    // 출력:HELLO!
 }
}
리스트1 C# 1.x 에서 컬렉션을 사용한 예

 
이 프로그램에서는, 가변 사이즈 1차원 리스트 컬렉션을 구현했다.
ArrayList 클래스에서 문자열을 넣고 나서 꺼내고 있다. 여기서, 아무일도 없어 문자열을 컬렉션에 넣을 수 있는데, 꺼낼 때는
 
(string)list[0]
 
이러한 캐스트가 필요하게 된다. 그리고, 이 캐스트 작업은 귀찮다.
 
우선, 쓰지 않으면 안되는 문자가 증가해 귀찮다. 본질적으로 필요가 없는 문자가 소스 코드상에 증가하면, 그만큼 기독성도 나빠져, 점검성도 떨어진다. 하지만, 이것은 아직 시초에 지나지 않는다.
 
캐스트를 사용하면, 그만큼 실행 속도가 저하된다. 캐스트는 무거운 처리인 것이다. 덧붙여서, C# 의 캐스트보다 as 연산자를 사용하는 것이 더 빠르지만, as 연산자는 캐스팅을 할 수 없을 때 null 이 리턴되고, 이 null 을 비교해야 하기 때문에 취급하기 어렵다.
 
그리고, 캐스트를 임용하면, 형변환의 타당성 체크를 컴파일시에 실시할 수 없게 되어, 성능이 저하된다. 이것을 [객체의 형변환은 실행시에 체크되기 때문에 문제 없다] 라고 주장하는 사람도 있지만, 본질적으로 이것은 무엇이 문제일까를 잃은 발언이라고 할 수 있다. 형변환이 런타임시 체크되는 것은, 체크가 지연된다는 것이 문제가 아니고, 형변환 체크가 [개별의 값]에 대해서 행해진다고 하는 문제인 것이다.
 
보다 구체적으로 보자. 캐스트를 임용하면, 우선 이하와 같은 상황이 발생한다.
 
★ 테스트를 할 때, 모든 행이 실행된다고는 할 수 없다. 즉, 실행되지 않은 행에 쓰여진 캐스트의 타당성은 체크되지 않는다.
 
이것은 테스트 지원 툴등을 이용하여, 커버리지(Coverage)가 100%를 목표로 하면 해소할 수 있을 지도 모른다.
 
★ 우연히 테스트 실행시에 주어진 값에 대해서 캐스트가 타당하다고 확인하지만, 실행시에 모든값에 대해서, 캐스트가 타당한가는 체크되지 않는다.
 
이러한 문제는, 작은 프로그램에서도 버그가 생길 수도 있고, 큰 규모의 프로그램에서는 상당한 리스크가 된다. 예를 들면, 컬렉션에 생각할 수 도 없는 형태의 값이 들어가 있고, 잘 되어야할 캐스트가 실패한다라고 가정하에, 사양 변경이 많은 프로젝트나, 의사소통이 불충분한 개발 프로젝트에서는 큰 충격이 될 수 밖에 없다.
 
이러한 문제는, 가능한 여러 형타입을 컴파일시에 실시하는것으로 해결할 수 있다.
 
이러한 작업이 큰 불만이 아닐 수도 있다. 그러나, C++ 의 template 이라 불리는 기능은, 컬렉션이나 클래스의 캐스트 지옥에서 벗어날 수 있었다. 때문에 C# 1.x 로서는 큰 불만이 될 수 있을거라 생각한다.
 
그리고, C# 2.0 은 이러한 문제를 해소할 수 있는 기능이 추가 되었다. 그것은 C++ 의 template 기능과 다른, 제네릭이라고 하는 비슷하면서도 다른 기능이다.
 
이 기능을 사용하면, 리스트1 예제로부터 캐스트를 추방할 수 있는 것을 알 수 있다.

using System;
using System.Collections.Generic;
 
class Program
{
 static void Main(string[] args)
 {
    List<string> list = new List<string>();
    list.Add("Hello!");
 
    Console.WriteLine(list[0].ToUpper()); // 캐스트불요
    // 출력:HELLO!
 }
}
리스트2 C# 의 제네릭을 사용한 예

 
여기서 list 변수를 선언할 때,
 
List<string>
 
과 같이, string 이라는 타입을 명시하는 것으로써, 소스코드에서 캐스트를 사용하지 않는다.
 
새로운 컬렉션의 소개
 
리스트1에서 사용하고 있는 List 클래스는, 기존의 ArrayList 에 상응하는 컬렉션 클래스다. 덧붙여서 List 클래스가 속하는 네임스페이스는 System.Collections 가 아니고 System.Collections.Generic 이다.
 
비쥬얼스튜디오 2005에서는 코드파일을 만들게 되면, 기본적으로
 
using System.Collections.Generic;
 
위와 같이 작성된다.
 
종래의 컬렉션
제네릭의 컬렉션
기능
ArrayList
List
가변 사이즈의1 차원 리스트
Hashtable
Dictionary
키/치 페어의 컬렉션
Queue
Queue
선입처 내밀기 컬렉션
SortedList
SortedList
키로 소트 된 키/치 페어의 컬렉션
Stack
Stack
후입선출 컬렉션
컬렉션·클래스의 대응표
제네릭의 컬렉션·클래스는.NET Framework 2.0 그리고 클래스·라이브러리에 추가된 것.물론 이것에는 종래의 컬렉션·클래스도 포함되어 있다.
 
덧붙여서, Stack 클래스와 같이 기존의 이름과 같은 제네릭 클래스가 있는것에 주의가 필요하다. 이것은 다른 네임스페이스에 속하는 다른 클래스다. 그중, 특히 큰 문제는 다음에 설명한다.
 
그런데, ArrayList 로부터 List 로의 이름 변경은, 자주 사용되는 클래스의 이름이 짧아졌다고 하는 것으로 사용하기 편해졌다고 말할 수 있을 것이다.( ArrayList 와 상응하는 List<string> 처럼, 짧아졌다고는 말하기 어렵지만…)
 
한편, HashTable 은 Dictionary 로의 이름변화는, 기존 이름과 전혀 다른 이름변화라고 할 수 있다. 이름으로 유추할 수 없고, 새로운 클래스명을 암기 할 수 밖에 없을 것이다.
 
새로운 컬렉션 클래스 – LinkedList 클래스
 
LinkedList 클래스는 1차원적인 배열만 취급한다고 하는 의미에서 ArrayList/List 클래스와 닮을 기능을 가지고 있지만, 다른 성질을 가지고 있다.
 
주된 차이점은 두가지 이다. 첫째, 인덱스에 의한 엑세스를 할 수 없다. 즉, 인덱서에 번호를 지정하여 엑세스 할 수 없는 것이다. 둘째, 리스트 중간에 다른 요소를 삽입/삭제를 빠르게 할 수 있다.
 
여기서, List 클래스와 LinkedList 클래스의 삽입 성능의 차이를 보자. 아래는 2개의 요소를 가지는 리스트의 중간에 1만개의 요소를 삽입하는 작업을 100회 반복하는 샘플 코드이다.
 

using System;
using System.Collections.Generic;
 
class Program
{
 static void Main(string[] args)
 {
    // List 클래스의이용
    DateTime start1 = DateTime.Now;
    for (int i = 0; i < 100; i++)
    {
      List<string> list1 = new List<string>();
      list1.Add("First");
      list1.Add("Last");
      for (int j = 0; j < 10000; j++)
      {
        list1.Insert(1, "Inserted");
      }
    }
    DateTime end1 = DateTime.Now;
    Console.WriteLine(end1 - start1);
 
    // LinkedList 클래스의이용
    DateTime start2 = DateTime.Now;
    for (int i = 0; i < 100; i++)
    {
      LinkedList<string> list2 = new LinkedList<string>();
      list2.AddFirst("First");
      list2.AddLast("Last");
      for (int j = 0; j < 10000; j++)
      {
        list2.AddAfter(list2.First, "Inserted");
      }
    }
    DateTime end2 = DateTime.Now;
    Console.WriteLine(end2 - start2);
 }
}
리스트3 List와 LinkedList 의 삽입 성능의 차이
Pentium D 3.2Ghz CPU 로 디버그 빌등에 의한 실행 결과

 

00:00:05.3480694 (List 클래스이용시)
00:00:00.0820164 (LinkedList 클래스이용시)
리스트3의 실행결과

 
위의 결과를 통해, 엄청난 성능 차이가 나고 있다.
 
LinkedListNode 클래스의 인스턴스는 리스트에 포함하거나 제외하거나 하는 작업에 용이하고, 그 특징을 잘 활용하면 리스트를 분해해 재구축하는 처리에 좋은 성능을 낸다. ArrayList/List 클래스를 사용하고 성능이 나오지 않는 경우는 LinkedList 클래스로 테스트해 보면 좋을 것이다.
 
다만, 메모리의 사용량(확보되는 오브젝트 수)는 LinkedList 클래스가 더 커진다고 하는 디메리트도 있다. 위의 프로그램을 따로따로 분리해 실행시키면, 프로그램 종료시점으로 List 클래스는 약 6Mbytes 정도의 메모리를 소비하고 있는데 비해, LinkedList 클래스는 보다 많은 약 11Mbytes 정도의 메모리를 소비하고 있다.
 
새로운 컬렉션 클래스 – SortedDictionary 클래스
 
C# 2.0 에서 추가된 SortedDictionary 클래스는 SortedList 클래스와 기능이 비슷하다. 그러나 완전히 같지는 않다. SortedDictionary 클래스와 SortedList 클래스의 관계는, LinkedList 클래스와 List 클래스의 관계와 비슷하다.
 
SortedDictionary 클래스와 SortedList 클래스의 차이는, 메모리의 사용 방법과 삽입 및 삭제의 속도이다. 주된 차이점은 다음과 같다.
 
SortedList 클래스는 SortedDictionary 클래스만큼 메모리를 사용하지 않는다.
 
SortedDictionary 클래스에는 정렬되지 않은 데이터에 대해서 고속 삽입 및 삭제가 가능
 
모든 리스트의 데이터를 한번에 꺼내는 경우 SortedList 클래스가 SortedDictionary 클래스보다 빠른 성능을 보인다.
 
Posted by 땡초 POWERUMC
TAG ,

댓글을 달아 주세요