Java 제네릭 완전 정복 — 타입 매개변수의 기초

제네릭이 해결하는 타입 안전성 문제, 타입 파라미터 이름 관례(T·E·K·V·N·R), 다이아몬드 연산자, 그리고 Box<T> 예제로 이해하는 제네릭의 핵심 개념

· 5 min read · PALDYN Team

지난 글에서 불변 객체를 설계하는 다섯 가지 규칙을 살펴봤다. 이번에는 **제네릭(Generics)**을 다룬다. 제네릭은 Java 5에서 도입된 기능으로, 클래스·인터페이스·메서드가 처리할 데이터의 타입을 선언 시점에 지정하지 않고 사용 시점에 결정하게 해 준다. “타입을 파라미터처럼 넘긴다”는 발상이다.

제네릭이 해결하는 문제

제네릭 도입 전 Java 코드에서는 컬렉션에서 꺼낸 값을 항상 캐스팅해야 했다. 캐스팅이 잘못되면 런타임에 ClassCastException 이 발생했고, 컴파일러는 이를 잡아주지 못했다.

// Java 5 이전 — Raw Type
List list = new ArrayList();
list.add("hello");
list.add(42);            // 실수로 Integer 삽입
String s = (String) list.get(1); // 런타임 ClassCastException!

제네릭을 사용하면 컴파일러가 타입 불일치를 코드 작성 시점에 알려준다.

// Java 5 이후 — Generic Type
List<String> list = new ArrayList<>();
list.add("hello");
// list.add(42);  // 컴파일 오류 — IDE가 즉시 표시
String s = list.get(0); // 캐스팅 불필요

제네릭 타입 안전성 비교

타입 파라미터 이름 관례

Java 커뮤니티는 타입 파라미터에 대문자 한 글자를 사용하는 관례를 따른다.

기호의미주요 사용처
TType일반 타입 파라미터
EElement컬렉션 원소
KKeyMap 키
VValueMap 값
NNumber숫자 타입
RResult함수 반환 타입
S, U, V다중 파라미터 2·3·4번째

Box<T> — 가장 간단한 제네릭 클래스

class Box<T> {
    private T value;

    public Box(T value) {
        this.value = value;
    }

    public T getValue() {
        return value;
    }
}

T는 클래스 이름 뒤 <> 안에 선언한다. 인스턴스를 생성할 때 구체 타입으로 교체된다.

// T = String 으로 바인딩
Box<String> strBox = new Box<>("Java");
String s = strBox.getValue(); // 캐스팅 없음

// T = Integer 로 바인딩
Box<Integer> intBox = new Box<>(42);
int n = intBox.getValue();    // 자동 언박싱

타입 파라미터 관례와 Box<T> 예제

다이아몬드 연산자 <>

Java 7부터 우변의 타입 인자를 생략하는 다이아몬드 연산자를 지원한다. 컴파일러가 좌변 타입을 보고 추론한다.

// 명시적 타입 인자 (Java 6 이하)
Box<String> box = new Box<String>("hello");

// 다이아몬드 연산자 — 타입 추론 (Java 7+)
Box<String> box = new Box<>("hello");

// Map에서도 동일
Map<String, List<Integer>> map = new HashMap<>();

제네릭의 세 가지 이점

타입 안전(Type Safety): 잘못된 타입 삽입을 컴파일 타임에 차단한다.

캐스팅 제거: 꺼낼 때 자동으로 올바른 타입을 반환하므로 (String) 같은 명시적 캐스팅이 불필요하다.

코드 재사용: Box<String>, Box<Integer> 등 모든 타입에 하나의 Box<T> 클래스가 동작한다. 타입별로 클래스를 따로 작성할 필요가 없다.

주의: 기본 타입은 사용 불가

제네릭 타입 파라미터 자리에는 참조 타입만 사용할 수 있다. int, double 같은 프리미티브는 불가능하다.

List<int> list = new ArrayList<>();     // 컴파일 오류!
List<Integer> list = new ArrayList<>(); // OK — 래퍼 타입 사용

오토박싱/언박싱 덕분에 intInteger 변환은 자동으로 처리되므로 실용적으로는 큰 불편이 없다. 단, 박싱/언박싱 비용이 성능에 민감한 코드에서는 주의해야 한다.


다음 글: 제네릭 클래스 — 타입 매개변수를 가진 클래스 설계


읽어주셔서 감사합니다. 😊