본문 바로가기

IT 살이/04. 기술 - 프로그래밍

LINQ 시리즈 03 - 제네릭(Generics)

제네릭이 뭔지 알아본다. LINQ 표현에 제네릭이 직접 표현되지 않더라도, C# 표현으로 변경하면 보이지 않던 제네릭 표현이 나타나게 된다. C# 표현의 쿼리를 이해할 수 있어야 LINQ 표현을 정확히 이해할 수 있는 바, 이 녀석을 모르고서는 LINQ 표현을 제대로 이해할 수 없다는 얘기가 되겠다.

다음과 같은 상황을 생각해보자. 메소드나 클래스를 정의할때 그 구성 요소에 대한 타입을 미리 알 수 없을때, 즉 여러 타입을 지원하고 싶을때 어떻게 해야 하나. object 타입을 사용하면 될 거라고 생각하고 있나. C# 1.X까지는 정답이다. 다음은 IList 타입의 컬렉션의 Add() 메소드에 대한 정의이다.

public interface IList : ICollection, IEnumerable

{

    int Add(Object value);

}

IList 인터페이스에는 Add()라는 메소드가 정의되어 있는데, 그 파라미터의 타입으로 object 타입을 사용하고 있다. 그럼 어떤 타입의 객체도 모두 포함시킬 수 있게 된다. 그러나 단점은 컬렉션에서 요소 하나를 가져올때, 컬렉션에 저장될때의 타입으로 변환이 이뤄져야 한다는 불편함이 있다.

String s = "test";

IList list = new ArrayList();

list.Add( s );

//...

String ss = (String)list[0];

그러나 단순히 불편함의 차원을 넘어서 안전하지 못한 면이 있다. 컴파일러가 그 타입 변환을 체크하지 않기 때문에 런타임에서야 타입 변환의 에러가 발생할 수 있다는 것이다. 또한 런타임시에 박싱, 언박싱이라는 단계를 거치기 때문에 성능상의 문제도 있을 수 있다.

C#2.0부터는 더 좋은 정답이 나왔다.  제네릭 !

public interface IList<T> : ICollection<T>,IEnumerable<T>, IEnumerable

{

    void Add(T item );

}

제네릭 타입의 IList에 대한 예이다. T 대신에 실제의 어떤 타입을 사용해도 된다. 제네릭을 사용한 코드는 좀 더 명확하고 안전하다. 명확하다는 말은 IList가 어떤 타입의 객체를 포함하는 컬렉션인가를 미리 알 수 있다는 것이고, 미리 알 수 있다는 것은 컴파일러가 추가되는 요소의 타입을 미리 체크할 수 있고 그리고 조회된 요소가 어떤 타입의 변수에 할당될 수 있는지를 미리 알 수 있다는 것이다. 그래서  컴파일 타임에 타입에 대한 체크를 할 수 있어서 좀 더 안전하다는 것이다. 

다음 그림은 Vistual Studio.NET 2008에서의 코딩하는 모습을 캡쳐하고 있다.

1092253364

String 타입의 항목을 받는 List 객체라는 것을 코딩시에도 알고 있기 때문에, Add() 메소드를 호출하면 그림처럼 "string item"처럼 String 타입의 항목을 넣으라는 인텔리센스 표시도 가능해진다.

성능 또한 박싱, 언박싱이 일어나지 않는다.

List<string> sList = new List<string>();

sList.Add("달봉이");


List<int> iList = new List<int>();

iList.Add(0);

이 코드에 해당하는 IL코드를 런타임시, JIT 컴파일러가 머신 언어로 다시 변경할때  String용 List와 int용 List 타입을 각각 만들어 내 버린다. 즉 런타임시에는 파라미터 또는 반환값 또는 코드내의 타입이 결정되어 버린다. 따라서 런타임시에 박싱/언박싱이 일어나지 않게 되는 것이다.

IList<T>라는 타입을 보면 <T>라는 표시가 있는데, "T타입의, Of T type"으로 읽으면 된다. 즉 "IList<String> ss"은 "String 타입의 IList 인스턴스 ss"라고 읽는다. 다르게 읽어도 상관은 엄따. T를 타입 매개변수(type parameter)라고 하는데, 여기서 반드시 문자열로 "T"를 사용할 필요는 없다. 다른 문자, 또는 문자열을 사용해도 된다.  그리고 <>안에 여러개의 타입 매개 변수를 사용할 수 있다. 다음과 같은 제네릭 메소드가 있을 수 있다.

T Method<T, A0>(A0 a, A0 b)

{

    //....

    return r;

}

두 개의 타입 파라미터를 이용하고 있다.  메소드 파라미터 a,b는 A0 타입이고 가공한 후의 리턴값 r은 타입 T임을 표현하고 있다. 

제네릭(generics), 제너릭 타입, 제네릭 메소드 등과 같은 표현을 보면 <T>와 같은 표현이 들어가 있는 타입, 함수 등을 생각하면 된다. 이때 T를 타입 매개변수(type parameter)라고 한다.

이 제너릭 표현에 편안해질수록 LINQ 공부가 그만큼 더 쉬워질 것으로 보인다. 이 포스트에서 제너릭에 대한 모든 문법적인 표현을 설명하지는 않는다. 제네릭에 대한 좀 더 자세한 문법적인 표현에 대해서는 다음 MSDN 도움말을 참고하기 바란다.

제네릭(C# 프로그래밍 가이드)

제네릭 메소드를 호출할때 메소드에서 이용하고 있는 타입을 모두 컴파일러에게 알려줘야 한다. 그러나 type inference( 타입 유추? 정도로 번역할 수 있겠다) 메커니즘을 사용하면 개발자가 직접 타입 파라미터에 해당하는 실제 타입을 알려주지 않아도 컴파일러가 타입을 유추할 수 있다. 이런 컴파일러의 기능을 이용하면 제너릭 메소드를 호출하는 표현이 간단해 질 수 있다. 타입 유추 기능 또한 LINQ 표현을 이해하는데 아주 중요한 개념이라고 할 수 있다. 제너릭 메소드 호출 및 타입 유추에 대해서는 다음 포스트에.