테크니컬 컬럼-자바

자바 제네릭스

JDK 5.0은 오랫동안 변하지 않았던 자바 언어의 구문에 적지 않은 변화를 가져왔다. 애너테이션, for each 문, varargs 등도 구문 변화에 기여했지만, 가장 눈에 띄는 변화는 역시 제네릭스라고 해야겠다.

제네릭스는 C++의 템플릿을 기억하는 프로그래머에게는 친숙할 수 있는 구문을 사용하지만, 조금 더 들여다보면 C++의 템플릿과는 다른 존재라는 것을 알게 된다.

자바의 제네릭스는 여타 언어의 것과는 상당히 다른 방식으로 구현되어 있다. 따라서, C++와 같은 언어에서의 사용 경험을 가지고 쉽게 접근하려 했다가 낭패를 볼 수 있다.

제네릭스가 내건 슬로건은 더 안전한 자료형의 세상이다.

컴파일러가 좀더 세밀하게 자료형 검사를 해서 엄밀하게 정의된 자료형을 사용하는 언어를 만든다는 게 제네릭스의 핵심이다.

여기에서는 제네릭스에 대한 개괄적 소개부터 몇 가지 사용 상의 주의점까지 다뤄보고자 한다.


윤경구 yoonforh at gmail dot com

티맥스소프트 기술연구소에서 BPM 팀을 이끌고 있다. 이전에는 자바 워드 프로세서 개발, PCS HLR 시스템 개발 등에 참여했다. 국내 최초로 자바 웹 게시판인 자바 묻고 답하기 게시판을 운영했으며(1997), 입문서인 지나와 함께 하는 자바 2(1999), 본격적인 자바 개발자를 위한 두꺼운 책인 자바 2 SDK 1.4 시작 그리고 완성(2003) 등의 책을 집필했다. 그외, 프로그램 세계 특집 애플릿의 향기(1996), 마소 주니어 자바 이야기(1999) 등 여러 차례 자바 관련 기사를 기고하였다. 국내 소프트웨어 산업의 어려운 현실 속에서 오래도록 코더로 남고 싶다는 소박한 희망을 가지고 있다.



가이드

운영체제 | 자바 2 SDK 5.0판 실행 가능 환경

개발도구 | 자바 2 SDK 5.0판

기본지식 | 자바 2에 관한 일반 지식


자바에 제네릭스를 도입하기 위한 연구는 이미 8년전인 1996년부터라고 한다. 실제로 자바에 제네릭스를 도입하는 몇 가지 방안들이 논문으로 나오기 시작한 것이 1998년 초임을 감안하면 무려 8년이 지난 후에야 JDK 5.0에 전격 채택되었다는 것은 이것이 얼마나 어려운 일이었나 하는 것을 보여준다.

자바의 스펙을 결정하는 표준화 절차인 JSR을 보면, 제네릭스를 논의하는 JSR 14가 형성된 것은 1999년이었다. 하지만, 아쉽게도 JDK 1.4가 나온 시점이었던 2002년까지 제네릭스를 자바에 적용할 수가 없었다. 제네릭스를 어떻게 구현할 것인가에서부터 제네릭스를 도입한 영향을 어떻게 최소화하고 기존 JDK 버전과의 역방향 호환성을 유지할 수 있을 것인가 하는 이슈에 이르기까지 제네릭스는 그야말로 뜨거운 감자였던 것이다.

조금 뒤늦은 감이 있긴 하지만, JDK 5.0이 출시된 지금도 여전히 다른 방식의 제네릭스 구현을 선호하는 그룹들이 있으며, “Thinking In Java”로 유명한 브루스 에켈은 서운한 감정을 자신의 블로그에서 여과없이 드러내고 있기도 하다.

물론 추후 JDK 버전에서 다른 방식의 제네릭스 구현이 채택되지 않는다는 보장은 없을 것이다. 특히 C#이라는 만만찮은 경쟁 상대를 두게 된 현실에서는 더욱 더 그러하다. C#에서도 최근 제네릭스가 채택되었으며 그 구현 방식은 자바와 반대이며 C++와 유사하다는 점에서 자바와 C#의 제네릭스 성공 여부가 주목을 받을 것 같다.


제네릭스 사용하기와 만들기

제네릭스 사용하기

자바 제네릭스 클래스들은 java.util 패키지의 컬렉션 라이브러리 클래스들과 밀접한 관련을 가지고 있다.

어떻게 보면 컬렉션 라이브러리를 사용할 때 좀더 자료형 안정성을 보장할 방법이 없을까 하는 용도로 만든 것이 자바 제네릭스가 아닐까 생각될 정도이다.

자바 5.0의 컬렉션 라이브러리 클래스들은 모두 제네릭스를 사용하도록 다시 정의되었다. 따라서, 컬렉션 라이브러리를 사용할 때 제네릭스는 분명한 효용성을 보인다.


<리스트 1> UseCollection.java 소스 코드

import java.util.*;


public class UseCollection {

    public static void main(String[] args) {

        List<String> slist = new ArrayList<String>(); // String의 리스트

        slist.add("hello");

        slist.add("generics");


        for (Object o : slist) {

            System.out.println("string value = " + o);

        }


        List<Integer> ilist = new ArrayList<Integer>(); // Integer의 리스트

        ilist.add(1);

        ilist.add(2);


        for (Object o : ilist) {

            System.out.println("integer value = " + o);

        }

    }

}


간단한 소스 코드이지만 자바 5.0을 처음 접하는 자바 프로그래머라면 몇 가지 어색한 점을 발견했을 것이다. 제네릭스 외에 사용된 두 가지 새로운 자바 언어의 기능은 오토박싱/언박싱과 foreach 스타일의 for 반복문이다.

ilist.add(new Integer(1))과 같이 사용하지 않고 ilist.add(1)과 같이 사용한 것은 자바 5.0에 추가된 기본 자료형과 해당 객체 자료형과의 오토박싱/언박싱(autobox/unbox) 기능을 사용한 것으로, 오토박싱이란 자바 컴파일러가 객체를 요구하는 곳에 기본 자료형이 대입될 경우 자동으로 해당하는 기본 자료형의 래퍼 객체(wrapper object)로 변환해주는 것을 말하며, 오토언박싱은 그 반대의 일을 뜻한다.

foreach 스타일의 for 반복문은 컬렉션 클래스에서 Iterator를 사용하는 번잡함을 자바 컴파일러가 대신 수행해주는 개념으로 for 반복문 조건식 괄호가 변수와 ‘:’ 부호 그리고 컬렉션 객체로 선언될 경우 지정된 컬렉션 객체의 Iterator를 순차하면서 그 내용이 되는 객체를 변수에 매번 대입해주는 개념이다. 영어로는 “foreach ~ in” 이라고 읽는다.


자, 이제 꺽쇠 괄호를 포함하는 리스트 선언들을 보자. 이 부분에서 바로 제네릭스가 사용되었다.

UseCollection 소스 코드에서 ArrayList 클래스와 List 인터페이스는 더 이상 모든 객체를 수용하는 리스트가 아니다. slist 변수는 String 객체만 받아들이며, ilist 객체는 Integer 객체만 받아들인다.

즉, 다음과 같이 사용한다면 자바 컴파일러가 컴파일 에러를 발생시킨다.


slist.add(3);

ilist.add("world!");


제네릭스를 사용하는 가장 적합하고 중요한 목적은 바로 컬렉션 클래스들에 사용될 객체들의 자료형을 엄밀하게 제한하는 것이다.


제네릭스 만들기

지금까지는 아주 순조롭게 자바의 새로운 기능 제네릭스를 느껴볼 수 있었다. 자, 이제 제네릭스 클래스를 한 번 만들어보자.

제네릭스 클래스를 만들기 전에 경고를 하나 해야겠다.

혹시 C++ 템플릿을 정의해본 적이 있다면, 그 경험으로 제네릭스 클래스도 쉽게 정의할 수 있다고 생각하면 큰 오산이다. “헬로, 제네릭스” 클래스는 조금 불편한 경험이 될 것이다.

자, MyVector 클래스 선언을 살펴보자.


<리스트 2> MyVector.java 소스 코드

public class MyVector<E> {

    public static final int ARRAY_SIZE = 10;

    private E[] elements;

    private int elementCount = 0;


    public MyVector() {

        elements = (E[]) new Object[ARRAY_SIZE]; // unchecked typecast warning

    }


    public void add(E value) {

        if (elementCount >= ARRAY_SIZE) {

            throw new IndexOutOfBoundsException("element count reached max size");

        }


        elements[elementCount++] = value;

    }


    public E get(int index) {

        if (index >= elementCount) {

            throw new ArrayIndexOutOfBoundsException(index);

        }


        return elements[index];

    }


    public static void main(String[] args) {

        MyVector<Integer> vector = new MyVector<Integer>();


        for (int i = 0; i < 10; i++) {

            vector.add(i);

        }


        for (int i = 0; i < 10; i++) {

            System.out.println("[" + i + "]th value : " + vector.get(i));

        }

    }

}


최대한 간단하게 구현하기 위해 크기가 항상 10으로 고정된 벡터 클래스를 상정하였다. 일단 클래스 선언에서 제네릭 자료형인 E를 선언하고 있다.

그리고 멤버 필드로 선언된 elements의 자료형이 역시 제네릭 자료형인 E, add 메소드의 인자도, get 메소드의 반환 유형도 모두 E로 선언되어 깔끔한 듯이 보인다.

C++ 개발자였다면 여기까지 당연하게 받아들이면서 자바 언어는 역시 조금이라도 더 간단한 구문으로 제네릭스를 지원하려니 하고 술술 넘어갔을 수도 있다.

그런데, 가만히 보면 생성자 내용이 조금 이상하다.


elements = (E[]) new Object[ARRAY_SIZE];


Object 배열 자료형에서 제네릭 자료형인 E의 배열 자료형으로 명시적인 형변환이 일어났다. 자바 제네릭스의 비밀을 모른다면 당연히 다음과 같이 시도했을 것이다.


elements = new E[ARRAY_SIZE];


자바 컴파일러는 무심하게도 이 코드를 “generic array creation"이라는 에러로 처리한다.


“자바의 제네릭 자료형은 객체를 생성할 수 없다!!!”


즉, 제네릭 자료형 이름이 T라면 new T()를 할 수 없다. 즉, 선언은 할 수 있지만 객체 인스턴스를 만들 수 없는 유령 자료형이라는 것이다.

자바 소스 코드의 제네릭 자료형이 컴파일 시까지만 존재하고 실제 컴파일된 바이트코드에는 존재하지 않기 때문에 실행 시간에 해당하는 제네릭 자료형의 인스턴스를 만드는 것은 원천적으로 불가능하다. 마찬가지 이유로 제네릭 자료형의 배열도 생성할 수가 없다. 생성자에서 컴파일러 에러가 난 이유는 제네릭 자료형의 배열을 생성하려고 시도했기 때문이다.

불쾌함은 약간 더 지속되는데, MyVector.java 소스 코드를 컴파일해보면 생성자 부근에서 컴파일러 경고가 나타난다. 컴파일러 경고 내용을 보려면 -Xlint:unchecked 옵션을 사용해야 한다.


javac -Xlint:unchecked MyVector.java

MyVector.java:29: warning: [unchecked] unchecked cast

found   : java.lang.Object[]

required: E[]

        elements = (E[]) new Object[ARRAY_SIZE];

                         ^

1 warning


이 경고 메시지는 물론 실제로는 Object 배열인 elements 멤버 필드를 제네릭 자료형인 E 자료형의 배열로 강제 형 변환을 했을 때, 형이 맞지 않아서 발생하는 일을 컴파일러는 책임질 수 없다는 뜻이다.

하지만, 자바가 실행 시간에는 제네릭 자료형 정보를 가지고 있지 않고, 또 MyVector 클래스는 제네릭 자료형이 String이든 Integer이든 실행 시간에는 모두 동일한 클래스로 간주되므로, 실제로 멤버 필드인 elements의 자료형은 모든 객체의 부모 클래스인 Object 배열로 처리된다. 여기에 대해서는 다시 설명할 것이다.

따라서, 첫 번째 제네릭 클래스인 MyVector 클래스는 제네릭스에 관한 한 최선을 다해 정확하게 선언된 셈이다.


제네릭스 들여다 보기

자료형 지우기

자바 제네릭스는 앞에서 살펴본 대로 만들어진 클래스를 사용하기에는 코드에서 강제 형변환을 많이 사라지게 하고, 버그의 가능성을 줄여주는 멋진 친구이지만, 직접 만들어 사용하기에는 상당히 불편한 녀석이다.

이것은 제네릭스의 구현 방법과 무관하지 않은데, 자바 제네릭스는 자료형 지우기(type erasure)라는 접근 방법으로 구현되었다. 자료형 지우기는 간단하게 표현하자면 컴파일러가 컴파일 시에 제네릭 자료형에 대한 정보를 모두 검사하고 이를 통과할 경우 제네릭 자료형 정보가 전혀 없는 바이트코드를 생성하는 방식이다.

따라서 다음은 “true”를 출력한다.


List<String> list1 = new ArrayList<String>();

List<Integer> list2 = new ArrayList<Integer>();

List list3 = new ArrayList();

System.out.println(list1.getClass() == list2.getClass() && list2.getClass() == list3.getClass());


클래스는 모두 공유하지만 실제 클래스의 제네릭 자료형 변수 값은 각 객체 인스턴스별로 달라질 수 있으므로, 같은 클래스 내에서 공유되는 static 문맥에서는 클래스에 선언된 제네릭 자료형을 참조할 수가 없다. 즉, static 변수, static 초기화 블록, static 메소드 등에서 클래스에 선언된 제네릭 자료형을 사용할 수가 없다.

마찬가지 이유로 다음 표현식은 컴파일 에러를 발생시킨다.


List<String> list = new ArrayList<String>();

System.out.println(list instanceof ArrayList<String>); // 컴파일 에러



즉, 실행 시에는 제네릭 자료형에 대한 정보가 없으므로, instanceof 연산자를 제네릭 자료형에 대해 실행할 수가 없는 것이다.

자료형 지우기와 반대의 구현 방법으로는 흔히 구상화(reification)라고 부르는 방법으로 바이트코드에 제네릭 자료형에 관련된 정보를 실제로 생성하는 방법이 있다. C#이 이러한 방식으로 1.1 버전에서 제네릭스를 구현했다고 하며, 자바에서는 기존 애플리케이션이나 라이브러리와의 호환성을 최우선으로 고려하여 자료형 지우기 방식을 채택했다고 한다.


제네릭스 와일드 카드

자바 제네릭스는 C++의 템플릿과 유사하다고 생각했던 사람들에게 또하나의 일탈을 느끼게 해주는 것이 바로 이 와일드 카드 제네릭 자료형일 것이다. 와일드 카드 자료형은 제네릭 자료형을 선언할 때 제네릭 자료형을 임의의 자료형 혹은 클래스 상속 계층 구조 상의 특정 범위를 지정할 수 있게 해준다.

자바 제네릭스에서 와일드 카드는 ‘?’ 문자로 표시된다. 다음에서와 같이 와일드 카드인 ‘?’ 문자로 표시된 제네릭 자료형은 임의의 모든 자료형을 가르킨다.


List<?> wclist = new ArrayList<String>();

List<?> wclist2 = new ArrayList<Integer>();


다음은 와일드 카드를 사용한 List의 예이다. 와일드 카드로 표현된 제네릭 자료형을 가진 wclist 변수는 String 리스트와 Integer 리스트를 모두 받을 수 있다.


<리스트 3> WildcardList.java 소스 코드

import java.util.*;


public class WildcardList {

    public static void main(String[] args) {

        List<String> slist = new ArrayList<String>();

        slist.add("hello");

        slist.add("world");


        List<Integer> ilist = new ArrayList<Integer>();

        ilist.add(1);

        ilist.add(2);


        List<?> wclist = slist;

        for (Object o : wclist) {

            System.out.println("value = " + o);

        }


        wclist = ilist;

        for (Object o : wclist) {

            System.out.println("value = " + o);

        }

    }

}


자바 제네릭스의 와일드 카드는 임의의 객체를 표현할 뿐만 아니라 클래스 상속 계층 구조 상의 경계를 지정할 수 있다. 이를 위해 super와 extends라는 두 예약어를 사용한다.


List<? extends Number>

List<? super Integer>


extends를 사용하는 와일드 카드는 흔히 와일드 카드의 상한을 지정한다고 하는데, 그 의미는 extends 다음에 나오는 클래스를 포함하여 그 자식 클래스들이 제네릭 자료형으로 올 수 있음을 나타낸다.

super를 사용하는 와일드 카드는 그 반대로 와일드 카드의 하한을 지정하는데, 그 의미는 super 다음에 나오는 클래스를 포함하여 그 부모 클래스들이 제네릭 자료형으로 올 수 있음을 나타낸다.

앞의 소스 코드에서 눈여겨 볼 점은 wclist 변수에서 직접 add() 메소드를 호출하지 않는다는 점이다. 실제 wclist 변수 안에 String 리스트가 대입되어 있다고 하더라도, 컴파일러는 add() 메소드의 시그너처가 원래 add(E)였으므로, E가 ?로 적용되어 있으므로 add(?)로 간주하고 입력된 인자의 자료형이 ‘?’이기를 기대한다. ‘?’은 임의의 객체이므로 자바 컴파일러는 String을 받아들일 수 있다는 확신을 하지 못한다.

만약 입력될 수 있는 객체 자료형의 범위가 String이거나 String의 부모 클래스로 제한된다면, 자바 컴파일러는 이 경우 String을 받아들일 수 있다고 판단한다.


// add 메소드가 입력 변수로 제네릭 변수를 받으므로 하한 경계의 와일드 카드를 사용함

List<String> slist = new ArrayList<String>();

List<? super String> wclist = slist;

wclist.add("wild");

wclist.add("card");


for (Object o : wclist) { // iterator()의 결과값은 Object로만 처리

    System.out.println("value = " + o);

}


일반적으로 자바 컴파일러는 입력 변수로 주어진 제네릭 자료형에 대해서는 와일드 카드의 하한 경계를 지정함으로써 자료형의 제약을 풀 수 있고, 반환 유형으로 주어진 제네릭 자료형에 대해서는 와일드 카드의 상한 경계를 지정함으로써 자료형의 제약을 풀 수 있다.


// iterator 메소드가 반환 변수로 제네릭 변수를 주므로 상한 경계의 와일드 카드를 사용함

List<? extends String> wclist2 = slist;

for (String s : wclist2) { // 상한 경계 덕분에 String 사용 가능

    System.out.println("string = " + s);

}


extends와 super를 사용하여 와일드 카드의 경계를 정하는 것에서 한 가지 유추해볼 만한 사실은 다음이다.

앞에서 List<String> 변수에 ArrayList<String> 변수를 대입하는 것은 자연스러웠다. 즉, ArrayList<String>은 List<String>의 자식 자료형이며 List<String>으로 취급할 수 있다. ArrayList 클래스가 List 인터페이스를 구현하고 있으므로 즉, 자식 자료형이므로 이것은 합리적이다.

하지만, ArrayList<String> 변수에 ArrayList<Object> 변수를 대입하는 것은 에러가 발생한다. 즉, ArrayList<String>은 ArrayList<Object>의 자식 자료형이 아니며 ArrayList<Object>로 취급할 수 없다는 것이다. 그렇기 때문에 ArrayList<String>과 ArrayList<Object>를 모두 취급 가능한 제네릭스 형태는 ArrayList<Object>가 아니라 ArrayList<? super String>이 되는 것이다.


제네릭 메소드

자바 제네릭스는 클래스나 인터페이스와 같은 자료형을 선언할 때 클래스나 인터페이스의 변경 가능한 자료형 변수로 지정하는 경우 외에도, 메소드에서도 사용할 수 있다.

클래스와 인터페이스에서 제네릭 자료형 변수를 사용할 때와 메소드에서 사용할 때에는 조금 의미가 다르다.

앞에서 MyVector 클래스를 선언할 때 MyVector 클래스에서 사용하는 제네릭 자료형 변수인 E가 멤버 필드나 멤버 메소드에 사용될 때, 이 E 자료형은 모두 동일한 자료형을 뜻하였다. 즉, 제네릭 자료형에 대입된 실제 클래스가 String이면 멤버 필드의 E 자료형도 String으로 간주되고, 멤버 메소드의 인자나 반환 유형으로 사용된 E 역시 String으로 간주되었다. 이 점을 유념하면서 제네릭 메소드를 알아보자.

제네릭 메소드는 메소드를 선언할 때, 메소드 시그너처 앞 부분에 꺾쇠 괄호 안에 제네릭 자료형 변수를 선언한다.

만약 제네릭 메소드에 사용된 제네릭 자료형 변수가 와일드 카드라면 별도로 선언할 필요가 없다.

다음 예에서는 T가 제네릭 자료형 변수로 선언되었다.


public static <T> T genericMethod(T a, Collection<T> b) {

  // ...

}


다음 코드는 두 개의 제네릭 메소드 getOne()과 getOneElement()를 예시하고 있다.


<리스트 4> PolymorphicMethod.java 소스 코드

import java.util.*;



public class PolymorphicMethod {

    private static boolean toggle = false;


    public static void main(String[] args) {

        List<String> list = new ArrayList<String>();

        list.add("hello");

        list.add("world");


        Set<Integer> set = new HashSet<Integer>();

        set.add(1);

        set.add(2);


        Collection<?> col = getOne(list, set);

        System.out.println(col);


        Object el = getOneElement(list, set);

        System.out.println(el);

    }


    static <T> T getOne(T a, T b) {

        toggle = !toggle;

        return toggle ? a : b;

    }


    static <T, U> Object getOneElement(List<T> a, Set<U> b) {

        toggle = !toggle;

        return toggle ? a.get(0) : b.iterator().next();

    }

}


getOne() 메소드의 경우, 제네릭 자료형 타입인 T가 두 개의 인자와 반환 유형 세 군데서 사용되고 있다. 이 경우 각 인자 T에 적용되는 자료형이 반드시 같은 필요는 없는데, 자바 컴파일러는 실제 이 메소드를 호출하는 곳에서 인자들의 자료형을 확인해서 제네릭 자료형 타입 변수에 들어갈 실제 자료형을 유추하는 기능을 제공한다.

예제의 경우, getOne() 메소드에 사용된 두 개의 인자가 하나는 List<String> 자료형이고, 다른 하나는 Set<Integer> 자료형이다. 자바 컴파일러는 이 두 자료형의 공통 부모 클래스인 Collection을 유추해내고, 또 String과 Integer를 모두 처리할 수 있는 와일드 카드인 ‘?’를 해당 Collection의 제네릭 자료형으로 유추해낸다.

즉, 이 경우에는 T가 Collection<?>으로 결정된다. 결과 값 역시 T 자료형이므로 getOne() 메소드를 호출한 결과값을 Collection<?> 자료형의 객체에 저장하면 아무런 경고 없이 컴파일된다.

두 번째 제네릭 메소드인 getOneElement() 메소드는 여러 개의 제네릭 자료형 타입을 선언하는 예를 보여주고 있다.


제네릭스와 배열

배열은 자료형 중에서 조금 특이하면서도 까다로운 존재이다.

코드에서 보는 대로 현재 자료형이 비록 Object 배열로 선언되어 있다 하더라도 배열을 생성할 때 String의 배열로 생성하였다면, 배열의 원소로 String이 아닌 Object 객체를 넣을 수가 없다.


Object[] objArray = new String[1];

objArray[0] = new Object(); // 실행 시간 에러!


이러한 배열의 특성 때문에 자바 제네릭스에서 배열의 컴포넌트 자료형만을 제네릭 자료형 변수로 사용할 수가 없다. 즉, 다음은 허용되지 않는다. 다만 컴포넌트 자료형에 상하한 제약 없는 ‘?’ 와일드 카드를 쓰는 것은 허용된다.


List<String>[] list = new List<String>[10]; // 제너릭 자료형 객체의 배열. 허용 안됨.

List<?>[] list = new List<?>[10]; // 제약 없는 와일드 카드 제너릭 자료형 객체의 배열. 허용됨.

public <T> T[] toArray(T[] a) { } // 제너릭 자료형 배열. 허용됨


언뜻 생각하기에 자료형 안정성 검사를 컴파일 시에 강화하는 자바 제네릭스의 취지에 비추어 이러한 배열의 자료형 검사 문제에 대해서도 개선이 있을 듯도 하지만, 적어도 자바 5.0의 제네릭스는 별다른 해법을 제시하지 않는다.


브리지 메소드

브리지 메소드는 제네릭스를 구현하는 방법에 의해 사용되는 독특한 형태의 자바 메소드이다. 브리지 메소드는 오버라이드한 메소드가 부모 클래스와 동일한 메소드 시그너처를 가지고 있지만 반환 자료형이 다를 경우에 발생한다.

다음 코드에서 CoB 클래스는 CoA 클래스를 상속하면서 create() 메소드를 오버라이드한다. 이때, 반환 유형이 부모 클래스와 달리 CoA가 아닌 CoB를 반환하도록 선언하였다.


<리스트 5> 반환 유형이 다른 메소드 오버라이딩

class CoA {

    public CoA create() {

        return new CoA();

    }

}

class CoB extends CoA {

    public CoB create() {

        return new CoB();

    }

}


이 코드를 컴파일한 후 역컴파일해 보면 CoB 클래스에는 부모로부터 물려받은 CoA를 리턴하는 create() 메소드가 여전히 존재함을 볼 수 있다. 이 상속받은 create() 메소드는 자식 클래스에서 재정의한 CoB를 리턴하는 create() 메소드를 호출해주는 다리 역할만을 수행한다.

CoB 클래스 파일 포맷을 분석해 보면 소스 코드에는 없었던 이 브리지 메소드에 대한 플래그값이 0x40, 0x1000 값이 설정되어 있음을 볼 수 있다. 0x40은 브리지 메소드를 표현하는 플래그이며 0x1000은 소스 코드에는 없이 인위적으로 생성한 메소드(synthetic method)임을 표현하는 플래그이다.


<리스트 5> javap 유틸리티를 사용한 CoB 클래스 역컴파일

javap -c CoB <엔터>


Compiled from "CovariantReturn.java"

class CoB extends CoA{

CoB();

  Code:

   0:   aload_0

   1:   invokespecial   #1; //Method CoA."<init>":()V

   4:   return


public CoB create();

  Code:

   0:   new     #2; //class CoB

   3:   dup

   4:   invokespecial   #3; //Method "<init>":()V

   7:   areturn


public CoA create();

  Code:

   0:   aload_0

   1:   invokevirtual   #4; //Method create:()LCoB;

   4:   areturn


}


브리지 메소드 역시 자바 5.0에서만 지원되는 개념이며, 자바 컴파일러에 의해 자동으로 생성된다. 또, 오버라이드한 메소드에서 반환 자료형을 부모 클래스의 반환 자료형보다 좀더 엄밀한 자료형 즉, 자식 자료형으로 정의하는 것도 자바 5.0 이후 버전에서만 지원되는 기능이다.

이 기능은 Iterator 인터페이스의 next() 메소드처럼 제네릭 자료형으로 반환 자료형이 선언되어 있는 경우, 구현 Iterator 클래스들의 상속 관계와 무관하게 정확한 제네릭 자료형을 반환하도록 선언해야 하는 필요성에 의해 채택되었다.


런타임 제네릭스

실행 시간에 아무런 제네릭 자료형 정보를 남기지 않는 자료형 지우기 방식의 특성 때문에 자바 제네릭스의 접근 방식에 대해 비난과 조롱이 적지 않았다.

예를 들어, 제네릭 자료형을 사용하여 객체를 생성하거나 제네릭 자료형을 컴포넌트 자료형으로 가지는 배열을 생성하거나 하는 일은 C++ 템플릿에 익숙해진 프로그래머에게는 너무나 당연한 일이지만, 자바 제네릭스로는 어려운 일이 된다. 이에 대한 자바 5.0이 내놓은 해법은 java.lang.Class 클래스를 활용하는 것이다.

제네릭스가 적용된 자바 5.0의 클래스들은 대부분 컬렉션 라이브러리 클래스들이지만, Class 클래스를 포함하여 ThreadLocal, WeakReference 등이 추가로 있다.

Class 클래스는 자바 5.0의 제네릭스에서 좀 독특한 역할을 수행하는데, 이 정보는 실행 시간까지 살아 있게 된다는 점이 다른 제네릭스와 크게 다른 점이다.

Class<T>와 같이 제네릭스 방식으로 표현된 인자가 String.class를 넘겨 받으면 T는 String이 된다. 즉, 다음과 같이 객체를 생성할 수 있다.


public static <T> T createObject(Class<T> clazz) throws Exception {

    return clazz.newInstance();

}


유사한 방법으로 배열도 생성할 수 있다. 이 경우에는 Array 클래스의 newInstance() 메소드가 Object를 리턴하기 때문에 경고를 피할 수 없다는 것이 단점이다.


public static <T> T[] createObjectArray(Class<T> clazz, int length) throws Exception {

    return (T[]) java.lang.reflect.Array.newInstance(clazz, length);

}


유쾌, 불쾌 뒤섞인 제네릭스

자바 5.0을 사용하면서 제네릭스는 전면에 와닿는 문제이다.

코드의 상당 부분에 꺾쇠 괄호가 채워지게 될 것이고, 또 컴파일할 때마다 강화된 자료형 검사에 당황하게 될 것이다. 그리고 자바 5.0으로 개발을 하는 동안 결코 유쾌하지 않은 경고 메시지와 종종 만나게 될 것이다. 이 경고 메시지는 실제 잘못된 형 변환을 지적할 수도 있으므로, 쉽게 무시해서는 안 된다. 가능하면 -Xlint:unchecked 기능을 항상 켜고 컴파일을 하는 것이 현명할 것이다.


Note: Recompile with -Xlint:unchecked for details.


제네릭스는 직접 제네릭스를 지원하는 자료 구조를 만들려고 하지 않는다면 기쁘게 사용하면 되는 선물일지도 모른다. 하지만, 프로그래밍이라는 것이 어느 한쪽 켠에만 숨어 지내게 하지 않는다. 결국 제네릭스의 깊은 부분, 어쩌면 자바 제네릭스의 어두운 부분과 맞닥뜨릴 일이 있을 것이다.

자바 5.0이 놀라운 수행 성능 개선을 보이면서 멋진 모습으로 다가왔다. 제네릭스가 온통 유쾌한 언어 기능이 아닐지는 모르지만, 자바 5.0의 멋진 면모를 보여주는 중요한 요소임에 분명하다.

기대에 조금 못 미치는 면도 있고, 사실 불필요하게 어려워진 부분도 있지만, 도구는 결국 활용하는 사람에 의해 그 가치가 드러나는 법이므로 자바 5.0이라는 도구를 최대한 활용하여 좋은 소프트웨어를 만들고, 또 다음 버전에는 더 나은 기능들이 채택될 수 있도록 노력하면 될 것이다.


정신없이 바쁘게 살다보니 벌써 한 해가 저문다. 코더로서, 소프트웨어 엔지니어로서 살아가는 게 정말 녹녹한 일이 아니다. 개인적으로는 XML과 웹 서비스, 비즈니스 프로세스의 수많은 스펙들과 또 수많은 구현들과 씨름을 한 해였다. 여러 공개 소스 프로젝트들에서 볼 수 있듯이, 능력 뛰어난 한두 명의 헌신에 따라 수많은 소프트웨어가 명멸하였다.

독자 제위들도 한 해 잘 마무리하고 내년 한해도 좋은 소프트웨어 만드시길 빈다.



참고자료

1 Generics in the Java Programming Language, Gilad Bracha, July 2004

http://java.sun.com/j2se/1.5/pdf/generics-tutorial.pdf

2 Java Specification Requests 14 : Add Generic Types To The JavaTM Programming Language

http://jcp.org/en/jsr/detail?id=14

3 Puzzling Through Erasure, Bruce Eckel, Sep., 2004

http://mindview.net/WebLog/log-0057

4 Puzzling Through Erasure : answer section, Neal Gafter, Sep., 2004

http://gafter.blogspot.com/2004/09/puzzling-through-erasure-answer.html

5 Proposed Changes to the Java Virtual Machine Specification, chapter 4 The class file format

http://java.sun.com/docs/books/vmspec/2nd-edition/ClassFileFormat-final-draft.pdf