제네릭(Generic)

de_sj_awa·2021년 5월 2일
0
post-custom-banner

1. 제네릭(Generic)이란?

인스턴스 변수의 타입을 확인하기 위해서는 instanceof라는 예약어를 사용하여 타입을 점검하면 된다.

그런데, 이렇게 타입을 일일히 점검해야 한다는 단점을 보완하기 위해서 Java 5부터 새롭게 추가된 제네릭(Generic)이라는 것이 있다.

제네릭은 타입 형 변환에서 발생할 수 있는 문제점을 "사전"에 점검하기 위해서 만들어 졌다. 여기서 "사전"이라고 하는 것은 실행시에 예외가 발생하는 것을 처리하는 것이 아니라, 컴파일할 때 점검할 수 있도록 한 것을 말한다.

예제 코드

package d.generic;

import java.io.Serializable;

public class CastingDTO implements Serializable{
    private Object object;
    public void setObject(Object object){
        this.object = object;
    }
    public Object getObject{
    	return object;
    }
}    

이렇게 되어 있으면 타입이 Object이므로 어떤 타입이든 사용할 수 있다. 이 클래스를 제네릭으로 선언하면 다음과 같다.

package d.generic;

import java.io.Serializable;

public class CastingGenericDTO<T> implements Serializable{
    private T object;
    public void setObject(T obj){
        this.object = obj;
    }
    public T getObject{
    	return object;
    }
}

별로 달라진 것은 없다. 단지, 클래스 선언문에 꺽쇠가 열리고 닫힌 것(< 와 >)을 볼 수 잇다. 그 안에는 그냥 알파벳 T가 적혀져 있을 뿐이다. 그리고, 또 앞의 소스에서는 Object라는 타입을 선언하였지만, 이번에 제네릭 소스를 보면 타입 부분이 전부 T로 바뀐 것을 알 수 있다.

여기에 있는 T는 아무런 이름이나 지정해도 컴파일하는 데 전혀 상관이 없다.

이처럼 꺽쇠 안에는 현재 존재하는 클래스를 사용해도 되고, 존재하지 않는 것을 사용해도 된다. 단, 되도록이면 클래스 이름의 명명 규칙과 동일하게 작성하는 것이 좋다. 꺽쇠 안에 선언한 그 이름은 클래스 안에서 하나의 타입처럼 사용하면 된다. 꺽쇠 안에 있는 그 이름은 가상의 타입 이름이라고 생각하면 된다.

그렇다면, 이렇게 선언한 클래스를 어떻게 사용하면 될까?

예제 코드

package d.generic;

public class GenericSample {
    public static void main(String[] args) {
        GenericSample sample = new GenericSample();
        sample.checkGenericDTO();
    }
    
    public void checkGenericDTO(){
        CastingGenericDTO<String> dto1 = new CastingGenericDTO<String>();
        dto1.setObject(new String());
        CastingGenericDTO<StringBuffer> dto2 = new CastingGenericDTO<StringBuffer>();
        dto2.setObject(new StringBuffer());
        CastingGenericDTO<StringBuilder> dto3 = new CastingGenericDTO<StringBuilder>();
        dto3.setObject(new StringBuffer());

그냥 언뜻 보기에는 별 차이가 없다. 오히려 객체를 선언할 때 꺽쇠 안에 각 타입을 명시해 줘서 귀찮게 된 것 만 같을 수도 있다. 하지만, 이 dto1 ~ dto3까지 객체의 getObject() 메소드를 사용하여 객체를 가져올 때는 다음과 같이 간단해진다.

String temp1 = dto1.getObject();
StringBuffer temp2 = dto2.getObject();
StringBuilder temp3 = dto3.getObject();

소스를 유심히 살펴보면 형 변환을 할 필요가 없어진 것을 볼 수 있다. 왜냐하면 해당 객체에 선언되어 있는 dto1 ~ dto3의 제네릭 타입은 각각 String, StringBuffer, StringBuilder이기 때문에 만약 잘못된 타입으로 치환하면 컴파일 자체가 안 된다. 따라서, "실행시"에 다른 타입으로 잘못 형 변환하여 예외가 발생하는 일은 없다. 이와 같이 명시적으로 타입을 지정할 때 사용하는 것이 제네릭이라는 것이다.

2. 제네릭 타입의 이름 정하기

제네릭 타입을 선언할 때에는 클래스 선언 시 꺽쇠 안에 어떤 단어가 들어가더라도 상관이 없다. 그런데, 자바에서 정의한 기본 규칙은 있다.

  • E : 요소(Element, 자바 컬렉션(Collection)에서 주로 사용됨)
  • K : 키
  • N : 숫자
  • T : 타입
  • V : 값
  • S, U, V : 두 번째, 세 번째, 네 번째에 선언된 타입

3. 제네릭 안의 ?

제네릭을 사용할 때 <> 안에 들어가는 타입은 기본적으로 어떤 타입이어도 상관 없다.

예제 코드

package d.generic;

public class WildcardGeneric<W> {
    W wildcard;
    public void setWildCard(W wildcard) {
        this.wildcard = wildcard;
    }
    public W getWildcard() {
        return wildcard;
    }
}

이 클래스를 사용하는 클래스는 다음과 같다.

package d.generic;

public class WildcardSample {

    public static void main(String[] args) {
        WildcardSample sample = new WildcardSample();
        sample.callWildcardMethod();
    }
    
    public void callWildcardMethod() {
        WildcardGeneric<String> wildcard = new WildcardGeneric<String>();
        wildcard.setWildcard("A");
        wildcardStringMethod(wildcard);
    }
    
    public void wildcardStringMethod(WildcardGeneric<String c>{
        String value = c.getWildcard();
        System.out.println(value);
    }
}

wildcardStringMethod()를 보면, 이 메소드의 매개 변수는 반드시 String을 사용하는 WildcardGeneric 객체만 받을 수 있다. 만약 다른 타입으로 선언된 WildcardGeneric 객체를 받으려면 어떻게 해야 할까? 예를 들면 WildcardGeneric<Integer>와 같이 선언된 객체를 받는 경우를 이야기 하는 것이다.

이 경우에는 지금까지 배운 방법으로는 답이 없다. 왜냐하면 제네릭한 클래스의 타입만 바꾼다고 Overriding이 불가능하기 때문이다. 그래서, 이런 경우에는 다음과 같이 선언하는 것이 가능하다.

public void wildcardStringMethod(WildcardGeneric<?> c) {
    Object value = c.getWildcard();
    System.out.println(value);
}

이렇게 String 대신에 ?를 적어두면 어떤 타입이 제네릭 타입이 되더라도 상관 없다. 하지만, 메소드 내부에서는 해당 타입을 정확히 모르기 때문에 앞서 사용한 것처럼 String으로 값을 받을 수는 없고, Object로 처리해야 한다. 여기서 ?로 명시한 타입을 영어로는 wildcard 타입이라고 부른다.

만약 넘어오는 타입이 두세 가지로 정해져 있다면, 다음과 같이 메소드 내에서 instanceof예약어를 사용하여 해당 타입을 사용하면 된다.

public void wildcardStringMethod(WildcardGeneric<?> c) {
    Object value = c.getWildcard();
    if(value instance String) {
        System.out.println(value);
    }
}

그리고, wildcard는 메소드의 매개 변수로만 사용하는 것이 좋다. wildcardStringMethod()를 호출한 callWildcardMethod()에서 만약 다음과 같이 사용한다면,

public void callWildcardMethod() {
    WildcardGeneric<?> wildcard = new WildcardGeneric<String>();
    wildcard.setWildcard("A");
    wildcardStringMethod(wildcard);
}

이 코드는 다음과 같은 에러 메시지를 뿌리며 정상적인 컴파일이 되지 않는다.

java: incompatible types: lang.WildcardGeneric<capture#2 of ?> cannot be converted to lang.WildcardGeneric<java.lang.String>

즉, 알 수 없는 타입에 String을 지정할 수 없다는 말이다. 다시 말해, 어떤 객체를 wildcard로 선언하고, 그 객체의 값을 가져올 수는 있지만, 와일드 카드로 객체를 선언했을 대에는 이 예제와 같이 특정 타입으로 값을 지정하는 것은 "불가능"하다.

4. 제네릭 선언에 사용하는 타입의 범위도 지정할 수 있다.

제네릭을 사용할 때 <> 안에는 어떤 타입이라도 상관 없다고 했지만, wildcard로 사용하는 타입을 제한할 수는 있다. 먼저 방법부터 알려주면 "?" 대신 "? extends 타입"으로 선택하는 것이다.

예제 코드 :

package d.generic;

public class Car {
    protected String name;
    public Car(String name) {
        this.name = name;
    }
    public String toString() {
        return "Car name="+name;
    }
}
package d.generic;

public class Bus extends Car {
    public Bus(String name) {
        super(name);
    }
    public String toString() {
        return "Bus name="+name;
    }
}
package d.generic;

public class CarWildcardSample{
    public static void main(String[] args) {
        CarWildcardSample sample = new CarWildcardSample();
        sample.callBoundedWildcardMethod();
    }
    
    public void callBoundedWildcardMethod() {
        WildcardGeneric<Car> wildcard = new WildcardGeneric<Car>();
        wildcard.setWildcard(new Car("Mustang"));
        boundedWildcardMethod(wildcard);
    }
    
    public void boundedWildcardMethod(WildcardGeneric<? extends Car> c) {
        Car value = c.getWildcard();
        System.out.println(value);
    }
}

앞서 우리가 사용했던 "?"라는 wildcard는 어떤 타입이 오더라도 상관이 없었다. 하지만, boundedWildCardMethod()에는 "?" 대신 "? extends Car"라고 적어준 것이 보일 것이다. 이렇게 정의한 것은 제네릭 타입으로 Car를 상속받은 모든 클래스들을 사용할 수 있다는 의미다. 따라서, boundedWildcardMethod()의 매개 변수에는 다른 타입을 제네릭 타입으로 선언한 객체가 넘어올 수 없다. 즉, 컴파일시에 에러가 발생하므로 반드시 Car 클래스와 관련되어 있는 상속한 클래스가 넘어와야만 한다.

실행 결과

이번에는 callBusBoundedWildcardMethod()를 다음과 같이 추가하고, 이 메소드를 수행하도록 한 후에 다시 컴파일 및 실행해보자.

public void callBusBoundedcardMethod() {
    WildcardGeneric<Bus> wildcard = new WildcardGeneric<Bus>();
    wildcard.setWildcard(new Bus("6900"));
    boundedWildcardMethod(wildcard);
}

실행 결과

"? extends 타입"과 같은 것을 "Bounded Wildcards"라고 부른다. Bound라는 말은 "경계"라는 의미도 있기 때문에, 매개 변수로 넘어오는 제네릭 타입의 경계를 지정하는 데 사용한다는 의미로 해석하면 된다. 앞 절에서 살펴본 "?"로 사용하는 wildcard와 마찬가지로, Bounded Wildcards로 선언한 타입에는 값을 할당할 수는 없다. 따라서, 이와 같이 조회용 매개 변수로 사용해야 한다.

5. 메소드를 제너릭하게 선언하기

앞에서 wildcard로 메소드를 선언하는 방법을 살펴보았다. 그런데, 이 방법에는 큰 단점이 있다. 매개 변수로 사용된 객체에 값을 추가할 수가 없다는 것이다. 그러면 아예 방법이 없을까? 아니다. 방법이 있다.

예제 코드

package d.generic;

public class GenericWildcardSample {
    public static void main(String[] args) {
        GenericWildcardSample sample = new GenericWildcardSample();
    }
    
    public <T> void genericMethod(WildcardGeneric<T> c, T addValue) {
        c.setWildcard(addValue);
        T value = c.getWildcard();
        System.out.println(value);
    }
}

메소드 선언부를 잘 보면 리턴 타입 앞에 <>로 제너릭 타입을 선언해 놓았다. 그리고, 매개 변수에서는 그 제네릭 타입이 포함된 객체를 받아서 처리한 것을 볼 수 있다. 게다가 메소드의 첫 문장을 보면 setWildcard() 메소드를 통하여 값을 할당까지 했다.
과연 이 코드가 컴파일이 잘 될까?

컴파일은 예상외로 잘 된다. 이처럼 메소드 선언시 리턴 타입 앞에 제네릭한 타입을 선언해 주고, 그 타입을 매개 변수에서 사용하면 컴파일할 때 전혀 문제가 없다. 게다가 값도 할당할 수 있다. 이 메소드가 잘 작동되는지 보기 위해서 호출하는 다음의 메소드를 하나 만들자.

public void callGenericMethod() {
    WildcardGeneric<String> wildcard = new WildcardGeneric<String>();
    genericMethod(wildcard, "Data");
}

실행 결과

?를 사용하는 Wildcard 처럼 타입을 두루뭉실하게 하는 것보다는 이처럼 명시적으로 메소드 선언시 타입을 지정해 주면 보다 더 견고한 코드를 작성할 수 있다. 물론 Bounded Wildcards처럼 사용할 수도 있다.

public <T extends Car> void boundedGenericMethod(WildcardGeneric<T> c, T addValue)

그렇다면 제네릭 타입이 두 개 일때는 어떻게 하면 될까? 한 개 이상의 제네릭 타입 선언은 콤마로 구분하여 나열해주면 된다.

public <S, T extends Car> void multiGenericMethod(WildcardGeneric<T> c, T addValue, S another)

이렇게 하면 S와 T라는 제네릭 타입을 메소드에서 사용할 수 있다.

참고

  • 자바의 신
profile
이것저것 관심많은 개발자.
post-custom-banner

0개의 댓글