Java Generic - 파라미터 제한 사용하기

제네릭은 타입 파라미터가 제한되어 있다. 이것이 어떤 의미인지 천천히 살펴보자

예제로 통해 동물 병원 클래스 살펴보자

  • 강아지 병원은 강아지만 입원 가능하고, 고양이 병원은 고양이만 입원한다.

CatHospital.class

public class CatHospital {
    private Cat animal;

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

    public void checkup() {
        System.out.println("동물의 이름 = " + animal.getName());
        System.out.println("동물의 몸무게 = " + animal.getSize());
        animal.sound();
    }

    public Cat bigger(Cat target) {
        return animal.getSize() > target.getSize() ? animal : target;
    }

}
  • 고양이 병원은 Cat 타입을 갖고 있다.
  • checkup()
    • 고양이의 이름과 몸무게를 출력하여 울음소리를 검진한다.
  • bigger()
    • 다른 고양이 크기를 비교하여 큰 고양이를 반환한다.

DogHospital.class

public class DogHospital {
    private Dog animal;

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

    public void checkup() {
        System.out.println("동물의 이름 = " + animal.getName());
        System.out.println("동물의 몸무게 = " + animal.getSize());
        animal.sound();
    }

    public Dog bigger(Dog target) {
        return animal.getSize() > target.getSize() ? animal : target;
    }

}
  • 강아지 병원은 Cat 타입을 갖고 있다.
  • checkup()
    • 강아지의 이름과 몸무게를 출력하여 울음소리를 검진한다.
  • bigger()
    • 다른 강아지 크기를 비교하여 큰 강아지를 반환한다.

Main.class

public class Main {
    public static void main(String[] args) {
        DogHospital dogHospital = new DogHospital();
        CatHospital catHospital = new CatHospital();

        Dog dog = new Dog("멍멍아", 20);
        Cat cat = new Cat("고양아", 10);

        // 강아지 병원 내원
        dogHospital.set(dog);
        dogHospital.checkup();

        // 고양이 병원 내원
        catHospital.set(cat);
        catHospital.checkup();

        // Generic 문제점1. 타입 안전성 체크.
        // 고양이 병원에 강아지 입원
        // catHospital.set(dog); // 컴파일 오류 발생

        // Generic 문제점2. 타입 반환
        dogHospital.set(dog);
        Dog dog2 = dogHospital.bigger(new Dog("리트리버", 30));
        System.out.println(dog2);
    }
}

동물의 이름 = 멍멍아
동물의 몸무게 = 20

동물의 이름 = 고양아
동물의 몸무게 = 10

Animal{name='리트리버', size=30}

요구사항에 맞춰 개발이 완료 되었다.

강아지와 고양이 병원을 별도의 클래스로 만들고 타입이 명확하기에 강아지병원은 강아지만, 고양이 병원은 고양이만 받는다. 만약 강아지 병원에 고양이를 입원하면 컴파일 오류로 진행할 수 없다.

추가로 bigger() 메소드로 통해 강아지 두마리 중 무거운 쪽으로 반환하도록 하였다.

코드 품질 정리

  • 코드 재사용이 되지 않고 있다.
  • 코드 안정성이 명확하여 안정적이다.

동물 병원 - 다형성 시도하기

앞에서 고양이 병원과 강아지 병원을 만들었다. 코드 중복이 있어서 다형성을 이용해 중복을 제거한 코드로 만들어주자.

AnimalHospital.class

public class AnimalHospital {
    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 bigger(Animal target) {
        return animal.getSize() > target.getSize() ? animal : target;
    }
}

생성한 AnimalHospital 안에 Cat, Dog 클래스의 부모 Animal 사용해 사용 못하는 메소드는 없었다. 모두 사용 가능한 것을 확인하였다면 다형성 요건은 맞춰졌다.

  • Animal 타입을 받아 사용
  • checkup(), getBigger() 메소드는 animal.getName(), animal.getSize(), animal.sound() 메소드 모두 Animal 클래스가 제공된 메소드로 모두 문제 없이 동작할 것이다.

Main.java

public class Main {
    public static void main(String[] args) {
        AnimalHospital dogHospital = new AnimalHospital();
        AnimalHospital catHospital = new AnimalHospital();

        Dog dog = new Dog("강아지", 15);
        Cat cat = new Cat("고양이", 8);

        // 강아지 동물 병원
        dogHospital.set(dog);
        dogHospital.checkup();

        // 고양이 동물 병원
        catHospital.set(cat);
        catHospital.checkup();

        // Generic 문제점1. 타입 안전성 체크.
        // 고양이 병원에 강아지 입원
        catHospital.set(dog); // 컴파일 오류 발생되지 않음 !! 문제가 생김

        // Generic 문제점2. 타입 반환
        dogHospital.set(dog);
        // 다형성으로 인해 캐스팅 해주어야함. 불편함 증가
        Dog dog2 = (Dog) dogHospital.getBigger(new Dog("리트리버", 30));
        System.out.println(dog2);
    }
}

동물의 이름 = 강아지
동물의 몸무게 = 15

동물의 이름 = 고양이
동물의 몸무게 = 8

Animal{name='리트리버', size=30}

코드 품질 정리

  • 코드 재사용 높음
    • 다형성을 통해 AnimalHospital 클래스로 고양이와 강아지 모두 처리할 수 있었다.
  • 타입 안정성 부족
    • 강아지 병원에 고양이가 입원하는 문제가 발생하였다.
    • Animal 타입으로 반환하므로 다운 캐스팅이 불가피하게 해야했다.
    • 극단적으로 고양이 병원에서 강아지 반환 시도에 캐스팅 오류가 발생될 것이다.

동물 병원 - 제네릭 코드로 구성하기

앞서 다형성으로 중복성을 제거했으나 타입 안정성 부족이라는 문제가 새로이 발견되었다.
타입 안정성 부족에는 제네릭으로 고쳐볼만하므로 진행해보도록 한다.

AnimalHospital.class

public class AnimalHospital<T> {
    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();

        // T 타입을 메소드를 정의하는 시점에는 알 수 없음. Object 기능만 사용함
        animal.toString();
        animal.equals(null);
    }

    public T getBigger(T target) {
        // 컴파일 오류 발생
        // return animal.getSize() > target.getSize() ? animal : target;
        return null;
    }
}
  • Object에 getName(), getName() 없으므로 호출이 불가능하다.
  • <T> 사용해서 제네릭 타입을 선언

제네릭 타입을 선언한 경우 T에 어떤 값이 들어올지 예측이 안된다. Animal 타입의 자식을 기대하고 있으나 코드에 Animal 정보가 없다. 호출자가 T 타입 인자로 Integer 들어올 수 있고, Dog가 들어올 수 있으나 그와 관련된 메소드는 코드에서 작성할 수 없다.

예> 호출자에서의 타입 인자로 객체 생성

AnimalHospital<Dog> dogHospital = new AnimalHospital<>();
AnimalHospital<Cat> catHospital = new AnimalHospital<>();
AnimalHospital<Integer> integerHospital = new AnimalHospital<>();
AnimalHospital<Object> objectHospital = new AnimalHospital<>();

첫 번째 문제

자바 컴파일러는 어떤 타입이 들어올지 알 수 없기에 T 는 어떤 타입이든 받을 수 있는 모든 객체의 부모인 Object 타입으로 선택한다.
원하는 기능인 Animal 클래스 타입에서 기능들을 사용하고 싶으나 오류로 사용할 수 없다.

두 번째 문제

AnimalHospital 클래스에 Integer, Object 동물과 상관 없는 타입으로 인자 전달이 되고 있다. Animal 객체만 받을 수 있도록 인자 제한이 필요한 상황이다.


동물 병원 - 타입 매개변수 제한 걸기

들어올 제네릭 타입에다가 특정 타입으로 제한할 수 있다.

AnimalHospital.class

public class AnimalHospital<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;
    }
}
  • 클래스 T 제네릭 선언 시 상속처럼 키워드를 붙였다. <T extends Animal>
  • 자바 컴파일러는 Animal 자식만 받도록 되어있도록 돕게 한다.
  • Animal 관련 getSize(), getName() 모두 사용할 수 있다.
    • Object 에서는 getSize(), getName() 호출이 불가능하다는 점에서 큰 장점이다.

호출자에서도 Animal 연관된 자식만 호출할 수 있도록 제한된다.

AnimalHospital<Animal>
AnimalHospital<Dog>
AnimalHospital<Cat>

자바는 T 입력되는 범위가 축소되고 예측할 수 있다.
T 타입 ㅇ니자로 Animal 연관된 Dog, Cat 클래스가 들어오며 메소드 기능 또한 모두 허용한다.

Main.class

public class Main {
    public static void main(String[] args) {
        AnimalHospital<Dog> dogHospital = new AnimalHospital<>();
        AnimalHospital<Cat> catHospital = new AnimalHospital<>();

        Dog dog = new Dog("강아지", 15);
        Cat cat = new Cat("고양이", 8);

        // 강아지 동물 병원
        dogHospital.set(dog);
        dogHospital.checkup();

        // 고양이 동물 병원
        catHospital.set(cat);
        catHospital.checkup();

        // Generic 문제점1. 타입 안전성 체크.
        // 고양이 병원에 강아지 입원
        // catHospital.set(dog); // 컴파일 오류 발생

        // Generic 문제점2. 타입 반환
        dogHospital.set(dog);
        // 다형성으로 자식은 부모의 Animal 타입을 받을 수 있음.
        Dog dog2 = dogHospital.getBigger(new Dog("리트리버", 30));
        System.out.println(dog2);
    }
}

동물의 이름 = 강아지
동물의 몸무게 = 15

동물의 이름 = 고양이
동물의 몸무게 = 8

Animal{name='리트리버', size=30}

타입에 입력될 수 있는 상한을 모두 지정되어 문제가 해결되었다.

  • AnimalHospital<Integer> 동물과 상관 없는 타입 인자로 컴파일에서 막음

코드 품질 정리

  • 타입 안정성 문제
    • 강아지 병원에 고양이가 입원하였는가? 아니요. => 해결
    • 제네릭으로 Animal 타입을 반환이 다운캐스팅 되었는가? => 예. 해결
  • 이전의 제네릭 문제점
    • 제네릭에서 어떠한 매개변수 타입이 들어왔다. => 해결
    • 어떤 타입으로 사용하는 Object로 사용하고 Object 기능만 사용되었다. => 해결
      • 파라미터 제한으로 Animal 기능이 사용 가능하게 되었다.

제네릭으로 인한 매개변수 상한(제한) 타입 안정성을 지키며 상위 타입의 원하는 기능도 사용하게 되었다. 코드 재사용과 타입 안정성을 모두 챙기게 되었다.