Generic은 JDK 5에서 추가된 대표적인 기능 중 하나이다.
Generic을 사용할때는 primitive 타입은 사용이 불가능하다. 따라서 int, double 등의 자료형을 쓰고 싶다면, Wrapper class인 Integer, Double등을 사용해야 한다.
가장 대표적으로 Generic을 사용하는 클래스인 List를 한번 살펴보자.
Generic을 사용해 String을 원소로 가지는 List를 생성하고, 값을 삽입하고, 얻는 코드는 다음과 같다.
List<String> list = new ArrayList<>();
list.add("A");
System.out.println(list.get(0));
기본적인 Generic 메서드의 구조는
[접근제한자] <[타입파라미터]> [리턴타입] [함수이름](파라미터)
이다. 아래 예시를 보자
public <T> void func(T arg){
...
}
기본적인 Generic 클래스의 구조는 아래와 같다.
class [클래스명]<[타입파라미터]> {
...
}
해당 클래스에서 타입파라미터를 사용하는 메서드를 작성하는 경우에는 아래 코드처럼 작성하면 된다.
class TestClass<T>{
public void func(T arg){
...
}
}
그렇다면 Generic이 추가되기 전인 JDK 1.4에서는 어떤식으로 코드를 작성했을까?
동일한 코드를 1.4 버전으로 작성하면 다음과 같다.
List list = new ArrayList();
list.add("A");
System.out.println((String) list.get(0));
타입 파라미터를 입력하는 "<>"가 사라지고, 사용할 때 캐스팅을 하는것을 볼 수 있다.
이러한 경우에는 Object 형태로 데이터가 add, get 되는 것은 많은 사람들이 알고 있을 것이다. 그렇다면 제네릭을 사용한 List는 Object가 아닌 해당 타입의 형태(예시의 경우 String)로 데이터를 add, get 하는것일까?
타입 제한이 걸려 있지 않은 이상, Generic을 사용하거나 말거나 JVM에서는 Object로 돌아간다. 바이트코드로 보면 완벽하게 동일하다는 것을 알 수 있다.
이렇게 Generic을 썼음에도 불구하고 Object로 변환되는 이유는 JVM의 Type Erasure 때문이다.
요약하자면, 이전버전 호환성을 위해서 타입 정보를 제거하고 동작하며, 필요한 경우에 캐스팅 연산을 삽입하거나, 브릿지 메서드를 생성하게 된다.
만약 제네릭 인자를 받아온 후에 Object형태로 직접 캐스팅한다면, 캐스팅 연산이 필요 없으므로, Object -> T -> Object로 이중 캐스팅하는것이 아닌, 단순히 캐스팅 연산이 제거된다.
타입 검사와 캐스팅을 개발자가 직접 하지 않고 컴파일러가 삽입해 준다는 것이다.
직접 타입검사를 하려면 instanceof 를 사용하는 방법이 있는데, 상속 관계가 존재하는 경우 굉장히 번거롭다.
명시적으로 캐스팅하는 작업 또한 굉장히 귀찮은데다가, 혹시라도 캐스팅을 빠트리고 잊어버리는 경우에는 굉장히 잡기 어려운 버그가 발생 할 수 있다.
제네릭을 사용하면 이 타입검사와 캐스팅을 컴파일러가 해주기 때문에, 타입이 맞지 않는 데이터를 사용 할 수 없게 제한 할 수 있다. 이를 통해 타입 안정성을 쉽게 확보 할 수 있다. 높은 타입 안정성은 예상치 못한 동작을 예방할 수 있고, 이는 곧 버그의 감소로 직결되므로, Raw Type이 아닌 제네릭의 사용을 권장하는 것이다.
분량상 다음 글에서 Generic에 대한 설명을 이어나가도록 하겠다.