반응형
제네릭 탄생 배경
1. 타입별 클래스 문제 발생
▸ 예시 코드
class IntegerBox {
private Integer value;
public void set(Integer value) { this.value = value; }
public Integer get() { return value; }
}
class StringBox {
private String value;
public void set(String value) { this.value = value; }
public String get() { return value; }
}
class DoubleBox {
private Double value;
public void set(Double value) { this.value = value; }
public Double get() { return value; }
}
- 다형성 부족: Integer, String, Double 등 타입마다 별도의 클래스가 필요하다.
- 코드 중복: 타입만 다를 뿐 동일한 로직을 반복 작성한다.
- 새로운 타입이 추가될 때마다 새 클래스를 계속 만들어야 한다.
2. Object 사용하여 다형성 문제 해결
Object는 모든 타입의 부모이므로 다형성 문제를 해결할 수 있다.
▸ 예시 코드
class ObjectBox {
private Object value;
public void set(Object value) {
this.value = value;
}
public Object get() {
return value;
}
}
// 사용 예시
ObjectBox integerBox = new ObjectBox();
integerBox.set(100);
ObjectBox stringBox = new ObjectBox();
stringBox.set("Hello");
ObjectBox doubleBox = new ObjectBox();
doubleBox.set(3.14);
- 다형성 문제 해결: Object로 모든 타입을 하나의 클래스에서 처리 가능하다.
- 코드 중복 제거: 하나의 클래스로 통합한다.
- 코드 재사용성 대폭 향상된다.
3. Object 사용의 문제 - 타입 안전성
문제 1. 반환 시 매번 다운캐스팅 필요하다.
▸ 예시 코드
ObjectBox box = new ObjectBox();
box.set(100);
// get()은 Object를 반환하므로 매번 캐스팅 필요
Integer value = (Integer) box.get(); // 위험한 다운캐스팅
- 반환(get) 타입이 Object이기 때문에 원하는 타입으로 정확하게 받을 수 없고, 항상 다운캐스팅을 해야한다.
문제 2. 잘못된 타입 입력 가능하다.
▸ 예시 코드
ObjectBox integerBox = new ObjectBox();
integerBox.set(100); // Integer 저장
integerBox.set("문자열"); // String도 저장 가능 (컴파일 에러 없음!)
// 꺼낼 때 문제 발생
Integer value = (Integer) integerBox.get(); // ClassCastException 발생!
- 입력(set) 시: 모든 타입을 받을 수 있어 실수로 잘못된 타입 전달 가능하다.
- 결과: 컴파일 시점에 타입 오류를 잡을 수 없고, 런타임에 ClassCastException 발생한다.
4. 제네릭 등장
“코드 재사용과 타입 안정성 모두 챙길 수 없을까?” ← 이 딜레마를 해결하기 위해 제네릭이 탄생했다.
▸ 예시 코드
class GenericBox<T> { // T는 타입 파라미터
private T value;
public void set(T value) {
this.value = value;
}
public T get() {
return value;
}
}
// Integer용
GenericBox<Integer> integerBox = new GenericBox<>();
integerBox.set(100);
Integer value1 = integerBox.get(); // 👍 캐스팅 불필요
// String용
GenericBox<String> stringBox = new GenericBox<>();
stringBox.set("Hello");
String value2 = stringBox.get(); // 👍 캐스팅 불필요
// 잘못된 타입 입력 시도
integerBox.set("문자열"); // ⚠️ 컴파일 에러 -> 타입 안전성 보장
정리
| 코드 재사용성 | 타입 안정성 | |
| 타입별 클래스 | X | O |
| Object 사용 | O | X |
| 제네릭 사용 | O | O |
제네릭(Generic)
제네릭은 타입을 파라미터화하는 기능이다.
클래스나 메서드를 정의할 때 구체적인 타입을 지정하지 않고, 사용 시점에 타입을 결정할 수 있게 해준다.
- <> 를 사용한 클래스를 제네릭 클래스라고 한다
- 클래스명 오른쪽에 <T> 와 같이 선언
- 제네릭 클래스를 사용할 때 타입(Integer, String 등)을 미리 결정하지 않는다.
class GenericBox<T> { } // 사용 예시 GenericBox<String> stringBox = new GenericBox<String>(); // 타입 직접 입력 GenericBox<String> stringBox = new GenericBox<>(); // 타입 추론- 이렇게 하면 GenericBox의 모든 T 자리에 String로 변환되어 컴파일에 반영된다.
→ 실제 사용하는 생성하는 시점에 원하는 타입을 결정한다.
- 이렇게 하면 GenericBox의 모든 T 자리에 String로 변환되어 컴파일에 반영된다.
- 타입 추론이 가능하다.
왼쪽에 있는 변수 선언할 때 타입을 보고, 오른쪽에 이는 객체를 생성할 때 필요한 타입 정보를 얻을 수 있기 때문에 - 원하는 모든 타입 사용 가능하다. Integer, String, Double 등등 (단, 기본형은 사용 불가)
- 한 번에 여러 타입 파라미터를 선언할 수 있다. (예: Map)
class CustomMap<K, V> {}
제네릭 문법
- <T>: 타입 파라미터를 선언
- 타입 파라미터로는 T(Type), E(Element), K(Key), V(Value), N(Number) 등을 일반적으로 사용
- 실제로는 어떤 이름이든 사용 가능하지만, 대문자 한 글자를 사용하는 것이 관례
- T value: 타입 파라미터를 변수 타입으로 사용
제네릭 용어 정리
- 제네릭(Generic): 특정 타입에 속한 것이 아니라, 일반적으로 사용할 수 있다는 의미
- 제네릭 타입(Generic Type): 클래스나, 인터페이스를 정의할 때 타입 파라미터를 사용하는 것을 의미
- 타입 파라미터(Type Parameter): 제네릭 타입이나 메서드를 사용되는 변수를 의미
- 타입 인자(Type Argument): 제네릭 타입을 사용할 때 제공되는 실제 타입

로 타입(Raw Type)
제네릭 타입을 사용할 때 타입 파라미터를 지정하지 않은 것을 말한다.
GenericBox integerBox = new GenericBox();
로 타입을 사용하면 내부의 타입 매개변수가 Object로 사용된다고 이해하면 된다.
따라서, 제네릭 타입의 장점(타입 안정성, 다운캐스팅 불필요 등)을 활용할 수 없다.
로 타입이 존재하는 이유
제네릭은 자바 5(JDK 1.5)부터 도입되어, 그 이전에 작성된 코드들과의 호환성을 유지하기 위해 로 타입을 지원한다.
결론: 로 타입은 사용하지 말자
- 항상 구체적인 타입 파라미터를 지정해서 사용하자.
- 만약에 Object 타입을 사용해야 한다면 타입 인자로 Object를 지정해서 사용하면 된다.
타입 매개변수 제한
제네릭을 사용하면 모든 타입을 받을 수 있습니다. 하지만 때로는 특정 타입과 그 하위 타입만 받고 싶을 때가 있다.
문제 상황
예를 들면, 상위타입(Spape) - 하위타입(Circle, Rectangle…)이 있다.
class ShapeCalculator<T> {
private T shape;
public void set(T shape) {
this.shape = shape;
}
public void calculate() {
// T의 타입을 메서드를 정의하는 시점에는 알 수 없다. Object의 메서드만 사용 가능
shape.toString()
// shape.getArea(); // ⚠️ 컴파일 에러: Shape 메서드 사용 불가
}
}
class Shape {
public double getArea() {
return 0;
}
public double getPerimeter() {
return 0;
}
}
class Circle extends Shape {
private double radius;
@Override
public double getArea() {
return Math.PI * radius * radius;
}
@Override
public double getPerimeter() {
return 2 * Math.PI * radius;
}
}
class Rectangle extends Shape {
private double width;
private double height;
@Override
public double getArea() {
return width * height;
}
@Override
public double getPerimeter() {
return 2 * (width + height);
}
}
// ⚠️ 도형 계산기인데 도형과 관계없는 타입도 들어갈 수 있음
ShapeCalculator<Integer> calc1 = new ShapeBox<>();
ShapeCalculator<String> calc2 = new ShapeBox<>();
// 이게 정상
ShapeCalculator<Circle> circleCalc = new ShapeBox<>();
ShapeCalculator<Rectangle> rectangleCalc = new ShapeBox<>();
문제점
- 상위 타입과 관련 없는 타입(Integer, String 등)이 들어올 수 있다.
- 타입 파라미터(T)는 어떤 타입이든 받을 수 있어 Object로 가정하고 해당 기능만 사용가능하다.
→ 필요한 상위 타입의 메서드를 사용할 수 없다.
해결: 타입 제한 (Bounded Type Parameter)
extends 키워드를 사용해 타입 파라미터의 범위를 제한할 수 있다.
class ShapeCalculator<T extends Shape> { // Shape 또는 그 하위 타입만 허용
private T shape;
public void set(T shape) {
this.shape = shape;
}
public void calculate() {
// 이제 Shape의 메서드를 사용할 수 있음!
System.out.println("넓이: " + shape.getArea());
System.out.println("둘레: " + shape.getPerimeter());
}
}
// Shape 하위 타입만 허용됨
ShapeCalculator<Circle> circleCalc = new ShapeCalculator<>();
ShapeCalculator<Rectangle> rectangleCalc = new ShapeCalculator<>();
ShapeCalculator<Shape> shapeCalc = new ShapeCalculator<>();
// ⚠️ Shape와 관계없는 타입은 컴파일 에러
ShapeCalculator<Integer> calc1 = new ShapeCalculator<>();
ShapeCalculator<String> calc2 = new ShapeCalculator<>();반응형