본문 바로가기

복습

[Java 복습] 제네릭이 필요한 이유!!

목차

  • 제네릭이 필요한 이유
  • 제네릭 적용
  • 제네릭 용어와 관례
  • 원시 타입
  • 제네릭 활용 예제

 

 

public class IntegerBox {

    private Integer value;

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

    public  Integer get() {
        return value;
    }
}
public class StringBox {

    private String value;

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

    public String get() {
        return value;
    }
}
public class BoxMain1 {

    public static void main(String[] args) {
        IntegerBox integerBox = new IntegerBox();
        integerBox.set(10); // 오토 박싱
        Integer integer = integerBox.get();
        System.out.println("integer = " + integer);

        StringBox stringBox = new StringBox();
        stringBox.set("hello");
        String str = stringBox.get();
        System.out.println("str = " + str);
    }
}

 

 

문제
이후에 Double , Boolean 을 포함한 다양한 타입을 담는 박스가 필요하다면 각각의 타입별로 DoubleBox , 
BooleanBox 와 같이 클래스를 새로 만들어야 한다. 담는 타입이 수십개라면, 수십개의 XxxBox 클래스를 만들어야 한다.

 

이 문제를 어떻게 해결할 수 있을까? => 모두의 부모(Object)로 해결할 수 있지 않을까?

 

 

다형성을 통한 중복 해결 시도

public class ObjectBox {

    private Object value;

    public void set(Object object) {
        this.value = object;
    }
    public Object get() {
        return value;
    }
}
public class BoxMain2 {

    public static void main(String[] args) {

        ObjectBox integerBox = new ObjectBox();
        integerBox.set(10);
        Integer integer = (Integer) integerBox.get(); // Object => Integer 캐스팅
        //Object object = integerBox.get();
        //Integer integer = (Integer) object;

        System.out.println("integer = " + integer);

        ObjectBox stringBox = new ObjectBox();
        stringBox.set("hello");
        String str = (String) stringBox.get(); // Object => String 캐스팅

        System.out.println("str = " + str);

        // 잘못된 타입의 인수 전달시
        integerBox.set("문자100");
        Integer integer2 = (Integer) integerBox.get();
        System.out.println("integer2 = " + integer2); // String => Integer 캐스팅, 형변환 예외
    }
}

 

여기에는 몇 가지 문제가 있다.

 

1. 반환 타입이 맞지 않는 문제

 

Integer = Object 는 성립하지 않는다. 자식은 부모를 담을 수 없다. 

따라서  (Integer) 같이 타입 캐스팅 코드를 넣어서 Object 타입을 Integer 타입으로 직접 다운 캐스팅해야 한다.

매번 이렇게 다운 캐스팅을 하게 되면 정말 귀찮을 것이다!!

 

 

2. 잘못된 타입의 인수 전달 문제

integerBox.set("문자100");

 

개발자의 의도는 integerBox 에는 변수 이름과 같이 숫자 타입이 입력되기를 기대했다.
하지만 set(Object ..) 메서드는 모든 타입의 부모인 Object 를 매개변수로 받기 때문에 세상의 어떤 데이터도

입력받을 수 있다. 따라서 이렇게 문자열을 입력해도 자바 언어 입장에서는 아무런 문제가 되지 않는다.

 

  • BoxMain1 : 각각의 타입별로 IntegerBox , StringBox 와 같은 클래스를 모두 정의
    • 코드 재사용X
    • 타입 안전성O
  • BoxMain2 : ObjectBox 를 사용해서 다형성으로 하나의 클래스만 정의
    • 코드 재사용O
    • 타입 안전성X

 

 

제네릭 적용

제네릭을 사용하면 코드 재사용과 타입 안전성이라는 두 마리 토끼를 한 번에 잡을 수 있다.
제네릭을 사용해서 문제를 해결해보자.

public class GenericBox<T> {

    private T value;

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

    public T get() {
        return value;
    }
}
  • <> 를 사용한 클래스를 제네릭 클래스라 한다. 이 기호( <> )를 보통 다이아몬드라 한다.
  • 제네릭 클래스를 사용할 때는 Integer , String 같은 타입을 미리 결정하지 않는다.
  • 대신에 클래스명 오른쪽에 <T> 와 같이 선언하면 제네릭 클래스가 된다. 여기서 T 를 타입 매개변수라 한다. 
  • 타입 매개변수는 이후에 Integer , String 으로 변할 수 있다.
  • 그리고 클래스 내부에 T 타입이 필요한 곳에 T value 와 같이 타입 매개변수를 적어두면 된다.

 

public class BoxMain3 {

    public static void main(String[] args) {
        GenericBox<Integer> integerBox = new GenericBox<Integer>(); // 생성 시점에 T의 타입 결정
        integerBox.set(10);
        //integerBox.set("문자100"); // Integer 타입만 허용, 컴파일 오류
        Integer integer = integerBox.get(); // Integer 타입 반환(캐스팅 X)
        System.out.println("integer = " + integer);

        GenericBox<String> stringBox = new GenericBox<String>();
        stringBox.set("hello");
        //stringBox.set(100); String 타입만 허용, 컴파일 오류
        String str = stringBox.get();
        System.out.println("str = " + str);

        // 원하는 모든 타입 사용 가능
        GenericBox<Double> doubleBox = new GenericBox<Double>();
        doubleBox.set(10.5);
        Double doubleValue = doubleBox.get();
        System.out.println("doubleValue = " + doubleValue);
        
        // 타입 추론: 생성하는 제네릭 타입 생략 가능
        GenericBox<Integer> integerBox2 = new GenericBox<>();
    }
}

 

 

T에 Integer를 적용한 GenericBox 클래스

public class GenericBox<Integer> {

    private Integer value;

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

    public Integer get() {
        return value;
    }
}

 

참고로 제네릭을 도입한다고 해서 앞서 설명한 GenericBox<String> , GenericBox<Integer> 와 같은 코드가 실제 만들어지는 것은 아니다. 대신에 자바 컴파일러가 우리가 입력한 타입 정보를 기반으로 이런 코드가 있다고 가정하고 컴파일 과정에 타입 정보를 반영한다. 이 과정에서 타입이 맞지 않으면 컴파일 오류가 발생한다. 

 

타입 추론

GenericBox<Integer> integerBox = new GenericBox<Integer>() // 타입 직접 입력
GenericBox<Integer> integerBox2 = new GenericBox<>() // 타입 추론

 

자바가 스스로 타입 정보를 추론해서 개발자가 타입 정보를 생략할 수 있는 것을 타입 추론이라 한다.
참고로 타입 추론이 그냥 되는 것은 아니고, 자바 컴파일러가 타입을 추론할 수 있는 상황에만 가능하다.

쉽게 이야기해서 읽을 수 있는 타입 정보가 주변에 있어야 추론할 수 있다.

 

 

제네릭 용어와 관례

제네릭의 핵심은 사용할 타입을 미리 결정하지 않는다는 점이다. 

클래스 내부에서 사용하는 타입을 클래스를 정의하는 시점에 결정하는 것이 아니라 

실제 사용하는 생성 시점에 타입을 결정하는 것이다.
이것을 쉽게 비유하자면 메서드의 매개변수와 인자의 관계와 비슷하다.

void method2(String param) {
    println(param);
}

void main() {
    method2("hello");
    method2("hi");
}

 

메서드는 실행 시점에 얼마든지 다른 값을 받아서 처리할 수 있다. 따라서 재사용성이 크게 늘어난다

 

 

 

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

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

 

정리하면 다음과 같다.

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

 

용어 정리

  • 제네릭(Generic) 단어
    • 제네릭이라는 단어는 일반적인, 범용적인이라는 영어 단어 뜻이다.
    • 풀어보면 특정 타입에 속한 것이 아니라 일반적으로, 범용적으로 사용할 수 있다는 뜻이다.
  • 제네릭 타입 (Generic Type)
    • 클래스나 인터페이스를 정의할 때 타입 매개변수를 사용하는 것을 말한다.
    • 제네릭 클래스, 제네릭 인터페이스를 모두 합쳐서 제네릭 타입이라 한다.
    • 타입은 클래스, 인터페이스, 기본형( int 등)을 모두 합쳐서 부르는 말이다.
    • 예: class GenericBox<T> { private T t; }
    • 여기에서 GenericBox<T> 를 제네릭 타입이라 한다.
  • 타입 매개변수 (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> {}


타입 인자로 기본형은 사용할 수 없다
제네릭의 타입 인자로 기본형( int , double ..)은 사용할 수 없다. 대신에 래퍼 클래스( Integer , Double )를 사용하면 된다.

 

 

 

 

원시 타입 - raw type

public class RawTypeMain {

    public static void main(String[] args) {
        GenericBox integerBox = new GenericBox();
        //GenericBox<Object> integerBox = new GenericBox<>(); // 권장
        integerBox.set(100);
        Integer result = (Integer) integerBox.get();
        System.out.println("result = " + result);
    }
}

 

제네릭 타입을 사용할 때는 항상 <> 를 사용해서 사용시점에 원하는 타입을 지정해야 한다.
그런데 다음과 같이 <> 을 지정하지 않을 수 있는데, 이런 것을 로 타입(raw type), 또는 원시 타입이라한다.

GenericBox integerBox = new GenericBox();
//GenericBox<Object> integerBox = new GenericBox<>(); // 권장
  • 원시 타입을 사용하면 내부의 타입 매개변수가 Object 로 사용된다고 이해하면 된다.
  • 자바의 제네릭이 자바가 처음 등장할 때 부터 있었던 것이 아니라 자바가 오랜기간 사용된 이후에 등장했기 때문
  • 제네릭이 없던 시절의 과거 코드와의 하위 호환이 필요했다. 
  • 그래서 어쩔 수 없이 이런 원시 타입을 지원한다.
  • 정리하면 원시 타입을 사용하지 않아야 한다.


만약에 Object 타입을 사용해야 한다면 다음과 같이 타입 인자로 Object 를 지정해서 사용하면 된다.

GenericBox<Object> integerBox = new GenericBox<>();

 

 

 

제네릭 활용 예제

 

 

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 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);
    }
}
findDog = Animal{name='멍멍이', size=100}
findCat = Animal{name='냐옹이', size=50}
findAnimal = Animal{name='동물', size=0}

 

Box<Animal> 의 경우 타입 매개변수 T 에 타입 인자 Animal 을 대입하면?

public class Box<Animal> {

    private Animal value;

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

    public Animal get() {
        return value;
    }
}

 

  • 따라서 set(Animal value) 이므로 set() 에 Animal 의 하위 타입인 Dog , Cat 도 전달할 수 있다.
  • 물론 이 경우 꺼낼 때는 Animal 타입으로만 꺼낼 수 있다.

 

public class AnimalMain2 {

    public static void main(String[] args) {

        Animal animal = new Animal("동물", 0);
        Dog dog = new Dog("멍멍이", 100);
        Cat cat = new Cat("냐옹이", 50);


        Box<Animal> animalBox = new Box<>();
        animalBox.set(animal);
        animalBox.set(dog); // Animal = dog;
        animalBox.set(cat); // Animal = cat;

        Animal findAnimal = animalBox.get();
        System.out.println("findAnimal = " + findAnimal);
    }
}
findAnimal = Animal{name='냐옹이', size=50}