안녕하세요, 여러분! 오늘은 김영한님의 자바 중급 강의에서 배운 제네릭의 꽃이라 할 수 있는 '와일드카드'에 대해 함께 알아볼게요. 💕 제네릭을 사용하다 보면 와일드카드를 만나게 되는데, 이 개념이 처음에는 조금 어렵게 느껴질 수 있어요. 하지만 차근차근 이해하면 자바 코딩의 새로운 세계가 열릴 거예요!
난이도: ⭐⭐⭐⭐☆
📌 오늘의 핵심 개념
- 와일드카드(Wildcard) 이해하기
- 제네릭 메서드와 와일드카드 비교하기
- 각각의 사용 필요 상황 및 차이점
- 타입 이레이저(Type Erasure) 알아보기
💡 와일드카드란 무엇일까요?
와일드카드는 하나의 특수 문자로 여러 문자를 대표할 수 있는 기호예요. 자바 제네릭에서는 주로 ? 기호를 사용하죠. 중요한 건 와일드카드로 제네릭 타입을 새로 구현하는 것이 아니라, 이미 구성된 제네릭 타입을 매개변수로 받을 때 활용한다는 점이에요!
자, 그럼 제네릭 메서드와 와일드카드의 차이점을 표로 정리해볼까요?
제네릭 메서드 | 타입 안전성, 명확한 반환 타입 지정 가능 | 구현이 더 복잡함 | 반환 타입이 필요할 때 / 메서드에만 제네릭 적용할 때 |
와일드카드 | 간결한 문법, 구현이 단순함 | 타입 반환 시 제한적 | 코드를 간결하게 만들 때 / 이미 만들어진 제네릭 타입을 활용할 때 |
와일드카드의 종류
와일드카드는 크게 세 가지 유형으로 나눌 수 있어요:
1. 비제한 와일드카드 (Unbounded Wildcard)
Box<?> box;
?만 사용한 와일드카드로, 어떤 타입이든 받을 수 있어요. Object를 사용하는 것과 비슷하지만, 제네릭의 타입 안전성을 유지한다는 장점이 있답니다.
2. 상한 와일드카드 (Upper Bounded Wildcard)
Box<? extends Animal> box;
extends 키워드를 사용해 상한선을 정하는 와일드카드예요. 위 예시에서는 Animal 또는 그 하위 클래스(Cat, Dog 등)만 허용한다는 의미입니다.
3. 하한 와일드카드 (Lower Bounded Wildcard)
Box<? super Animal> box;
super 키워드를 사용해 하한선을 정하는 와일드카드예요. Animal 또는 그 상위 클래스(Object 등)만 허용한다는 의미입니다.
🧩 실제 코드로 비교해볼까요?
아래 코드를 통해 제네릭 메서드와 와일드카드의 차이점을 살펴볼게요.
먼저 우리가 사용할 Box 클래스와 Animal 계층 구조를 간단히 정의해볼게요:
// 간단한 제네릭 Box 클래스
public class Box<T> {
private T value;
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
}
// Animal 클래스 계층 구조
public class Animal {
private String name;
public Animal(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
public class Dog extends Animal {
private int age;
public Dog(String name, int age) {
super(name);
this.age = age;
}
public int getAge() {
return age;
}
}
public class Cat extends Animal {
// 생략
}
제네릭 메서드 vs 와일드카드 메서드
이제 실제로 제네릭 메서드와 와일드카드를 사용한 메서드를 비교해볼게요:
public class WildcardEx {
// 제네릭 메서드 버전 1
static <T> void printGenericV1(Box<T> box) {
System.out.println("T = " + box.getValue());
}
// 와일드카드 버전 1
static void printWildcardV1(Box<?> box) {
System.out.println("? = " + box.getValue());
}
// 제네릭 메서드 버전 2 (타입 제한)
static <T extends Animal> void printGenericV2(Box<T> box) {
T t = box.getValue();
System.out.println("이름 : " + t.getName());
}
// 와일드카드 버전 2 (타입 제한)
static void printWildcardV2(Box<? extends Animal> box) {
Animal animal = box.getValue();
System.out.println("이름 : " + animal.getName());
}
// 반환 타입이 있는 제네릭 메서드
static <T extends Animal> T printAndReturnGeneric(Box<T> box) {
System.out.println("이름 : " + box.getValue().getName());
return box.getValue();
}
// 반환 타입이 있는 와일드카드 메서드
static Animal printAndReturnWildcard(Box<? extends Animal> box) {
System.out.println("이름 : " + box.getValue().getName());
return box.getValue();
}
}
두 방식의 중요한 차이점
위 코드에서 가장 중요한 차이점은 마지막 두 메서드에서 확인할 수 있어요:
// 제네릭 메서드는 정확한 타입 T를 반환할 수 있어요
static <T extends Animal> T printAndReturnGeneric(Box<T> box) { ... }
// 와일드카드는 정확한 타입을 반환할 수 없고, 상한 타입인 Animal로만 반환해요
static Animal printAndReturnWildcard(Box<? extends Animal> box) { ... }
메인 메서드에서 이 차이를 확인해볼까요?
public class WildcardMain1 {
public static void main(String[] args) {
Box<Dog> dogBox = new Box<>();
dogBox.setValue(new Dog("멍멍이", 100));
// 두 방식 모두 출력은 동일하게 작동해요
WildcardEx.printGenericV1(dogBox);
WildcardEx.printWildcardV1(dogBox);
WildcardEx.printGenericV2(dogBox);
WildcardEx.printWildcardV2(dogBox);
// 여기서 차이가 나타나요!
Dog dog = WildcardEx.printAndReturnGeneric(dogBox); // 정확한 Dog 타입으로 반환
Animal animal = WildcardEx.printAndReturnWildcard(dogBox); // Animal 타입으로만 반환 가능
// 아래 코드는 컴파일 에러가 발생해요
// Dog anotherDog = WildcardEx.printAndReturnWildcard(dogBox); // 에러!
}
}
🌟 하한 와일드카드(super)의 활용
하한 와일드카드는 주로 데이터를 넣을 때(쓰기) 유용해요. 예를 살펴볼게요:
public class WildcardMain2 {
public static void main(String[] args) {
Box<Object> objectBox = new Box<>();
Box<Animal> animalBox = new Box<>();
Box<Dog> dogBox = new Box<>();
Box<Cat> catBox = new Box<>();
// Animal 또는 그 상위 타입의 Box만 전달 가능해요
writeBox(objectBox); // Object는 Animal의 상위 타입이므로 가능
writeBox(animalBox); // Animal은 가능
// writeBox(dogBox); // Dog는 Animal의 하위 타입이므로 불가능
// writeBox(catBox); // Cat도 마찬가지로 불가능
Animal animal = animalBox.getValue();
System.out.println("animal = " + animal);
}
// 하한 와일드카드 - Animal이나 그 상위 타입을 받을 수 있어요
static void writeBox(Box<? super Animal> box) {
box.setValue(new Dog("멍멍이", 100));
}
}
🔍 타입 이레이저(Type Erasure)
제네릭의 또 다른 중요한 개념은 타입 이레이저예요. 타입 이레이저란 컴파일 시점에는 타입 체크를 하지만, 런타임에는 타입 정보가 제거되는 것을 말해요.
public class EraserBox<T> {
// 아래 코드는 컴파일 에러가 발생해요! T 타입 정보는 런타임에 사라지기 때문이죠.
// public boolean instanceCheck(Object param) {
// return param instanceof T; // 컴파일 에러!
// }
// public T create() {
// return new T(); // 컴파일 에러!
// }
}
이런 제한이 있는 이유는 자바의 제네릭이 '소거(erasure)' 방식으로 구현되었기 때문이에요. 컴파일러는 타입 안전성을 검사한 후 타입 매개변수 정보를 지워버리고, 필요한 곳에 타입 캐스팅 코드를 넣어줍니다.
🚀 실전 예제: 스타크래프트 유닛 관리하기
마지막으로, 제네릭과 와일드카드를 활용한 실전 예제를 살펴볼게요. 스타크래프트의 유닛을 관리하는 셔틀을 구현해볼게요!
// 기본 유닛 클래스
public class BioUnit {
private String name;
private int hp;
public BioUnit(String name, int hp) {
this.name = name;
this.hp = hp;
}
public String getName() {
return name;
}
public int getHp() {
return hp;
}
@Override
public String toString() {
return "BioUnit{" +
"name='" + name + '\'' +
", hp=" + hp +
'}';
}
}
// 종족별 유닛 클래스들
public class Marine extends BioUnit {
public Marine(String name, int hp) {
super(name, hp);
}
}
public class Zergling extends BioUnit {
public Zergling(String name, int hp) {
super(name, hp);
}
}
public class Zealot extends BioUnit {
public Zealot(String name, int hp) {
super(name, hp);
}
}
// 제네릭을 활용한 셔틀 클래스
public class Shuttle<T extends BioUnit> {
private T target;
public T getTarget() {
return target;
}
public void setTarget(T target) {
this.target = target;
}
public void in(T unit) {
setTarget(unit);
}
public void showInfo() {
System.out.println("이름 : " + target.getName() + ", HP : " + target.getHp());
}
}
이제 유닛 프린터 클래스를 만들어볼게요. 제네릭 메서드와 와일드카드 두 가지 방식으로 구현해볼게요:
public class UnitPrinter {
// 제네릭 메서드 버전
public static <T extends BioUnit> void printV1(Shuttle<T> shuttle) {
System.out.println("이름 : " + shuttle.getTarget().getName()
+ ", HP : " + shuttle.getTarget().getHp());
}
// 와일드카드 버전
public static void printV2(Shuttle<? extends BioUnit> shuttle) {
System.out.println("이름 : " + shuttle.getTarget().getName()
+ ", HP : " + shuttle.getTarget().getHp());
}
}
마지막으로, 특정 유닛의 체력을 비교하는 유틸리티 클래스를 만들어볼게요:
public class UnitUtil {
// 두 유닛 중 체력이 높은 유닛을 반환하는 제네릭 메서드
public static <T extends BioUnit> BioUnit maxHp(T unit1, T unit2) {
return unit1.getHp() >= unit2.getHp() ? unit1 : unit2;
}
}
테스트해볼까요?
public class UnitUtilTest {
public static void main(String[] args) {
Marine m1 = new Marine("마린1", 40);
Marine m2 = new Marine("마린2", 50);
BioUnit resultMarine = UnitUtil.maxHp(m1, m2);
System.out.println("resultMarine = " + resultMarine);
Zealot z1 = new Zealot("질럿1", 100);
Zealot z2 = new Zealot("질럿2", 150);
BioUnit resultZealot = UnitUtil.maxHp(z1, z2);
System.out.println("resultZealot = " + resultZealot);
}
}
💭 정리해볼게요
오늘 배운 와일드카드와 제네릭 메서드는 각각 장단점이 있어요:
- 제네릭 메서드는 정확한 타입을 반환할 수 있어서 타입 안전성이 높지만, 문법이 조금 복잡해요.
- 와일드카드는 문법이 간결하고 이미 만들어진 제네릭 타입을 활용하기 좋지만, 정확한 타입 반환에는 제한이 있어요.
사용 시점을 간단히 정리하자면:
- 명확한 타입 반환이 필요하다면 → 제네릭 메서드
- 코드를 간결하게 유지하고 싶다면 → 와일드카드
마지막으로, 타입 이레이저로 인해 제네릭에는 몇 가지 제약이 있다는 점을 기억해주세요:
- instanceof T와 같은 런타임 타입 체크 불가
- new T()와 같은 인스턴스 생성 불가
- 정적(static) 필드에 타입 매개변수 사용 불가
여러분도 실제 코딩을 하면서 제네릭과 와일드카드를 활용해보시면 더 깊이 이해하실 수 있을 거예요. 처음에는 어렵게 느껴질 수 있지만, 연습하다 보면 자바 코딩의 강력한 도구가 될 거예요! 💪
궁금한 점이 있으시면 댓글로 남겨주세요! 다음에는 컬렉션 프레임워크에 대해 알아볼 예정이에요. 함께 공부해요! 😊
다음 학습 계획
- [25.04.07] 김영한의 자바 중급 -2 섹션 4 컬렉션 프레임워크 - ArrayList
'📚 학습 기록 > Java 기초 & 중급' 카테고리의 다른 글
ArrayList 완전 정복: 동적 배열 구현과 활용 기법 (1) | 2025.04.09 |
---|---|
자바 컬렉션 프레임워크: ArrayList와 동적 배열 구현 이해하기 (1) | 2025.04.07 |
자바 제네릭 마스터하기: 타입 매개변수 제한과 제네릭 메서드 완벽 가이드 (0) | 2025.04.02 |
자바 제네릭(Generic) 완벽 정리: 개념부터 활용까지 (0) | 2025.04.01 |
자바로 만든 나만의 쇼핑몰 여행기 (3) - UI 계층 구현편 (5) | 2025.03.27 |