Programming/Java

Java Generic 제네릭 기본적인 개념 이해하기

Jan92 2021. 12. 24. 00:26

Generic

 

'제네릭(Generic) 기본적인 개념 이해하기'

 

'데이터 형식에 의존하지 않고, 하나의 값이 여러 다른 데이터 타입들을 가질 수 있도록 하는 방법'

어떤 자료 구조를 만들어서 사용하려고 할 때 String 타입도 지원하고 싶고, Integer 타입도 지원하고 싶고, 다른 타입들도 지원하고 싶은 경우가 있습니다. 그럴 때 String에 대한 클래스, Integer에 대한 클래스 등 타입에 따라 각각의 클래스들을 모두 만드는 것은 너무 비효율적입니다. 자바에서는 이러한 문제를 해결하기 위해 java 1.5부터 제네릭을 사용하게 되었는데요.

 

제네릭(Generic)은 클래스 내부에서 지정하는 것이 아닌, 외부에서 사용자에 의해 지정되는 것을 의미하며, 한마디로 특정(Specific) 타입을 미리 지정해주는 것이 아니라 필요에 의해서 지정할 수 있도록 하는 일반(Generic) 타입을 이야기합니다.

(정확하게는 지정된 것보다 타입의 경계를 지정하고, 컴파일 때 해당 타입으로 캐스팅하여 매개변수화 된 유형을 삭제하는 것)

 

사용되는 측면에서 보면 제네릭 객체는 인스턴스 별로 다르게 동작할 수 있도록 만들어졌고, 다룰 객체의 타입을 미리 명시해줌으로써 번거로운 형 변환을 줄여주게 됩니다.

제네릭을 모르면 Java API 문서를 제대로 볼 수 없다고 할 정도로 제네릭은 자바에서 중요한 기능입니다.

 

 

'제네릭(Generic)의 장점'

  • 잘못된 타입이 들어올 수 있는 것을 컴파일 단계에서 방지할 수 있습니다.
  • 클래스 외부에서 타입을 지정해주기 때문에 따로 타입을 체크하고 변환해줄 필요가 없어 관리하기가 편합니다.
  • 비슷한 기능을 지원하는 경우 코드의 재사용성을 높일 수 있습니다.

 

***

다양한 타입의 객체들을 다루는 메서드나 컬렉션 클래스는 어떤 자료를 담을지 알 수 없기 때문에 최상위 객체인 Object 타입으로 저장, 관리됩니다. 이 경우 의도치 않은 자료형이 담겨 실행 시에 오류가 발생할 수 있는데요. 해당 오류는 컴파일 시에는 알 수 없는 오류지만 제네릭(Generic) 타입을 지정하면 컴파일 시 오류를 확인할 수 있게 됩니다.

 

 


 

 

'제네릭의 사용'

// 클래스
public class ClassName <T> { ... }

// 인터페이스
public Interface InterfaceName <T> { ... }

// 제네릭 타입을 두 개 이상 사용하는 경우
public class MultiGeneric <K, V> { ... }

 

클래스나 인터페이스에 선언한 경우와 제네릭 타입을 두 개 이상 사용하는 경우입니다.

이때 T, K, V 타입은 해당 블록 {...} 안에서까지 유효합니다.

 

 

public class Box<T> {

    private T object;

    public void set(T object) {
        this.object = object;
    }
    public T get() {
        return object;
    }
}

<T>의 T를 '타입 변수'라고 합니다. 임의의 참조형 타입을 의미합니다.

 

 

T : Object       <T>
E : Element      <E>
K : Key          <K>
V : Value        <V>
N : Number       <N>

* 타입 변수(Type Variable)

기호의 종류만 다를 뿐 '임의의 참조형 타입'을 의미한다는 것은 모두 같으며, 상황에 맞게 의미 있는 문자를 선택해서 사용합니다.

(통상적으로 쓰이는 암묵적인 규칙이며, 많이 사용되는 예시일 뿐 꼭 한 글자일 필요는 없습니다.)

 

 

***

주의해야할 점은 타입 변수로 명시할 수 있는 것은 참조 타입(Reference Type)밖에 올 수 없다는 것입니다.

즉 int, double, char 같은 Primitive Type은 올 수 없습니다. 그래서 int형, double형 등의 Primitive Type의 경우 Integer, Double 같은 Wrapper Type으로 사용해야 합니다.

 

 

Test<Integer> test = new Test<Integer>();
List<String> list = new ArrayList<>();

실제 사용에서는 꺽쇠 괄호  <> 안에 있는 String을 실 타입 매개변수(Actual Type Parameter)라고 하며, 실제 List 인터페이스에 선언되어 있는 List<E>의 E를 형식 타입 매개변수(Formal Type Parameter)라고 합니다.

 

 

***

제네릭 타입은 컴파일 시 컴파일러에 의해 제거됩니다.

자바 코드에서 선언되고 사용된 제네릭 타입은 컴파일 시 컴파일러에 의해 자동으로 검사 되어 타입으로 변환됩니다. 그렇게 코드 내의 모든 제네릭 타입은 제거되어, 컴파일된 class 파일에는 어떠한 제네릭 타입도 포함되지 않게 됩니다. 제네릭이 이런 식으로 동작하는 이유는 제네릭을 사용하지 않는 코드와의 호환성을 유지하기 위해서입니다.

 

 


 

 

'제네릭의 제한'

class Box<T>{
    static T item; //에러
    static int compare(T t1, T t2) {} //에러
}

- static 멤버

 

제네릭은 객체별로 다르게 동작하기 위해서 만들어졌습니다. 때문에 모든 객체에 대해 동일하게 동작해야 하는 static 멤버에 타입 변수를 사용할 수 없습니다. 타입 변수는 인스턴스 변수로 간주됩니다. 

 

다시 이야기하면 static 변수는 인스턴스에 종속되지 않는 클래스 변수로써 모든 인스턴스가 공통된 저장 공간을 공유하게 되는 변수입니다.

static 변수에 제네릭을 사용하려면 GenericArrayList<Integer>에서는 Integer 타입으로, GenericArrayList<String>에서는 String 타입으로 사용될 수 있어야 하는데, 하나의 고유 변수가 생성되는 인스턴스에 따라 타입이 바뀐다는 개념 자체가 말이 안 되는 것입니다.

(static 변수, static 함수 등 static이 붙은 것들은 기본적으로 프로그램 실행 시 메모리에 이미 올라가 있습니다.)

 

 

public class Test<T> {
    T[] tArr1;
    T[] tArr2 = new T[10];    // error
}

- 제네릭 타입의 배열

 

제네릭 배열 타입의 참조 변수를 선언한 것은 가능하지만, 배열을 생성하는 것은 안 됩니다.

new 연산자는 컴파일 시점에서 타입 변수가 어떤 것인지 정확하게 알아야 하는데 제네릭 클래스는 컴파일하는 시점에 타입 변수가 어떤 타입이 될지 전혀 알 수가 없기 때문입니다.

 

(new 연산자는 heap 영역에 충분한 공간이 있는지 확인한 후 메모리를 확보하는 역할로, 충분한 공간이 있는지 확인하기 위해서는 타입을 알아야 하는데 컴파일 시점에서 타입 T가 무엇인지 알 수 없기 때문에 제네릭으로 배열을 생성할 수 없습니다.)

 

* instanceof 메서드도 같은 이유로 타입 변수를 사용할 수 없습니다.

 

* 제네릭 타입의 배열을 생성해야 하는 경우에는 'Reflection API'의 newInstance()와 같이 동적으로 객체를 생성하는 메서드로 배열을 생성하거나, Object 배열을 생성해서 복사한 후 'T[]'로 형 변환하는 방법 등을 사용해야 합니다.

 

 


 

 

'제네릭 메서드(Generic Method)'

public <T> T genericMethod(T object) {	
		...
}
 
[접근 제어자] <제네릭타입> [반환타입] [메소드명]([제네릭타입] [파라미터]) {
		...	
}

클래스나 인터페이스에 제네릭을 사용하는 것처럼 메서드에도 제네릭을 적용할 수 있습니다. 주로 static 유틸리티 메서드에 유용하게 쓰일 수 있는데요.

위 예시에서 볼 수 있는 것처럼 제네릭 메서드의 타입 매개변수를 선언할 때 타입 매개변수의 위치는 메서드의 접근 지시자와 반환 타입 사이가 됩니다.

 

제네릭 메서드를 정의할 때 중요한 것은 리턴 타입이 무엇인지와는 상관없이 해당 메서드가 제네릭 메서드라는 것을 컴파일러에게 알려주는 것입니다. 그러기 위해서 리턴 타입을 정의하기 전에 제네릭 타입에 대한 정의가 반드시 필요합니다.

 

또 하나 중요한 것은 제네릭 클래스가 아닌 일반 클래스 내부에서도 제네릭 메서드를 정의할 수 있다는 것인데요. 그 말은 클래스에 지정된 타입 파라미터와 제네릭 메서드에 지정된 타입 파라미터는 상관이 없다는 것입니다.

즉, 제네릭 클래스에 <T>를 사용하고, 같은 클래스 내부의 제네릭 메서드에도 <T>로 같은 이름을 가진 타입 파라미터를 사용하더라도 이 둘은 전혀 상관이 없다는 것입니다.

 

 

***

static 변수에는 제네릭을 사용할 수 없지만 static 메서드에는 제네릭을 사용할 수 있는 이유가 무엇일까요?

static 변수의 경우에는 앞에서 이야기한 것처럼 제네릭을 사용하면 여러 인스턴스에서 어떤 타입으로 공유되어야 할지 지정할 수가 없어서 사용할 수 없었습니다. static 변수는 값 자체가 공유되기 때문에 값 자체가 공유되려면 타입에 대한 정보도 있어야 하기 때문입니다.

 

반면 static 메서드의 경우 메서드의 틀만 공유된다고 생각하면 됩니다. 그리고 그 틀 안에서 지역변수처럼 타입 파라미터가 다양하게 오가는 형태로 사용될 수 있는 것입니다.

 

 

 

'제네릭 메서드(Generic Method) 예시'

public static <T extends CharSequence> void printFirstChar(T param) {
    System.out.println(param.charAt(0));
}

제네릭 메서드 선언 시 <T>만 사용해도 상관없습니다. 위 예시의 경우 charAt() 메서드를 호출하기 위해서 CharSequence의 서브타입만 가능하다는 제약을 넣은 것입니다.

printFirstChar() 제네릭 메서드를 GenericArrayList에 정의해 주었다면 호출은 아래와 같이 하면 됩니다.

 

 

GenericArrayList.<String>printFirstChar("JAN");

그런데 여기서 "JAN"을 통해 인자의 타입이 String인 것을 컴파일러가 추론할 수 있으므로 <String>은 생략 가능합니다.

대부분의 경우 타입 추론이 가능하므로 아래와 같이 타입은 생략하고 호출할 수 있습니다.

 

 

GenericArrayList.printFirstChar("JAN");

 <String> 생략

 

 


 

 

'와일드카드(Wildcards)'

public Map<String, ? super Object> getErrorMap() {
    return errorMap;
}

와일드카드는 기호 '?'를 사용합니다. 

타입 변수는 보통 단 하나의 타입만 지정하지만 와일드카드를 이용하면 하나 이상의 타입을 지정할 수 있습니다.

타입 변수의 다형성을 적용하여 어떠한 타입도 적용할 수 있게 되는데요.

 

 

<K extends T>	// T와 T의 자손 타입만 가능 (K는 들어오는 타입으로 지정 됨)
<K super T>	// T와 T의 부모(조상) 타입만 가능 (K는 들어오는 타입으로 지정 됨)
 
<? extends T>	// T와 T의 자손 타입만 가능
<? super T>	// T와 T의 부모(조상) 타입만 가능
<?>		// 모든 타입 가능. <? extends Object>랑 같은 의미

(K는 특정 타입으로 지정된다는 의미이고 ? 는 타입이 지정되지 않는다는 의미입니다.)

 

 

 

* 와일드카드가 고안된 이유

static 메서드에 제네릭을 적용한 경우, 타입 매개변수는 사용하지 못하므로 특정 타입을 지정해야 합니다. 그렇게 되면 해당 메서드는 특정 타입의 객체만을 사용할 수 있게 되어 다른 타입의 객체를 매개변수로 오게 하려면 타입 변수만 다른 똑같은 메서드를 만들어야 합니다.

 

static void method(Box<TypeA> b) {}   //   Compile error
static void method(Box<TypeB> b) {}   //   Compile error
static void method(Box<TypeC> b) {}   //   Compile error

이때 제네릭 타입이 다른 것만으로는 오버로딩이 성립되지 않기 때문에 메서드가 중복으로 정의되게 되는데요. 와일드카드는 이러한 상황에 사용하기 위해 고안되었습니다.

 

 

 

 

< 참고 자료 >

 

자바 [JAVA] - 제네릭(Generic)의 이해

정적언어(C, C++, C#, Java)을 다뤄보신 분이라면 제네릭(Generic)에 대해 잘 알지는 못하더라도 한 번쯤은 들어봤을 것이다. 특히 자료구조 같이 구조체를 직접 만들어 사용할 때 많이 쓰이기도 하고

st-lab.tistory.com

 

 

[Java] Generics - Dico

Java의 Generics에 대한 글입니다.

dico.me

 

 

자바 제네릭 이해하기 Part 1

개요 제네릭이란? 제네릭을 사용하는 이유 제네릭을 사용할 수 없는 경우 제네릭 메서드란? 제네릭 타입 제한하기 (Bounded Type Parameter)

yaboong.github.io

 

 

Generic Methods (The Java™ Tutorials > Learning the Java Language > Generics (Updated))

The Java Tutorials have been written for JDK 8. Examples and practices described in this page don't take advantage of improvements introduced in later releases and might use technology no longer available. See Java Language Changes for a summary of updated

docs.oracle.com

 

< 고급 참고 자료 >

 

[ Java] Java의 Generics

Java 언어에서 언어적으로 가장 이해하기 어렵고 제대로 사용하기가 어려운 개념이 Generics가 아닐까 싶다. 평소에 클래스나 인터페이스 설계 시 Generics를 자주 사용하긴 했지만 어떠한 계기로 인

medium.com