본문 바로가기
IT 개인 공부/Java

[Java] 제네릭(Generic)

by Libi 2021. 7. 13.
반응형

Java에서 컬렉션을 사용할 때 보통 다음과 같은 방식으로 '<'와 '>' 사이에 컬렉션에 넣을 변수의 타입을 선언할 것이다.

List<Integer> integer_List = new ArrayList<>();
List<String> string_List = new ArrayList<>();

 

이러한 문법을 제네릭이라고 부른다. 이번 기회에 한 번 제네릭에 대해서 공부 및 정리해보자.

제네릭 프로그래밍(Generic Programming)이란 JDK 1.5부터 도입된 문법으로 일반적인 코드를 작성하고 이 코드를 다양한 타입의 객체에 대하여 재사용하는 객체 지향 기법이다. 간단하게 말해서 클래스를 정의할 때, 구체적인 타입을 적지 않고 변수 형태로 적어 놓는 것이다.

그렇다면 먼저 제네릭을 왜 사용하는 것일까? 제네릭을 사용함으로써 잘못된 타입 변환을 했을 시 컴파일 과정에서 제거할 수 있기 때문이다.

자바 컴파일러는 제네릭 코드에 대해 강한 타입 체크를 함으로써 사전에 타입 에러를 방지함으로써 실행 시 타입 에러가 나는 것을 막아준다. 또한, 불필요한 타입 변환을 줄여주기 때문에 프로그램 성능이 향상되는 장점이 있다.

간단한 예시를 통해 제네릭을 사용하지 않았을 때 발생할 수 있는 문제를 보도록 하자.

JDK 1.5 이전에는 제네릭 프로그래밍을 하려면 Object 클래스를 활용한 다형성을 이용했다. 모든 클래스는 Object 클래스의 하위 클래스이기 때문에 다형성에 의하여 Object 참조 변수는 어떤 객체든지 참조할 수 있기 때문이다.

다음과 같은 하나의 data를 가지는 Box 클래스가 있다고 하자.

class Box {
	Object data;
}

 

Box 클래스의 data는 Object로 선언되었기 때문에 어떠한 타입의 데이터든지 저장할 수 있다.

Box box = new Box();
box.data = 100; //Integer 
box.data = "Hello"; //String

 

하지만 이 방법은 문제가 있다. 먼저 데이터를 꺼낼 때마다 타입 변환을 해줘야 한다는 것이다.

String str = (String) box.data; //String 타입으로 캐스팅해줘야 함

 

이는 부주의하게 다른 타입으로 형변환을 할 수도 있다는 문제점이 존재한다.

Integer num = (Integer) box.data; //String -> Integer : 잘못된 형변환

 

이러한 문제는 실행 도중(런타임)에 알 수 있기 때문에 제네릭을 사용함으로써 실행 전 컴파일 시 방지할 수 있도록 해주는 것이다.

Box 클래스에 제네릭 문법을 적용시키면 다음과 같다.

class Box<T> {
	T data;
}

Box<Integer> box_Integer = new Box<>(); //Integer
Box<String> box_String = new Box<>(); //String
box_Integer.data = 100;
box_String.data = "Hello";

 

제네릭을 통해 객체 생성 시에 타입 매개변수가 결정됨으로써 잘못된 형변환 오류를 줄일 수 있다.

Box 뒤의 T를 타입 매개변수(Type Parameter)라고 부른다. 제네릭 클래스는 여러 개의 타입 매개변수를 가질 수 있으나 타입의 이름은 클래스나 인터페이스 안에서 유일해야 한다. 주로 사용하는 타입 매개변수는 다음과 같다.

 

제네릭은 클래스뿐만 아니라 메서드에도 적용이 가능하다. 이 경우 타입 매개변수의 범위가 메서드 내부로 제한된다. 또한, 제네릭 메서드의 타입 매개변수는 반드시 메서드의 수식자와 반환형 사이에 위치해야 한다. 그 이유는 컴파일러에게 제네릭 메서드라는 것을 알려주기 위해서이다.

public <T> T get_LastElement(T[] arr) { //반드시 반환형 앞에 <T> 선언해줘야 함
    	return arr[arr.length - 1];
}

 

때때로 타입 매개변수로 전달되는 타입의 종류를 제한하고 싶을 경우가 있을 것이다. 이를 가능하게 해주는 것이 바로 한정된 타입 매개변수(Bounded Type Parameter)이다. extends라는 키워드를 사용한다.

만약 배열에서 가장 큰 값을 반환하는 제네릭 메서드가 다음과 같이 있다고 하자.

public <T> T get_MaxElement(T[] arr) {
    T max = arr[0];
    for (int i = 1; i < arr.length; ++i) {
    	if (max.compareTo(arr[i]) > 0) {
    		max = arr[i];
    	}
   	}
   	return max;
}

 

위의 메서드는 문제가 있다. 바로 T 클래스가 Comparable 인터페이스의 compareTo를 재정의하지 않았기 때문이다. 이러한 실수를 줄이기 위해서 T가 가리킬 수 있는 클래스의 범위를 extends를 사용하여 제한하는 것이다.

public <T extends Comparable<T>> T get_MaxElement(T[] arr) {
    T max = arr[0];
    for (int i = 1; i < arr.length; ++i) {
    	if (max.compareTo(arr[i]) > 0) {
    		max = arr[i];
    	}
   	}
   	return max;
}

 

주의해야 할 점은 인터페이스여도 implements를 사용하는 것이 아니라 extends를 사용한다는 점이다.

타입 매개변수가 어떤 타입이어도 상관없을 경우 '?'를 통해 표현하는데 이를 와일드 카드라고 한다. 와일드 카드 타입에는 3가지의 형태가 존재한다.

 

한도가 없는 와일드 카드 : 제네릭타입<?>

  • 말 그대로 어떠한 타입의 매개변수든지 가능하다는 뜻이다.

 

상한이 있는 와일드 카드 : 제네릭타입<? extends 상위타입>

  • 이전에 배운 extends 키워드를 사용하여 상위 클래스를 상속받는 타입만 올 수 있다는 뜻이다.

하한이 있는 와일드 카드 : 제네릭타입<? super 하위타입>

  • super라는 키워드를 사용하여 하위 클래스보다 위에 있는 타입만 올 수 있다는 뜻이다.

마지막으로 자바 컴파일러에 의해 컴파일된 .class 파일에는 제네릭 타입이 존재하지 않는다. 이는 컴파일 시 자바 컴파일러에 의해 자동으로 검사되어 적절한 타입으로 변환 후 제거되기 때문이다.

제네릭을 제거하는 이유는 제네릭을 사용하지 않는 코드와의 호환성을 유지하기 위해서이다.

반응형

댓글