Menu

자바 제네릭: 타입 안전한 클래스와 메서드

자바 제네릭이란 무엇인지, 제네릭 클래스와 메서드를 작성하는 방법, 한정된 타입 매개변수, 와일드카드, 그리고 타입 소거가 왜 중요한지 설명합니다.

이 페이지에는 실행 가능한 에디터가 있습니다 - 편집하고 실행하면 결과를 바로 볼 수 있습니다.

제네릭이 존재하는 이유

제네릭 타입을 사용하면 타입 안전성을 포기하지 않고도 코드를 한 번만 작성해 여러 타입에 재사용할 수 있습니다. IntBox, StringBox, UserBox를 각각 따로 작성하는 대신, T가 호출하는 쪽이 채우는 자리 표시자인 단 하나의 Box<T>를 작성합니다.

ArrayList<String>이나 HashMap<String, Integer>를 작성할 때마다 여러분은 이미 제네릭을 사용해 온 것입니다. <...> 부분이 타입 인자입니다. 이 페이지에서는 직접 제네릭을 작성하는 방법을 보여 줍니다.

대안 - 모든 것을 Object로 저장하는 방식 - 은 타입 정보를 버리고, 보기 흉하고 오류가 나기 쉬운 캐스팅을 강요합니다.

마지막 줄의 그 캐스팅은 런타임에 ClassCastException을 던집니다 - 바로 제네릭이 불가능하게 만들도록 설계된 종류의 버그입니다.

제네릭 클래스

클래스 이름 뒤에 꺾쇠괄호로 타입 매개변수를 선언합니다. 관례상 대문자 한 글자를 씁니다. "type(타입)"의 T, "element(요소)"의 E, 키/값의 K/V입니다.

Box 안에서 각 T는 호출하는 쪽이 제공한 것으로 바뀝니다. Box<String>String만 담고 반환하는 상자입니다. 컴파일러는 프로그램이 실행되기도 전에 name.set(99)를 거부합니다.

오른쪽의 빈 <>(다이아몬드 연산자)는 컴파일러가 왼쪽에서 타입 인자를 추론하게 하므로, <String>을 두 번 반복하지 않아도 됩니다.

제네릭 메서드

하나의 메서드가 클래스와 독립적으로 자신만의 타입 매개변수를 가질 수 있습니다. 매개변수 <T>를 반환 타입 앞에 둡니다.

T를 명시적으로 전달하는 일은 결코 없습니다 - 컴파일러가 인자로부터 추론합니다. Collections.sortList.of 같은 유틸리티가 어떤 요소 타입에서도 타입 안전하게 유지되는 것이 바로 제네릭 메서드 덕분입니다.

한정된 타입 매개변수

때로는 제네릭 타입이 일부 타입에만 의미가 있습니다. extends는 매개변수를 제약하여 그 경계의 메서드를 호출할 수 있게 합니다. 여기서 T extends NumberTNumber이거나 그 하위 클래스(Integer, Double, ...)임을 의미하므로 doubleValue()를 사용할 수 있습니다.

여기서 extends는 "~의 하위 타입이다"라는 뜻이며, 클래스와 인터페이스 모두에 동작합니다 - 요소를 비교해야 할 때 <T extends Comparable<T>>는 매우 흔합니다.

와일드카드: ? extends와 ? super

미묘한 함정: IntegerNumber임에도 List<Integer>List<Number>아닙니다. 제네릭은 불변(invariant) 입니다. 읽기만 하거나 쓰기만 하면 될 때 와일드카드가 이 제약을 완화합니다.

읽어 들이는 생산자에는 ? extends T를, 써 넣는 소비자에는 ? super T를 사용합니다("PECS" 규칙 - Producer Extends, Consumer Super).

? extends Number 리스트는 요소를 Number로 읽게 해 주지만 거기에 추가하지는 못합니다(컴파일러가 정확한 요소 타입을 알 수 없기 때문입니다). ? super Integer 리스트는 Integer를 추가하게 해 주지만, 읽기는 Object로 돌아옵니다. 데이터가 흐르는 방식에 맞는 와일드카드를 고르세요.

타입 소거와 그 한계

제네릭은 컴파일 시점의 기능입니다. 컴파일 후에는 타입 매개변수가 소거 되어, 런타임에는 Box<String>Box<Integer>가 모두 그냥 Box입니다. 이는 제네릭이 오래된 코드와 하위 호환되도록 유지해 주지만, 실제적인 제약을 부과합니다.

// 이 중 어느 것도 컴파일되지 않는다 - 타입 매개변수는 런타임에 존재하지 않는다:
T value = new T();          // 타입 매개변수는 인스턴스화할 수 없다
T[] array = new T[10];      // 제네릭 배열은 생성할 수 없다
if (list instanceof List<String>) { } // 타입 인자는 검사할 수 없다

런타임에는 타입이 사라지므로, 리플렉션으로 "T가 무엇이었나?"라고 물을 수 없고, 제네릭 인자만 다른 메서드를 오버로딩할 수도 없습니다(foo(List<String>)foo(List<Integer>)는 같은 시그니처로 소거됩니다). 런타임에 정말로 타입이 필요하다면, Class<T> 토큰을 생성자나 메서드의 매개변수로 전달하세요.

다음: 람다 표현식

제네릭 메서드가 타입을 매개변수로 받는다는 것을 보았습니다. 다음 단계는 동작 을 매개변수로 다루는 것입니다. 람다 표현식을 사용하면 코드 조각 - 함수 - 을 메서드에 전달할 수 있으며, 이것이 바로 방금 타입 안전하게 작성하는 법을 배운 제네릭 컬렉션을 정렬하고 필터링하고 변환하는 방식입니다.

자주 묻는 질문

자바에서 제네릭이란 무엇인가요?

제네릭을 사용하면 클래스나 메서드를 하나의 구체적인 타입에 묶어 두는 대신, 나중에 지정하는 타입과 동작하도록 작성할 수 있습니다. 꺾쇠괄호 안에 타입 매개변수를 선언하고 - class Box<T> - 호출하는 쪽이 실제 타입을 채웁니다 - Box<String>. 그러면 컴파일러가 그 타입을 모든 곳에서 강제하므로, 불일치를 컴파일 시점에 잡아내고 수동 캐스팅을 생략할 수 있습니다.

Object 대신 제네릭을 사용하는 이유는 무엇인가요?

Object를 사용하면 모든 타입 정보가 사라집니다. 컴파일러는 엉뚱한 것을 넣는 것을 막지 못하고, 꺼내는 값마다 캐스팅해야 합니다(런타임에 ClassCastException이 발생할 위험이 있습니다). 제네릭은 그 검사를 컴파일 시점으로 옮깁니다. List<String>Integer를 아예 받지 않고, get()은 이미 String을 반환하므로 캐스팅도 없고 런타임의 예기치 못한 사고도 없습니다.

자바 제네릭에서 타입 소거란 무엇인가요?

타입 소거란 제네릭 타입 정보가 컴파일 시점에만 존재한다는 뜻입니다. 컴파일 후 런타임에는 List<String>List<Integer>가 모두 그냥 List이며, 타입 매개변수는 소거됩니다. 그래서 new T[10]을 작성하거나, list instanceof List<String>을 호출하거나, 리플렉션으로 타입 매개변수를 읽을 수 없습니다. 제네릭은 컴파일 시점의 안전성을 줄 뿐, 런타임의 타입 데이터를 주지는 않습니다.

Coddy programming languages illustration

Coddy로 코딩 배우기

시작하기