제네릭 메서드란?
자바 프로그래밍에서 제네릭(Generic)은 타입(type)을 파라미터로 하는 기법으로, 컴파일 시 강한 타입 체크를 할 수 있게 해줍니다. 이는 클래스, 인터페이스, 그리고 메서드에 적용될 수 있으며, 코드 재사용성, 타입 안정성 향상, 캐스팅 문제를 줄여주는 장점이 있습니다.
제네릭 메서드는 메서드의 선언부에서 타입 파라미터를 갖는 메서드를 의미합니다. 이 타입 파라미터는 메서드의 반환 타입, 매개변수 타입, 내부 로컬 변수 타입 등 메서드 내에서 사용될 수 있습니다. 제네릭 메서드는 클래스나 인터페이스 자체가 제네릭이 아니어도 사용될 수 있으며, 각 메서드 호출 시점에 타입 인자를 제공하여 다양한 타입에 대응할 수 있는 유연성을 제공합니다.
제네릭 메서드는 반환 타입 앞에 타입 파라미터 목록을 갖습니다. 이 구문은 메서드가 하나 이상의 제네릭 타입 파라미터를 가짐을 나타냅니다. 예를 들어, 다음은 제네릭 메서드의 간단한 예입니다:
public <T> T genericMethod(T param) {
return param;
}
여기서 <T>는 타입 파라미터를 선언하는 부분이며, T는 메서드의 매개변수 타입과 반환 타입으로 사용됩니다. 이렇게 함으로써 메서드는 다양한 타입으로 사용될 수 있습니다.
제네릭 메서드를 사용할 때는 대부분의 경우 컴파일러가 매개변수의 타입을 보고 타입 파라미터를 추론할 수 있습니다. 예를 들어, 위의 genericMethod를 다음과 같이 사용할 수 있습니다:
String result = genericMethod("Hello, World!");
Integer number = genericMethod(123);
이 경우, 컴파일러는 첫 번째 호출에서 T를 String으로, 두 번째 호출에서 T를 Integer로 추론합니다.
제네릭 메서드는 바운디드 타입 파라미터를 사용하여 특정 클래스의 하위 타입만 받을 수 있도록 제한할 수 있습니다. 예를 들어, <T extends Number>는 T가 Number 또는 그 하위 클래스만 될 수 있음을 의미합니다. 또한, 와일드카드 타입은 메서드의 매개변수 타입으로 사용되어, 예를 들어 List<?>는 '알 수 없는 타입의 객체를 담는 리스트'를 의미하며, 이는 메서드를 더 유연하게 만듭니다.
자바에서 제네릭이 어떻게 동작하는가
자바의 제네릭은 타입 소거(Type Erasure) 메커니즘을 통해 구현됩니다. 타입 소거란 컴파일 시점에 제네릭 타입을 검사하여 타입 안정성을 확보한 후, 런타임에는 해당 타입 정보를 제거(소거)하는 과정을 말합니다. 이렇게 함으로써 자바는 제네릭을 사용하여 컴파일 타임에 타입 안정성을 보장하면서도, 런타임에는 추가적인 타입 정보 없이도 실행될 수 있습니다.
컴파일러는 제네릭 타입을 이용한 코드를 컴파일할 때, 다음과 같은 변환을 수행합니다:
T, E 등)는 그것의 경계(bound)로 대체됩니다. 만약 경계가 지정되지 않았다면 Object로 대체됩니다.예를 들어, List<T>는 컴파일 후에는 단순히 List로 변환됩니다. 그러나 타입 안전성을 위한 타입 검사와 캐스팅은 필요한 경우 남게 됩니다.
제네릭을 사용하여 정의된 간단한 Box 클래스가 있다고 가정해 보겠습니다.
public class Box<T> {
private T t;
public void set(T t) {
this.t = t;
}
public T get() {
return t;
}
}
이 Box 클래스는 타입 파라미터 T를 사용하여 어떤 타입의 객체든지 보관할 수 있습니다.
예를 들어, Integer나 String 객체를 Box에 넣을 수 있습니다.
컴파일러는 타입 소거 과정을 거쳐 제네릭 타입을 제거합니다. 타입 소거가 적용된 후 Box 클래스는 대략적으로 다음과 같이 변환됩니다.
public class Box {
private Object t;
public void set(Object t) {
this.t = t;
}
public Object get() {
return t;
}
}
컴파일 과정에서 T는 Object로 대체되었습니다. 이는 제네릭 타입 파라미터가 실제 타입 정보를 런타임에 유지하지 않음을 의미합니다. 따라서, Box<Integer>와 Box<String> 사이에는 런타임에 구분이 없습니다. 모두 Box 타입으로 취급됩니다.
타입 소거로 인해 발생할 수 있는 상황을 보여주는 예시입니다.
public class GenericTest {
public static void main(String[] args) {
Box<Integer> integerBox = new Box<>();
Box<String> stringBox = new Box<>();
integerBox.set(new Integer(10));
stringBox.set(new String("Hello World"));
// 런타임에는 타입 정보가 소거되므로, 아래의 코드는 컴파일 에러를 발생시키지 않습니다.
// 하지만, 이는 잠재적인 타입 안정성 문제를 야기할 수 있습니다.
// 예를 들어, stringBox를 integerBox로 잘못 캐스팅하는 등의 문제가 발생할 수 있습니다. } }
위 코드에서 컴파일 시점에 Box<Integer>와 Box<String>은 명확히 구분되지만, 런타임 시에는 이러한 구분이 사라집니다. 따라서, 개발자가 타입 안정성을 보장하기 위해 더욱 주의 깊게 코드를 작성해야 합니다.
Box rawBox = new Box(); // 원시 타입 Box
Box<Integer> integerBox = new Box<>(); // Integer를 저장하기 위한 Box
integerBox.set(10);
Box<String> stringBox = new Box<>(); // String을 저장하기 위한 Box
stringBox.set("Hello");
// 타입 소거로 인해 런타임에는 모두 단순히 Box 타입으로 취급됩니다.
// 아래와 같이 잘못된 캐스팅을 시도할 수 있습니다.
rawBox = stringBox; // stringBox를 rawBox에 할당 (여기까지는 문제 없음)
integerBox = (Box<Integer>) rawBox; // stringBox가 할당된 rawBox를 Box<Integer>로 캐스팅 (잘못된 캐스팅)
// integerBox를 사용하여 값을 꺼내려고 하면, 런타임 에러가 발생할 수 있습니다.
Integer value = integerBox.get(); // ClassCastException 발생 가능성
위 코드에서 stringBox를 Box<String>에서 Box로, 그리고 다시 Box<Integer>로 캐스팅하는 과정은 컴파일 시점에는 에러를 발생시키지 않습니다. 그 이유는 타입 소거로 인해 런타임에는 모든 제네릭 타입이 원시 타입(Box)으로 취급되기 때문입니다. 하지만, 실제로 integerBox에는 String 타입의 객체가 저장되어 있기 때문에, integerBox.get()을 호출할 때 ClassCastException이 발생할 수 있습니다.
Box 대신 Box<Integer> 등)의 사용을 피합니다.instanceof 연산자를 사용하여 타입 체크를 수행합니다.타입 소거 방식은 자바가 제네릭을 도입하면서도 기존에 작성된 코드와의 호환성을 유지할 수 있게 해줍니다. 이는 기존 자바 라이브러리와의 호환성을 유지하는데 중요한 역할을 합니다. 하지만, 이러한 방식은 런타임에 타입 정보의 상실을 초래하여 제네릭을 사용하는 일부 상황에서 제약사항이 될 수 있습니다.