제네릭 (Generic)이란?

클래스 내부에서 지정하는 것이 아닌 외부에서 사용자에 의해서 지정되는 것입니다. 즉 데이터의 타입(data type)을 일반화(generalize)하는 것입니다. 글만으로는 이해가 잘 가지 않아 코드로 살펴보겠습니다.

class GenericsBox<T>{ // 제네릭을 사용하는 클래스
    private T type; // 타입을 외부에서 주입받습니다.

    public GenericsBox(T type) {
        this.type = type;
    }

    @Override
    public String toString() {
        return "this is "+type.getClass()+" class";
    }
}

public class Main {
    public static void main(String[] args) throws IOException {
        GenericsBox<Integer> integerBox = new GenericsBox<>(1); // Integer 타입이라고 명시합니다.
        System.out.println(integerBox.toString());

        GenericsBox<String> stringBox = new GenericsBox<>("string"); // String 타입이라고 명시합니다.
        System.out.println(stringBox);
    }
}

이처럼 데이터의 내부에서 명시하는 것이 아닌 외부에서 지정할 수 있습니다.


제네릭으로 얻는 이점

1. 타입만 다를 경우 코드의 재사용이 가능합니다.

  • 같은 기능이지만 Integer를 사용하는 class와 String을 사용하는 class를 두 개 만들어야 하지만 제네릭을 이용하면 한 개의 class로 해결 가능합니다.

2. 컴파일 단계에서 타입 체크를 할 수 있습니다. (코드로 보겠습니다.)

class GenericsBox<T>{
    private T type;

    public GenericsBox(T type) {
        this.type = type;
    }

    public void setType(T type) { // type의 값을 변경
        this.type = type;
    }
}

public class Main {
    public static void main(String[] args) throws IOException {
        GenericsBox<Integer> integerBox = new GenericsBox<>(1);
        integerBox.setType("String"); // 타입으로 인한 컴파일 오류
        
    }
}

 

 


제네릭 사용하기

제네릭의 <> 안에 들어가는 것은 어떠한 것을 명시해도 무방하지만 일반적으로 사용되는 규칙(Convention)이 있습니다. 

 명시   쓰임새
<T> Type
<K> Key
<E> Element
<V> Value
<N> Number

 

1. 기본 문법

  • class 혹은 interface {이름}<T> 을 붙여 사용 가능합니다. 
class GenericsBox<T>{  // Class 이름 뒤에 <>를 명시합니다.
    private T type; // 제네릭 타입의 변수를 생성합니다.

    public GenericsBox(T type) {
        this.type = type;
    }

    public void setType(T type) {
        this.type = type;
    }
}
  • 만든 제네릭을 사용자가 외부에서 지정하여 사용가능합니다. 
public class Main {
    public static void main(String[] args) throws IOException {
        GenericsBox<Integer> integerBox = new GenericsBox<>(1);
        //GenericsBox<Integer> integerBox = new GenericsBox<Integer>(1); 위와 같은 코드 하지만 뒤의 생성 부분은 제네릭 생략가능
        integerBox.setType(10);
        System.out.println(integerBox.toString());

    }
}
  • 제네릭 여러개 사용하기 (T, E 두 개의 제네릭을 지정 가능합니다.)
class GenericsBox<T, E>{
    private T type;
    private E type2;

    public GenericsBox(T type, E type2 ) {
        this.type = type;
        this.type2= type2;
    }

    public void setType(T type) {
        this.type = type;
    }

    @Override
    public String toString() {
        return "First is "+type+"\n"+"Second is "+type2;
    }
}

2. 타입 인자 제한하기(extends)

어떠한 클래스에는 특성과 용도가 있습니다. 용도와 특성에 맞게 제한을 둘 필요가 있습니다.

ex) <T extends Number>이라면 Number를 상속받은 클래스나 Number만이 Generic으로 명시될 수 있습니다. 

코드로 살펴보겠습니다.

  • Fruit 클래스는 상위 클래스이고, 상속받는 Apple과 Banana가 있습니다. 
class Fruit{  // 과일 클래스
    String description;

    public Fruit(String description) {
        this.description = description;
    }
}

class Apple extends Fruit{  //사과는 과일이므로 클래스를 상속받습니다.
    int price;

    public Apple(String description, int price) {
        super(description);
        this.price = price;
    }
}

class Banana extends Fruit{ // 바나나는 과일이므로 클래스를 상속받습니다.
    int price;
    public Banana(String description, int price) {
        super(description);
        this.price = price;
    }
}
  • 과일을 담는 제네릭 형태의 ShoppingBag 클래스가 있습니다. 
class ShoppingBag<T extends Fruit>{ // 과일을 담는다. 
    T products;

    public ShoppingBag(T products) {
        this.products = products;
    }
}
  • ShoppingBag 클래스를 생성합니다. Integer는 Fruit 클래스도 아니고, 상속받지 않아 컴파일 오류가 뜹니다. 
public class Main {
    public static void main(String[] args) throws IOException {
        // ShoppingBag<Integer> integerShoppingBag = new ShoppingBag<Integer>(); Fruit를 상속받지도 않고 Fruit가 아니므로 오류가 뜹니다.
        ShoppingBag<Fruit> fruitShoppingBag = new ShoppingBag<>(new Fruit("전체 과일")); // Fruit이므로 오류가 없습니다. 
        ShoppingBag<Banana> bananaShoppingBag = new ShoppingBag<>(new Banana("바나나",1000)); // 상속받아 가능합니다.
        ShoppingBag<Apple> appleShoppingBag = new ShoppingBag<>(new Apple("사과",2000)); //상속받아 가능합니다.
    }
}

3. 와일드카드

아래의 코드 문제점을 알아보겠습니다. 

class GenericBox<T>{
    T data;

    public GenericBox(T data) {
        this.data = data;
    }

    public void setData(GenericBox<Object> changeBox){
        this.data=(T)changeBox.getData();
    }

    public T getData(){
        return data;
    }
}
public class Main {
    public static void main(String[] args) throws IOException {
        GenericBox<String> box = new GenericBox<>("hi");
        box.setData(new GenericBox<String>("a")); //오류 발생
    }
}

모든 클래스들은 Object 클래스를 상속받습니다. 따라서 String extend Object가 돼있다고 볼 수 있습니다.

setData()는 오류 없이 실행돼야 하지만 컴파일 오류가 뜹니다.

 

이유는 Object와 String이 상속관계를 가지는 것이지 GenericBox <Object>와 GenericBox <String>가 상속 관계를 가지는 것이 아니기 때문입니다.

 

이를 해결하기 위해 와일드카드가 등장합니다. 아래와 같이 변경합니다.

class GenericBox<T>{
    T data;

    public GenericBox(T data) {
        this.data = data;
    }

    public void setData(GenericBox<?> changeBox){ // <Object> -> <?> 변경
        this.data=(T)changeBox.getData();
    }

    public T getData(){
        return data;
    }
}
public class Main {
    public static void main(String[] args) throws IOException {
        GenericBox<String> box = new GenericBox<>("hi");
        box.setData(new GenericBox<String>("bye"));
    }
}

문제없이 실행되는 것을 볼 수 있습니다.

4. 와일드카드 상한, 하한 

  • 상한 예시: <?> extends Number  

상한은? 가 Number이거나, Number을 상속받았다면 데이터 타입으로 들어올 수 있습니다. 

  • 하한 예시: <?> super Number

하한은? 가 Number, Object만 가능합니다. 즉 Number 또는 Number가 상속하는 클래스여야 합니다. 

  • 상한, 하한을 사용하는 이유 코드로 보겠습니다.
class GenericBox<T>{
    T data;

    public GenericBox(T data) {
        this.data = data;
    }

    public T get(){
        return data;
    }
}
class Handler{ //GenericBox를 다루는 객체입니다. 
    public static void getGenericBox(GenericBox<? extends Integer> box){
        System.out.println(box.get());
    }
    public static void setGenericBox(GenericBox<? extends String> box){
        System.out.println(box.get());
    }
}
public class Main {
    public static void main(String[] args) throws IOException {

        GenericBox<Integer> box = new GenericBox<>(10);
        Handler.getGenericBox(box);
        // Handler.setGenericBox(box); 오류 발생 
    }
}

Main에서 만든 box는 값을 바꾸지 못하고 불러오기만 하는 기능을 설계했습니다.

Handler.getGenericBox()를 이용해서 데이터를 불러올 수 있습니다.

하지만 Handler.setGenericBox()의 인자를 <? extends String>으로 함으로써 기능을 사용하지 못하도록 막았습니다. 

따라서 값을 불러올 수만 있고, 변경은 못합니다. 

이처럼 와일드카드를 이용해서 기능을 제한할 수가 있습니다. 

 

 

지금까지 JAVA 제네릭(Generic)에 대해서 알아봤습니다.

 

물론 제네릭을 이용하면 가독성 높고 재활용이 가능한 코드를 만들 수 있습니다. 하지만 계층구조가 복잡하다면, 목적과 다르게 작동할 수 있습니다. 

 

따라서 자신이 만든 아키텍처에서 어떠한 객체에 어떻게 적용할 것인지 알고 사용해야 할 것 같습니다.  

 

(잘못된 부분에 대한 지적 환영입니다. 감사합니다.) 

 

'JAVA' 카테고리의 다른 글

JAVA Enum  (0) 2022.02.24
JAVA Stream  (0) 2022.02.23
JAVA Optional<T>  (0) 2022.02.23
JAVA 람다(lambda)  (0) 2022.02.21
JAVA Comparable And Comparator  (0) 2022.02.21