본문 바로가기
Back-end

12 지네릭스, 열거형, 애너테이션 (1)

by 신재권 2021. 7. 17.

지네릭스란?

지네릭스는 다양한 타입의 객체들을 다루는 메서드나 컬렉션 클래스에 컴파일 시의 타입 체크(compile-time type check)를 해주는 기능이다. 객체의 타입을 컴파일 시에 체크하기 때문에 객체의 타입 안정성을 높이고 형변환의 번거로움이 줄어든다.

타입 안정성을 높인다는 것은 의도하지 않은 타입의 객체가 저장되는 것을 막고, 저장된 객체를 꺼내올 때 원래의 타입과 다른 타입으로 잘못 형변환되어 발생할 수 있는 오류를 줄여준다는 뜻이다.

예를 들어 ArrayList와 같은 컬렉션 클래스는 다양한 종류의 객체를 담을 수 있긴 하지만 보통 한 종류의 객체를 담는 경우가 더 많다. 그런데도 꺼낼 때마다 타입체크를 하고 형변환을 하는 것은 불편하다. 게다가 원치않는 종류의 객체가 포함되는 것을 막을 방법이 없다는 것도 문제다. 이러한 문제를 지네릭스가 해결해준다.

지네릭스의 장점

  1. 타입 안정성을 제공한다.
  2. 타입 체크와 형변환을 생략할 수 있으므로 코드가 간결해진다.

간단히 얘기하면 다룰 객체의 타입을 미리 명시해줌으로써 번거로운 형변환을 줄여준다.

지네릭 클래스의 선언

지네릭 타입은 클래스와 메서드에 선언할 수 있다.

예를 들어 클래스 Box가 다음과 같이 정의되어 있다 하자.

class Box{
	Object item;
	void setItem(Object item) { this.item = item; }
	Object getitem() {return item; }
}

이 클래스를 지네릭 클래스로 변경하면 다음과 같이 클래스 앞에 <T>를 붙이면 된다. 그리고 Object를 모두 T로 변경한다.

class Box<T>{
	T item;
	void setItem(T item) { this.item = item; }
	T getitem() {return item; }
}

Box<T>에서 T를 타입 변수(type variable)라고 하며, Type의 첫글자에서 따온 것이다. 타입 변수는 T가 아닌 다른 것을 사용해도 된다. ArrayList<E>의 경우 타입변수 E는 Element(요소)의 첫글자를 따서 사용한 것이다.

타입 변수가 여려개인 경우 Map<K,Y>와 같이 콤마를 구문자로 나열하면된다.

상황에 맞게 의미있는 문자를 선택해서 사용하는 것이 좋다.

이들은 기호의 종류만 다를 뿐 임의의 참조형 타입을 의미한다는 것은 모두 같다.

기존에는 다양한 종류의 타입을 다루는 메서드의 매개변수나 리턴타입으로 Obejct타입의 참조변수를 많이 사용했고, 그로 인해 형변환이 불가피했지만, 이제는 원하는 타입을 지정하기만 하면 된다.

이제 지네릭 클래스가 된 Box클래스의 객체를 생성할 때는 다음과 같이 참조변수와 생성자에 타입 T대신에 사용될 실제 타입을 지정해주어야 한다.

Box<String> b = new Box<String>();  //타입 T대신 실제 타입 지정
b.setItem(new Object());  //에러 String이외의 타입은 지정 불가
b.setItem("ABC"); 
String item = (String) b.getItem();  //형변환이 필요없다.

위의 코드에서 타입 T대신에 String타입을 지정해줬으므로, 지네릭 크래스 Box<T>는 다음과 같이 정의된 것과 같다.

class Box{ //지네릭 타입을 String으로 지정
		String item;
		void setItem(String item) {this.item = item;}
		String getItem() {return item;}
}

지네릭이 도입되기 이전의 코드와 호환을 위해 지네릭 클래스인데도 예전의방식으로 객체를 생성하는 것이 허용된다. 다만 지네릭타입을 지정하지 않아서 안전하지 않다는 경고가 발생한다.

 

아래와 같이 타입 변수 T에 Object타입을 지정하면,타입을 지정하지 않은 것이 아니라 알고 적은 것이므로 경고는 발생하지 않는다.

Box<Object> b = new Box<Object>();
b.setItem("ABC");  //경고 발생 안함
b.setItem(new Object());  //경고 발생 안함

 

지네릭스 용어

지네릭스에서 사용되는 용어는 다음과 같다.

다음과 같이 지네릭 클래스 Box가 선언되어 있다고 할 때,

class Box<T> { }

Box<T> : 지네릭 클래스, 'T의 Box'또는 'T Box'라고 읽는다.

T : 타입변수 또는 타입 매개변수.(T는 타입 문자)

Box : 원시타입(raw type)

타입 문자 T는 지네릭 클래스 Box<T>의 타입 변수 또는 타입 매개변수라고 하는데, 메서드의 매개변수와 유사한 면이 있기 때문이다. 그래서 아래와 같이 타입 매개변수에 타입을 지정하는 것을 '지네릭 타입 호출'이라고 하고, 지정된 타입 'String'을 매개변수화 된 타입(parameterized type)이라고 한다.

예를 들어 , Box<String>과 Box(Integer>는 지네릭 클래스 Box<T>에 서로 다른 타입을 대입하여 호출한 것일 뿐, 이 둘의 별개의 클래스를 의미하는 것은 아니다. 이는 마치 매개변수의 값이 다른 메서드 호출, 즉 add(3,5)와 add(2,4)가 서로 다른 메서드를 호출하는 것이 아닌 것과 같다.

컴파일 후에 Box<String>과 Box<Integer>는 이들의 원시타입인 Box로 바뀐다. 즉 지네릭 타입이 제거된다.

지네릭스의 제한

지네릭 클래스 Box의 객체를 생성할 떄, 객체별로 다른 타입을 지정하는 것은 적절하다.

지네릭스는 이처럼 인스턴스 별로 다르게 동작하도록 하려고 만든 기능이기 때문이다.

Box<Apple> appleBox = new Box<Apple>();
Box<Grape> grapeBox = new Box<Grape>();

그러나 모든 객체에 대해 동일하게 동작해야 하는 static 멤버에 타입 변수 T를 사용할 수 없다. T는 인스턴스변수로 간주되기 때문이다. 이미 알고 있는 것처럼 static멤버는 인스턴스 변수를 참조할 수 없다.

class Box<T> {
		static T item;  //에러
		static int compare(T t1, T t2) { } // 에러
}

static 멤버는 타입변수에 지정된 타입, 즉 대입된 타입의 종류에 관계없이 동일한 것이어야 하기 때문이다. 즉 Box<Apple>.item과 Box<Grape>.item이 다른 것이어서 안된다는 뜻이다. 그리고 지네릭 타입의 배열을 생성하는 것도 허용되지 않는다. 지네릭 배열 타입의 참조변수를 선언하는 것은 가능하지만, new T[10]과 같이 배열을 생성하는 것은 안된다는 뜻이다.

class Box<T>{
		T[] itemArr;  // T타입의 배열을 위한 참조변수
		T[] toArray(){
			T[] tmpArr = new T[itemArr.lengh]; // 에러, 지네릭배열 생성 불가
			..
			return tmpArr;
		}
		...
}

지네릭 배열을 생성할 수 없는 것은 new 연산자 때문인데, 이 연산자는 컴파일 시점에 타입 T가 뭔지 정확히 알아야 한다. 그런데 위의 코드에 정의된 Box<T> 클래스를 컴파일하는 시점에서는 T가 어떤 타입이 될지 전혀 알 수 없다. instanceof 연산자도 new 연산자와 같은 이유로 T를 피연산자로 사용할 수 없다.

 

꼭 지네릭 배열을 생성해야할 필요가 있을 때는 , new 연산자 대신 Reflection API의 newInstance()와 같이 동적으로 객체를 생성하는 메서드로 배열을 생성하거나, Object 배열을 생성해서 복사한 다음에 T[]로 형변환 하는 방법등을 사용한다.

'Back-end' 카테고리의 다른 글

Spring 개념  (0) 2021.07.18
SQL 문법(2)  (0) 2021.07.18
11 컬렉션 프레임웍(7)  (0) 2021.07.11
SQL 문법(1)  (0) 2021.07.11
11 컬렉션 프레임웍(6)  (0) 2021.07.10