본문 바로가기

Java

Java (제네릭스)

제네릭스 : 컴파일 단계에서 자료형을 체크해주는 도구 

 

제네릭스라는 개념은 처음 들을 때, 많이 낯설고 어렵게만 들리는 개념이기도 하며, 위의 말처럼 듣기만 했을 때는 잘 이해가 가질 않게 된다. 지금부터 천천히 알아보자.

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