자바 제네릭이 없던 시절(JDK 1.5 이전)에는 모든 컬렉션이 Object 타입을 저장했습니다. 이는 어떤 타입이든 담을 수 있다는 유연성을 제공했지만, 꺼낼 때마다 명시적으로 형변환(casting)을 해야 했고, 실행 중에 ClassCastException이 발생할 위험이 항상 존재했습니다.
제네릭을 사용하면 컴파일러가 타입을 검증하므로, 잘못된 타입을 넣으려는 시도가 컴파일 단계에서 차단됩니다. 이는 런타임 에러를 사전에 방지하고, 코드의 의도를 명확하게 표현할 수 있게 해줍니다.
타입 안정성(Type Safety)
프로그램이 실행되기 전에 타입 오류를 미리 발견할 수 있는 성질입니다. 자바는 정적 타입 언어로, 변수의 타입이 컴파일 타임에 결정되며, 잘못된 타입 사용은 컴파일 에러로 나타납니다.
타입 캐스팅(Type Casting)
한 타입의 객체를 다른 타입으로 변환하는 작업입니다. 예를 들어 (String) obj처럼 명시적으로 타입을 지정하는 것이 타입 캐스팅입니다. 이 과정에서 실제 객체의 타입이 다르면 ClassCastException이 발생합니다.
컴파일 타임 vs 런타임
컴파일 타임은 소스 코드가 바이트코드로 변환되는 시점, 런타임은 프로그램이 실제로 실행되는 시점입니다. 에러를 컴파일 타임에 잡으면 배포 전에 수정할 수 있지만, 런타임 에러는 사용자가 프로그램을 사용하는 중에 발생하므로 훨씬 위험합니다.
제네릭이 해결한 세 가지 문제
1. 타입 안정성 부재
// 제네릭 이전 (Java 1.4)
List list = new ArrayList();
list.add("Hello");
list.add(123); // 아무 문제 없이 컴파일됨
String s = (String) list.get(1); // 💥 런타임에 ClassCastException!
// 제네릭 사용 (Java 5+)
List<String> list = new ArrayList<>();
list.add("Hello");
list.add(123); // ❌ 컴파일 에러! "incompatible types"
String s = list.get(0); // 형변환 불필요
2. 반복적인 타입 캐스팅
제네릭 이전에는 컬렉션에서 값을 꺼낼 때마다 (String), (Integer) 같은 캐스팅 코드를 작성해야 했습니다. 이는 코드를 장황하게 만들고, 실수할 여지를 남깁니다.
3. 코드 재사용성 저하
동일한 로직을 여러 타입에 적용하려면 타입별로 클래스를 만들거나, Object로 일반화하되 안전성을 포기해야 했습니다. 제네릭은 타입을 파라미터화하여 하나의 코드로 여러 타입을 안전하게 처리할 수 있게 합니다.
실전에서의 가치
List<T>, Map<K,V> 등)는 제네릭 덕분에 타입 안전하면서도 범용적으로 사용 가능합니다.ResponseEntity<T>, Hibernate의 TypedQuery<T> 등 다양한 타입을 다루는 프레임워크에서 제네릭은 필수입니다.제네릭의 한계: 타입 소거(Type Erasure)
자바 제네릭은 하위 호환성을 위해 컴파일 후 타입 정보를 지웁니다. 즉, List<String>과 List<Integer>는 런타임에 모두 List가 됩니다. 이로 인해 new T()나 instanceof T 같은 작업은 불가능합니다.
제네릭은 컴파일러가 타입을 검증하도록 하여 런타임 에러를 사전에 방지하고, 불필요한 캐스팅을 제거합니다. 이는 코드의 안정성과 가독성을 동시에 향상시킵니다.
| 비교 항목 | 제네릭 이전 | 제네릭 사용 |
|---|---|---|
| 타입 검증 시점 | 런타임 (실행 중) | 컴파일 타임 (빌드 시) |
| 타입 캐스팅 | 필수 (매번 명시) | 불필요 (자동 처리) |
| 에러 발견 | 사용자 환경에서 발생 | 개발 단계에서 발견 |
| 코드 가독성 | 캐스팅으로 장황함 | 타입이 명시되어 명확 |
| 유지보수성 | 타입 추론 어려움 | 타입 정보로 리팩토링 용이 |