Java

[Java] 제네릭(Generic) 정리

leevigong 2025. 10. 30. 23:00
반응형

제네릭 탄생 배경

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로 변환되어 컴파일에 반영된다.
      실제 사용하는 생성하는 시점에 원하는 타입을 결정한다.
    • 타입 추론이 가능하다.
      왼쪽에 있는 변수 선언할 때 타입을 보고, 오른쪽에 이는 객체를 생성할 때 필요한 타입 정보를 얻을 수 있기 때문에
    • 원하는 모든 타입 사용 가능하다. 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<>();
반응형