제네릭에 대한 설명에 앞서, Java에서 다수의 데이터를 쉽고 효과적으로 처리할 수 있는 표준화된 방법을 제공하는 클래스의 집합을 컬렉션 프레임워크(collection framwork)라고 합니다.
자주 쓰이는 컬렉션 프레임워크의 클래스 구조도를 보면 아래의 이미지와 같습니다. (아래의 이미지에 나와있는 구조도는 간략화되어 있으며, 실제로는 훨씬 더 많고 복잡합니다.)
제네릭(Generic)
위 이미지에서 보면 <E>라던가 <K, V>라는 표현을 볼 수 있습니다. 해당 표현은 제네릭을 의미하며, 제네릭이란 사전적 의미로 "일반적인"이라는 의미를 갖고 있는 단어입니다.
Java에서도 비슷한 의미로 사용되며, 클래스 내부에서 사용할 데이터 타입을 외부에서 파라미터 형태로 지정하여 일반화를 한다는 의미입니다.
즉, 클래스 및 인터페이스 구현 시 가상의 자료형을 정의 후, 객체를 정의할 때 타입 매개변수를 선언하여 사용하게 되며 클래스 및 인터페이스 이름 뒤에 "<타입 매개변수>"를 작성하여 사용합니다.
추가로 타입 매개변수에 기본 타입은 넣을 수 없기 때문에 wrapper class를 사용해야 합니다. (해당 내용은 아래에서 자세히 다루겠습니다.)
글로 보면 무슨 내용인지 감이 안 잡힐 수 있습니다. (저도 글만 보고는 이게 무슨 말인가 싶었고.. 지금도 그렇습니다.)
그러니 코드 예제로 쉽게 접근을 해보도록 하겠습니다.
public class 클래스명<타입 매개변수> { ... }
public interface 인터페이스명<타입 매개변수> { ... }
위 형태로 구현하여 사용하게 되며, 타입 매개변수에 들어가는 값은 정해진 규칙이 있는 건 아니지만 일반적으로 대문자 알파벳 한 글자를 사용하여 표현합니다. 자주 사용되는 타입 매개변수들이 있기에 아래 표로 정리를 해보았습니다.
타입 인자 | 설명 |
<T> | Type |
<E> | Element |
<K> | Key |
<N> | Number |
<V> | Value |
<R> | Result |
제네릭의 필요성
Java의 제네릭은 개발자들이 개발을 함에 있어서 필요하다고 느껴 Java5부터 추가되어 사용된 기능입니다.
그렇다면 제네릭이 왜 추가되었고 사용되는지 필요성에 대해 알아보도록 하겠습니다.
제네릭은 다형성을 쉽게 구현하기 위해서 추가되었다고 볼 수 있습니다.
제네릭을 사용하지 않고 다형성을 구현하는 방법 중 하나로 최상위 클래스인 Object 클래스를 사용하여 다양한 클래스를 받아 사용하는 방법이 있습니다. 아래의 코드와 같은 방식입니다.
class Basket {
private Object obj;
public void set(Object obj) {
this.obj = obj;
}
public Object get() {
return obj;
}
}
public class Main {
public static void main(String[] args) {
Basket basket = new Basket();
basket.set("String"); // 문자열 대입, String class
basket.set(10); // 정수형 대입, 정확히는 Integer wrapper class
basket.set('C'); // 캐릭터형 대입, 정확히는 Character wrapper class
basket.set(10.0); // 실수 대입, 정확히는 Double wrapper class
}
}
위 예제 코드를 보면 Object 타입의 obj 변수에 set 메서드를 통해 여러 가지 타입을 저장하는 것을 볼 수 있습니다.
이렇게 하나의 변수에 여러 가지 타입을 저장할 수 있게 됨으로써 다형성을 구현하는 방식인 것입니다.
하지만 이 방식에 단점이 존재하는데, 이러한 단점은 get 메서드를 통해 obj 변수의 값을 반환받아 사용할 때 발생합니다.
현재 obj 변수에 저장된 데이터의 타입이 무엇인지 확인을 해야 한다는 것과 사용을 위해 명시적 형 변환이 필요하게 된다는 것입니다. 이때 형 변환을 잘 못하게 되면 에러가 발생하게 되기도 합니다. 아래의 예제 코드처럼 말이죠.
public class Main {
public static void main(String[] args) {
Basket basket = new Basket();
basket.set("String");
String s = (String) basket.get(); // 정상적으로 수동 형 변환 가능
// Integer i = (Integer) basket.get(); // 문자열을 정수로 수동 형 변환을 할 수 없기에 ClassCastException 에러 발생
}
}
이러한 단점을 해소하고자 제네릭이 나오게 된 것입니다.
제네릭의 장점으로는 타입 확인과 형 변환을 생략하여 간결한 코드 작성이 가능해진다는 것, 클래스나 메서드 내부에서 사용되는 객체의 타입 안정성 제공이 있습니다.
제네릭 사용법
위 제네릭이란 무엇인가에 대한 내용에서 제네릭의 정의에 대해서 언급을 했기에 구현 방법은 대강 알 것도 같지만, 사용법에 대해선 언급이 없었기에 어떻게 사용하는지 알아보면서 구현 방법에 대한 내용도 자세히 알아보도록 하겠습니다.
class Basket<T> { // 제네릭 타입 매개변수 1개 T를 받아서 사용
private T t;
public T get() { return t; }
public void set(T t) { this.t = t; }
}
public class Main {
public static void main(String[] args) {
Basket<String> basket1 = new Basket<String>(); // 객체 생성시 어떤 타입의 값을 넣을지 지정, 여기선 String 사용
basket.set("String");
String str = basket1.get();
Basket<Integer> basket2 = new Basket<Integer>(); // 객체 생성시 어떤 타입의 값을 넣을지 지정, 여기선 Integer 사용
basket2.set(10);
int integer = basket2.get();
}
}
위 예제 코드를 보면 클래스 정의 시 <T>를 통해 제네릭 타입 매개변수 1개를 받겠다고 정의하여 사용하는 것을 확인할 수 있습니다. 그리고 이를 이용해 변수 및 메서드의 반환, 설정 타입을 지정하여 사용하게 되고, 객체 생성 시 <String>, <Integer>를 통해 해당 객체의 타입을 지정하여 사용하게 되는 것을 알 수 있습니다.
이때 위에서도 언급했듯이 타입 매개변수에는 기본 타입을 넣을 수 없기 때문에 기본 타입을 클래스로 변환시킨 wrapper class를 사용해야 합니다. 그렇기에 해당 예제 코드에서도 <int>가 아닌 <Integer>가 사용된 것입니다.
이러한 wrapper class는 "java.lang" 패키지에 정의되어 있으며, 기본 타입별 매칭되는 wrapper class는 다음과 같습니다.
기본 타입 | 래퍼 클래스 |
byte | Byte |
short | Short |
int | Integer |
long | Long |
float | Float |
double | Double |
char | Character |
boolean | Boolean |
wrapper class 중 기본 타입과 이름이 달라지는 타입은 int와 char가 유일하며, 다른 타입의 경우 첫 글자를 대문자로 바꾸면 wrapper class가 됩니다.
wrapper class는 class이기 때문에 변수를 만들 시 참조 변수가 되며, 해당 변수에 null 값 할당이 가능하게 됩니다. (기본 타입과의 차이점)
다이아몬드 연산자
다이아몬드 연산자란 "<>"을 뜻하는 연산자입니다. 생긴 모양이 다이아몬드 같다고 하여 붙혀진 것으로 압니다.
제네릭에 대한 설명을 하다가 갑자기 웬 다이아몬드 연산자가 튀어나왔냐면, 당연히 제네릭에 연관이 있기 때문입니다.
위 제네릭 예제 코드를 보면 객체 생성 시 생성자에도 "new Basket<String>();" 형태로 타입 매개변수를 넣어주는 것을 볼 수 있습니다. 해당 부분을 조금 자세히 보면 변수 생성 시에도 "Basket<String> basket1"이라고 타입 매개변수를 넣어서 생성한 것을 알 수 있습니다.
그럼 여기서 이러한 의문이 생길 수 있습니다. 아니 변수에서 이미 <String>이라고 선언을 했으니 당연히 <String> 타입 매개변수를 갖는 객체가 생성돼야 할 텐데 생성자에도 굳이 <String>이라고 적어줘야 하나?
이러한 의문에 대한 답변은 Java 버전에 따라 달라지게 됩니다.
맨 처음 제네릭이 Java에 추가된 버전이 Java5라고 위에서 언급했었습니다. 제네릭이 추가된 Java5와 이후 Java6까지는 생성자에도 똑같이 타입 매개변수를 작성해 줘야 했습니다.
하지만 Java7부터는 컴파일러가 문맥을 통해 타입 매개변수가 무엇인지 추론할 수 있는 상황에서는 타입 매개변수를 명시하지 않고 "<>"만 작성하여 동작할 수 있게 되었습니다.
따라서 Java5, 6에서는 항상 명시적으로 작성을 해줘야 하고, Java7부터는 위와 같은 상황에서는 아래와 같이 작성이 가능하게 되었습니다.
Basket<String> basket1 = new Basket<>();
와일드카드
와일드카드(wildcard)는 어떠한 것이 와도 제한을 두지 않겠다는 의미이며, '?'기호를 통해서 나타낼 수 있습니다.
Java에서 와일드카드는 타입 매개변수 또는 컬렉션 프레임워크에 사용이 가능한 기능입니다.
와일드카드를 사용하게 되면 들어올 수 있는 타입에 대한 제한을 두거나, 두지 않을 수 있습니다.
와일드카드의 사용 방법은 아래와 같으며, super 키워드의 경우 타입 매개변수에서는 사용이 불가능합니다.
<?> : 타입 매개변수에 모든 타입 사용 가능
<? extends 상위타입A> : A타입 및 A타입을 상속받는 하위 클래스 타입만 사용 가능 (상한선)
<? super 하위타입B> : B타입 및 B타입과 상속 관계인 상위 클래스 타입만 사용 가능 (하한선)
해당 내용은 제 기준으로 생각보다 내용이 많기 때문에 다른 분의 블로그를 공유드립니다. (링크)
제네릭 메서드
제네릭은 클래스 전체에 선언하여 사용할 수 있지만 클래스 내부에서 메서드에만 적용하여 사용할 수 있는데, 이러한 메서드를 제네릭 메서드(generic method)라고 합니다.
제네릭 클래스의 경우에 객체를 생성할 때 타입이 지정되지만, 제네릭 메서드의 경우에는 메서드가 호출될 때 타입이 지정된다는 차이가 있습니다.
따라서 제네릭 클래스의 타입은 전역 변수처럼 사용이 된다고 볼 수 있고, 제네릭 메서드의 타입은 해당 메서드에서만 사용 가능한 지역 변수처럼 사용된다고 볼 수 있습니다.
그리고 이러한 이유로 인하여 메서드를 정의하는 시점에선 어떠한 타입이 들어올지 알 수 없으므로 length()와 같이 String 클래스의 메서드는 사용할 수 없지만, 모든 클래스가 상속을 받는 최상위 클래스인 Object 클래스의 메서드는 사용할 수 있습니다. (Object 클래스의 메서드를 참고할 수 있는 Java Object class document 링크를 공유드립니다.)
제네릭에 대한 이번 글은 여기서 마치도록 하겠습니다.
혹시나 해당 글에서 잘 못 된 내용이 있거나 추가되면 좋을 것 같은 내용을 댓글로 남겨주시면 반영하도록 하겠습니다.
여담으로 지금까지 공부는 했지만 블로그에 정리 못 한 내용들을 천천히 올리고 있는데.. 아직 산더미처럼 남아있네요...
아마도 다음 정리 글은 컬렉션 프레임워크가 될 것 같습니다. 그 이유는.. 사실 해당 글이 컬렉션 프레임워크를 정리하려고 했던 글인데 제네릭을 알아야 컬렉션 프레임워크를 이해할 수 있다고 판단되어서 중간에 제네릭으로 변경이 된 것이기 때문입니다. (그래서 맨 처음에 컬렉션 프레임워크에 대한 내용이 있는거라죠 ㅎㅎㅎㅎㅎㅎㅎㅎㅎㅎㅎㅎ)
'Develop > Java' 카테고리의 다른 글
Java - File read 2가지 방법 (0) | 2022.11.29 |
---|---|
JPA Fetch Type (1) | 2022.09.09 |
Collection framwork (0) | 2022.07.26 |
Java 클래스 검색법 (0) | 2022.07.25 |
클래스와 인스턴스 - class & instance (0) | 2022.07.25 |