자바에서 Collection을 사용하다보면 <>
안에 Element의 타입을 종종 보곤하는데요. 이 부분을 '제네릭'이라고 합니다. 그럼 이 제네릭을 왜 쓰는 것일까요? 다음과 같은 사용자가 만든 클래스가 있다고 생각해봅시다.1
public class CastingDto implements Serializable {
private Object object;
public Object getObject() {
return object;
}
public void setObject(Object object) {
this.object = object;
}
}
그리고 몇 개의 객체를 생성해보겠습니다.
public class GenericSample {
public static void main(String[] args) {
GenericSample sample = new GenericSample();
sample.checkGenericDto();
}
private void checkGenericDto() {
GenericDto dto1 = new GenericDto();
dto1.setObject(new String());
GenericDto dto2 = new GenericDto();
dto1.setObject(new StringBuffer());
GenericDto dto3 = new GenericDto();
dto1.setObject(new StringBuilder());
}
}
이 경우 Object
타입으로 GenericDto 객체에 넣어두고 꺼내기 때문에 해당 타입으로 정의하려면 casting이 필요할 것입니다.
String temp1 = (String)dto1.getObject();
StringBuffer temp2 = (StringBuffer)dto2.getObject();
StringBuilder temp3 = (StringBuilder)dto3.getObject();
이렇게 타입 형 변환에서 발생할 수 있는 문제점을 사전에 없애기 위해 Generic이 만들어졌습니다. JDK5부터 등장했다고 하는데요. 그전엔 이게 없이 모두 형변환을 하여서 사용했었나봐요. 그럼 Generic을 사용해서 위 코드를 바꿔볼까요?
public class GenericDto<T> implements Serializable {
private T object;
public T getObject() {
return object;
}
public void setObject(T object) {
this.object = object;
}
}
이전의 코드와 다른 점은 <T>
를 통해 해당 클래스 내 타입을 통일시킨다는 점입니다. 따라서 애초 객체 생성시 지정된 타입에 따라 그 안의 메서드들도 해당 타입으로 사용되는 것이죠. 그렇다보니 자연스럽게 해당 타입을 사용하는 부분이면 모두 T
로 선언되어 있죠. 따라서 이전의 checkGenericDto()
부분은 다음과 같이 수정할 수 있습니다.
private void checkGenericDto() {
GenericDto<String> dto1 = new GenericDto<>();
dto1.setObject(new String());
GenericDto<StringBuffer> dto2 = new GenericDto<>();
dto2.setObject(new StringBuffer());
GenericDto<StringBuilder> dto3 = new GenericDto<>();
dto3.setObject(new StringBuilder());
}
제네릭(<>
)안에 사용될 타입을 지정해두고 그에 따라 객체를 생성합니다. 만약 객체 생성시 지정한 타입과 실제값을 넣을 때 타입이 일치하지 않으면 컴파일시 오류를 발생시켜 실수를 미연에 방지하는 것이죠. 이렇게 지정하였기 때문에 사실 값을 꺼내올 때도 따로 형변환을 할 필요가 없습니다.
String temp1 = dto1.getObject();
StringBuffer temp2 = dto2.getObject();
StringBuilder temp3 = dto3.getObject();
위에서 GenericDto
클래스 생성시 Generic안에 T로 정의해두었는데요. 사실 T이외에 따른 알파벳을 써도 문제없습니다. 다만 자바에서 정한 규칙이 있는데요. 다음과 같습니다.
- E : 요소(Element, 자바 컬렉션(Collection)에서 주로 사용)
- K : 키
- N : 숫자
- T : 타입
- V : 값
- S, U, V : 두번째, 세번째, 네번째에 선언된 타입
좀 더 나아가 <?>
와 <? extends 특정클래스>
를 사용할 수 있는데요. 이 부분은 가볍게 다루고 이후 직접적으로 필요성을 느꼈을 때 좀 더 상세하게 정리해볼게요. <?>
(wildcard, 와일드카드) 은 다음과 같은 상황에서 사용합니다.
private void wildCardGeneric(GenericDto<?> anything) {
Object value = anything.getObject();
System.out.println(value);
}
보통은 메서드의 매개변수로 사용되고 또 그렇게 사용되는 것을 권장하는데요. GenericDto
의 객체를 받는데 어떤 타입으로 올지 모를 때 위와 같이 <?>
를 사용할 수 있는 것이죠. 주의할 점은 위와 같이 객체를 wildcard로 선언하고 값을 가져올 수는 있지만 해당 객체에 값을 지정하는 것을 불가능하다는 것입니다.
wildcardGeneric<?> wildcard = new WildcardGeneric<String>();
wildcard.setWildcard("A"); // 불가능
비록 String
타입의 객체를 받았다고 하더라도 wildcard로 선언되어 있다면 값 지정은 안되는 것입니다.
<? extends 특정클래스>
는 이를 사용하는 소스코드를 먼저 살펴보겠습니다.
private void boundedWildcardMethod(GenericDto <? extends Car> c) {
Car value = c.getObject();
System.out.println(value);
}
위 소스코드를 살펴보면 위 wildcard와 유사하지만 모든 타입을 받는 것이 아니라 Car
를 상속한 클래스에 대해서만 받는다는 것입니다. 예를들어 Bus
, Truck
과 같은 클래스가 Car
를 상속하고 있다면 해당 타입을 받을 수 있지만 이외 Car
을 상속받지 않는 클래스는 오류가 발생하는 것입니다. wildcard에서 받을 수 있는 범위를 제한시킨 것이라고 이해하면 될 것 같습니다.
이로써 제네릭에 대해 대략적으로 모두 살펴보았습니다. 막상 알면 쉽고 어렵지 않지만 모르면 외계코드처럼 보이고 두려워지는 문법인 것 같습니다. 저도 이참에 정리하니 앞으로 제네릭 타입을 보더라도 크게 당황하진 않을 수 있을 것 같아요!
'Language > Java' 카테고리의 다른 글
JVM(Java Virtual Machine) (0) | 2019.01.29 |
---|---|
예외처리 (0) | 2018.10.25 |
enum (2) | 2018.10.22 |
인터페이스(Interface) (0) | 2018.10.15 |
상속(Inheritance) (3) | 2018.10.11 |