Java

Java 제네릭 와일드카드에 대하여

통촏하여주시옵소서 2024. 11. 18. 19:55

Java 제네릭 와일드카드에 대하여

Java 제네릭(Generic)은 코드의 재사용성을 높이고, 타입 안전성을 보장하며, 런타임 시 타입 캐스팅을 줄이기 위해 도입된 기능입니다. 제네릭은 컬렉션이나 메서드에 타입을 정의할 수 있는 강력한 기능을 제공하지만, 때로는 제네릭 타입을 유연하게 다루어야 하는 경우가 생깁니다. 이러한 상황에서 사용되는 것이 바로 와일드카드(Wildcard) 입니다.

이 글에서는 Java 제네릭 와일드카드의 개념, 종류, 사용법, 그리고 장단점에 대해 자세히 살펴보겠습니다.


1. 제네릭 와일드카드란?

와일드카드는 ? 기호로 표현되며, 제네릭 타입의 상한 또는 하한을 정의하거나 제네릭 타입을 보다 유연하게 다룰 수 있도록 설계되었습니다.
와일드카드는 크게 세 가지로 나뉩니다:

  1. Unbounded Wildcard: 제한이 없는 와일드카드 (?)
  2. Upper Bounded Wildcard: 상한이 지정된 와일드카드 (<? extends Type>)
  3. Lower Bounded Wildcard: 하한이 지정된 와일드카드 (<? super Type>)

2. 와일드카드의 종류와 사용법

2.1 Unbounded Wildcard (?)

Unbounded Wildcard는 특정 타입에 제한을 두지 않고, "아무 타입이나 허용"하는 경우에 사용됩니다. 주로 제네릭 타입에 대해서 타입에 의존하지 않는 코드를 작성할 때 유용합니다.

사용 예시

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

동작 설명

  • List<?>는 어떤 타입의 리스트라도 전달받을 수 있습니다.
  • 내부적으로는 Object 타입으로 처리합니다.

주요 특징

  • 읽기 전용으로 사용하는 것이 일반적입니다.
  • 타입이 명확하지 않으므로 데이터를 추가(insert)하는 동작에는 적합하지 않습니다.

2.2 Upper Bounded Wildcard (<? extends Type>)

상한이 지정된 와일드카드는 특정 타입과 그 서브타입만 허용합니다. 주로 읽기 전용의 데이터를 다룰 때 사용됩니다.

사용 예시

public double sum(List<? extends Number> numbers) {
    double sum = 0.0;
    for (Number number : numbers) {
        sum += number.doubleValue();
    }
    return sum;
}

동작 설명

  • List<? extends Number>Number와 그 서브타입(Integer, Double, Float 등)의 리스트만 받을 수 있습니다.
  • 메서드 내부에서 읽기 작업은 가능하지만, 타입이 불확실하므로 추가(insert) 작업은 불가능합니다.

주요 특징

  • "읽기 전용(Read-Only)" 작업에 적합합니다.
  • 데이터를 안전하게 읽기 위해 사용하는 경우가 많습니다.

2.3 Lower Bounded Wildcard (<? super Type>)

하한이 지정된 와일드카드는 특정 타입과 그 슈퍼타입만 허용합니다. 주로 쓰기(insert) 작업에 유용합니다.

사용 예시

public void addNumbers(List<? super Integer> list) {
    list.add(1);
    list.add(2);
    list.add(3);
}

동작 설명

  • List<? super Integer>Integer와 그 슈퍼타입(Number, Object)의 리스트를 받을 수 있습니다.
  • 메서드 내부에서 안전하게 Integer 값을 추가할 수 있습니다.
  • 반대로, 읽기 작업 시에는 타입이 Object로 처리됩니다.

주요 특징

  • "쓰기 전용(Write-Only)" 작업에 적합합니다.
  • 데이터를 삽입(insert)하기 위한 작업에서 주로 사용됩니다.

3. 와일드카드가 필요한 이유

3.1 제네릭 타입의 유연성 확보

제네릭은 타입 안정성을 보장하지만, 특정 상황에서는 너무 엄격하게 작동하여 불편함을 초래할 수 있습니다. 와일드카드는 이러한 엄격함을 완화하고, 유연한 코드를 작성할 수 있게 합니다.

3.2 코드의 재사용성 증가

와일드카드는 다양한 타입을 처리할 수 있어, 코드의 재사용성을 높이는 데 큰 역할을 합니다. 예를 들어, 상한 바운드를 사용하면 특정 타입 계층에 대한 로직을 하나의 메서드로 처리할 수 있습니다.


4. 와일드카드 사용 시 주의점

4.1 읽기와 쓰기의 제한

  • <? extends T>: 읽기는 가능하지만, 쓰기는 제한됩니다.
  • <? super T>: 쓰기는 가능하지만, 읽기는 제한됩니다.
  • <?>: 읽기는 가능하지만, 쓰기는 거의 불가능합니다.

4.2 타입 안전성 보장

와일드카드는 타입 안정성을 일부 포기하는 대신 유연성을 제공합니다. 따라서 잘못된 타입 캐스팅이나 타입 불일치로 인한 런타임 오류를 방지하려면, 설계 시 신중하게 고려해야 합니다.


5. 와일드카드와 제네릭 메서드 비교

와일드카드와 제네릭 메서드는 비슷해 보이지만, 용도가 다릅니다.

와일드카드 예시

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

제네릭 메서드 예시

public <T> void print(List<T> list) {
    for (T element : list) {
        System.out.println(element);
    }
}

차이점

  • 와일드카드는 불특정 타입(?)을 처리하는 데 사용되며, 구체적인 타입 명시 없이 유연성을 제공합니다.
  • 제네릭 메서드는 특정 타입(T)을 정의하고, 타입에 의존적인 로직을 작성하는 데 적합합니다.

6. 와일드카드와 PECS 원칙

Java에서 와일드카드를 사용할 때 PECS 원칙을 기억하는 것이 좋습니다.

  • Producer Extends, Consumer Super
  • 데이터를 제공(Produce)할 때는 <? extends Type>를 사용합니다.
  • 데이터를 소비(Consume)할 때는 <? super Type>를 사용합니다.

PECS 적용 예시

// Producer - 데이터 제공
public void readData(List<? extends Number> numbers) {
    for (Number num : numbers) {
        System.out.println(num);
    }
}

// Consumer - 데이터 소비
public void writeData(List<? super Integer> numbers) {
    numbers.add(10);
    numbers.add(20);
}

7. 실무에서의 활용 사례

7.1 타입 계층 구조에서의 데이터 처리

public void processShapes(List<? extends Shape> shapes) {
    for (Shape shape : shapes) {
        shape.draw();
    }
}

7.2 컬렉션의 유연한 데이터 추가

public void addToCollection(List<? super String> collection) {
    collection.add("Hello");
    collection.add("World");
}

8. 장단점 요약

장점

  • 코드의 재사용성을 높이고, 유연성을 제공합니다.
  • 다양한 타입 계층을 안전하게 처리할 수 있습니다.
  • 읽기와 쓰기 작업을 명확하게 구분할 수 있습니다.

단점

  • 읽기와 쓰기 작업의 제약 조건을 이해하고 사용해야 하므로 학습 곡선이 있습니다.
  • 타입 안정성을 일부 포기하는 경우가 있습니다.