본문 바로가기
Java/Java Basic

[Java Basic] 39 - Generic 개요와 사용법

by Rosmary 2022. 9. 12.
728x90
반응형

 

 

 

 

Java의 클래스를 하나 만들어보자. 컬렉션 프레임워크와 유사하게 add() 매서드로 매개인자의 값을 컬렉션 프레임워크에 저장하고, toString()으로는 저장된 컬렉션 프레임워크 객체를 출력하는 기능을 넣어보자. 아래와 유사한 코드가 작성될 것이다.

 

 

 

이제, 이 클래스를 기반으로 DummyList의 각 인스턴스 내에 존재하는 arrList가 단일 객체 타입의 데이터만 저장되어야한다는 가정을 추가해보자. 만약 정수형이라면 아래와 같이 코드가 변경될 것이다.

 

 

 

나머지 자료형 역시 위와 같은 방식의 DummyList 클래스를 복사해서 이름만 변경하면 된다. 하지만 이 방법의 문제점은 7가지 기본 자료형 외의 객체형 타입 자료가 추가되는 경우, 그 객체 타입에 맞는 클래스를 다시 추가해서 정의해야한다는 것이 첫 번 째 문제고, 중복되는 코드가 여러 클래스에 산재되어 있기 때문에 향후 코드 수정이나 유지보수 시에도 상당히 비효율적이 된다는 것이 두 번 째 문제다. 

 

코드를 아래와 같이 변경하면 어떨까? 

 

 

 

동작은 한다. 하지만 add() 매서드 내의 조건 분기문으로 인해 코드 가독성이 좋지가 않다. 

 

이제 오늘 포스팅을 진행할 Generic을 사용하여 위의 코드를 간략하게 만들어보려한다. 

 

 

 

 

동일한 결과이지만 클래스 내부 코드는 훨씬 정갈해졌다. 지금부터는 필자가 위에서 사용한 Generics(제네릭스)에 대해 하나씩 살펴보려 한다.

 

 

1. Generics 개요

 

Generics는 클래스나 매서드가 다양한 객체(Object)를 다룰 수 있으나, 객체 타입 체크가 필요한 경우 사용하는 Java의 기능이다. 클래스나 매서드가 매개인자로 다양한 타입을 받아야 하는 상황에서 인스턴스마다 Java 타입을 분리하여 관리가 필요한 상황에서 사용한다.

 

위의 예시에서도 클래스 자체는 add() 매서드를 통해 여러 객체를 받을 수 있도록 매개변수 타입이 Object로 설정되어 있으나, 실제 인스턴스를 Integer로 사용한다고 호출하고 다른 타입을 매서드 매개변수로 사용하면 에러가 발생한다. 

 

"Integer를 사용하기로 했는데 왜 문자를 주나요?" 라는 컴파일러의 불만이다.

 

또한 Generics를 사용하면 코드에 타입을 검사하는 조건 분기문 자체가 사라지기 때문에 코드 가독성은 물론이거나 향후 유지보수도 매우 편리하다. 

 

그럼, Generics 클래스와 매서드의 선언 방법에 대해 알아보자.

 

 

 

2. Generics 클래스 선언

 

개요 부분에서 언급한대로 Generics는 클래스와 매서드에 적용할 수 있다. 이렇게 Generics가 적용된 클래스와 매서드를 Generics 클래스, Generics 매서드라고 한다. Java Documentation 문서의 ArrayList를 잠깐 보자.

 

 

 

ArrayList 클래스 선언부를 보면 public class ArrayList 가 아닌 public class ArrayList<E> 라고 표시된 것이 보인다. 즉, ArrayList는 일반적인 클래스가 아니라 Generics 클래스다.

 

Generic 클래스의 선언 코드에서 클래스명은 RawType(원시 타입)이라는 이름으로, 꺽쇠 부분은 타입 변수(또는 타입 문자)라는 이름으로 불린다. 즉 Generics 클래스 선언 코드는 다음과 같은 형태를 띈다.

 

Generics Class 선언문 = class 원시타입<타입문자>

 

 

이번에는 ArrayList 클래스에 정의된 매서드를 하나 선택하여 Documentation 내용을 확인해보자. 

 

 

 

매서드를 보면 클래스 선언 시 꺽쇠 내에 작성한 대문자 알바벳이 매개 변수의 타입을 나타내고 있음을 알 수 있다. 위의 내용을 코드로 해석하면 아래와 같이 나타낼 수 있다.

 

 

 

꺽쇠 내에 사용하는 알파뱃은 보통 대문자 한 글자만을 사용한다. Java에서는 T는 타입(type)의 약자로, E는 (Element)의 약자로, K, V는 키와 값(Key, Value)의 약자로 사용한다. 하지만 코드를 작성하는 사람의 마음에 따라 이 문자는 변경이 가능하다.

 

 

 

 

 

3. Generic 클래스 호출

 

Generic 클래스의 인스턴스 호출 방식은 일반 클래스의 인스턴스 호출과 별반 다르지 않다. 단지 원시타입명 옆에 지정할 자료타입만 작성해주면 된다. ArrayList 클래스를 Generics를 적용하여 호출해보자.

 

 

 

Generics 클래스 호출 시, 원시 타입 옆에 타입 변수를 지정해주는데, 이 타입 변수는 Java의 기본 자료형 7가지는 지원하지 않는다. 따라서 기본 자료형이 아닌 Wrapper 클래스를 사용해야 에러가 발생하지 않는다. 타입 변수가 int가 아닌 Integer라는 것을 확인하자.

 

 

앞서 보았듯이 ArrayList는 클래스 정의 시 Generics가 적용되어 있기 때문에 호출 역시 Generics를 적용하는 것이 사실 올바른 방식이다. 하지만 Generics를 적용하지 않고 호출하더라도 컴파일 에러는 발생하지 않는다. 대신, Generics 클래스임에도 불구하고 호출 시 Generics가 적용되어있지 않다면, 컴파일러는 경고 문구를 발생시킨다. 당장 위의 Eclipse에도 noGenerics 객체변수 선언 및 초기화 코드 왼쪽에 경고 아이콘이 뜨는 것을 확인할 수 있다. ArrayList가 원시 타입이므로 Generic 타입을 따르라는 경고다. 

 

 

 

여기까지는 일반적인 클래스의 선언 및 인스턴스화와 크게 다른 점이 없다. 단지 Generics 타입을 명시해준다는 점만 빼면 말이다. 

 

그런데, 필자가 ArrayList에 숫자와 관련된 데이터만을 집어넣고 싶다고 가정해보자. 즉 정수든 실수든 상관없이 숫자 형태면 저장이 되도록 말이다. Wrapper에서 숫자와 관련된 하위 클래스는 Short, Integer, Long, Float, Double이 있는데, ArrayList는 단 하나의 Generics만 지정할 수 밖에 없기 때문에 얼핏 보면 불가능해보인다.

 

그럼 이건 어떨까? 숫자와 관련된 Wrapper 하위 클래스가 공통적으로 상속하는 클래스는 Number라는 클래스다. Number를 타입 변수로 지정하면 어떻게 될까?

 

 

 

Number의 하위 클래스인 Short, Integer, Long, Float, Double 모두 사용이 가능하다. 다른 말로 하자면, 인스턴스 선언 시 지정한 타입 변수 객체와 상속 관계에 있는 클래스 중 자식 클래스는 모두 사용이 가능하다. 만약 Generics 클래스 사용 시, 특정 객체만을 사용해야 하는 상황이 발생한다면, 사용할 클래스들이 공통적으로 상속할 수 있는 (추상) 클래스를 하나 정의하고, 타입 변수로 추상 클래스 타입을 지정하면 된다는 것이다.

 

 

 

3. Generics 사용 제한

 

그러나 위의 방법도 문제가 존재하는데,  Generics 클래스 선언 시 타입 변수를 단순히 <T>로 지정하는 경우, 인스턴스 호출 시 모든 객체를 사용할 수 있게 된다는 것이다. 위의 예시에서는 단지 인스턴스 호출 시에만 사용 객체를 제한하는 것이기 때문에, 인스턴스 타입 변수를 Object로 지정하면 아무 객체나 적용할 수 있다는 문제가 생긴다는 말이다. 아래의 예시를 보자.

 

 

 

 

그럼 Generics 클래스 선언 시 사용할 객체를 제한할 수 있는 방법은 없을까? 이를 위해, Java는 Generics 타입 변수 입력 공간에 extends 라는 키워드를 제공한다.

 

< T extends Number >  :  Number 객체를 포함 및 상속하는 모든 객체 타입을 사용할 수 있음

 

extends는 클래스 상속 시에도 사용되어 매우 익숙한 키워드다. 말 그대로 키워드 뒤에 명시된 클래스를 상속한다는 의미를 가지기 때문에 꺽쇠 내에 extends를 적용하게 되면 지정한 클래스를 포함한 하위 클래스만 사용이 가능하게 된다.

 

 

 

타입 변수에서 extends 키워드는 클래스 뿐만 아니라 인터페이스도 지정이 가능하다. List의 경우 Java에서 인터페이스로 정의되어 있는데, 배열 값으로 List 형태의 객체를 받고 싶다면 아래와 같이 Generics 클래스를 정의하면 된다.

 

 

 

 

이런 경우는 어떨까? 필자가 Collection 인터페이스를 구현하는 Generics 클래스를 만들었는데, 부득이하게 타입 변수는 List를 구현받는 컬렉션 프레임워크만 넣어야되는 상황이 발생한다면, 필자는 Collection과 List를 동시에 상속받도록 Generics 클래스를 선언해야 한다. 이렇게 둘 이상의 인터페이스와 클래스를 동시에 구현/상속 받는 타입 객체를 정의하려면, extends 뒤에 두 클래스를 &로 묶어 입력해주면 된다.

 

 

 

 

 

4. Generics 와일드 카드

 

위에서 필자가 만든 GenericsTest 클래스 중 Number를 상속하는 모든 클래스 객체를 타입 변수로 지정한 클래스로 돌아가 보자. 이 클래스 내에 sumElements()라는 매서드를 만들고, arrList 내의 모든 요소 합이 int로 반환되도록 정의한다.

 

 

 

그런 다음, 수정한 GenericsTest 클래스의 인스턴스 객체를 매개 변수로 삼는 SumGenericsListElements를 정의하는데, 이 클래스는 인스턴스 생성 시, 매개 변수로 지정된 GenericsSTest 클래스의 sumElement()를 호출하여 그 결과를 출력하는 기능으로 만들어보자.

 

 

 

새로 만든 SumGenericsListElement의 매개변수는 GenericsTest 객체다. GenericsTest가 Generics 클래스이기 때문에 매개변수에도 타입 변수를 붙여 호출하는 것이 가능하다. 하지만 타입 변수를 매개변수 클래스명 뒤에 명시하는 순간, SumGenericsListElements 클래스는 타입 변수 외의 객체는 호출을 할 수 없게 된다.

 

 

 

 

위의 경우, 실제 GenericsTest 클래스 내 ArrayList가 가질 수 있는 데이터의 타입은 Short, Integer, Long 등 Number 클래스를 상속하는 클래스다. 하지만 SumGenericsListElements의 생성자 매개 변수에 사용되는 GenericsTest는 단지 Integer로 명시된 GenericsTest만 받을 수 있도록 정의되어 있다. 이렇게 되면 new GenericsTest<Double>, new GenericsTest<Long> 등으로 초기화된 인스턴스 객체는 SumGenericsListElement의 생성자로 사용이 불가능해진다.

 

그럼, 생성자를 오버로딩 하는 방법은 어떨까?

 

 

타입 변수가 다르지만 매개변수로 받아들이는 값의 타입이 GenericsTest로 기존의 생성자와 동일하기 때문에 오버로딩이 진행되지 않는다. 즉, 타입 변수의 차이만으로는 생성자 오버로딩을 진행할 수 없다.

 

이러한 이유로 Java는 와일드 카드라는 개념을 제공한다. 와일드 카드는 매서드나 클래스 생성자에 사용되는 매개변수가 Generics가 적용된 객체이며, 하나 이상의 객체를 인스턴스로 생성할 수 있는 경우, 매개변수에 사용 가능한 클래스를 정의하는 방법이라 보면 된다. 매개변수 타입명이 작성된 꺽쇠 내에 다음의 내용 중 하나를 넣게 된다.

 

<? extends 상한제한클래스객체> : 상한제한클래스객체를 상속받는 모든 자손만 사용 가능

<? super 하한제한클래스객체>: 하한제한클래스객체의 조상만 사용 가능

<?>                                            : 제한 없음 

 

현재 GenericsTest는 Number를 상속받고 있으니 매개변수의 타입 변수 역시 아래와 같이 변경하면 된다.

 

 

 

이 말은 GenericsTest 타입 변수로 지정된 객체 타입 중, Number 클래스를 상속받는 타입 변수로 지정된 GenericsTest만 매개 변수로 사용하겠다는 의미다. 위에서 에러로 인해 주석처리한 부분의 주석을 해제해보자. 방금 전과 달리 에러 없이 정상적으로 코드가 동작함을 확인할 수 있다.

 

 

 

만약 합산 결과를 GenericsTest<Integer>와 GenericsTest<Number>, GenericsTest<Object>로 선언된 인스턴스에 대해서만 진행한다고 하면, extends Number 대신 super Integer로 타입 변수의 와일드 카드를 변경해주면 된다.

 

 

 

 

5. Generics 매서드의 선언과 사용

 

이번에는 매서드에 Generics를 적용해보자. 필자가 추가로 정의한 SumGenericsListElements 클래스의 생성자의 코드를 매서드 sumElements()로 제작하려 한다.

 

 

 

여기까지는 이해가 크게 어렵지 않은 코드다. 하지만 필자가 매서드를 통해 진행하려는 내용은, 매개변수로 Generics 타입 변수를 받는 것이 아니라, 매서드 자체에서 특정 타입 변수만 사용이 가능하도록 정의하고 싶은 것이다. 마치 Generics 클래스의 인스턴스에 타입 변수를 명시하는 것이 아니라 클래스 자체에 사용할 타입을 Generics로 정의하듯이 말이다.

 

매서드에서 Generics를 적용하는 방법은, 반환 타입 앞에 꺽쇠 및 타입 변수를 작성하는 것이다. 만약 매서드에서 사용하는 타입 변수가 매개변수에도 적용이 된다면 매개변수의 타입 변수는 매서드에서 명시한 타입 문자를 대입해주면 된다.

 

 

 

위의 내용을 해석하면, sumElements는 GenericsTest라는 이름의 Generics 클래스 객체를 매개변수로 받으며, 이 매개변수에 적용되는 타입 변수는 매서드에 정의된 대로 Number 클래스의 자식 객체만이 지정될 수 있다는 의미다.

 

이렇게 정의된 Generics 매서드의 호출은 Generics 클래스 호출과 약간 다르다. Generics 매서드 호출 시 타입 변수는, Generics 클래스 호출과 달리 매서드 명 앞에 타입 변수를 명시해준다. Generics 클래스와 동일하게 클래스명 뒤에 타입 변수를 명시하면 매서드인지 클래스인지 헷갈릴 우려가 있어 포맷을 달리 가져간 듯 싶다.

 

 

 

참고로 Generics 클래스 안에 작성된 Generics 매서드의 경우, 타입변수로 사용된 알파벳이 같더라도 서로 별개의 타입으로 보아야 한다. 지금 필자가 작성한 매서드를 기존의 GenericsTest 클래스 내에 static 매서드로 재정의한다고 해보자.

 

 

 

클래스의 타입 변수 T는 Number 클래스의 자식인 객체만이 위치할 수 있으나, 매서드는 단지 Integer 클래스의 자식 객체만 지정이 가능하다. 타입 변수는 동일하게 T로 두었음에도 별개의 Generics 타입으로 인식한다는 의미다. 또한 매서드의 매개 변수에 지정된 타입 변수도, Generics 매서드의 타입 변수가 지정된 경우 Generics 클래스의 타입 변수를 따르는 것이 아니라 매서드 타입 변수를 따르게 된다. 따라서 sumElements() 매서드를 Integer 외의 값이 저장된 arrList를 매개변수로 호출하면 에러가 발생하는 것을 확인할 수 있다.

 

 

 

Generics 클래스와 달리 Generics 매서드는 static 매서드에서도 사용이 가능한데, 매서드를 호출하기 전에 타입이 결정되기 때문이다. Generics 클래스 타입 변수를 static 매개변수의 타입 변수로 사용하지 못하는 이유는, 호출은 했으나 어떤 타입을 사용할 지 결정이 되지 않기 때문이라는 것을 다시 상기해보면 이해가 된다.

 

클래스 선언 -> static 변수 및 매서드의 매모리 로딩 -> static 매서드에 타입이 지정된 경우, 인스턴스 초기화가 필요하므로 에러 발생.

 

정리하자면, Generics 매서드의 타입 변수는, Generics 클래스의 타입 변수와는 구별되는 개념이며, 매서드의 지역변수처럼 동작한다고 생각하면 된다.

 

 

 

6. Generics 타입의 형변환

 

앞서 보았던 내용 중에, Generics 클래스로 정의되어 있음에도 불구하고 인스턴스 생성 시 타입 변수를 지정하지 않아도 경고만 뜰 뿐 동작에는 큰 문제가 없음을 언급했던 내용이 있다. 그럼 여기서 의문이 하나 생긴다. 

 

ArrayList test1 = new ArrayList();

ArrayList<Integer> test2 = new ArrayList<Integer>();

 

둘 사이 형 변환은 가능할까? 

 

 

 

형 변환이 가능하다. 단지 ArrayList가 Generics로 정의되어 있기 때문에 타입 안전성 문제로 경고만 발생한다. 그럼, 타입이 서로 다른 Generics 클래스는 형 변환이 가능할까?

 

 

 

직접적인 형 변환(tmp3)은 불가능하지만 원시타입을 거치는 형변환(tmp1, tmp2)은 가능한 것을 확인할 수 있다. 아무래도 Integer와 Double은 직접적인 상속 관계를 이루지 않기 때문에 어떻게 보면 당연한 결과다. 그럼, 서로 상속 관계를 가지는 Number와 Integer는 변환이 가능할까? 

 

 

 

상속 관계를 이루는 객체라고 하더라도 바로 형 변환은 불가한 것을 알 수 있다. 그럼, Integer와 <? extends Number> 사이의 형 변환은 가능할까?

 

 

 

tmp2, 즉 Integer를 저장하는 List가 tmp3로 복사가 이루어지지는 않았지만, 형 변환은 가능한 것이 확인된다. 매개변수에 적용한 Generics매서드의 매개변수에서 와일드카드를 사용하더라도 특정 클래스(상한/하한 제한 클래스)를 명시하여 매개변수로 사용할 수 있는 이유가 여기에 있다.

 

와일드카드로 명시된 Generics 타입 간의 형변환도 가능하기는 하지만 타입이 확정되지 않은 상태로 변환되기 때문에 경고가 나타난다. 많이 사용하는 방법은 아니니 예시는 생략한다..

 

 

 


 

 

Java에서 Generics를 모르더라도 기본적인 프로그램 제작에는 큰 문제가 없다. 코드를 조금 더 섬세하고 장황하게 쓰면 말이다. 그럼에도 불구하고 Generics의 기본 개념 정도는 숙지하는 것이 좋은 이유는 Java의 Documentation 때문이다. Java Documentation에는 Generics 타입으로 작성된 클래스나 매서드들이 상당히 많은데, 이들의 의미를 모르면 클래스와 매서드를 제대로 사용할 수 없기 때문이다.

 

다음 포스팅에서는 열거형(Enumeration)에 대해 알아보려 한다.

 

 

Fin.

 

 

 

반응형

댓글