제네릭스 : 컴파일 단계에서 자료형을 체크해주는 도구
제네릭스라는 개념은 처음 들을 때, 많이 낯설고 어렵게만 들리는 개념이기도 하며, 위의 말처럼 듣기만 했을 때는 잘 이해가 가질 않게 된다. 지금부터 천천히 알아보자.
int [] iArray = {1, 2, 3, 4, 5};
double[] dArray = {1.0, 2.0, 3.0, 4.0, 5.0};
String[] sArray = {"A", "B", "C", "D", "E"};
3개의 배열을 임의로 생성하였고 각 자료형 타입이 다 다르다. 만약 위 배열들을 출력하고 싶을 때는 다음의 코드를 한다.
private static void printStringArray(String[] sArray) {
for (String s :
sArray) {
System.out.print(s + " ");
}
System.out.println();
}
private static void printDoubleArray(double[] dArray) {
for (double d :
dArray) {
System.out.print(d + " ");
}
System.out.println();
}
private static void printIntArray(int[] iArray) {
for (int i :
iArray) {
System.out.print(i + " "); // 1,2,3,4,5
}
System.out.println();
}
printIntArray(iArray);
printDoubleArray(dArray);
printStringArray(sArray);
각 메서드를 정리하고 다음과 같이 출력해야 한다. 하지만 이렇게 타입이 다르면 각 타입에 맞게 메서드를 계속 생성하여야 한다는 큰 문제점이 생긴다. 이는 코드가 길어질 뿐만 아니라 비효율적이게 된다. 이럴 때 쓰는 개념이 제네릭스이다.
어느 타입이던지 개의치 않고 사용할 수 있는 메서드를 만드는 것을 제네릭스를 이용해서 만들어보자.
private static <T> void printAnyArray(T[] array) {
for (T t :
array) {
System.out.println(t + " ");
}
System.out.println();
}
코드를 보면 <T> 라고 적혀있고 따로 자료형 타입은 적어놓지 않았다. 참고로 말하자면 굳이 <T>일 필요 없이 아무 글자나 적어도 상관은 없지만 (파라미터)와 (스태틱과 반환타입 사이) 쓰이는 문자가 같아야 한다,
그 후 매개변수 명은 임의로 정한 뒤 for each 문을 통하여 출력해내는 구조이다. 즉 제네릭스로 타입을 지정 시, 아무 타입이여도 작동을 하게 된다.
printAnyArray(iArray);
printAnyArray(dArray);
printAnyArray(sArray);
이런 식으로 각 타입이 다른 배열도 제네릭스를 활용한 메서드를 통하여서 안전하게 출력해 낸다. 하지만 지금 현 상태에서 위와 같은 메서드 출력 코드를 작성하여도 오류가 나타난다. 왜냐하면 제네릭스는 wrapper 클래스를 사용하기 때문이다.
그래서 다음과 같이 배열의 타입을 기본 타입형에서 wrapper 클래스로 바꾸어줘야 한다.
Integer[] iArray = {1, 2, 3, 4, 5};
Double[] dArray = {1.0, 2.0, 3.0, 4.0, 5.0};
String[] sArray = {"A", "B", "C", "D", "E"};

int -> Integer,
double -> Double
바꿔줘야만 정상적으로 제네릭스 메서드가 작동
사실 이는 Object 라는 개념과 비슷하다. Object는 모든 객체를 다 받게 자바에서 지원하므로 아무 타입이나 받을 수 있는 건 마찬가지다.
하지만 Object로 타입 지정 시, 매개변수로 엉뚱한 것을 받을 수 있어서 오류를 야기시킨다. 이는 형 타입 오류를 불러 일으키므로 함부로 사용하면 안 된다. 하지만 제네릭스 같은 경우는 <> 사이에 참조하는 클래스 명을 적을 수 있기에 Object와같은 오류를 일으키지 않는다. 정리하고 개념을 추가하자면,
Object 사용: Object 타입은 모든 객체를 담을 수 있으므로, 컴파일러가 타입 검사를 수행하지 않는다. 따라서 실행 시에 오류가 발생할 수 있다. 또한 타입 변환이 필요하다
제네릭스 사용: 제네릭스를 사용하면 컴파일러가 컴파일 시 타입을 검사하여 잘못된 타입의 객체가 들어가는 것을 방지한다. 이로써 코드의 안정성이 향상된다. 그리고 타입 변환이 필요하지 않다. 이를 제네릭스 클래스를 통해서 알아보자.
public class CoffeeByName {
public Object name; // Interger, Double, String, BlackBox... 등등
public CoffeeByName(Object name) {
this.name = name;
}
public void ready() {
System.out.println("커피 준비 완료" + name);
}
}
다음과 같은 클래스를 생성한 뒤, 타입을 Object를 사용해보았다. Object로 인하여 매개변수가 어떠한 타입이든 잘 된다. 즉 위의 코드 예제에서는 CoffeeByName 클래스를 사용하여 Object 타입을 활용하여 다양한 데이터 유형을 처리하는 방법으로 이 클래스는 Object 타입의 필드를 사용하여 어떠한 데이터 유형이라도 다룰 수 있게끔 설계되었고 이러한 유연성은 하나의 클래스로 여러 종류의 데이터를 처리하고 재사용하는 데 도움이 된다.
CoffeeByName c3 = new CoffeeByName(23);
c3.ready();
CoffeeByName c4 = new CoffeeByName("박명수");
c4.ready();
int c3Name = (int) c3.name;
System.out.println("주문 고객 번호 : " + c3Name);
// 값을 꺼내서 다른 곳에 저장하려고 하여도, 정해진 형은 Object 이기에 계속 형변환이 필요함
String c4Name = (String) c4.name;
System.out.println("주문 고객 이름 : " + c4Name);
Object 타입으로 데이터를 저장할 경우, 변수에 저장할 때, 변수의 자료형 타입에 맞게 형 변환(Casting)을 해야한다. 예를 들어, c3 객체에서 name 필드의 값을 정수로 사용하기 위해서는 (int) c3.name과 같이 형 변환을 해야한다. 이렇게 함으로써 데이터를 원하는 형식으로 읽고 처리할 수 있지만, 잘못된 형 변환을 시도하면 런타임 오류가 발생할 수 있으므로 주의가 필요하다.
또한 C4Name = (String) c4.name 과 같이 매개변수의 타입과 형 변환을 위하여 작성한 타입의 형태가 서로 다를 경우, 오류가 발생하게 된다. 제네릭스 클래스를 사용할 경우, 따로 형 변환을 해주거나 오류가 발생할 가능성을 없애준다.
public class Coffee <T> {
public T name;
public Coffee(T name) {
this.name = name;
}
public void ready() {
System.out.println("커피 준비 완료 : " + name);
}
}
Coffee<Integer> c5 = new Coffee<>(35);
c5.ready();
int c5Name = c5.name; // 형 변환 없이 위에서 <> 안에 형을 지정하였기에 바로 변수에 값을 대입할 수 있다
System.out.println("주문 고객 번호 : " + c5Name);
Coffee<String> c6 = new Coffee<>("소진영");
c6.ready();
String c6Name = c6.name; // <- 따로 형 변환 필요 없음
System.out.println("주문 고객 이름 : " + c6Name);
제네릭스 클래스를 지정하고, 해당 클래스의 객체를 만들 때의 코드이며, Object와는 달리 변수에 저장할 때, 변수의 자료형 타입에 맞게 형 변환이 필요가 없으며 <> 에 wrapper 클래스를 적으니 매개변수 안에 들어가 자료형을 틀리는 오류를 없애준다.
Coffee<T> 클래스는 제네릭타입 T를 사용하여 다양한 타입의 커피를 다룰 수 있도록 설계되었다. 하지만 CoffeeByUser<T extends User>와 같이 제한된 타입 매개변수를 사용하는 경우, T는 User 클래스 또는 User 클래스를 상속하는 클래스만을 받을 수 있다. 예시로는 아래와 같다.
public class CoffeeByUser<T extends User> {
public T user;
public CoffeeByUser(T user) {
this.user = user;
}
public void ready() {
System.out.println("커피 준비 완료 : " + user.name);
user.addPoint();
}
}
CoffeeByUser<User> coffee1 = new CoffeeByUser<>(new User("사용자1"));
coffee1.ready();
CoffeeByUser<VIPUser> coffee2 = new CoffeeByUser<>(new VIPUser("VIP 사용자"));
coffee2.ready();
위 예제에서 coffee1은 User 클래스의 객체를 받고, coffee2는 VIPUser 클래스의 객체를 받는다. 이러한 방식으로 CoffeeByUser 클래스에서는 엉뚱한 클래스의 객체를 받지 않고 타입 안정성을 보장할 수 있다.
다음으로는 제네릭 메서드에 대해서 배워보겠다.
orderCoffee("김영철");
orderCoffee(36);
orderCoffee("김영철", "라뗴");
orderCoffee(21, "아메리카노");
public static <T> void orderCoffee(T name) {
System.out.println("커피 준비 완료 : " + name);
}
// 제네릭스 T도 오버로딩 하여 매개변수를 늘릴 수 있다.
public static <T, V> void orderCoffee(T name, V coffee) {
System.out.println(coffee + " 준비 완료 : " + name);
}
제네릭 메서드는 메서드의 매개변수나 반환값에 제네릭 타입을 사용하는 것을 의미한다. 이렇게 하면 메서드를 호출할 때 다양한 타입의 데이터를 처리할 수 있다.
메서드 오버로딩을 통해 같은 이름의 메서드를 다양한 매개변수와 함께 정의할 수 있다. 여기서는 제네릭스 메서드를 사용하여 메서드의 매개변수를 더 다양하게 오버로딩 할 수 있다.
'Java' 카테고리의 다른 글
| Java (익명 클래스) (1) | 2024.01.06 |
|---|---|
| Java (컬렉션 프레임워크) (0) | 2024.01.06 |
| Java (추상 클래스와 인터페이스, 의존성 주입) (2) | 2024.01.03 |
| Java (클래스) (2) | 2024.01.02 |
| Java (메서드) (2) | 2024.01.02 |