Welcome! Everything is fine.

[Java/Study] 김영한의 실전 자바 중급 2편 - 스터디 11회차 본문

Java

[Java/Study] 김영한의 실전 자바 중급 2편 - 스터디 11회차

개발곰발 2025. 1. 7.
728x90

 

인프런 강의 <김영한의 실전 자바 - 중급 2편>을 보고 정리한 내용입니다.

매주 모여 각자 정리한 내용을 기반으로 발표하고 질문 공유하는 스터디입니다.


 

📘제네릭이 필요한 이유

다음과 같이 Integer 타입을 담고, 꺼낼 수 있는 클래스가 있다고 하자. Integer 타입 말고도 Double, Boolean, String 등의 다양한 타입을 담아야 한다면 어떨까? 각 타입에 맞는 새로운 클래스를 계속 만들어야 할 것이다.

public class IntegerBox {

    private  Integer value;

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

    public Integer get() {
        return value;
    }
}

 

만약 다음과 같이 Object 타입을 받는 ObjectBox를 만들면 여러 타입을 받는 것에 대한 문제는 해결할 수 있다. 그러나 몇 가지 문제가 발생할 수 있다.

 

1) 서로 다른 값을 넣어도 반환 타입은 항상 Object 타입이기 때문에 직접 다운 캐스팅 해야한다.

2) 잘못된 타입의 인수를 전달할 경우 꺼낼 때 잘못 캐스팅하여 예외가 발생할 수 있다.

public class ObjectBox {

    private Object value;

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

    public Object get() {
        return value;
    }
}

 

잘못된 타입의 값을 전달하면 값을 꺼낼 때 문제가 발생한다. 아래와 같이 문자를 넣었는데 Integer로 캐스팅한다면 캐스팅을 할 수 없다는 예외가 발생하고 프로그램이 종료될 것이다.

// 잘못된 타입의 인수 전달 시
ObjectBox integerBox = new ObjectBox();
integerBox.set("문자100");
Integer result = (Integer) integerBox.get();
System.out.println("result = " + result);

 

이러한 문제들은 제네릭을 이용해 해결할 수 있다!

📘제네릭(Generic)

제네릭(Generic)은 '일반적인', '범용적인'이라는 의미를 가지고 있다. 특정 타입에 속하지 않고 일반적으로 사용할 수 있다는 뜻이다.제네릭을 이용하면 코드 재사용성과 타입 안정성을 모두 충족시킬 수 있다. 다음과 같이 <>(다이아몬드)를 사용한 클래스를 제네릭 클래스라고 한다.

  • 클래스명 오른쪽에 <T>와 같이 선언한다.
  • T는 타입 매개변수로, 다양한 타입으로 변할 수 있다.
  • 클래스 내부에 T 타입이 필요한 곳에 적어둔다.
public class GenericBox<T> {
    // T : 타입 매개변수
    private T value;

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

    public T get() {
        return value;
    }
}

 

 

이렇게 만든 제네릭 클래스는 다음과 같이 원하는 타입을 넣어 객체를 생성해 사용한다.

GenericBox<Integer> integerBox = new GenericBox<Integer>();
GenericBox<String> stringBox = new GenericBox<String>();
GenericBox<Double> doubleBox = new GenericBox<Double>();

 

참고로 생성하는 제네릭 타입은 생략이 가능하다.

// 타입 추론: 생성하는 제네릭 타입 생략 가능
GenericBox<Integer> integerBox = new GenericBox<>();

 

 따라서 생성 시점에 T의 타입을 결정하면 set()을 할 때나 get()을 할 때도 해당 타입만 허용된다. 따라서 다른 타입을 넣게 되더라도 Object 타입처럼 모두 허용되는 것이 아니라 생성 시점에 결정한 타입만 허용되는 것이어서 컴파일 오류가 발생한다.

public static void main(String[] args) {
    GenericBox<Integer> integerBox = new GenericBox<>();
    integerBox.set(10);
    //integerBox.set("문자열"); // Integer 타입만 허용, 컴피일 오류
    Integer integer = integerBox.get(); // Integer 타입 반환(캐스팅X)
    System.out.println("integer = " + integer);
}
integer = 10

 

더보기

💡 제네릭 관련 용어 정리

  • 제네릭(Generic) : 일반적인, 범용적인이라는 뜻.
  • 제네릭 타입(Generic Type) : 클래스나 인터페이스 정의 시 타입 매개변수를 사용하는 것, 제네릭 클래스와 제네릭 인터페이스를 모두 통틀어 제네릭 타입이라고 함.
  • 타입 매개변수(Type Parameter) : 제네릭 타입이나 메서드에서 사용되는 변수. GenericBox<T> 에서 T를 의미함.
  • 타입 인자(Type Argument) : 제네릭 타입을 사용할 때 제공되는 실제 타입. GenericBox<Integer>에서 Integer를 의미함.

💡 제네릭 명명 관례

일반적으로 대문자를 사용하며, 용도에 맞는 단어의 첫글자를 사용함.

  • E - Element
  • K - Key
  • N - Number
  • T - Type
  • V - Value
  • S,U,V etc. - 2nd, 3rd, 4th types

💡 기타

  • 한 번에 여러 타입매개변수를 선언할 수 있다.
class Data<K, V> {}
  • 타입 인자로 기본형은 사용할 수 없다.

💡 raw type

자바는 제네릭이 없던 시절 과거 코드와의 하위 호환을 위해 로 타입(raw type)을 지원한다. 결론부터 말하자면, 로 타입은 사용하지 않아야 한다. 로 타입은 <>을 지정하지 않는 것을 의미한다. <>을 지정하지 않으면 내부의 타입 매개변수가 Object로 사용된다.

GenericBox integerBox = new GenericBox();

 

제네릭 타입을 사용할 때는 반드시 <>를 사용해서 사용시점에 타입을 지정하도록 하자!

 

📘제네릭 활용

Animal 클래스와  Animal 클래스를 상속받는 Dog, cat 클래스가 있다고 하자. 그리고 해당 클래스 타입을 모두 담을 수 있는 제네릭 클래스를 만든다면 다음과 같이 만들 수 있다.

public class Box<T> {

    private T value;

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

    public T get() {
        return value;
    }
}

 

그렇게 하면 이렇게 각자 다른 클래스 타입을 타입 인자로 넣고 사용할 수 있다.

public class AnimalMain1 {

    public static void main(String[] args) {
        Animal animal = new Animal("동물", 0);
        Dog dog = new Dog("멍멍이", 100);
        Cat cat = new Cat("냐옹이", 50);
        
        Box<Dog> dogBox = new Box<>();
        dogBox.set(dog);
        Dog findDog = dogBox.get();
        System.out.println("findDog = " + findDog);

        Box<Cat> catBox = new Box<>();
        catBox.set(cat);
        Cat findCat = catBox.get();
        System.out.println("findCat = " + findCat);

        Box<Animal> animalBox = new Box<>();
        animalBox.set(animal);
        Animal findAnimal = animalBox.get();
        System.out.println("findAnimal = " + findAnimal);

    }
}

📘타입 매개변수 제한

extends 키워드를 이용해 타입 매개변수를 특정 타입으로 제한할 수 있다.

 

다음과 같이 <T extends Animal>이라고 해두면, Animal과 그 자식들만 받을 수 있도록 제한을 둘 수 있다. 즉 T의 상한이 Animal이 된다. 기존에 그냥 <T>만 해두었을 때는 Animal 타입뿐만 아니라 전혀 상관없는 Integer나 String 타입도 들어올 수 있었다. 하지만 이렇게 타입 매개변수를 제한하면 이런 문제를 해결할 수 있다.

public class AnimalHospitalV3<T extends Animal> {
    // Animal이나 Animal의 자식들만 올 수 있음.(Object도 못옴)
    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 bigger(T target) {
        return animal.getSize() > target.getSize() ? animal : target;
    }
}

📘와일드카드

와일드카드란 제네릭을 쉽게 쓸 수 있도록 도와주는 도구로, 타입 인자가 정해진 제네릭 타입을 전달 받아서 활용할 때 사용한다. 다음과 같이 <> 안에 ? 를 넣어 사용한다. 

static void printWildcardV1(Box<?> box) {
    System.out.println("? = " + box.get());
}
  • ?의 뜻은 모든 타입을 다 받을 수 있다는 뜻이다.
  • 매개변수로 제네릭 타입을 받을 수 있는 일반적인 메서드이다.
  • 더 단순하기 때문에 사용이 권장된다.

와일드카드에도 다음과 같이 extends 키워드를 이용해 상한 제한을 둘 수 있다. Box<? extends Animal>과 같이  쓴다면, Animal과 그 하위 타입만 입력받을 수 있다.

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

 

또한 와일드카드는 super 키워드를 이용해 하한도 지정할 수 있다. Box<? super Animal> 과 같이  쓴다면, Animal과 그 상위 타입만 입력받을 수 있다. 즉, 아래 예시에서는 Animal과 Object 타입만 허용된다.

static void writeBox(Box<? super Animal> box) {
    box.set(new Dog("망망이", 100));
}

📘타입 이레이저

타입 이레이저란 자바 컴파일 단계에서만 제네릭이 사용되고, 컴파일 이후에는 제네릭 정보가 삭제되는 것을 말한다.

 

예를 들어 다음과 같이 제네릭 타입을 선언한 후에 Integer 타입 인자를 전달했다면,

public class Box<T> {

    private T value;

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

    public T get() {
        return value;
    }
}

 

자바 컴파일러는 컴파일 시점에 다음과 같이 이해한다.

public class Box<Integer> {

    private Integer value;

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

    public Integer get() {
        return value;
    }
}

 

 

그리고나서 컴파일이 끝나면 제네릭 관련 정보를 모두 삭제해서 .class 파일에는 다음과 같은 정보가 생성된다.

public class Box {

    private Object value;

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

    public Object get() {
        return value;
    }
}

 

값을 꺼낼 때도 자바 컴파일러가 Integer로 캐스팅하는 코드를 추가해주기 때문에 문제가 발생하지 않는다.

void main() {
    GenericBox box = new GenericBox();
    box.set(10);
    Integer result = (Integer) box.get(); // 컴파일러가 캐스팅 추가
}

 

하지만, 이런 타입 이레이저 방식으로 인해 다음과 같은 코드를 작성할 수 없다. 여기서 T는 모두 런타임에 Object로 바뀌기 때문에, instanceof는 항상 Object와 비교하게 되어서 소용이 없어진다. new T() 또한 항상 new Object가 된다.

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

 

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