신입으로 취업을 준비하면서 108번의 면접을 보았는데, 살다라는 기업에서 처음으로 나에게 큰 임팩트를 주었다.
이 기술을 왜 사용했고, 다른 대안은 없었는가?
왜 이 언어를 사용했는가?
기능을 구현할 때 왜 이 문법을 사용했는가?
부트캠프에서 공부를 하면서 기술 익히기에 급급했던 과거의 나는 이 기업에서 면접을 보고나서 공부에 대한 태도를 바꿔야겠다는 생각이 들었다.
당시 "제네릭(Generic)을 왜 사용하셨나요?"에 대한 질문이 왔을 때 큰 충격이 있었는데,
지금 다시 자바 기본 문법부터 공부를 하다가 다시 만난 제네릭 !
"타입을 제한하기 위해서입니다"와 같은 단답에서 벗어나서 "why"에 초점을 맞춰서 내용을 정리해보려고 한다.
package generic;
public class IntegerBox {
private Integer value;
public void setValue(Integer value) {
this.value = value;
}
public Integer getValue() {
return value;
}
}
package generic;
public class StringBox {
private String value;
public void setValue(String value) {
this.value = value;
}
public String getValue() {
return value;
}
}
package generic;
public class BoxMain1 {
public static void main(String[] args) {
IntegerBox integerBox = new IntegerBox();
integerBox.setValue(10); // Auto Boxing
Integer integer = integerBox.getValue();
System.out.println("integer = " + integer);
StringBox stringBox = new StringBox();
stringBox.setValue("Hello Java !");
String string = stringBox.getValue();
System.out.println("string = " + string);
}
}
문제점
이후에 Double, Boolean 등 다양한 타입의 박스가 필요하다면?
각각의 타입별로 XxxBox 클래스를 새로 만들어야 한다.
매우 비효율적이기 때문에 이 문제를 단계별로 해결해 나가보자.
Object는 모든 타입의 부모이기 때문에, 다형성(다형적 참조)를 사용해서 해결해보자.
부모는 자식을 담을 수 있으므로 세상의 모든 타입을 ObjectBox에 담을 수 있다.
package generic;
public class ObjectBox {
private Object value;
public void setValue(Object value) {
this.value = value;
}
public Object getValue() {
return value;
}
}
package generic;
public class BoxMain2 {
public static void main(String[] args) {
ObjectBox integerBox = new ObjectBox();
integerBox.setValue(10);
// Object integer = integerBox.getValue();
Integer integer = (Integer) integerBox.getValue(); // Object -> Integer 캐스팅
System.out.println("integer = " + integer);
ObjectBox stringBox = new ObjectBox();
stringBox.setValue("Hello Java !");
// Object value = stringBox.getValue();
String string = (String) stringBox.getValue(); // Object -> String 캐스팅
System.out.println("value = " + string);
// 잘못된 타입의 인수 전달
integerBox.setValue("문자 10");
Integer result = (Integer) integerBox.getValue();
System.out.println("value = " + result);
}
}
실행 결과
integer = 10
value = Hello Java !
Exception in thread "main" java.lang.ClassCastException: class java.lang.String cannot be cast to class java.lang.Integer (java.lang.String and java.lang.Integer are in module java.base of loader 'bootstrap')
at generic.BoxMain2.main(BoxMain2.java:19)
// Object integer = integerBox.getValue();
Integer integer = (Integer) integerBox.getValue(); // Object -> Integer 캐스팅
Object 타입을 Integer 타입으로 직접 다운캐스팅 해야한다.
다운 캐스팅을하게 되면 몇가지 문제점을 신경써야 한다.
1. ClassCastException 발생 가능성
컴파일러는 다운 캐스팅이 가능하다고 단정할 수 없기 때문에, 런타임 시점에 검증을 해야한다.
// 잘못된 타입의 인수 전달의 경우 처럼, 다른 객체로 반환하려고 한다면 예외가 발생해 프로그램이 종료된다.
2. 타입 안정성
다운 캐스팅은 개발자가 직접 타입을 보장해주는 것과 마찬가지인데, 잘못된 타입이 저장되었을 때 문제를 사전에 감지할 수 있는 방법이 없다.
3. 불필요한 코드 복잡성
다운 캐스팅을 반복해서 사용하게 되면 코드가 불필요하게 복잡해지고, 각 다운 캐스팅에 대한 타입 확인 로직을 추가하는 등으로 가독성이 떨어질 수 있다.
다형성을 활용한 장점
1. 코드 중복 제거
2. 기존 코드를 재사용 (클래스를 계속해서 XxxBox로 만들지 않음)
문제점
1. 타입 안정성 문제 (실수로 원하지 않는 타입이 들어갈 수 있다.)
2. 데이터 반환 시점에 원하는 타입을 정확하게 받을 수 없다.
3. 위험한 다운 캐스팅을 시도해야 한다.
코드 재사용성을 늘리기 위해 Object의 다형성을 사용하면 타입 안정성이 떨어지는 문제가 발생한다.
두 마리 토끼를 다 잡을 수는 없을까???
package generic.ex1;
public class GenericBox<T> {
private T value;
public void setValue(T value) {
this.value = value;
}
public T getValue() {
return value;
}
}
<T>
와 같이 선언하면 제네릭 클래스가 된다.T
를 타입 매개변수라 한다.T value
와 같이 타입 매개변수를 적어두면 된다.new GenericBox<Integer>()
제네릭 클래스는 생성하는 시점에 <> 사이에 원하는 타입을 지정한다.
이렇게 지정하면, GenericBox의 T가 다음과 같이 지정한 타입으로 변환한 다음 생성된다.(실제로 만들어지는 것은 아님 !)
String string = stringBox.getValue(); // 캐스팅 X
new GenericBox<Double>()
new GenericBox<Boolean>()
new GenericBox<MyClass>()
GenericBox<Integer> integerBox = new GenericBox<Integer>() // 타입 직접 입력
GenericBox<Integer> integerBox2 = new GenericBox<>() // 타입 추론
제네릭을 사용한 덕분에 코드 재사용과 타입 안정성이라는 두마리 토끼를 모두 잡을 수 있었다. 🐰🐰
제네릭의 핵심은 사용할 타입을 미리 결정하지 않는다는 점이다.
클래스 내부에서 사용하는 타입을 클래스를 정의하는 시점에 결정하는 것이 아니라, 실제 사용하는 생성 시점에 타입을 결정하는 것이다.
void hello() {
System.out.println("Hello Java!");
}
void hello(String param) {
println(param);
}
void main() {
method2("Hello");
method2("Java!");
}
제네릭(Generic)
타입 매개변수
GenericBox <T>
T
를 타입 매개변수라 한다.타입 인자
GenericBox<Integer>
Integer
를 타입 인자라 한다.제네릭 타입(Generic Type)
ex)
class GenericBox<T> {
private T t;
}
여기에서 GenericBox<T>를 제네릭 타입이라 한다.