jingyulog

Generic 본문

Language/Java

Generic

jingyulog 2025. 10. 4. 15:46

제네릭의 타입 매개변수와 타입 인자

제네릭의 핵심은 사용할 타입을 미리 결정하지 않는다는 점이다. 즉, 클래스 내부에서 사용하는 타입을 클래스를 정의하는 시점에 결정하는 것이 아니라, 해당 클래스를 실제 사용하는 생성 시점에 클래스 내부에서 사용할 타입을 결정하는 것이다. 이를 쉽게 비유하면 메서드의 매개변수와 인자의 관계와 비슷하다. 차이가 있다면 메서드의 매개변수는 사용할 값에 대한 결정을 나중으로 미루는 것이고, 제네릭의 타입 매개변수는 사용할 타입에 대한 결정을 나중으로 미루는 것이다.

  • 메서드는 매개변수에 인자를 전달해서 사용할 값을 결정한다.
  • 제네릭 클래스타입 매개변수타입 인자를 전달해서 사용할 타입을 결정한다.

용어 정리

  • 제네릭 타입(Generic Type)
    클래스나 인터페이스를 정의할 때, 타입 매개변수를 사용하는 것을 말한다. 예를 들어, class GenericBox<T> { private T t; }에서 GenericBox<T>를 제네릭 타입이라고 한다.
  • 타입 매개변수(Type Parameter)
    제네릭 타입이나 메서드에서 사용되는 변수로 실제 타입으로 대체된다. 예를 들어, GenericeBox<T>에서 T를 타입 매개변수라고 한다.
  • 타입 인자(Type Argument)
    제네릭 타입을 사용할 때 제공되는 실제 타입이다. 예를 들어, GenericBox<Integer>에서 Integer를 타입 인자라고 한다.

제네릭 메서드

제네릭 타입과 제네릭 메서드는 둘 다 제네릭을 사용하기는 하지만 서로 다른 기능을 제공한다. 제네릭 메서드는 메서드를 호출하는 시점에 타입 인자를 전달해서 타입을 지정한다.

제네릭 타입

  • 정의: GenericClass<T>
  • 타입 인자 전달: 객체를 생성하는 시점
       new GenericClass<String>

제네릭 메서드

  • 정의: <T> T genericeMethod(T t)
  • 타입 인자 전달: 메서드를 호출하는 시점
          GenericMethod.<Integer>genericeMethod(i)

인스턴스 메서드, static 메서드

제네릭 메서드는 인스턴스 메서드와 static 메서드에 모두 적용할 수 있다.

class Box<T> {
    static <V> V staticMethod(V v) { } // static 메서드에 제네릭 메서드 도입
    <Z> Z instanceMethod(Z z) { } // 인스턴스 메서드에 제네릭 메서드 도입
}

하지만, 제네릭 타입은 static 메서드에 타입 매개변수를 사용할 수 없다. 왜냐하면, 제네릭 타입은 객체를 생성하는 시점에 타입이 정해지고, static 메서드는 인스턴스 단위가 아니라 클래스 단위로 작동하기 때문이다. 따라서 static 메서드에 제네릭을 도입하려면 제네릭 메서드를 사용해야 한다.

활용

public class GenericMethodTest {
    public static <T extends Animal> void checkup(T t) {
        t.sound();
    }

    public static <T extends Animal> T getBigger(T t1, T t2) {
        return t1.getSize() > t2.getSize() ? t1 : t2;
    }
}

public class MainTest {
    public static void main(String[] args) {
        Dog dog = new Dog("코코", 3);
        Cat cat = new Cat("나비", 8);

        GenericMethodTest.checkup(dog);
        GenericMethodTest.checkup(cat);

        Dog targetDog = new Dog("리트리버", 20);
        Dog biggerDog = GenericMethodTest.getBigger(dog, targetDog);
        System.out.println("biggerDog = " + biggerDog);
    }
}

제네릭 타입과 제네릭 메서드의 우선순위

인스턴스 메서드는 제네릭 타입과 제네릭 메서드 둘 다 적용할 수 있는데, 제네릭 타입과 제네릭 메서드의 타입 매개변수를 같은 이름으로 사용하면 제네릭 타입보다 제네릭 메서드가 높은 우선순위를 가진다. 하지만 이는 모호하므로 이렇게는 사용하지 않는게 좋겠다.

public class ComplexBox<T extends Animal> {
    private T animal;

    public void set(T animal) {
        this.animal = animal;
    }

    public <T> T printAndReturn(T t) {
        System.out.println("animal.className: " +
        animal.getClass().getName());
        System.out.println("t.className: " + t.getClass().getName());
        // t.getName(); // 호출 불가 메서드는 <T> 타입이다. <T extends Animal> 타입이
    아니다.
        return t;
    }
}

와일드카드

와일드카는 *, ?와 같이 하나 이상의 문자들을 상징하는 특수 문자를 뜻한다. 즉, 여러 타입이 들어올 수 있다는 소리로, 제네릭 타입을 조금 더 편리하게 사용하는 용도로 사용한다. 이때, 와일드 카드는 ?를 사용해서 정의한다.

public class Animal {
    private String name;
    private int size;

    public Animal(String name, int size) {
        this.name = name;
        this.size = size;
    }

    public String getName() {
        return name;
    }

    public int getSize() {
        return size;
    }

    public void sound() {
        System.out.println("동물 울음 소리");
    }

    @Override
    public String toString() {
        return "Animal{" +
            "name='" + name + '\'' +
            ", size=" + size +
            '}';
    }
}

public class Dog extends Animal {
    public Dog(String name, int size) {
        super(name, size);
    }

    @Override
    public void sound() {
        System.out.println("멍멍");
    }
}

public class Cat extends Animal {
    public Cat(String name, int size) {
        super(name, size);
    }

    @Override
    public void sound() {
        System.out.println("냐옹");
    }
}

public class Box<T> {
    private T value;

    public void set(T value) {
        this.value = value;
    }

    public T get() {
        return value;
    }
}

public class WildcardEx {

    static <T> void printGenericV1(Box<T> box) {
        System.out.println("T = " + box.get());
    }

    static void printWildcardV1(Box<?> box) {
        System.out.println("? = " + box.get());
    }

    static <T extends Animal> void printGenericV2(Box<T> box) {
        T t = box.get();
        System.out.println("이름 = " + t.getName());
    }

    static void printWildcardV2(Box<? extends Animal> box) {
        Animal animal = box.get();
        System.out.println("이름 = " + animal.getName());
    }

    static <T extends Animal> T printAndReturnGeneric(Box<T> box) {
        T t = box.get();
        System.out.println("이름 = " + t.getName());
        return t;
    }

    static Animal printAndReturnWildcard(Box<? extends Animal> box) {
        Animal animal = box.get();
        System.out.println("이름 = " + animal.getName());
        return animal;
    }
}

public class WildcardMain1 {
    public static void main(String[] args) {
        Box<Object> objBox = new Box<>();
        Box<Dog> dogBox = new Box<>();
        Box<Cat> catBox = new Box<>();

        dogBox.set(new Dog("멍멍이", 100));

        WildcardEx.printGenericV1(dogBox);
        WildcardEx.printWildcardV1(dogBox);

        WildcardEx.printGenericV2(dogBox);
        WildcardEx.printWildcardV2(dogBox);

        Dog dog = WildcardEx.printAndReturnGeneric(dogBox);
        Animal animal =  WildcardEx.printAndReturnWildcard(dogBox);
    }
}

이러한 와일드카드는 제네릭을 정의할 때 사용하는 것이 아니라, Box<Dog>, Box<Cat>같이 타입 인자가 전달된 제네릭 타입을 활용할 때 사용하는 것으로 전달한 타입을 명확하게 반환할 수 없다는 단점을 가진다. 따라서 메서드의 타입들을 특정 시점에 변경하려면 제네릭 타입이나 제네릭 메서드를 사용해야 한다.
즉, 와일드카드는 이미 만들어진 제네릭 타입을 전달받아서 활용할 때 사용하며, 메서드의 타입들을 타입 인자를 통해 변경할 수 없으며, 일반적인 메서드에 사용한다고 생각하면 된다.

타입 이레이저

제네릭은 자바 컴파일 단계에서만 사용되고, 컴파일 이후에는 제네릭 정보가 삭제된다. 즉, 제네릭에 사용한 타입 매개변수는 모두 런타임 시점에 모두 삭제된다. 이를 타입 이레이저라고 한다.
따라서 컴파일 전인 .java에는 제네릭의 타입 매개변수가 존재하지만, 컴파일 이후인 자바 바이트코드인 .class에는 타입 매개변수가 존재하지 않는 것이다.

// 컴파일 전
public class AnimalHospitalV3<T extends Animal> {

    private T animal;

    public void set(T animal) {
        this.animal = animal;
    }

    public void checkup() {
        System.out.println("동물 이름: " + animal.getName());
        System.out.println("동물 크기: " + animal.getSize());
        animal.sound();
    }

    public T getBigger(T target) {
        return animal.getSize() > target.getSize() ? animal : target;
    }
}

// 컴파일 후
public class AnimalHospitalV3 {
    private Animal animal;

    public void set(Animal animal) {
        this.animal = animal;
    }

    public void checkup() {
        System.out.println("동물 이름: " + animal.getName());
        System.out.println("동물 크기: " + animal.getSize());
        animal.sound();
    }

    public Animal getBigger(Animal target) {
        return animal.getSize() > target.getSize() ? animal : target;
    }
}

이렇게 보면 자바의 제네릭은 결국에 개발자가 직접 캐스팅하는 코드를 컴파일러가 대신 처리해주는 것이고, 자바는 컴파일 시점에 제네릭을 사용한 코드에 문제가 없는지 완벽하게 검증하기 때문에 자바 컴파일러가 추가하는 다운 캐스팅에는 문제가 발생하지 않는 것이다.

타입 이레이저의 한계

컴파일 이후에는 제네릭의 타입 정보가 존재하지 않는다. 즉, .class를 자바로 실행하는 런타임에는 타입 정보가 모두 제거되므로, 런타임에 타입을 활용하는 instanceofnew같은 코드는 작성할 수 없다.

class EraserBox<T> {
    public boolean instanceCheck(Object param) {
        return param instanceof T; // 오류
    }

    public T create() {
        return new T(); // 오류
    }
}
  • instanceof: 항상 Object와 비교하게 되어 항상 참을 반환한다. 따라서 자바는 타입 매개변수에 instanceof를 허용하지 않는다.
  • new T: 항상 new Object가 되어 개발자가 의도하지 않은 코드가 된다. 따라서 타입 매개변수에 new를 허용하지 않는다.