Object 타입의 위험성
타입이 불특정될 때 Object 타입을 사용하는 것의 문제
public class CastingDTO implements Serializable {
private Object object;
public void setObject(Object obj) {
object = obj;
}
public Object getObject() {
return object
}
}
- 타입이 불특정적일 때 위처럼 Object 로 타입을 선언할 수 있다.
public static void main(String[] args) {
CastingDTO dto = new CastingDTO();
dto.setObject(new String());
String s1 = (String) dto.getObject();
String s3 = (StringBuilder) dto.getObject(); // error
}
- Object 클래스를 사용할 때 원하는 클래스로 사용하기 위해서는 형변환을 해야한다.
- 하지만 이러한 경우에 타입이 혼동될 수 있어서 잘 못 형변환하게 되면 에러를 반환한다.
- instanceof 라는 예약어를 사용해도 되지만 보일러 플레이트 코드가 많아진다는 단점이 있다.
제네릭
제네릭의 사용 이유
- 위에서 다뤘듯이 Object 는 타입 안정성이 떨어져 런타임시에 에러를 반환할 수 있고 개발자가 인지하기 힘들다.
- 제네릭은 이렇게 타입이 부정확한 객체에 컴파일 단계의 타입 안정성을 부여하기 위해 자바에서 제공하는 기능이다.
제네릭 사용
public class 클래스명<T> {}
- 클래스 선언문에 꺽쇠(<>)를 추가한다. 꺽쇠 안에는 가상의 타입 이름을 지정한다.
가상 타입 이름
꺽쇠 안에 들어가는 가상 타입 이름은 아무런 이름이나 지정해도 컴파일하는 데 전혀 상관 없다. 현재 존재하는 클래스를 사용해도 되고, 존재하지 않는 것을 사용해도 된다.
- 단, 되도록이면 클래스 이름의 명명 규칙과 동일하게 지정하는 것이 좋다.
- 이 이름은 클래스 안에서 하나의 타입 이름처럼 사용된다.
제네릭 사용 예제
public class CastingDTO<T> implements Serializable {
private T object;
public void setObject(T obj) {
object = obj;
}
public T getObject() {
return object;
}
}
public class GenericSample {
public static void main(String args[]) {
GenericSample sample = new GenericSample();
sample.checkGenericDTO();
}
public void checkGenericDTO() {
CastingGenericDTO<String> dto1=new CastingGenericDTO<String>();
dto1.setObject(new String());
CastingGenericDTO<StringBuffer> dto2=new CastingGenericDTO<StringBuffer>();
dto2.setObject(new StringBuffer());
CastingGenericDTO<StringBuilder> dto3=new CastingGenericDTO<StringBuilder>();
dto3.setObject(new StringBuilder());
}
}
- 위 Object 예제를 제네릭으로 변환했다.
- 이제 사용했을 때 형변환을 사용할 필요 없으며 컴파일 단계의 타입 안정성이 보장된다.
제네릭 타입 이름
앞서 클래스 선언 시 <> 안에 어떤 단어가 들어가더라도 상관없다고 했다. 그렇지만, 자바에서 정의한 기본 규칙은 있다.
- E: 요소 (Element, 자바 컬렉션에서 주로 사용됨)
- K: 키
- N: 숫자
- T: 타입
- V: 값
- S, U, V: 두 번째, 세 번째, 네 번째에 선언된 타입
꼭 이 규칙을 지켜야만 컴파일이 되는 것은 아니다.
하지만, 다른 어떤 사람이 보더라도 쉽게 이해할 수 있도록 하려면 이 규칙을 따르는 것이 좋다.
와일드카드
public class WildcardGeneric<W> {
W wildcard;
public void setWildcard(W wildcard) {
this.wildcard=wildcard;
}
public W getWildcard() {
return wildcard;
}
}
public class WildcardSample {
public void wildcardStringMethod(WildcardGeneric<String> c) {
String value=c.getWildcard();
System.out.println(value);
}
}
- 위 예제에서 wildcardStringMethod 메서드가 WildcardGeneric<String> 말고 다른 제네릭 타입은 받을 수 없을까?
- 지금까지 배운 내용으로는 불가능하다 이유는 제네릭한 클래스의 타입만 바꾼다고 Overloading 이 불가능하기 때문이다.
와일드 카드
public class WildcardSample {
public void wildcardMethod(WildcardGeneric<?> c) {
Object value=c.getWildcard();
if(value instanceof String) {
System.out.println(value);
}
}
}
- 여기서 ? 로 명시한 타입을 영어로 wildcard 타입이라고 부른다.
- 넘어오는 타입이 두세 가지로 정해져 있다면, 다음과 같이 메소드 내에서 instanceof 예약어를 사용하여 해당 타입을 확인하면 된다.
- 어떤 객체를 wildcard 로 선언하고, 그 객체의 값은 가져올 수는 있지만, 와일드 카드로 객체를 선언했을 때에는 이 예제와 같이 특정 타입으로 값을 지정하는 것은 불가능하다.
bounded wildcard : 제네릭 선언에 사용하는 타입의 범위 지정
< ? extends 타입>
- 제네릭 타입으로 타입 을 상속받은 모든 클래스를 사용할 수 있다는 의미다.
- 반드시 타입 클래스와 관련되어 있는 상속한 클래스가 넘어와야만 한다.
- wildcard와 마찬가지로, Bounded Wildcards로 선언한 타입에는 값을 할당할 수는 없다.
메소드를 제네릭하게 선언하기
wildcard 의 단점
- wildcard로 메소드를 선언하면 매개 변수로 사용된 객체에 값을 추가할 수가 없다는 큰 단점이 있다.
- 매개 변수로 제네릭 타입을 받지 않고 메소드 안에서 값을 부여해주려고 하면 오류가 난다.
public void genericMethod(WildcardGeneric<T> c) {
c.setWildcard("string");
T value=c.getWildcard();
System.out.println(value);
}
이 때, 아래와 같이 사용하면 객체에 값을 추가할 수 있다.
- 리턴 타입 앞에 <> 로 제네릭 타입을 선언
- 매개 변수에서 제네릭 타입이 포함된 객체를 받는다.
- 매개 변수로 받은 타입을 set 해준다.
public <T> void genericMethod(WildcardGeneric<T> c, T addValue) {
c.setWildcard(addValue);
T value=c.getWildcard();
System.out.println(value);
}
- 이처럼 메소드 선언 시 리턴 타입 앞에 제네릭한 타입을 선언해 주고, 그 타입을 매개 변수에서 사용하면 컴파일할 때 전혀 문제가 없다.
- 즉, 외부에서 넘겨주는 값을 T라는 제네릭 타입으로 받아서, 마찬가지로 T라는 제네릭 타입으로 선언된 객체에 넣어주는 것은 가능하다.
- 그렇지만 메소드 내부에서는 이 T가 어떤 타입인지를 모르기 때문에, String 값을 직접 넣어줄 수는 없는 것이다.
WildcardGeneric<String> wildcard=new WildcardGeneric<String>();
genericMethod(wildcard,"Data");
- Wildcard 처럼 타입을 두리뭉실하게 하는 것보다, 명시적으로 메소드 선언 시 타입을 지정해주면 보다 더 견고한 코드를 작성할 수 있다.
Bounded Wildcard처럼 사용하기
메소드 선언 시, 아래와 같이 Bounded Wildcard처럼 사용할 수 있다.
public <T extends Car> void genericMethod(WildcardGeneric<T> c, T addValue)
한 개 이상의 제네릭 타입 선언은 콤마로 구분하여 나열하면 된다.
// S와 T라는 제네릭 타입을 메소드에서 사용가능
public <S,T extends Car> void genericMethod(WildcardGeneric<T> c, T addValue, S another)
제네릭 deep dive
공변 비공변?
- 제네릭 문서나 관련 블로그 글을 찾다 보면 제네릭은 “불공변이다” 와 같은 글을 볼 수 있다.
- 공변과 불공변의 의미와 해당 개념이 왜 제네릭에 중심이 되는지 살펴보자
배열은 공변이다
- 공변이란 상속 관계가 있는 타입들이 복합 타입(배열같은)내에서 상속 관계가 성립하는 성질을 의미한다.
Number[] numbers = new Integer[3]; // O
Object[] objects = numbers; // O
- Integer는 Number 의 서브타입이다.
- 이러한 상속 관계가 성립하는 타입으로 이루어진 복합타입에서도 상속 관계가 성립하는 성질을 공변이라한다.
- 즉, 배열은 공변이다.
공변의 문제점
public class Main {
public static void main(String[] args) throws CloneNotSupportedException {
Number[] numbers = new Double[3];
numberArrayTest(numbers);
}
public static void numberArrayTest(Number[] numbers) {
numbers[0] = 11; // 런타임 에러 java.lang.ArrayStoreException: java.lang.Integer
}
}
- 공변의 문제점은 타입 안정성이 떨어지는 점이다.
- 공변인 타입으로 상위 클래스로 참조를 한다면 대체 가능함으로 개발의 유연성이라는 이점이 있다
- 하지만 위 예제처럼 실제 객체 타입을 알지 못해 런타임 예외로 해당 코드의 오류를 발견된다는 문제가 발생한다.
제네릭은 불공변이다.
class GenericItem<T>{
private T item;
public T getItem() {
return item;
}
public void setItem(T item) {
this.item = item;
}
}
- 위처럼 제네릭 타입을 만들어주자
GenericItem<Number> genericItem = new GenericItem<Integer>();
- 제네릭 타입은 원본 타입이 상하관계에 있다고 복합타입인 제네릭 타입은 상하관계를 성립하지 않다. 이를 불공변(Invariant) 라 한다.
불공변의 문제점
public static void printItem(GenericItem<Integer> genericItem) {
System.out.println(genericItem.getItem());
}
public static void printItem2(GenericItem<Double> genericItem) {
System.out.println(genericItem.getItem());
}
- 불공변은 상하관계를 성립하지 않으므로 GernericItem<Number> 처럼 상위 클래스로 참조하는 것은 불가능하다.
- 제네릭은 위에서 다뤘듯이 타입 안정성을 보장한다.
- 하지만 다형성으로 인한 개발의 유연함이 없다는게 단점이 될 수 있다.
- 따라서 java 에서는 와일드카드(?) 를 제공한다.
public static void printItem(GenericItem<?> genericItem) {
Object item = genericItem.getItem();
System.out.println(genericItem.getItem());
}
와일드 카드의 의미
GenericItem<?> genericItem = new GenericItem<Integer>();
GenericItem<?> genericItem2 = new GenericItem<Number>();
GenericItem<?> genericItem3 = new GenericItem<String>();
GenericItem<?> genericItem4 = new GenericItem<?>(); // 컴파일 에러
- 와일드 카드는 알 수 없는 타입(Unknown Type)을 나타낸다.
- 와일드 카드는 참조 제네릭 타입에만 사용할 수 있다
GenericItem<?> genericItem = new GenericItem<Integer>();
genericItem.setItem(new Integer(1)); // 컴파일 에러 발생
- 와일드 카드를 사용한 제네릭 타입은 set 할 수 없다는 제약이 존재한다.
- 이유는 해당 참조 변수는 타입을 알 수 없기 때문에 매게 변수의 타입 파라미터를 유추할 수 없기 떄문이다.
한정적 와일드 카드
- 와일드 카드의 범위를 결정할 수 있는데 이를 한정적 와일드카드라 부른다.
- 한정적 와일드 카드에는 상한, 하한이 존재한다.
- 상한 : ? extends T (T 또는 T를 상속받은 하위 타입만 가능)
- 하한 : ? super T (T 또는 T의 상위 타입만 가능)
제네릭 공변처럼 사용하기
- 제네릭은 불공변이라서 다형성을 사용할 수 없다는 단점이 존재한다.
- 상한 와일드 카드를 사용해서 공변처럼 사용하는 것이 가능하다.
GenericItem<Number> genericItem = new GenericItem<Integer>(); // 컴파일 에러 발생
- 위에서 다뤘듯이 제네릭은 불공변이기 때문에 위처럼 타입 변환이 가능하지 않다.
- 이를 상한 와일드 카드로 해결할 수 있다.
GenericItem<? extends Number> numberGenericItem;
GenericItem<? extends Integer> integerGenericItem = new GenericItem<Integer>();
numberGenericItem = integerGenericItem;
- 위처럼 상한 와일드 카드 방식을 사용하면 공변 관계처럼 현성된다.
- 하지만 약간의 제약이 존재하는데 unbounded 제네릭처럼 set 이나 add 를 못한다.
반공변? 제네릭 반공변처럼 사용하기
- 반공변이란 개념이 등장한다.
- 제네릭은 불공변이지만 하한 와일드카드를 사용하여 반공변처럼 사용할 수 있다.
GenericItem<? super Number> numberGenericItem = new GenericItem<Number>();
GenericItem<? super Integer> integerGenericItem;
integerGenericItem = numberGenericItem;
- 위처럼 상속관계가 반전된 것을 볼 수 있다( number ← integer , ? super Integer ← ? super Number)
GenericItem<? super Integer> integerGenericItem = new GenericItem<Integer>();
integerGenericItem.setItem(10);
Object item = integerGenericItem.getItem();
- 제네릭 하한을 이용하여 반공변처럼 사용한다면 set , add 같은 메서드를 사용할 수 있다.
결론
- 제네릭은 Object 타입의 문제점인 타입 안정성을 컴파일 타입에서 체크할 수 있는 미정 타입을 선언할 수 있는 기능이다.
- 하지만 제네릭은 불공변이므로 다형성의 이점을 얻을 수 없다.
- 상황에 따라 unbounded wildcard 혹은 bounded wildcard 를 통해서 공변처럼 혹은 반공변처럼 사용하여 안정성과 제약을 조절할 수 있다.
'JAVA' 카테고리의 다른 글
| System.currentTimeMills(), System.nanoTime() 알고 사용하자 (0) | 2025.12.16 |
|---|---|
| 깊은복사와 얕은복사란? Cloneable 을 사용하지 말자 (0) | 2025.11.24 |
| java 에서 equals 와 hashcode 를 재정의 해야하는 이유 (0) | 2025.11.11 |
| [java의 병렬 프로그래밍] complete 할 수 있는 Future -CompletableFuture 의 원리와 사용법 (0) | 2025.07.30 |
| [java의 병렬 프로그래밍] Thread를 사용하지 말자 - Executor, Executors, ExecutorService 원리 와 사용법 (0) | 2025.07.28 |