본문 바로가기

와일드카드<?>가 무엇인가?

@정소민fan2025. 8. 23. 23:03

자바를 사용하면서 제네릭에 <?> 또는 <? extends T> 처럼 사용되는 물음표 기호를 본 적이 있다.

당췌 이건 뭐하는데 쓰이는 문법인가?

 

제네릭은 기본적으로 사용될 타입을 제한할 때 사용된다. 하지만 내가 클래스나 컬렉션에 어떤 타입을 사용할 지 아직 모를때는 어떻게 하면 될까? 그때 사용하는 것이 <?> 와일드카드이다

 

와일드카드는 3가지로 분류된다.

  1. <?> 무제한 와일드카드
  2. <? extends T> 상한 제한 와일드카드
  3. <? super T> 하한 제한 와일드카드

무제한 와일드카드

import java.util.*;

public class Wildcard {
    public static void printList(List<?> list) {
        for (Object obj : list) {
            System.out.println(obj);
        }
    }

    public static void main(String[] args) {
        List<String> strings = Arrays.asList("A", "B", "C");
        List<Integer> numbers = Arrays.asList(1, 2, 3);

        printList(strings); // String 리스트도 가능
        printList(numbers); // Integer 리스트도 가능
    }
}

어떤 타입이 오든 상관없을 때 사용한다. printList의 인자 list는 List<String>이 오든 List<integer>가 오든 상관하지 않고 받아들인다. 만약 와일드카드가 없었다면, 메소드 오버로딩을 통해 printIntegerList, printStringList를 따로 만들어야 했을 것이다.

import java.util.*;

public class Wildcard {
    public static void printIntegerList(List<Integer> list) {
        for (Integer obj : list) {
            System.out.println(obj);
        }
    }
    
    public static void printStringList(List<String> list) {
        for (String obj : list) {
            System.out.println(obj);
        }
    }

    public static void main(String[] args) {
        List<String> strings = Arrays.asList("A", "B", "C");
        List<Integer> numbers = Arrays.asList(1, 2, 3);

        printStringList(strings); // String 리스트만 가능
        printIntegerList(numbers); // Integer 리스트만 가능
    }
}

이렇게 말이다. 자연스럽게 함수가 많아지고, 코드가 길어진다.

읽기만 가능하다

무제한 와일드카드를 사용할 경우에는 읽기만 가능해진다. 그 이유는 타입 안정성 때문이다.

자바의 제네릭은 런타임에는 타입 정보가 지워진다. List<String>이든 List<Integer>이든 List<?>이든 런타임에는 그냥 List일 뿐이라는 것이다. 컴파일 시점에야 컴파일러가 타입을 검사해주기 때문에 문제가 없지만, List<?>는 타입이 정해지지 않은 와일드카드이기에 문제가 생긴다.

List<String> strings = new ArrayList<>();
List<?> list = strings;

// 만약 쓰기가 허용된다고 하면?
list.add(100); // string 타입의 List에 Integer가 추가됨

바로 위 코드처럼 말이다. 그래서 무제한 와일드카드는 쓰기가 불가능하다.

읽기의 경우에는 최상위 타입인 Object를 이용하기 때문에 가능하다 !!

상한 제한 와일드카드

상한 제한 와일드카드는 <? extends T> 처럼 사용하는데, 이는 T 또는 T의 서브타입만을 허용한다는 의미이다. 다시 말하면, T와 T를 상속받은 타입만 가능하다.

무제한 와일드카드와 다르게 Object를 사용하지 않고 상위 타입으로 캐스팅해서 꺼내어 필드나 함수를 사용할 수 있다는 차별점이 있다. 하지만 상한 제한 와일드카드도 읽기만 가능하고 쓰기가 불가능하다.

다음과 같은 상황을 가정하자.

List<? extends Animal>
   ├─ List<Dog>
   ├─ List<Cat>
   └─ List<Animal>

이렇게 Animal을 상속받은 Dog와 Cat이 있다. 이 때, List<? extends Animal> 에서 읽기를 위해 꺼낸 원소는 Animal 타입으로 업캐스팅이 당연히 가능하기 때문에 Animal 클래스의 원소나 함수를 사용 가능하다.

import java.util.*;

class Animal {
    void sound() { System.out.println("동물 소리"); }
}

class Dog extends Animal {
    void sound() { System.out.println("멍멍"); }
}

class Cat extends Animal {
    void sound() { System.out.println("야옹"); }
}

public class ExtendsExample {
    public static void printAnimalSounds(List<? extends Animal> list) {
        for (Animal a : list) {
            a.sound(); 
        }
    }

    public static void main(String[] args) {
        List<Dog> dogs = Arrays.asList(new Dog(), new Dog());
        printAnimalSounds(dogs);
        List<Cat> cats = Arrays.asList(new Cat(), new Cat());
        printAnimalSounds(cats);
    }
}

위 코드처럼 사용이 가능하다.

add와 같은 쓰기가 불가능한 이유는 <? extends Animal>의 경우에는 Cat, Dog, Animal이 포함되는데 List<Animal>에는 Cat이나 Dog가 add될 수 있지만, 만약 List<Dog>인데 Cat 타입이 추가된다면? List<Cat>인데 Dog가 추가된다면? 당연히 불가능한 사항이기 때문에 컴파일 시점에서 컴파일러가 막는 것이다.

이 때문에 하한 제한 와일드카드는 데이터를 소비하는 Producer 용도로 사용된다.

하한 제한 와일드카드

하한 제한 와일드카드는 <? super T> 처럼 사용되는데, T와 T의 상위 타입들을 가지고 있다! 라는 의미다. 따라서, T와 T의 하위 타입들을 쓰기 가능하다. 왜냐? T의 상위 타입이라는 의미는, T의 하위 타입보다는 무조건 상위의 타입이라는 의미이니까!!

import java.util.*;

public class SuperExample {
    public static void addNumbers(List<? super Integer> list) {
        list.add(10);
        list.add(20);
        Object o = list.get(0); // 꺼낼 때는 Object로만 가능
    }

    public static void main(String[] args) {
        List<Number> numbers = new ArrayList<>();
        addNumbers(numbers);  // Number는 Integer의 상위
        System.out.println(numbers); // [10, 20]
    }
}

위 코드를 보자. main에서 Integer의 상위 타입은 Number 타입의 List를 하나 만들어서  addNumbers로 넘기고 있다.

addNumbers는 Integer 또는 Integer 그 상위 타입을 와일드카드로 갖는 List를 인자로 갖는다. 따라서 Integer 또는 Integer를 상속받는 하위 타입이 있다면 추가 가능하다.

글로 하니까 조금 어려운데, 그림으로 이해해보자.

대충 그려본 생물 분류 다이어그램

위 그림처럼 상속 관계가 이어져 있다고 가정하자. 동물계가 최상위 클래스고 유대목, 영장목, 고래목이 최하위 클래스이다.

유대목, 영장목, 고래목은 포유강을 상속받고, 상위 클래스들인 동물계, 척삭동물문, 포유강의 public, protected 필드와 함수에 모두 접근 가능하다.

List<? super 포유강>을 인자로 받는 함수가 있다고 하자.

void addList(List<? super 포유강> list){
    list.add(new 포유강());
    list.add(new 유대목());
    list.add(new 영장목());
    list.add(new 고래목());
    Object obj = list.get(0);
}

그럼 이 함수에는 다음 List들이 모두 들어갈수 있다.

List<동물계> 동물계 = new ArrayList<>();
List<척삭동물문> 척삭동물문 = new ArrayList<>();
List<포유강> 포유강 = new ArrayList<>();

어째서 포유강과 포유강 하위의 타입들이 add가 가능한걸까? 포유강이나 포유강 하위 타입을 인자로 받았는데도 말이다.

생각해보면 간단하다. 어차피 포유강과 그 하위 타입들은 포유강의 상위 타입의 필드나 함수들을 모두 사용 가능하다 !! (public이나 protected면) 그래서 이렇게 추가가 가능한 것이다. 그러면 왜 꺼낼때는 Object로만 꺼낼 수 있는걸까?

무제한 와일드카드에서 말했듯이, 런타임 시점에는 제네릭 타입이 소거된다. 따라서 지금 이 List가 무슨 타입인지 전혀 알 수가 없다. 그래서 컴파일러는 최악의 경우를 상정해 최상위 타입인 Object로 읽기만 허용하는 것이다.

 

이론적으로 어렴풋이 알고있던 개념이지만, 아직까지 실제 프로젝트에서 사용해본적은 없는것같다. 아직 내가 다형성을 제대로 사용하고 있지 않다는 뜻이겠지. 좀더 정진해야겠다.

 

'Java' 카테고리의 다른 글

JVM 튜?닝?  (1) 2025.11.27
자바 가비지 컬렉터 (GC)  (0) 2025.09.07
JVM  (0) 2025.09.06
SOLID 원칙  (0) 2025.09.05
Supplier는 왜 쓰는걸까?  (0) 2025.08.16
정소민fan
@정소민fan :: 코딩은 관성이야

코딩은 관성적으로 해야합니다 즐거운 코딩 되세요

목차