JAVA

[JAVA] 제네릭 (Generic)

경딩 2024. 11. 8. 19:53

처음 제네릭에 관해 책을 읽었을 때 잘 이해가 가지 않아 해당 내용을 정리하게 되었다.

단계별로 천천히 알아보자

 

 

제네릭이란?


제레릭은 다양한 타입의 객체들을 다루는 메서드나 컬렉션 클래스에 컴파일 시 타입 체크(compile - type check)를 해주는 기능이다. 객체의 타입을 컴파일 시 체크하기 때문에 객체의 타입 안정성을 높이고 형변환의 번거로움이 줄어든다.

 

타입 안정성을 높힌다? 무슨 의미일까?

package generic.ex1;

public class IntegerBox {
    private Integer value;

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

    public Integer getValue() {
        return value;
    }
}

 단순히 숫자를 꺼내고 보관하는 클래스

package generic.ex1;

public class StringBox {
    private String value;

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

    public String getValue() {
        return value;
    }
}


 단순히 String 를 꺼내고 보관하는 클래스

package generic.ex1;

public class BoxMain1 {

    public static void main(String[] args) {
        IntegerBox integerBox = new IntegerBox();
        integerBox.setValue(10);  // 오토박싱에 의해 int 가 Integer 로 자동형 변환됨
        Integer value1 = integerBox.getValue();
        System.out.println("value1 = " + value1);

        StringBox stringBox = new StringBox();
        stringBox.setValue("제네릭이 무엇일까?");
        String value = stringBox.getValue();
        System.out.println("value = " + value);
    }
}

 

IntegerBox , StringBox 이외에도 DoubleBox, Boolean 박스와 같은 클래스가 필요하다면 어떻게 할까? 

 

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


Object는 모든 타입의 부모이므로 다형성 (다형적 참조)를 사용하면 이 문제를 간단히 해결할 수 있을 것이다.

package generic.ex1;

public class ObjectBox {
    Object object;

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

    public Object getObject() {
        return object;
    }
}
package generic.ex1;

public class BoxMain2 {
    public static void main(String[] args) {
        ObjectBox integerBox = new ObjectBox();
        integerBox.setObject(10);
        Integer object = (Integer)integerBox.getObject(); // Object -> Integer 캐스팅
        System.out.println("object = " + object);


        ObjectBox stringObject = new ObjectBox();
        stringObject.setObject("제네릭이 무엇일까?");
        String str = (String) stringObject.getObject();  // Object -> String 캐스팅
        System.out.println("str = " + str );

        stringObject.setObject(11); // string 으로 넣어야 할 곳을 int 로 잘못된 타입의 인수 전달
        String result1 = (String)stringObject.getObject(); // Integer -> String 캐스팅 예외
        System.out.println("result1 = " + result1);
     

    }
}

 

실행 결과

잘 동작하지만 형변환시 문제가 발생하였다.

 

반환 타입이 맞지 않는 문제

integerBox 에 숫자 10을 보관 후 get 메서드를 호출하면 반환타입은 Object이다.

Integer = Object는 성립하지 않는다. 자식을 부모를 담을 수 없다. 따라서 다음과 같이 (Integer)  타입캐스팅 코드를 넣어서 Object 타입을 Integer 타입으로 직접 다운 캐스팅 해줘야 한다.

Integer integer = (Integer) integerBox.get() //1
Integer integer = (Integer) (Object)value //2
Integer integer = (Integer) value // 3

 

StringBox 도 Object로 반환되므로 String으로 다운 캐스팅 해야 한다.

stringObject.setObject(11); 와 같이 stringObject에 실수로 11 값을 넣었다. 왜냐하면 메서드는 모든 타입의 부모인 Object를 매개변수로 받기 때문이다.  int 값을 넣고 get 메서드 호출 시 String으로 형변환하면 무슨 일이 발생할까?

 

int를 String으로 바꾸니 ClassCastException  즉 캐스팅할 수 없다는 에러가 발생하면서 프로그램은 종료된다.

 

ObjectBox라는 다형성을 활용하여 코드의 중복을 제거하고, 기존 코드를 재사용할 수 있게 되었지만 입력할 때 어떠한 값이든 들어갈 수 있어 타입 안정성 문제가 발생한다. 

 

BoxMain1 : 각각의 타입별로 IntegerBox, StringBox와 같은 클래스 정의

코드 재사용성 X , 타입의 안정성 O

BoxMain2 :  ObjectBox를 사용해서 다형성으로 하나의 클래스만 정의

코드 재사용성 O, 타입의 안정성 X

 

 

제네릭 적용


<> : 다이아몬드라 불림

package generic.ex1;

public class GenericBox <T>{
    private T value;
    public void setValue(T value){
        this.value = value;
    }

    public T getValue(){
        return value;
    }
}

 

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

public class BoxMain3 {
    public static void main(String[] args) {
        GenericBox<Integer> integerBox = new GenericBox<>();
        integerBox.setValue(10);
        Integer value = integerBox.getValue();
        System.out.println("value = " + value);

        GenericBox<String> stringBox = new GenericBox<>();
        stringBox.setValue("제네릭 박스");
        String value1 = stringBox.getValue();
        System.out.println("value1 = " + value1);

        GenericBox<Double> doubleBox = new GenericBox<>();
        doubleBox.setValue(11.2);
        Double value2 = doubleBox.getValue();
        System.out.println("value2 = " + value2);

    }

}

 

생성시점에 원하는 타입 지정

제네릭 클래스는 다음과 같이 정의한다.

class GenericBox<T>

 

제네릭 클래스는 생성하는 시점에 <> 사이에 원하는 타입을 지정한다.

new GenericBox<Integer>()

 

이렇게 하면 앞서 정의한 GenericBox의 T 가 다음과 같이 지정한 타입으로 변한 다음 생성된다.

public class GenericBox <Integer>{
    private Integer value;
    public void setValue(Integer value){
        this.value = value;
    }

    public Integer getValue(){
        return value;
    }
}

T가 모두 Integer로 변환되어 Integer 타입을 입력하고 조회할 수 있다.

set(Integer value) 이므로 이 메서드에서는 Integer 숫자만 담을 수 있다.

Integer 타입만 허용되고 나머지 타입을 넣으면 컴파일 오류가 발생한다. 

 

get()의 경우에도 Integer를 반환하기 때문에 타입캐스팅 없이 숫자로 조회할 수 있다.

 Integer integer = integerBox.get(); // Integer 타입 반환 (캐스팅 X)

 

제네릭 클래스를 사용하면  객체 생성 시점에 원하는 타입을 마음껏 지정할 수 있다.

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

이 과정에서 타입이 맞지 않으면 컴파일 오류가 발생한다.

 

 

타입 추론

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

첫 번째 줄에 코드를 보면 변수 선열과 객체 할당 시 Integer 가 두 번 나온다. 자바는 왼쪽에 있는 변수를 선언할 때 Integer를 보고 오른쪽에 있는 객체를 생성할 때 필요한 타입 정보를 얻을 수  있다. 따라서 두 번째 줄의 오른쪽 코드 new GenericBox <>()와 같이 타입 정보를 생략할 수 있다. 이렇게 자바가 스스로 타입 정보를 추론해서 개발자가 타입 정보를 생략할 수 있는 것을 타입 추론이라 한다.

 

정리 

제네릭을 사용하면 코드의 재사용성과 타입의 안정성을 높일 수 있다.

제네릭 용어와 관례


제네릭의 핵심은 사용할 타입을 미리 결정하지 않고 클래스 내부에서 사용하는 타입을 클래스를 정의하는 시점에 결정하는 것이 아니라 실제 사용하는 생성시점에 타입을 결정하는 것이다.

 

메서드에 필요한 값을 메서드 정의 시점에 미리 결정

메서드에 필요한 값을 이렇게 메서드 정의 시점에 미리 결정하게 되면, 이 메서드는 오직 hello라는 값만 출력할 수 있다.

코드의 재사용이 떨어진다.

 void method1() {
 println("hello");
 }

메서드에 필요한 값을 인자를 통해 매개변수로 전달해서 결정

 void method2(String param) {
 println(param);
 }
 void main() {
 method2("hello");
 method2("hi");
 }

 

메서드에 필요한 값을 메서드를 정의하는 시점에 미리 결정하는 것이 아니라. 메서드를 실제 사용하는 시점으로 미룰 수 있다.

메서드에 매개변수를 지정하고, 메서드를 이용할 때 원하는 값은 인자("hello" , "hi")로 전달하면 된다.

매개변수를 정의하고 , 실행 시점에 인자를 통해 원하는 값을 매개변수에 전달하였다.

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

 

제넥시의 타입 매개변수와 타입 인자

제네릭도 앞서 설명한 메서드의 매개변수와 인자의 관계와 비슷하게 작동한다.

제네릭 클래스를 정의할 때 내부에서 사용할 타입을 미리 결정하는 것이 아니라 클래스를 생성하는 시점에 내부에서 사용할 타입을 결정하는 것이다. 차이가 있다면 메서드의 매개변수는 사용할 값에 대한 결정을 나중으로 미루는 것이고, 제네릭의 타입 매개변수는 사용할 타입에 대한 결정을 나중으로 미루는 것이다.

 

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

제네릭에서 사용하는 용어도 매개변수, 인자 용어를 그대로 가져다 사용한다. 다만 값이 아니라 타입을 결정하는  것이기 때문에 앞에 타입을 붙인다.

 

타입 매개변수 : GenericBox(T)에서 T

타입 인자:

  • GenericBox <Integer>에서 Integer
  • GenericBox <String>에서 String

제네릭 타입의 타입매개변수 <T>에 타입인자를 전달해서 제네릭의 사용 타입을 결정한다.

GenericBox(T) 

  • Integer -> GenericBox <Integer> 
  • String -> GenericBox <String>

용어 정리

  • 제네릭(Generic) 단어
    • 제네릭이란 단어는 일반적인, 범용적인 영어 단어 뜻이다.
    • 풀어보면 특정 타입에 속한 것이 아니라 일반적으로, 범용적으로 사용할 수 있다는 뜻이다.
  • 제네릭 타입
    • 클래스나 인터페이스를 정의할 때 매개변수를 사용하는 것을 말한다.
    • 제네릭 클래스, 제네릭 인터페이스를 모두 합쳐서 제네릭 타입이라 한다.
      • 타입은 클래스, 인터페이스, 기본형 (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..) 은 사용할 수 없고 대신 레퍼 클래스를 사용하면 된다.

 

raw Type

public class RawTypeMain {
    public static void main(String[] args) {
        GenericBox integerBox = new GenericBox();
        integerBox.setValue(10);
        Integer value = (Integer) integerBox.getValue();
        System.out.println("value = " + value);
    }
}

 

제네릭 타입을 사용할 떄는 항상 <> 를 사용해서 사용 시점에 원하는 타입을 지정해야한다.

그런데 다음과 같이 <> 을 지정하지 않을 수 있는데 이것을 raw type  또는 원시 타입이라 한다.

 GenericBox integerBox = new GenericBox();

 

원시 타입을 사용하면 내부의 타입 매개변수가 Object 로 사용된다고 이해하면 된다.

 

제네릭 타입을 사용할 때 항상 <>  를 사용해서 사용시점에 타입을 지정해야 한다. 그런데 왜 이런 raw 타입을 지원할까?

자바의 제네릭이 처음 자바가 등장할 떄 부터 있던것이 아니라 오랜기간 자바가 사용된 후  등장했디 때문에 제네릭이 없던 시절의 과거 코드와의 하위 호한이 필요했다. 그래서 어쩔 수 없이 이런 로 타입을 지원한다.

 

정리하면 raw 타입을 사용하지 않아야 한다.

만약에  Object  타입을 사용해야 한다면 다음과 같이 타입 인자로

GenericBox<Object> integerBox = new GenericBox<>(); 를 지정해서 사용하면 된다.

 

더보기

제니릭 활용 예제

 

 

 

package generic.animal;

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

 

이름(name) , 크기(size) 정보를 가지는 부모클래스이다.

package generic.animal;

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

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

 

 

package generic.animal;

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

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

 

Animal 을 상속받는다.

부모클래스에 정의된 생성자가 있기 때문에 맞추어 super(name, size) 를 호출한다.

 

package generic.ex2;

public class Box<T> {
    private T value;

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

    public T getValue(){
        return value;
    }
}

 객체를 보관할 수 있는 제네릭 클래스이다.

package generic.ex2;

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

public class AnimalMain1 {
    public static void main(String[] args) {
        Animal animal = new Animal("동물", 0);
        Dog dog = new Dog("멍멍이", 20);
        Cat cat = new Cat("냐옹이", 5);

        Box<Dog> dogBox = new Box<>();
        dogBox.setValue(dog);
        Dog value = dogBox.getValue();
        System.out.println(value);

        Box<Cat> catBox = new Box<>();
        catBox.setValue(cat);
        Cat cat1 = catBox.getValue();
        System.out.println("cat1 = " + cat1);

        Box<Animal> animalBox = new Box<>();
        animalBox.setValue(animal);

        Animal value1 = animalBox.getValue();
        cat1.sound();
        System.out.println("value1 = " + value1);

    }
}

 

Box 제네릭 클래스에 각각의 타입에 맞는 동물을 보관하고 꺼낸다.

Box<Dog> dogBox : Dog 타입을 보관할 수 있다

Box<Cat> catBox : Cat 타입을 보관할 수 있다.

Box<Animal> animaBox : Animal 타입을 보관할 수 있다.

 

여기서 Box<Animal> 의 경우 매개변수 T 에 타입 인자 Animal 을 대입하면 다음 코드와 같다.

public class Box<Animal> {
    private Animal value;

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

    public Animal getValue(){
        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("멍멍이", 20);
        Cat cat = new Cat("냐옹이", 5);

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

        Animal value = animalBox.getValue();
        System.out.println("value = " + value);

    }
}

 

 

 

참고 자료 : 

자바의 신2, 자바의 정석

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