오늘은 자바의 제네릭과 관련해서 글을 써내려가려 한다. 자바의 제네릭은 어렵다. 필자가 생각하기엔 자바에서 가장 어려운 문법? 부분에 속한다고 할 수 있다. 누군가가 자바에서 가장 어려운게 무엇이냐고 물어보면 1초도 망설이지 않고 제네릭이라고 대답했을 것이다. 그만큼 나에겐 어렵다.
자바의 제네릭을 아주 자유자재로 사용할 수 있는 개발자는 많지 않을 것이라고 생각한다. (내가 못해서 그렇게 생각할지도..)

자바의 Generic은 처음 나왔을 때 부터 있었던 것은 아니다. 1996년에 자바가 처음 발표되고 8년이 지난 2004년에 java5가 발표되면서 Generic이 추가되었다. java5가 발표되면서 아주아주 많은 변화가 있었다. java8의 람다만큼 큰 변화가 많이 있었다. (어쩌면 람다보다 더..) 그 중에서 가장 대표적인 것들은 Generic, 오토박싱/언박싱, foreach, enum type, 가변인자(varargs), 어노테이션(Annotation) 등 무수히 많은 변화를 들고 나왔다. 그 때 당시에는 자바개발자들이 고생을 많이 했겠다. 구현한 사람이나 사용하는 사람이나..

제네릭 작성해보기

일단 간단하게 제네릭 타입인 List<E>를 작성해보자. 뭐 자주 사용하는 인터페이스니 예제를 남길 필요도 없을 듯 한데..

List<Integer> numbers = new ArrayList<>();
numbers.add(1);
numbers.add(2);
numbers.add(3);

우리가 흔히 작성하는 코드이다. List의 <E>를 타입 파라미터라고 부른다. 이것이 있기에 우리는 컴파일 타임에 해당 타입이 일치하는지 확인 할 수 있다. 좀 더 안전하게 사용할 수 있게 되었다. 예전 자바5 이전에는 Generic이 존재 하지 않았기 때문에 아무 객체나 넣을 수 있었다.

List numbers = new ArrayList();
numbers.add(1);
numbers.add("2");
numbers.add(3);

위와 같이 말이다. 하지만 현재도 위와 같이 작성해도 문제 없이 컴파일이 되고 실행된다. 그 이유는 아래가서 설명하겠다.
그럼 한번 타입파라미터가 있는 클래스를 간단하게 만들어보자.

MyList

자바의 ArrayList를 참고해서 나만의 리스트를 만들어보자.

public class MyList<E> {
  private static final int DEFAULT_CAPACITY = 10;
  private Object element[];
  private int index;

  public MyList() {
    element = new Object[DEFAULT_CAPACITY];
  }

  public void add(E e) {
    this.element[index++] = e;
  }

  public E get(int index){
    return (E) element[index];
  }
}

아주 심플하게 나만의 리스트를 만들었다. 보기에도 심플하고 만들기도 심플하다. 물론 예외처리라든지 리스트의 길이 동적으로 변한다는지는 나중 문제지 여기서 핵심은 그게 아니라서 제외시켰다. 물론 알겠지만..
아무튼 우리는 자바의 Generic을 이용해서 클래스를 만들어 봤다. MyList라는 클래스는 타입 파라미터가 존재한다. 이는 MyList라는 클래스에 사용될 클래스를 제한한다는 뜻이다. add(E) 라는 메서드의 E는 클래스의 타입과 동일해야만 한다. 마찬가지로 get 메서드의 리턴타입도 E 로 정의해 두었으니 클래스의 타입과 동일해야 된다.
만약 String으로 정의했는데 int, long, double, pojo나 기타 다른 타입이 들어간다면 컴파일 타임에 에러를 발생시킨다.
한번 사용해보자.

MyList<String> myList = new MyList<>();
myList.add("wonwoo");
myList.add("seungwoo");
myList.add(1); //컴파일 에러
System.out.println(myList.get(0));
System.out.println(myList.get(1));

우리는 안전하게 해당 타입에 맞는 List를 만들수 있어서 보다 버그나 에러를 줄일 수 있게 되었다.

근데 코드를 자세히보면 뭔가 꺼림칙하다. element을 Object로 선언하고 (Object 선언은 봐줄수 있을 언정) get() 메서드를 보면 E 타입으로 캐스팅까지 한다. 뭔가 마음에 들지 않는다.

Type erasure

꺼림칙하니 코드를 바꾸어 보자.

public class MyList<E> {
  private static final int DEFAULT_CAPACITY = 10;
  private E element[];
  private int index;

  public MyList() {
    element = new E[DEFAULT_CAPACITY];
  }

  public void add(E e) {
    this.element[index++] = e;
  }

  public E get(int index){
    return element[index];
  }
}

Object 타입을 제거하고 E 타입으로 변경하였다. 그러고 나니 get() 메서드에 캐스팅하는 부분이 사라졌다. 깔끔하다. 하지만 안타깝게도 이코드는 동작하지 않는다. 아니 컴파일 조차 되지 않는다. (필자가 그렇게 한 이유가 다..) 컴파일 에러가 발생하는 부분은 다음과 같다.

element = new E[DEFAULT_CAPACITY];

어떤 에러인지 IDEA에서 살펴보면 Type Parameter 'E' cannot be instantiated directly 이와 같은 에러가 발생한다. 타입파라미터 E는 직접적으로 인스턴스화 할 수 없다. 엥 이건 또 무슨 말인가? 왜 타입파라미터는 인스턴화 할 수 없을까?
그 이유는 눈치빠른 사람들은 알겠지만 소 제목에 있다시피 type erasure 때문이다. 그렇다면 type erasure란 뭘까?

자바에서는 제네릭 클래스를 인스턴화화 할 때 해당 타입 타입을 지워버린다. 그 타입은 컴파일시까지만 존재하고 컴파일된 바이트코드에서는 어떠한 타입파라미터의 정보를 찾아볼 수 없다. 필자 말이 진짜인지 한번 살펴보자.

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

//기타 생략

아까 사용했던 List를 바이트 코드 레벨에서 한번 살펴보자.

L0
LINENUMBER 11 L0
NEW java/util/ArrayList
DUP
INVOKESPECIAL java/util/ArrayList.<init> ()V
ASTORE 1

//기타 생략

보시다시피 ArrayList를 생성할 때 어떠한 타입정보도 들고 있지 않다. new ArrayList()로 생성한 것과 동일하게 바이트 코드가 생성된다.
그럼 자바가 이렇게 한 이유는 무엇일까? 이유는 단 한개 밖에 없는 듯하다. (물론 다른 이유도 있을 수 있겠지만 필자 추측에는) 당시 특히나 자바는 하위 호환성을 매우 중요시했다. 그래서 위와 같은 결정을 내린듯 하다. 그래서 제네릭을 사용한 java5에서 컴파일된 코드를 java4에서도 실행 시킬수 있고 제네릭을 사용하지 않았던 레거시 코드들도 java5이상에서 무사히 잘 실행 될 수 있었던 이유이다.
그래서 우리는 아래와 같은 코드들를 만들 수 없다.

if(numbers instanceof List<Integer>) {

}

List<Integer>.class

타입파라미터와 Object

그럼 타입파라미터가 있는 클래스를 컴파일 해보면 어떨까? 어떠한 타입으로 변환을 시켜야 되는데 그 어떠한 타입은 무엇일까?
그건 바로 Object 타입으로 변경시킨다.

class Node<T> {
  private T data;
  public T getData() {
    return data;
  }

  public void setData(T data) {
    this.data = data;
  }
}

위와 같이 Node라는 클래스에 타입파라미터가 존재 한다고 가정하자. 그럼 자바 컴파일러는 아래와 같이 변경한다.

class Node {
  private Object data;
  public Object getData() {
    return data;
  }

  public void setData(Object data) {
    this.data = data;
  }
}

그렇다면 만약 타입파라미터에 범위를 제한한다면 어떨까? 아래와 같이 Comparable을 상속받은 클래스만 가능하다고 해보자.

class Node<T extends Comparable<T>> {

  private T data;

  public T getData() {
    return data;
  }

  public void setData(T data) {
    this.data = data;
  }
}

만약 위와 같이 작성된 코드라면 아래와 같이 변경 된다.

class Node {

  private Comparable data;

  public Comparable getData() {
    return data;
  }

  public void setData(Comparable data) {
    this.data = data;
  }
}

참 자바 컴파일러는 열심히도 일한다..

이렇게 오늘은 자바의 제네릭에 대해서 살펴봤다. 위와서 봤던 클래스에도 타입파라미터를 사용할 수 있지만 메서드에도 사용할 수 있다. 또한 바로 위에서 봤던 제네릭에 범위를 제한 할 수도 있다. 이건 예전에 글쓴게 있는데.. 어디 있더라..
제네릭 제한 여기에 보면 허접하지만 간단하게 제네릭 제한에 관련된 글이 있으니 참고 하면 되겠다.

위의 예제만 보고 쉽다고 생각하면 큰 오산일지 모른다. 인터넷에 많은 예제도 살펴보고 제네릭을 많이 사용한 라이브러리를 참고히면 더욱 많은 도움이 될 수 있을 듯 하다.