제네릭(Generic)의 기본 개념
Generic의 뜻
영어사전을 통해 확인할 수 있는 generic의 정의는 여러 가지가 있지만 다음과 같은 뜻을 가지고 있다.
형용사
1. <명칭 등이> 일반적인, 포괄적인(general)
2. 총칭적인
3. <상품명·약 등이> 상표 등록이 되어 있지 않은
명사
1. (상표명이 아닌) 일반명
그렇다면 자바에서의 제네릭이란?
자바에서 제네릭은 데이터의 타입을 일반화한다(generalize)는 것을 의미한다. 조금 더 정확하게 표현하자면 클래스나 메소드에서 사용할 내부 데이터 타입을 실제로 클래스를 사용하는 Actor에 의해 지정할 수 있는 방법이다. 즉 사용자 호출에 의해 클래스 내부 데이터 타입을 지정하는 것이다.
제네릭을 이용한 타입 지정은 컴파일 시에 진행이 된다. 컴파일러에 의해 자동으로 검사 되어 타입이 변환되기 때문에 컴파일된 바이트코드(*.class) 파일에는 제네릭 타입이 포함되지 않는다.
제네릭은 언제부터 도입된걸까?
제네릭은 JDK 1.5에 도입된 기술이다. 제네릭이 도입되기 이전에는 타입 일반화를 위해 주로 Object 타입을 사용하였다. 하지만 Object를 사용한 일반화는 반환되는 객체를 다시 원하는 타입으로 변환해야 하는 타입 캐스팅 과정이나 타입 체크 과정이 필요했고 이 과정에서 오류가 발생하는 경우가 많았다.
제네릭의 사용이유 (장점)
특정한 타입에 종속되지 않는 것만으로도 제네릭은 유용하지만 구체적인 사용이유는 다음과 같다.
- 재사용성 증가
여러 타입으로 객체를 생성할 수 있어 간결한 코드 작성이 가능하고 재사용성이 증가한다.
예를 들어 동일한 기능을 하는 메서드에 파라미터 타입만 다르게 할 경우 제네릭 타입이 유용하게 사용될 수 있다. - 컴파일 시 타입 에러 발견 가능
제네릭 타입은 컴파일 시에 타입 체크를 수행하기 때문에 컴파일 이후 런타임 단계에서의 타입 문제 발생 가능성을 방지해 준다. - 컴파일러가 타입 변환 수행
컴파일러에 의해 자동으로 타입 캐스팅이 수행되기 때문에 별도의 타입 캐스팅 코드가 필요하지 않다.
위와 같이 제네릭을 사용하면 객체의 타입 안정성을 높일 수 있고 타입 캐스팅과 같은 번거로운 작업을 생략할 수 있다.
제네릭의 사용 시 주의사항!
제네릭을 사용할 때 알고 있어야 몇 가지 사항이 있다.
첫 번째, 자바의 원시 타입(Primitive type)은 사용할 수 없다.
제네릭은 클래스와 인터페이스에만 적용되기 때문에 원시 타입에는 사용될 수 없다.
public void exampleMethod(){
ArrayList<int> primitiveList = new ArrayList<>(); //error: 원시타입 사용 불가
ArrayList<Integer> primitiveList = new ArrayList<>();
}
두 번째, 제네릭 타입을 사용한 객체는 생성이 불가하다.
제네릭을 일반화를 위해 있는 타입이기 때문에 기본적으로 실체화가 불가능하다. 따라서 제네릭을 통한 객체 생성은 불가능하다.
public void exampleMethod(){
T genericObj = new T(); // error: 불가능
T[] genericArr = new T[]; // error: 불가능
Class<?> genericCls = T.class; // error: 불가능
}
세 번째, static 멤버에는 제네릭을 사용할 수 없다.
static 멤버는 클래스가 동일하게 공유하는 변수로서 객체가 생성되기도 전에 이미 타입이 정해져 있다. 때문에 논리적인 오류로 static 멤버에는 제네릭을 사용할 수 없다.
class Person<T> {
private String name;
private int age = 0;
// error: static 메서드의 반환 타입으로 사용 불가
public static T addAge(int n) {
}
// error: static 메서드의 매개변수 타입으로 사용 불가
public static void getKoreanAge(T n) {
}
}
네 번째, 배열 생성 시 선언만 가능하다.
제네릭으로 배열 선언은 불가능하지만 선언은 가능하다.
class Person<T> {
}
public class Main {
public static void main(String[] args) {
Person<Integer>[] arr1 = new Person<>[10]; //error: 제네릭 클래스 자체를 배열로 생성은 불가능
Person<Integer>[] arr2 = new Person[10];
//위에서 이미 타입을 정의했기 때문에 Integer가 자동으로 지정됨
arr2[0] = new Person<Integer>();
arr2[1] = new Person<>();
// ! Integer가 아닌 타입은 저장 불가능
arr2[2] = new Person<String>();
}
}
제네릭 사용방법
class ExampleClass<T> {
private T attr;
public void setAttr(T attr) { this.attr = attr; }
public T getAttr() { return attr; }
}
public class Main {
public static void main(String[] args) {
ExampleClass<String> str = new ExampleClass<>();
}
}
자바에서 제네릭은 위와 같은 방법으로 클래스와 메서드에 선언하고 사용할 수 있다.
제네릭의 타입 변수
위에서 볼 수 있듯이 제네릭은 <> 키워드를 사용하는데 이를 다이아몬드 연산자라고 한다. 그리고 이 연산자 안에 식별자 기호를 지정함으로써 타입을 파라미터화 할 수 있다. 이것이 마치 메서드가 매개변수를 받아 사용하는 것과 비슷하여 제네릭의 타입 매개변수(parameter) / 타입 변수라고 부른다.
class Person<T> { //T가 타입 매개변수(parameter) / 타입 변수이다.
}
public class Main {
public static void main(String[] args) {
Person<Integer> arr1 = new Person<>(); // Integer 타입이 매개변수처럼 사용된다.
}
}
💡 구체화(Specialization)
위 예제처럼 제네릭 클래스를 만들고 이를 인스턴스화하는 과정에서 타입을 할당하게 된다. 이때 타입 변수 T가 지정된 타입으로 모두 변환되어 클래스의 타입이 지정되게 된다. 이를 그림으로 표현하면 아래 그림과 같이 제네릭 타입 전파가 행해진다고 볼 수 있다. 이를 전문 용어로 구체화(Specialization)라고 한다.
중복되는 제네릭 타입 생략
자바를 사용하는 사람이라면 본인이 직접 제네릭 클래스를 만들어보진 않았더라도 컬렉션 프레임워크를 통해 제네릭을 접해봤을 것이다. (컬렉션에 대한 글은 아래 게시물을 참고해 주세요.) 이때 개발자들마다 new 생성자 부분에는 제네릭 타입을 생략하는 경우가 있는데 사실 JDK 1.7부터 가능한 선택지이다.
기존의 제네릭 사용문법을 보면 타입과 new 생성자 부분 두 군데 제네릭 타입을 지정하는 것을 볼 수 있다. 하지만 이는 불필요한 코드 중복으로 볼 수 있고 JDK 1.7부터는 new 생성자 부분의 제네릭 타입은 생략이 가능하도록 개선되었다.
public void exampleMethod(){
// JDK 1.7 이전 문법
List<Integer> myList = new ArrayList<Integer>(); // Integer라는 타입이 중복됨
// JDK 1.7부터 new 생성자 부분의 제네릭 타입 변수는 생략이 가능
List<Integer> myList = new ArrayList<>();
}
타입 변수의 명명 규칙(Naming Rule)
위 예제에서 타입 변수를 모두 T로 사용했지만 사실 다른 문자를 사용하더라도 문법적으로 문제는 없다. 다만 일반적으로 for 문의 루프 변수명을 i로 지정하듯이 관용적이고 통상적인 명명 규칙이다. 개발자가 맘대로 명명할 수도 있지만 대중적인 명명 규칙이 있다면 향후 유지보수를 생각하여 지켜주는 것이 좋다.
타입 | 설명 |
<T> | 타입(Type) |
<E> | 요소(Element) 예를 들어 List |
<K> | 키(Key) 예를 들어 Map<K, V> |
<V> | 리턴 값 또는 매핑된 값(Variable) |
<N> | 숫자(Number) |
<S, U, V> | 2, 3, 4번째 선언된 타입 |
제네릭 타입 범위 한정
제네릭을 사용함으로써 타입 안정성과 프로그램의 유연성을 확보할 수 있다는 것은 아주 큰 장점이지만 문제는 너무 유연하다는 것이다.
예를 들어 계산기 클래스가 있다고 가정한다. 정수, 실수 모두 받을 수 있게 제네릭으로 클래스로 설계하였다. 하지만 단순히 <T>로 지정하게 되면 숫자에 관련된 래퍼 클래스뿐만 아니라 String이나 다른 클래스들도 대입이 가능하다는 문제가 있다. 설계한 개발자의 의도는 계산기 클래스의 타입 변수로 숫자형 자료형만 들어오도록 하고 문자열 자료형은 사용이 불가능하도록 하고 싶다. 이러한 것이 가능하도록 하는 것이 extends와 super, ?이다. 특히 ?는 와일드카드라고 불리며 '알 수 없는 타입'을 나타낼 때 사용된다. 간단한 예시는 아래와 같다.
<K extends T> // T와 T의 자손 타입만 가능 (K는 들어오는 타입으로 지정 됨)
<K super T> // T와 T의 부모(조상) 타입만 가능 (K는 들어오는 타입으로 지정 됨)
<? extends T> // T와 T의 자손 타입만 가능
<? super T> // T와 T의 부모(조상) 타입만 가능
<?> // 모든 타입 가능. <? extends Object>랑 같은 의미
일반적으로 아래와 같이 구분된다.
- extends T: 상한 경계
- ? super T: 하한 경계
- <?>: 와일드 카드
제너릭 타입 한정은 조금 더 자세히 설명할 수 있는 별도의 포스팅을 작성할 예정이니 제네릭에 대한 글은 여기서 마치도록 하겠다.
글을 마치며
컬렉션 프레임워크를 사용하면서 항상 접해왔던 제네릭에 대해 정리해 보았다. 지난번에 우연찮게 기능을 개발하다 제네릭 클래스를 사용한 적이 있어 언제 한번 개념을 정리해 두면 좋겠다고 생각을 했었다. 조금 늦었지만 늦게라도 정리한 내가 너무 기특하다. 이 포스팅에 제네릭 타입 한정에 대한 것까지 모두 포함시키면 좋겠지만 글이 너무 길어질 것 같아 별도의 게시물로 분리하기로 결정했다!
참고 자료 및 사이트
- https://www.youtube.com/watch?v=Vv0PGUxOzq0
- https://www.youtube.com/watch?v=w5AKXDBW1gQ
- https://inpa.tistory.com/entry/JAVA-%E2%98%95-%EC%A0%9C%EB%84%A4%EB%A6%ADGenerics-%EA%B0%9C%EB%85%90-%EB%AC%B8%EB%B2%95-%EC%A0%95%EB%B3%B5%ED%95%98%EA%B8%B0#%EC%A0%9C%EB%84%A4%EB%A6%AD_%ED%83%80%EC%9E%85_%EB%B2%94%EC%9C%84_%ED%95%9C%EC%A0%95%ED%95%98%EA%B8%B0