Java의 제네릭(Generic)은 클래스나 메서드에서 사용할 데이터 타입을 일반화하여, 컴파일 시 타입 안정성을 보장하고, 코드의 재사용성을 높이는 기능입니다.
예를 들어, List<T>처럼 타입 파라미터를 사용하면, List<String>, List<Integer> 등 다양한 타입에 대해 타입 안정성을 갖는 코드를 작성할 수 있습니다.
이를 통해 형 변환(casting) 없이도 안전하게 데이터를 다룰 수 있고, 컴파일 타임에 타입 오류를 방지할 수 있습니다.
제네릭은 클래스, 메서드, 인터페이스에 모두 적용 가능하며, 와일드카드(?)나 제한된 타입(extends, super)도 사용할 수 있습니다.
컴파일 이후에는 타입 정보가 제거되는 타입 소거(Type Erasure) 개념이 존재합니다.
개념 설명
제네릭(Generic)이란?
Java 5부터 도입된 문법
클래스나 메서드의 타입을 파라미터화하여 다양한 타입에 유연하게 대응
구분
예시
설명
타입 파라미터
T, E, K, V
제네릭 선언 시 사용하는 자리 표시자(Placeholder)
타입 인자
String, Integer
제네릭 사용 시 실제로 넣는 구체적인 타입(Concrete Type)
객체(인스턴스)가 아니라 클래스 타입
제네릭의 장점
장점
설명
타입 안전성
컴파일 시 타입 체크를 통해 런타임 오류 방지
형변환 제거
(String) obj와 같은 명시적 캐스팅 불필요
재사용성
동일한 로직을 다양한 타입에 적용 가능
제네릭 사용 예시
와일드카드와 타입 제한
문법
설명
예시
<?>
모든 타입 허용 (Unbounded Wildcard)
List<?>
<? extends T>
T 또는 T의 하위 타입만 허용 (Upper Bounded Wildcard)
List<? extends Number>
<? super T>
T 또는 T의 상위 타입만 허용 (Lower Bounded Wildcard)
List<? super Integer>
타입 소거(Type Erasure)
제네릭 타입은 컴파일 시점에만 존재, 런타임에는 제거됨
예: List<String>과 List<Integer>는 런타임에 같은 리스트처럼 취급됨
추가 특징
제네릭은 기본형(primitive) 사용 불가 → int 대신 Integer 사용
static 필드에는 사용 불가
제네릭 타입은 리플렉션으로 확인 불가
추가 질문
Object와 Generic의 차이는 무엇인가요?
Object로 값을 꺼낼 때는 항상 명시적으로 형변환을 해야 하고, 잘못된 타입을 넣거나 꺼내면 런타임 오류가 발생할 수 있습니다.
반면, 제네릭은 컴파일 시점에 타입을 명확히 지정해두기 때문에 형변환이 필요 없고, 타입 오류도 컴파일 타임에 바로 잡아낼 수 있어 훨씬 안전한 코드를 작성할 수 있다는 이점이 있습니다.
Java는 왜 타입 소거(Type Erasure)를 사용하나요?
Java의 제네릭은 비교적 나중에 도입된 기능인데, 기존 JVM과의 호환성을 유지하기 위해 타입 소거 방식을 채택했습니다.
즉, 컴파일 시에는 타입 정보를 바탕으로 검사하지만, 런타임에는 제네릭 타입 정보가 지워지고 일반 타입처럼 동작합니다.
이 방식 덕분에 제네릭을 도입한 이후에도 이전 버전의 라이브러리나 클래스 파일들과 충돌 없이 함께 사용할 수 있습니다.
// 제네릭 클래스 예시
public class Response<T> {
private T data;
private int statusCode;
public Response(T data, int statusCode) {
this.data = data;
this.statusCode = statusCode;
}
public T getData() { return data; }
public int getStatusCode() { return statusCode; }
}
// 사용 예시
Response<String> msg = new Response<>("Success", 200);
Response<User> user = new Response<>(new User("Alice"), 200);