package generic.ex1;
public class BoxMain1 {
public static void main(String[] args) {
IntegerBox integerBox = new IntegerBox();
integerBox.set(10); // autoBoxing
Integer integer = integerBox.get();
System.out.println("integer = " + integer);
StringBox stringBox = new StringBox();
stringBox.set("hello");
String str = stringBox.get();
System.out.println("str = " + str);
}
}
BoxMain1은 main 클래스로, 실행 단계에서 IntegerBox, StringBox와 같은 타입의 인스턴스가 생성된다. 이 클래스들은 단순히 특정 타입의 데이터를 보관하고 get할 수 있는 Box 역할을 한다.
하지만 만약 main에서 수많은 타입의 XxxTypeBox 클래스들이 생성되어야 한다면, 매번 새로운 XxxTypeBox 클래스를 만들어야 하고, main에서도 이를 처리하기 위해 많은 코드가 필요해진다. 이렇게 되면 코드의 유지보수성이 떨어지고, 반복적인 코드 작성이 많아지므로 효율적이지 않다.
package generic.ex1;
public class ObjectBox {
private Object value;
public void set(Object object) {
this.value = object;
}
public Object get() {
return value;
}
}
///////===========////////
package generic.ex1;
public class BoxMain2 {
public static void main(String[] args) {
ObjectBox integerBox = new ObjectBox();
integerBox.set(10);
Integer integer = (Integer) integerBox.get();
System.out.println("integer = " + integer);
ObjectBox stringBox = new ObjectBox();
stringBox.set("hello");
String string = (String) stringBox.get();
System.out.println("string = " + string);
}
}
기존에는 타입에 해당하는 박스마다 별도의 클래스를 만들어야 했다. 하지만 Object는 모든 타입의 부모이므로, Object 타입에는 어떤 데이터가 들어와도 문제가 없다. 예를 들어, Object 타입에는 10이든, 다른 데이터가 들어오든 상관없다.
다만, main에서 최종적으로 Integer나 String과 같은 구체적인 타입으로 활용하려면, 해당 값을 다운캐스팅해야 한다. 타입을 명시적으로 변환해야 하기 때문에 추가적인 코드가 필요하다.
// 잘못된 타입의 인수 전달시
integerBox.set("문자100");
Integer result = (Integer) integerBox.get();
System.out.println("result = " + result);
만약 ObjectBox의 인스턴스에 IntegerBox나 StringBox를 넣는다면, String은 Object의 자식이므로 아무 문제 없이 담을 수 있다. 그러나 integerBox와 같은 이름의 변수에 Integer를 넣고 이를 (String)로 다운캐스팅하려고 한다면, 런타임 에러가 발생할 것이다.
String의 부모는 Object이기 때문에, 다운캐스팅할 때 문법적으로는 문제가 없고 컴파일러는 이를 검사하지 않는다. 즉, 컴파일 시점에서는 문제를 발견할 수 없고, 실제로 실행될 때 잘못된 캐스팅이 이루어져 런타임 에러가 발생할 수 있다.
자바를 사용한다면, 다운캐스팅 코드는 가능한 한 기피하는 것이 좋다. 다운캐스팅은 타입이 일치하지 않을 경우 런타임 오류를 발생시킬 수 있기 때문에, 예기치 않은 버그를 초래할 수 있다.
즉, BoxMain1에서 개선을 목적으로 BoxMain2를 만들어 클래스 설계에서 코드 노동의 반복 횟수를 줄이고, 코드 재사용성이 높은 ObjectBox를 작성하여 활용하였다.
ObjectBox는 입력으로 무엇이든 받을 수 있고 출력으로 무엇이든 뱉을 수 있는 클래스가 되었다.
하지만, 다운캐스팅 시 입력으로 들어간 타입이 다운캐스팅때 선언한 타입과 맞지 않으면 치명적인 오류가 발생할 수 있다.
BoxMain1의 단점을 개선하고자 Object타입을 활용한BoxMain2를 만들었다.
하지만 BoxMain1이 가지는 타입 안정성을 취하지 못한다.
이러한 trade-off를 해결해줄 수 있는 것이 바로 Generic이다.
//GenericBox Class
package generic.ex1;
public class GenericBox<T> {
private T value;
public void set(T value) {
this.value = value;
}
public T get() {
return value;
}
}
//BoxMain3
package generic.ex1;
public class BoxMain3 {
public static void main(String[] args) {
GenericBox<Integer> integerBox = new GenericBox<Integer>(); // 생성 시점에 T의 타입이 결정된다.
integerBox.set(10);
Integer integer = integerBox.get();
System.out.println("integer = " + integer);
GenericBox<String> stringBox = new GenericBox<String>();
stringBox.set("hello");
String string = stringBox.get();
System.out.println("string = " + string);
}
}
제네릭은 class명 뒤에 다이아몬드(<>)를 선언하고 그 안에 타입 변수를 적어주면 된다. (일반적으로 타입 변수는 규칙에 따라 T를 사용한다.)
그리고 클래스 내부에서 T(타입 변수)와 동일하게 쓰일 곳에 T를 사용한다. 이제 T에 String이 들어온다면, 내부의 모든 T는 String이 되고, Integer가 들어온다면 내부의 모든 T는 Integer가 된다.

위의 컴파일 오류를 보면, integerBox의 제네릭으로 Integer를 주어서 객체를 만들었고, set 메서드에 String을 넣으려 할 때 컴파일 오류가 발생하고 있다. 이는 제네릭이 타입을 강제하기 때문에, Integer 타입을 지정한 박스에 String을 넣을 수 없기 때문이다.
BoxMain1의 경우, 수많은 클래스를 만들어야 했다. 그 이유는 들어오는 타입이 마치 상수처럼 하나의 타입으로 결정되었기 때문이다.
들어오는 타입의 동적 처리를 위해 Object타입으로 받게된다면, 타입을 맞추기 위해 수많은 클래스를 만들지 않아도 되었다.
하지만, 결국 들어오거나 나가는 데이터는 모두 Object이기에 이를 맞춰 쓰기 위해 다운캐스팅이 필요했고, 그 결과 타입 안전성에 문제가 생겼다.
Generic을 통해 들어오는 타입의 고정성을 풀 수 있었다.
Object, String, Integer 등 하나의 타입으로 고정되는 것이 아니라, <T>라는 타입 변수를 만들어 들어오는 타입에 따라 동적으로 내부 선언들을 변경할 수 있게 되었다.
T에 원하는 타입을 전달하여 하나의 클래스로 코드 재사용성을 확보했고, 타입만 전달하면 해당 인스턴스는 전달된 타입으로 고정되므로 타입 안전성(컴파일 오류 이용 가능) 문제도 해결되었다.
// (1)
GenericBox<Integer> integerBox = new GenericBox<Integer>();
// (2)
GenericBox<Integer> integerBox = new GenericBox<>();
(1) 코드를 보면 타입 변수를 Integer로 지정한 제네릭 클래스로 만든 인스턴스임은 분명히 알 수 있다. 그러나 굳이 타입에 들어올 Integer를 두 번이나 명시해야 할 필요가 있을까? 자바는 이러한 비효율성을 제거하고자, 선언 시의 객체명을 나타내는 부분에만 타입 변수를 할당하면 되게끔 간편화했다.
즉, GenericBox라는 제네릭 클래스에 Integer 타입 변수를 할당한 integerBox를 new GenericBox<>()로 만들겠다는 것이다. 객체가 생성될 때 Integer를 전달하면, 자바는 이를 추론하여 원하는 객체를 만들어 준다. 말이 어려울 수 있지만, 굳이 두 번 명시할 필요 없이 앞 쪽에서 한 번만 쓰면 된다는 것이다.