2-1. 타입 매개변수 제한

shin·2024년 7월 7일
  • 동물 병원 생성
  • 요구사항 : 개 병원은 개만 받을 수 있고, 고양이 병원은 고양이만 받을 수 있음
package generic.ex3;

import generic.animal.Dog;

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;
    }
    
}
  • 개 병원은 내부에 Dog 타입을 가짐
  • checkup() : 개의 이름과 크기를 출력하고, 개의 sound() 메서드를 호출함
  • bigger() : 다른 개와 크기를 비교하고, 둘 중에 큰 개를 반환함

package generic.ex3;

import generic.animal.Cat;

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 getBigger(Cat target) {
 		return animal.getSize() > target.getSize() ? animal : target;
    }
    
 }
  • 고양이 병원은 내부에 Cat 타입을 가짐
  • checkup() : 고양이의 이름과 크기를 출력하고, 고양이의 sound() 메서드를 호출함
  • bigger() : 다른 고양이와 크기를 비교하고, 둘 중 큰 고양이를 반환함

package generic.ex3;

import generic.animal.Cat;
import generic.animal.Dog;

public class AnimalHospitalMainV0 {

 	public static void main(String[] args) {
    
 		DogHospital dogHospital = new DogHospital();
 		CatHospital catHospital = new CatHospital();
        
 		Dog dog = new Dog("멍멍이1", 100);
		Cat cat = new Cat("냐옹이1", 300);
         
         // 개 병원
        dogHospital.set(dog);
        dogHospital.checkup();
        
 		// 고양이 병원
        catHospital.set(cat);
        catHospital.checkup();
        
 		// 문제1: 개 병원에 고양이 전달
		// dogHospital.set(cat); // 다른 타입 입력: 컴파일 오류
		
        // 문제2: 개 타입 반환
        dogHospital.set(dog);
 		Dog biggerDog = dogHospital.bigger(new Dog("멍멍이2", 200));
 		System.out.println("biggerDog = " + biggerDog);
        
    }
    
 }

실행 결과

동물 이름: 멍멍이1
동물 크기: 100
멍멍
동물 이름: 냐옹이1
동물 크기: 300
냐옹
biggerDog = Animal{name='멍멍이2', size=200}

  • 위 코드는 처음에 제시한 요구사항을 명확하게 지킴

    • 요구사항 : 개 병원은 개만 받을 수 있고, 고양이 병원은 고양이만 받을 수 있어야 함
  • 여기서 개 병원과 고양이 병원을 각각 별도의 클래스로 만듦

  • 각 클래스 별로 타입이 명확하기 때문에 개 병원은 개만 받을 수 있고, 고양이 병원은 고양이만 받을 수 있음

  • 따라서 개 병원에 고양이를 전달하면 컴파일 오류가 발생함

  • 그리고 개 병원에서 bigger()로 다른 개를 비교하는 경우 더 큰 개를 Dog 타입으로 반환함

  • 코드 재사용성 X : 개 병원과 고양이 병원은 중복이 많이 보임
  • 타입 안전성 O : 타입 안전성이 명확하게 지켜짐


1. 다형성 시도

  • Dog, CatAnimal이라는 명확한 부모 타입이 있음
  • 다형성을 사용하여 중복 제거 시도
package generic.ex3;

import generic.animal.Animal;

public class AnimalHospitalV1 {

 	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;
    }
    
}

-Animal() 타입을 받아서 처리

  • checkup(), getBigger()에서 사용하는 animal.getName(), animal.getSize(), animal.sound() 메서드는 모두 Animal 타입이 제공하는 메서드
    • 따라서 아무 문제없이 모두 호출할 수 있음

package generic.ex3;

import generic.animal.Cat;
import generic.animal.Dog;

public class AnimalHospitalMainV1 {

    public static void main(String[] args) {
        
        AnimalHospitalV1 dogHospital = new AnimalHospitalV1();
        AnimalHospitalV1 catHospital = new AnimalHospitalV1();
        
        Dog dog = new Dog("멍멍이1", 100);
        Cat cat = new Cat("냐옹이1", 300);
        
        // 개 병원
        dogHospital.set(dog);
        dogHospital.checkup();
        
        // 고양이 병원
        catHospital.set(cat);
        catHospital.checkup();
        
        // 문제1: 개 병원에 고양이 전달
        dogHospital.set(cat); // 매개변수 체크 실패: 컴파일 오류가 발생하지 않음
        
        // 문제2: 개 타입 반환, 캐스팅 필요
        dogHospital.set(dog);
        Dog biggerDog = (Dog) dogHospital.getBigger(new Dog("멍멍이2", 200));
        System.out.println("biggerDog = " + biggerDog);
        
    }
    
}

실행 결과

동물 이름: 멍멍이1
동물 크기: 100
멍멍
동물 이름: 냐옹이1
동물 크기: 300
냐옹
biggerDog = Animal{name='멍멍이2', size=200}

  • 코드 재사용 O : 다형성을 통해 AnimalHospitalV1 : 하나로 개와 고양이를 모두 처리함
  • 타입 안전성 X
    • 개 병원에 고양이를 전달하는 문제가 발생함
    • Animal 타입을 반환하기 때문에 다운 캐스팅을 해야 함
    • 실수로 고양이를 입력했는데, 개를 반환하는 상황이라면 캐스팅 예외가 발생함


2. 제네릭 도입과 실패

  • 제네릭을 도입해서 코드 재사용을 늘리고, 타입 안전성 문제도 해결 시도
package generic.ex3;

public class AnimalHospitalV2<T> {

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

 	public void checkup() {
 		// T의 타입을 메서드를 정의하는 시점에는 알 수 없다. Object의 기능만 사용 가능
        animal.toString();
        animal.equals(null);
        
 		// 컴파일 오류
		//System.out.println("동물 이름: " + animal.getName());
 		//animal.sound();
    }
    
 	public T getBigger(T target) {
		// 컴파일 오류
		//return animal.getSize() > target.getSize() ? animal : target;
 		return null;
    }
    
 }
  • <T>를 사용해서 제네릭 타입을 선언함

  • 제네릭을 선언하면 자바 컴파일러 입장에서 T에 어떤 값이 들어올지 예측할 수 없음

    • 우리는 Animal 타입의 자식이 들어오기를 기대했지만, 위 코드 어디에도 Animal에 대한 정보는 없음
    • T에는 타입 인자로 Integer가 들어올 수도 있고, Dog가 들어올 수도 있음
    • 물론 Object가 들어올 수도 있음

다양한 타입 인자

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

  • 자바 컴파일러는 어떤 타입이 들어올지 알 수 없기 때문에 T를 어떤 타입이든 받을 수 있는 모든 객체의 최종 부모인 Object 타입으로 가정함

    • 따라서 Object가 제공하는 메서드만 호출할 수 있음
    • 원하는 기능을 사용하려면 Animal 타입이 제공하는 기능들이 필요한데, 이 기능을 모두 사용할 수 없음
  • 추가적인 문제점은 동물 병원에 Integer, Object 같은 동물과 전혀 관계없는 타입을 타입 인자로 전달할 수 있다는 점

    • 우리는 최소한 Animal이나 그 자식을 타입 인자로 제한해야 하고 싶은 상태

package generic.ex3;

import generic.animal.Cat;
import generic.animal.Dog;

public class AnimalHospitalMainV2 {

	public static void main(String[] args) {
 		AnimalHospitalV2<Dog> dogHospital = new AnimalHospitalV2<>();
		 AnimalHospitalV2<Cat> catHospital = new AnimalHospitalV2<>();
 		AnimalHospitalV2<Integer> integerHospital = new AnimalHospitalV2<>();
 		AnimalHospitalV2<Object> objectHospital = new AnimalHospitalV2<>();
    }
    
}
  • 제네릭에서 타입 매개변수를 사용하면 어떤 타입이든 들어올 수 있음
  • 따라서 타입 매개변수를 어떤 타입이든 수용할 수 있는 Object로 가정하고, Object의 기능만 사용할 수 있음
  • 발생한 문제점들을 생각해보면 타입 매개변수를 Animal로 제한하지 않았기 때문임
    • 따라서 타입 인자가 모두 Animal과 그 자식만 들어올 수 있게 제한해야 함


3. 타입 매개변수 제한

  • 타입 매개변수를 특정 타입으로 제한할 수 있음
package generic.ex3;
import generic.animal.Animal;

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;
    }
    
}
  • 핵심은 <T extends Animal>
  • 타입 매개변수 TAnimal과 그 자식만 받을 수 있도록 제한을 두는 것
  • 타입 인자로 들어올 수 있는 값이 Animal과 그 자식으로 제한됨
AnimalHospitalV3<Animal>
AnimalHospitalV3<Dog>
AnimalHospitalV3<Cat>
  • 자바 컴파일러는 이제 T에 입력될 수 있는 값의 범위를 예측할 수 있음
  • 타입 매개변수 T에는 타입 인자로 Animal, Dog, Cat만 들어올 수 있음
    • 이를 모두 수용할 수 있는 AnimalT의 타입으로 가정해도 문제가 없음
    • 따라서 Animal이 제공하는 getName(), getSize() 같은 기능을 사용할 수 있음

package generic.ex3;

import generic.animal.Cat;
import generic.animal.Dog;

public class AnimalHospitalMainV3 {

	public static void main(String[] args) {
    
 		AnimalHospitalV3<Dog> dogHospital = new AnimalHospitalV3<>();
 		AnimalHospitalV3<Cat> catHospital = new AnimalHospitalV3<>();
 		       
 		Dog dog = new Dog("멍멍이1", 100);
 		Cat cat = new Cat("냐옹이1", 300);
        
 		//개 병원
        dogHospital.set(dog);
        dogHospital.checkup();
        
 		//고양이 병원
        catHospital.set(cat);
        catHospital.checkup();
        
 		// 문제1 해결: 개 병원에 고양이 전달
		// dogHospital.set(cat); // 다른 타입 입력: 컴파일 오류

		// 문제2 해결: 개 타입 반환
        dogHospital.set(dog);
 		Dog biggerDog = dogHospital.getBigger(new Dog("멍멍이2", 200));
		System.out.println("biggerDog = " + biggerDog);
        
    }
    
 }

실행 결과

동물 이름: 멍멍이1
동물 크기: 100
멍멍
동물 이름: 냐옹이1
동물 크기: 300
냐옹
biggerDog = Animal{name='멍멍이2', size=200}
  • 타입 매개변수에 입력될 수 있는 상한을 지정해서 문제를 해결함
    • AnimalHospitalV3<Integer>와 같이 동물과 전혀 관계없는 타입 인자를 컴파일 시점에 막음
    • 제네릭 클래스 안에서 Animal의 기능을 사용할 수 있음


기존 문제와 해결

타입 안전성 문제

  • 개 병원에 고양이를 전달하는 문제가 발생 -> 해결
  • Animal 타입을 반환하기 때문에 다운 캐스팅을 해야 함 -> 해결
  • 실수로 고양이를 입력했는데, 개를 반환하는 상황이라면 캐스팅 예외가 발생함 -> 해결

제네릭 도입 문제

  • 제네릭에서 타입 매개변수를 사용하면 어떤 타입이든 들어올 수 있음 -> 해결
  • 그리고 어떤 타입이든 수용할 수 있는 Object로 가정하고, Object의 기능만 사용할 수 있음 -> 해결
    • 여기서는 Animal을 상한으로 두어서 Animal의 기능을 사용할 수 있음

정리

  • 제네릭에 타입 매개변수 상한을 사용해서 타입 안전성을 지키면서 상위 타입의 원하는 기능까지 사용할 수 있음
  • 덕분에 코드 재사용과 타입 안전성이라는 두 마리 토끼를 동시에 잡을 수 있음


강의 출처 : 김영한의 실전 자바 - 중급 2편

profile
Backend development

0개의 댓글