제네릭(Generics)은 자바에서 타입의 안전성을 보장하고 코드 재사용성을 높이기 위해 도입된 중요한 기능입니다. 하지만 제네릭을 사용할 때 공변(Covariance), 반공변(Contravariance), 무공변(Invariance) 개념을 이해하지 못하면, 와일드카드(? extends T, ? super T)나 특정 타입 제한을 설정할 때 혼란스러울 수 있습니다. 이번 글에서는 자바에서의 제네릭 변성(Variance)에 대해 쉽게 정리해보겠습니다.
1. 공변(Covariance) - ? extends T
공변이란 서브타입 관계가 유지되는 것을 의미합니다. 즉, List<Cat>이 List<Animal>의 하위 타입으로 간주될 수 있는 경우입니다.
예제 코드
class Animal {
void speak() { System.out.println("Animal speaks"); }
}
class Cat extends Animal {
void meow() { System.out.println("Meow"); }
}
public class CovarianceExample {
static void makeAnimalsSpeak(List<? extends Animal> animals) {
for (Animal animal : animals) {
animal.speak(); // Animal의 메서드는 호출 가능
}
}
public static void main(String[] args) {
List<Cat> cats = List.of(new Cat());
makeAnimalsSpeak(cats); // List<Cat>을 List<? extends Animal>로 전달 가능
}
}
특징
- ? extends T를 사용하면 읽기(read)는 가능하지만 쓰기(write)는 불가능합니다.
- makeAnimalsSpeak(List<? extends Animal> animals)에서 animals.add(new Animal()) 같은 코드는 컴파일 오류가 발생합니다.
- 이유는 animals가 List<Dog>일 수도 있기 때문입니다. Dog의 리스트에 Animal을 추가하면 타입 안정성이 깨질 수 있습니다.
✅ 정리: ? extends T는 "읽기 전용"을 허용하지만, 타입 안전성을 위해 새로운 객체 추가는 금지됩니다.
2. 반공변(Contravariance) - ? super T
반공변이란 슈퍼타입 관계가 유지되는 것을 의미합니다. 즉, List<Animal>이 List<Cat>의 상위 타입으로 간주될 수 있는 경우입니다.
예제 코드
import java.util.*;
class Animal {
void speak() { System.out.println("Animal speaks"); }
}
class Cat extends Animal {
void meow() { System.out.println("Meow"); }
}
public class ContravarianceExample {
static void addCats(List<? super Cat> cats) {
cats.add(new Cat());
}
public static void main(String[] args) {
List<Animal> animals = new ArrayList<>();
addCats(animals); // List<Animal>을 List<? super Cat>으로 전달 가능
}
}
특징
- ? super T를 사용하면 쓰기(write)는 가능하지만 읽기(read)는 제한적입니다.
- cats.add(new Cat())는 가능하지만, cats.get(0).meow() 같은 코드는 컴파일 오류가 발생합니다.
- 이유는 cats가 List<Object>일 수도 있기 때문입니다. Object 타입으로 반환하면 meow() 메서드를 보장할 수 없습니다.
✅ 정리: ? super T는 "쓰기 전용"을 허용하지만, 가져올 때는 Object로만 사용할 수 있습니다.
3. 무공변(Invariance) - T (기본적인 제네릭)
무공변이란 서브타입 관계가 유지되지 않는 것을 의미합니다. 즉, List<Cat>은 List<Animal>로 자동 변환되지 않습니다.
예제 코드
List<Animal> animals = new ArrayList<Cat>(); // ❌ 컴파일 오류
- List<T>는 기본적으로 무공변이므로, List<Cat>을 List<Animal>에 할당할 수 없습니다.
- 이를 해결하려면 ? extends T 또는 ? super T를 사용해야 합니다.
✅ 정리: 제네릭을 사용할 때 명시적인 변성을 지정하지 않으면 무공변이 기본적으로 적용됩니다.
4. 공변, 반공변, 무공변 비교
변성 유형 | 키워드 | 읽기 | 쓰기 | 예제 |
공변(Covariance) | ? extends T | ✅ 가능 | ❌ 불가능 | List<? extends Animal> |
반공변(Contravariance) | ? super T | ⚠️ 제한적 | ✅ 가능 | List<? super Cat> |
무공변(Invariance) | T | ✅ 가능 | ✅ 가능 | List<T> |
5. 정리 및 활용 팁
- ? extends T: 읽기 전용 리스트를 만들 때 사용 (예: List<? extends Number>)
- ? super T: 쓰기 전용 리스트를 만들 때 사용 (예: List<? super Integer>)
- T: 일반적인 제네릭 타입으로 사용 (예: List<String>, List<Integer>)
제네릭 변성을 이해하면 자바의 컬렉션 프레임워크나 API를 활용할 때 더욱 안전하고 유연한 코드를 작성할 수 있습니다.
'Programming > Java' 카테고리의 다른 글
Java를 활용한 SFTP 파일 다운로드: 가장 최근 변경된 파일 찾기 (1) | 2025.04.08 |
---|---|
Java Collections Framework (JCF) 상세 설명 (0) | 2025.04.07 |
자바에서 List 중복 제거 방법 (0) | 2025.04.07 |
Java에서 Object 타입을 String으로 변환하는 방법 (String) 캐스팅 vs String.valueOf() (0) | 2025.04.03 |
EFFECTIVE JAVA 3/E(이펙티브 자바 3/E 정리) - 1. 객체 생성과 파괴 (0) | 2022.08.16 |