정보공간_1

[2기 광주 조영진] Template과 Generic 본문

IT 놀이터/Elite Member Tech & Talk

[2기 광주 조영진] Template과 Generic

알 수 없는 사용자 2012. 11. 18. 22:08

안녕하세요

광주멤버십 21-1기 조영진입니다.

이번 포스팅에서는 저번 포스팅에서 언급했던 

C++의 Template과 JAVA, C#의 Generic에 대해서 알아보도록 하겠습니다.

먼저 C#과 JAVA에서 사용되는 Generic이라는 개념에 대해서 알아보도록 하겠습니다.

일단 C#과 JAVA는 상당히 비슷한 언어죠.

따라서 기본적인 개념 자체는 거의 같기 때문에 따로 나눠서 설명하지 않고

필요한 부분에서만 나눠서 언급하도록 할게요.

저는 C#을 좋아하므로 C#을 가지고 기본적인 설명을 하고 필요한 부분에 있어서

JAVA를 언급하도록 하겠습니다^^


1. Generic

먼저 Generic이 등장하게된 배경부터 알아보도록 하겠습니다.

C#과 JAVA는 "완전한 객체지향을 추구하는" 언어입니다.

C++처럼 "객체지향을 지원하는" 언어 와는 조금 다른개념입니다.

C#과 JAVA는 완전한 객체지향을 추구하기 때문에 모든타입은 

Object라는 최상위 Class로부터 파생되게 됩니다.

따라서 모든 타입의 인스턴스는 Object의 인스턴스로 변환될 수 있습니다.

즉, 다음과 같은 코드가 문제가 전혀 없는 것이죠.

실행 결과도 모두가 예상하는 것처럼

원하는대로 동작을 잘 하죠?

그래서 과거의 C#이나 JAVA에서는 일반화 프로그래밍 자체가 필요가 없었습니다.

모든 타입을 Object에 저장하고 필요한 형식으로 변환해서 사용하면 됐으니까요.

하지만 위와 같은 코드에는 몇가지 문제점이 있습니다.

o라는 객체가 지금처럼 바로 사용되지 않고 여러개의 메소드를 거쳐서 사용되는 경우

거기서 o가 어떤 데이터를 저장하고 있는지 다시 한번 확인을 해야 합니다.

C#에서는 is 또는 as 키워드를 통해서 확인하고 JAVA에서는 instance of 키워드를 사용해서

확인을 하게 되는데 위 연산자들을 호출하는데 있어서 오버헤드가 발생하게 됩니다.

그리고 실제 타입을 확인했다고 하더라도 실제 사용하기 위해서는

해당하는 타입으로 변환을 해 줘야 합니다.

이부분에서 또한번의 오버헤드가 발생하구요.

사실 위의 코드에서 Object o = 32; 라고 썼는데 저기서도 Object형식으로 변환하기 위한

오버헤드가 또 발생을 하게 됩니다.

이러한 성능상 문제점도 있지만 또 다른 문제점도 있는데요.

다음과 같이 작성된 코드를 보시겠습니다.

실제 실행해보기 전에는 위 코드가 문제가 있는지 확인할 방법이 없습니다.

C#에서는 언어적 지원으로 잘못된 캐스팅을 하게 되면 Exception이 발생하게 되지만

만약 이게 C++과 같은 언어였다면 프로그램은 정상적으로 동작하지만

우리가 의도한 32가 아닌 이상한 값이 출력됐을 것입니다.

이러한 여러가지 문제점을 가지고 있지만 하나의 메소드로 모든 타입을 처리할 수 있다는 장점때문에

과거의 C#이나 JAVA에서는 많이 사용되던 코드였다고 합니다.

하지만 그 머리좋은 사람들이 모여서 만든 언어에 이러한 치명적 결함을 계속해서 안고가는건

자존심이 허락을 하지 않았는지는 확실치 않지만 어쨌든 이러한 단점을 해결해 보자!

하면서 등장한 것이 Generic 입니다.

사실 Generic은 C++의 Template과 많은 차이는 없습니다.

아니 개념상으로 보자면 차이점 자체가 없다고 봐도 무방합니다.

다양한 타입에 대해서 컴파일시간에 컴파일러가 자동적으로 코드를 생성해준다는 개념 자체는

Generic과 Template은 동일하다고 생각할 수 있습니다.

하지만 위에서 잠시 언급했던 언어상 설계의 차이점이 Generic과 Template의 차이점을 만드는 것이죠.


2. 배열과 Generic & Template

객체지향에 대해서 배울 때 자주 등장하는 예제중 하나인 도형에 대해서 이야기해보도록 하겠습니다.

Shape라는 SuperClass를 정의하고 이를 상속받아서 Circle, Rect, Triangle 등의 Class를 구현합니다.

간단한 예제이므로 Circle과 Rect만 구현하도록 하고 먼저 C#코드로 보시겠습니다.

아주아주 심플하죠? 그리고 다들 잘 아시는 것처럼 Shape 타입의 객체가 

Circle 객체를 저장하는 것은 가능합니다.

하지만 다음과 같은 코드는 어떻게 생각하시나요?

위 코드는 문법상 아무런 하자가 없는 문법이기 때문에 컴파일은 완벽하게 수행됩니다.

하지만 실제 실행을 하게 되면 예외가 발생하게 됩니다.

JAVA는 C#과 동일하니 비슷한 예제를 C++에서 보도록 하겠습니다.

도형에 대한 부분은 비슷하니 Pass하고 main쪽만 살펴보죠.

위 코드의 실행 결과는 어떻게될까요?

Error? Circle!? Rect!?

실행해보시면 아시겠지만 Circle!이 실행됩니다.

이는 언어적 측면에서 차이가 나기 때문입니다.

C#이나 JAVA와 같은 경우 배열을 생성하는 것은 객체를 생성하는 것이 아닙니다.

즉 main의 첫번째 라인이 실행되더라도 실제 객체가 생성되지는 않습니다.

하지만 C++에서는 main의 첫번째 라인이 실행되면 5개의 Circle 객체가 생성됩니다.

그리고 C#과 JAVA는 CLR과 JVM이 s라는 타입에 대한 정보를 관리하고 있기 때문에

자기가 알고있는 타입과 다른 타입으로 저장하려고 할 때 예외를 발생시키지만

C++과 같은 경우에는 타입에 대한 정보를 관리하지 않습니다.

따라서 최초 생성된 타입으로 Fix를 시키게 되고 이는 실행시에도 동일하게 적용되므로

Circle이 출력되게 되는 것입니다.

사실상 실행시에 정상적으로 동작하지 않는 코드이므로 이러한 경우에는

컴파일시간에 에러를 발생시켜주면 개발하는데 많은 도움이 되지 않을까요?

그래서 Generic과 Template을 사용하는 경우에는 컴파일에러가 발생하게 됩니다.

먼저 C#코드를 보시죠.

Generic Container인 List를 사용해서 Shape와 관련된 객체들을 출력하려고 시도한 경우

위와 같이 완전히 일치하는 형식이 아니면 컴파일 에러가 발생하게 됩니다.

다음으로 C++코드를 보도록 하겠습니다.

Template Container인 vector를 사용해서 Shape와 관련된 객체들을 저장하려고 하는 경우

마찬가지로 컴파일에러가 발생합니다.

동일한 상황에서 JAVA의 경우를 한번 확인해 보겠습니다.

똑같이 Error가 발생하죠?

이러한 부분은 C++의 Template과 C#, JAVA의 Generic의 공통점이라고 볼 수 있는 부분입니다.

하지만 C++과 C#의 경우에는 위와같은 상황을 해결하기 위해서는 

타입을 정확하게 일치시켜서 Template이나 Generic을 사용하는 방법밖에 없습니다.

그런데 JAVA에는 다음과 같은 wildcard라는 해결책이 있습니다.

<? extends Shape>의 의미는 "어떤 타입으로 사용될지는 모르겠는데 그것은 Shape를 상속받았다."

라는 의미입니다.

따라서 Circle이든 Rect든 Shape로부터 파생된 클래스이기 때문에 

PrintList 메소드에서 받아들일 수 있는 것입니다.

그럼 이제 다음으로 차이점에 대해서 알아볼까요?


3. Generic과 Template의 차이점

맨 처음 Template을 소개할 때, 다양한 타입에 대해서 같은 로직을 적용할 때 사용한다고 말한 적이 있죠?

그런 상황을 다시 한번 살펴보도록 하겠습니다.

위의 프로그램은 정상적으로 수행됩니다.

그럼 비슷한 기능을 하도록 C#이나 JAVA에서는 어떻게 구현해야할까요?

결론부터 말씀드리자면 위와 똑같이 동작하는 C# 또는 JAVA 프로그램은 없습니다.

구현하고자 한다면 몇가지 테크닉을 사용해서 우회해야만 합니다.

그 이유는 사실 C++의 Template같은 경우에도 만약 + 연산자를 사용할 수 없는

사용자 정의형 타입이라면 위 코드는 컴파일이 되지 않고 에러가 발생합니다.

C++은 한번 컴파일이 되면 그 뒤로는 동일한 기계어 코드만 실행하게 됩니다.

하지만 C#같은 경우에는 먼저 한번 컴파일을 해서  IL코드로 컴파일이 되고 

이 컴파일된 결과물이 CLR을 통해서 실행될 때, 현재 CLR이 실행되고 있는 환경에 맞춰서 

다시 한번 기계어 코드로 컴파일이 진행되게 됩니다.

즉 C++은 컴파일이 1번 일어나고 C#은 컴파일이 2번 일어납니다.

Template은 실제로 사용되지 않으면 아예 코드가 생성되지 않지만

C#의 Generic은 사용되지 않더라도 그에 관련된 정보를 저장하기 위한 메타데이터가 생성됩니다.

그리고 이러한 메타데이터를 통해서 실제 사용될 때, 관련 코드들을 생성하는 것입니다.

JAVA의 경우에는 C#과 비슷하게 처음에 ByteCode로 컴파일이 되고

JVM이 프로그램 실행시 이 ByteCode를 기계어로 다시한번 컴파일을 하는 구조로 되어있습니다.

JAVA도 C#과 비슷하게 ByteCode에 Generic과 관련된 메타데이터를 저장해두고 실행시

관련 코드로 번역하기는 하지만 C#과 JAVA의 경우에는 차이점이 하나 있습니다.

이 둘의 차이점은 조금 있다 자세히 알아보도록 하고 먼저 Template에서는 가능한 코드가

Generic에서 불가능한 이유에 대해서 생각해 보죠.

Template의 경우에서는 위에서 언급했다시피 1번의 컴파일로 최종 결과물이 나오게 됩니다.

따라서 현재 컴파일하는 시점에 해당 코드가 유효한지 모두 검증을 할 수 있다는 얘기입니다.

위에서 언급한 add라는 함수가 현재 컴파일되는 환경에서 사용이 되는지 안되는지 확인하고

사용이 된다면 구현이 가능한지 검증하여 구현이 가능하다면 해당 add함수를 생성하여

컴파일을 수행하고 구현이 불가능하다면 컴파일에러를 발생시키게 됩니다.

그에비해 Generic의 경우에는 IL 또는 ByteCode로 컴파일이 되더라도 이게 최종 결과물이 아닙니다.

이 IL 또는 ByteCode를 사용하여 다른 결과물을 만들어 낼 수도 있다는 얘기입니다.

그렇기 때문에 IL 또는 ByteCode에는 Generic과 관련된 메타데이터들을 저장해 두고

실제 최종 결과물을 만들어 낼 때 구현을 하기 때문에 현재 정의된 Generic이 어떻게 사용될지는

1번째 컴파일시에는 결정할 수 없습니다.

그래서 Generic에는 한정이라는 개념이 도입되었는데요 이는 해당 Generic을 사용하기 위해서는

Generic을 사용하는 타입이 어떠한 제약조건을 만족해야 한다라고 명시하는 것입니다.

한가지 예를 더 들어보죠.

첫 포스팅에서 언급했던 지정된 타입인 경우에만 다르게 동작하도록 하는 Template Specialization.

이 역시도 C#이나 JAVA의 Generic으로는 구현이 불가능합니다.


4. C#의 Generic과 JAVA의 Generic의 차이점

좀 전에 C#과 JAVA의 Generic에는 차이점이 하나 있다고 했는데요 그부분을 살펴보도록 하죠~!

먼저 C#의 경우입니다.

Generic을 사용해서 String을 저장하는 List를 생성하면 Object를 대입할 수 없는 것을 확인할 수 있습니다.

Object를 삽입하는 코드를 제거하고 생성된 IL 코드를 확인해 보면 다음과 같습니다.

그리고 이번에는 Generic을 사용하지 않고 ArrayList를 사용해서 Object 형식을 사용하는 경우를 보겠습니다.

Generic을 사용할 때와는 3번째 라인부터 약간씩 다르죠?

4번째 라인에서 보시다시피 ArrayList는 Object타입으로 변환해서 데이터를 저장하기 때문에

3번째 라인처럼 다시 꺼내서 사용하기 위해서는 원하는 타입으로 변환을 해 줘야 합니다.

ArrayList를 사용했을 때 IL코드입니다.

일단 단순히 비교하기에도 코드 크기가 좀 커졌죠?

11번째 라인에 보면 System.String 형식으로 변환을 한다는 것을 알 수 있습니다.

그리고 자세히 보시면 코드가 약간씩 다르긴 하지만 List를 생성하고 "hello"를 Add하고 get_Item을 통해서

다시 꺼내는 동작 자체는 동일한 것을 확인할 수 있습니다.

그럼 이제 JAVA쪽 코드를 보겠습니다.

먼저 Generic을 사용하지 않는 경우를 한번 확인해 보겠습니다.

Object 타입에 대한 add에도 에러없이 잘 동작하는 것이 보이고

다시 꺼내서 사용할 때는 String 형식으로 캐스팅해서 사용해야 하는 것도 확인할 수 있습니다.

그럼 이제 컴파일된 결과인 ByteCode를 보겠습니다.

ByteCode에 대한 문법을 모르더라도 11번째 라인을 보시면 ArrayList.get 메소드를 통해서

Object 타입을 하나 꺼내고 12번째 라인에서 java.lang.String으로 캐스팅하고 있다는 것을

확인할 수 있습니다.

그럼 이어서 Generic을 사용한 경우에 대해서 확인해 보겠습니다.

마찬가지로 Generic을 사용해서 String을 저장하는 List를 생성하면 Object는 add할 수 없습니다.

여기까진 C#과 동일한데 ByteCode로 컴파일된 결과를 한번 보겠습니다.

어? Generic을 사용하지 않았을 때와 완전히 동일하네요?

그림을 잘 못올린건 아닙니다.

이게 JAVA의 장점이자 단점이 되는 부분입니다.

C#은 Generic을 도입함으로써 컴파일 타임에 에러를 검출할 수 있도록 하고

불필요한 캐스팅을 하지 않도록 하여 성능면에서도 Generic을 사용하지 않을 때에 비해 개선되었습니다.

JAVA는 Generic을 사용하면 컴파일 타임에 에러를 검출할 수는 있지만

실제 동작하는 코드 자체는 Generic을 사용하지 않았을때와 동일하기 때문에 성능개선은 없습니다.

이부분은 C#이 가지는 JAVA에 비한 장점이라고 볼 수 있습니다.

C#의 경우 Generic이 .NET Framework 2.0버전부터 도입되었는데

.NET Framework 2.0이상에서 개발된 어플리케이션은 .NET Framework 2.0미만이 설치된 시스템에서는

구동이 되지 않습니다.

그에비해 JAVA는 1.5버전부터 Generic이 도입되었는데 실제 동작하는 코드는 그 전과 별 차이가 없습니다.

따라서 1.5버전 이상에서 개발된 어플리케이션도 1.5미만이 설치된 시스템에서도 잘 동작합니다.

이러한 유연성은 JAVA가 가지는 C#에 비한 장점이라고 볼 수 있습니다.

즉, C#은 성능과 타입 안정성을 확보함으로써 약간의 하위호환성을 포기했다면

JAVA는 호환성과 타입 안정성을 확보하고 약간의 성능을 포기한 것이죠.

그럼 JAVA는 대체 동일한 코드를 가지고 list.add(new Object()); 코드에 대해서

한쪽은 에러를 다른 한쪽은 성공을 어떻게 판단하는 걸까요?

다음 두개의 화면을 보시면 이해가 될 듯 하네요~

먼저 Generic을 사용한 경우입니다.

다음은 Generic을 사용하지 않은 경우입니다.

모두 동일한데 마지막에 Local variable type table이라는 부분의 유무만 다릅니다.

그리고 그 곳을 보시면 local 변수중 list라는 애는 type이 java.util.ArrayList<java.lang.String> 이다.

라고 명확하게 명시가 되어 있네요.

즉, JAVA는 이 정보를 가지고 컴파일에러를 찾아내는 것입니다.


5. 결론

Template과 Generic은 등장하게된 배경과 실제로 사용하는 방법이 상당히 유사합니다.

하지만 이들을 구현한 언어적 특성에 따라서 Template에서 구현 가능한 기능들이

Generic에서는 구현 불가능하기도 하며 그 반대의 경우도 있습니다.

하지만 이러한 것들은 Template과 Generic의 차이라기 보다는

그것들을 구현한 언어의 차이라고 받아들여야 할 것 같습니다.


6. 참고문헌

The C++ Programming Language - Bjarne Stroustrup 저

C# in Depth - Skeet, Jon 저