[Java 개념노트 54] 제한된 제네릭과 와일드카드 이해하기
안녕하세요. Java 개념노트 시리즈 쉰네 번째 글입니다.
지난 글에서는 직접 타입 파라미터를 사용하는 제네릭 클래스와 제네릭 메서드를 만드는 방법을 정리했습니다. 이번 글에서는 제네릭을 조금 더 안전하고 유연하게 사용하는 방법인 제한된 제네릭과 와일드카드에 대해 알아보겠습니다.
제네릭을 사용하다 보면 <T extends Number>, <?>, <? extends Number>, <? super Integer> 같은 문법을 만나게 됩니다. 처음 보면 어렵게 보이지만 핵심은 간단합니다. 사용할 수 있는 타입의 범위를 제한하거나, 아직 정확히 모르는 타입을 유연하게 받기 위한 문법입니다.
1. 왜 타입을 제한할까?
지난 글에서 만든 Box<T>는 어떤 타입이든 담을 수 있었습니다.
class Box<T> {
private T value;
public void setValue(T value) {
this.value = value;
}
public T getValue() {
return value;
}
}
이 구조는 유연하지만, 어떤 상황에서는 너무 넓을 수 있습니다. 예를 들어 숫자 계산을 하는 상자라면 String이나 Product 같은 타입은 들어오면 안 됩니다. 숫자 타입만 받도록 제한하고 싶을 수 있습니다.
쉽게 말하면
제한된 제네릭은 “아무 타입이나 받지 말고, 특정 부모 타입을 기준으로 허용하자”는 문법입니다.
2. 제한된 제네릭이란?
제한된 제네릭은 타입 파라미터에 사용할 수 있는 타입의 범위를 제한하는 문법입니다.
자바에서는 extends 키워드를 사용해서 타입의 상한을 정할 수 있습니다.
class 클래스이름<T extends 부모타입> {
// T는 부모타입 또는 그 자식 타입만 가능
}
예를 들어 숫자 타입만 허용하고 싶다면 다음처럼 작성할 수 있습니다.
class NumberBox<T extends Number> {
private T value;
}
이제 T에는 Number 또는 Number를 상속받은 타입만 올 수 있습니다.
3. Number 클래스와 숫자 Wrapper 클래스
자바의 숫자 Wrapper 클래스 중 일부는 Number 클래스를 상속받습니다.
| 타입 | Number의 자식인가? | 설명 |
Integer |
맞음 | 정수 Wrapper 클래스 |
Double |
맞음 | 실수 Wrapper 클래스 |
Long |
맞음 | long Wrapper 클래스 |
Float |
맞음 | float Wrapper 클래스 |
String |
아님 | 문자열 클래스 |
따라서 <T extends Number>라고 제한하면 Integer, Double, Long 같은 숫자 타입은 사용할 수 있지만 String은 사용할 수 없습니다.
4. NumberBox 만들기
숫자 타입만 받을 수 있는 NumberBox 클래스를 만들어보겠습니다.
class NumberBox<T extends Number> {
private T value;
public NumberBox(T value) {
this.value = value;
}
public T getValue() {
return value;
}
public double getDoubleValue() {
return value.doubleValue();
}
}
T extends Number로 제한했기 때문에 value는 숫자 계열 타입입니다. 그래서 Number 클래스가 가진 doubleValue() 같은 메서드를 사용할 수 있습니다.
핵심 포인트<T extends Number>처럼 제한하면 T를 Number 타입처럼 다룰 수 있습니다.
5. NumberBox 사용하기
NumberBox는 Integer, Double 같은 숫자 타입으로 사용할 수 있습니다.
public class Main {
public static void main(String[] args) {
NumberBox<Integer> intBox = new NumberBox<>(100);
NumberBox<Double> doubleBox = new NumberBox<>(3.14);
System.out.println(intBox.getDoubleValue());
System.out.println(doubleBox.getDoubleValue());
}
}
class NumberBox<T extends Number> {
private T value;
public NumberBox(T value) {
this.value = value;
}
public double getDoubleValue() {
return value.doubleValue();
}
}
실행 결과는 다음과 같습니다.
100.0
3.14
Integer와 Double은 모두 Number의 자식 타입이므로 사용할 수 있습니다.
6. 제한에 맞지 않는 타입은 사용할 수 없다
NumberBox<T extends Number>는 숫자 타입만 허용합니다. 따라서 String은 사용할 수 없습니다.
NumberBox<Integer> intBox = new NumberBox<>(100);
NumberBox<Double> doubleBox = new NumberBox<>(3.14);
// NumberBox<String> stringBox = new NumberBox<>("Java"); // 오류 발생
String은 Number를 상속받지 않았기 때문에 타입 제한에 걸립니다. 이렇게 제한된 제네릭은 잘못된 타입 사용을 컴파일 단계에서 막아줍니다.
7. 제한된 제네릭 메서드
제네릭 클래스뿐만 아니라 제네릭 메서드에도 타입 제한을 걸 수 있습니다.
public static <T extends Number> void printNumber(T value) {
System.out.println(value.doubleValue());
}
이 메서드는 Number 계열 타입만 받을 수 있습니다.
public class Main {
public static void main(String[] args) {
printNumber(100);
printNumber(3.14);
// printNumber("Java"); // 오류 발생
}
public static <T extends Number> void printNumber(T value) {
System.out.println(value.doubleValue());
}
}
실행 결과는 다음과 같습니다.
100.0
3.14
8. extends는 클래스와 인터페이스 모두에 사용한다
제네릭 제한에서 extends는 클래스뿐만 아니라 인터페이스에도 사용합니다.
interface Printable {
void print();
}
class Printer<T extends Printable> {
public void printItem(T item) {
item.print();
}
}
T extends Printable은 Printable을 구현한 타입만 사용할 수 있다는 뜻입니다. 인터페이스를 기준으로 제한할 때도 implements가 아니라 extends를 사용합니다.
주의
제네릭 타입 제한에서는 클래스 기준이든 인터페이스 기준이든 extends를 사용합니다.
9. 와일드카드란?
와일드카드는 제네릭에서 정확한 타입을 알 수 없거나, 여러 타입을 유연하게 받고 싶을 때 사용하는 문법입니다.
와일드카드는 물음표 ?로 표현합니다.
List<?>
List<?>는 “어떤 타입의 List인지는 정확히 모르지만, List이긴 하다”는 뜻입니다.
쉽게 말하면
와일드카드 ?는 “정확한 타입은 모르지만 어떤 타입이든 올 수 있다”는 표시입니다.
10. 제네릭은 상속 관계가 그대로 적용되지 않는다
Integer는 Number의 자식 타입입니다. 하지만 ArrayList<Integer>가 ArrayList<Number>의 자식이 되는 것은 아닙니다.
ArrayList<Integer> intList = new ArrayList<>();
// ArrayList<Number> numberList = intList; // 오류 발생
이 부분이 제네릭에서 자주 헷갈리는 지점입니다. Integer와 Number 사이에는 상속 관계가 있지만, ArrayList<Integer>와 ArrayList<Number> 사이에는 같은 방식의 상속 관계가 적용되지 않습니다.
11. 왜 ArrayList<Integer>를 ArrayList<Number>로 받을 수 없을까?
만약 ArrayList<Integer>를 ArrayList<Number>로 받을 수 있다고 가정해보겠습니다. 그러면 문제가 생길 수 있습니다.
ArrayList<Integer> intList = new ArrayList<>();
// 만약 아래 코드가 가능하다고 가정하면
// ArrayList<Number> numberList = intList;
// numberList.add(3.14); // Double 추가 가능해져 버림
// intList는 Integer만 담아야 하는데 Double이 들어갈 수 있음
이런 문제를 막기 위해 자바는 ArrayList<Integer>를 ArrayList<Number>에 바로 대입하지 못하게 합니다. 이럴 때 필요한 문법이 와일드카드입니다.
12. 비제한 와일드카드 <?>
<?>는 어떤 타입이든 받을 수 있는 와일드카드입니다. 정확한 타입은 모르지만, 값을 읽어서 출력하는 정도는 할 수 있습니다.
import java.util.ArrayList;
public class Main {
public static void main(String[] args) {
ArrayList<String> names = new ArrayList<>();
names.add("Java");
names.add("Spring");
ArrayList<Integer> scores = new ArrayList<>();
scores.add(90);
scores.add(80);
printList(names);
printList(scores);
}
public static void printList(ArrayList<?> list) {
for (Object item : list) {
System.out.println(item);
}
}
}
실행 결과는 다음과 같습니다.
Java
Spring
90
80
printList()는 ArrayList<String>도 받고, ArrayList<Integer>도 받을 수 있습니다. 다만 정확한 타입을 모르기 때문에 값을 꺼낼 때는 보통 Object로 다룹니다.
13. <?>에는 값을 추가하기 어렵다
ArrayList<?>는 어떤 타입의 리스트인지 정확히 모릅니다. 그래서 특정 값을 추가하기 어렵습니다.
public static void addValue(ArrayList<?> list) {
// list.add("Java"); // 오류 발생
// list.add(100); // 오류 발생
}
이 리스트가 ArrayList<String>인지, ArrayList<Integer>인지, ArrayList<Product>인지 알 수 없기 때문에 자바는 값 추가를 막습니다.
단순히 여러 타입의 리스트를 읽고 출력하는 용도라면 <?>가 유용합니다.
14. 상한 와일드카드 <? extends 타입>
<? extends 타입>은 해당 타입 또는 그 자식 타입을 받을 수 있다는 뜻입니다.
ArrayList<? extends Number>
이 코드는 Number 또는 Number의 자식 타입을 담은 ArrayList를 받을 수 있다는 뜻입니다.
| 전달 가능 | 이유 |
ArrayList<Integer> |
Integer는 Number의 자식 |
ArrayList<Double> |
Double은 Number의 자식 |
ArrayList<Number> |
Number 자신 |
ArrayList<String> |
전달 불가, String은 Number 계열이 아님 |
15. <? extends Number> 예제
숫자 리스트의 합계를 구하는 메서드를 만들어보겠습니다.
import java.util.ArrayList;
public class Main {
public static void main(String[] args) {
ArrayList<Integer> intList = new ArrayList<>();
intList.add(10);
intList.add(20);
ArrayList<Double> doubleList = new ArrayList<>();
doubleList.add(1.5);
doubleList.add(2.5);
System.out.println(sum(intList));
System.out.println(sum(doubleList));
}
public static double sum(ArrayList<? extends Number> list) {
double total = 0;
for (Number number : list) {
total += number.doubleValue();
}
return total;
}
}
실행 결과는 다음과 같습니다.
30.0
4.0
sum() 메서드는 ArrayList<Integer>와 ArrayList<Double>을 모두 받을 수 있습니다. 숫자 계열 타입만 받도록 제한했기 때문에 각 값을 Number로 꺼내서 doubleValue()를 사용할 수 있습니다.
16. extends 와일드카드는 읽기에 강하다
<? extends Number>는 Number 계열 값을 읽어오는 데 좋습니다. 하지만 값을 추가하는 것은 제한됩니다.
public static void addNumber(ArrayList<? extends Number> list) {
// list.add(10); // 오류 발생
// list.add(3.14); // 오류 발생
}
이유는 간단합니다. 이 리스트가 실제로 ArrayList<Integer>인지, ArrayList<Double>인지 알 수 없기 때문입니다.
ArrayList<? extends Number> list
실제 타입이 ArrayList<Integer>일 수도 있음
실제 타입이 ArrayList<Double>일 수도 있음
따라서 Integer를 넣어도 되는지,
Double을 넣어도 되는지 확정할 수 없음
기억하기<? extends 타입>은 값을 읽는 데 적합하지만, 값을 추가하는 데는 제한이 큽니다.
17. 하한 와일드카드 <? super 타입>
<? super 타입>은 해당 타입 또는 그 부모 타입을 받을 수 있다는 뜻입니다.
ArrayList<? super Integer>
이 코드는 Integer 또는 Integer의 부모 타입을 담는 ArrayList를 받을 수 있다는 뜻입니다.
| 전달 가능 | 이유 |
ArrayList<Integer> |
Integer 자신 |
ArrayList<Number> |
Number는 Integer의 부모 |
ArrayList<Object> |
Object는 모든 클래스의 부모 |
ArrayList<Double> |
전달 불가, Double은 Integer의 부모가 아님 |
18. <? super Integer> 예제
<? super Integer>를 사용하면 Integer 값을 안전하게 추가할 수 있습니다.
import java.util.ArrayList;
public class Main {
public static void main(String[] args) {
ArrayList<Integer> intList = new ArrayList<>();
ArrayList<Number> numberList = new ArrayList<>();
ArrayList<Object> objectList = new ArrayList<>();
addIntegers(intList);
addIntegers(numberList);
addIntegers(objectList);
System.out.println(intList);
System.out.println(numberList);
System.out.println(objectList);
}
public static void addIntegers(ArrayList<? super Integer> list) {
list.add(10);
list.add(20);
}
}
실행 결과는 다음과 비슷합니다.
[10, 20]
[10, 20]
[10, 20]
ArrayList<Integer>, ArrayList<Number>, ArrayList<Object> 모두 Integer 값을 담을 수 있으므로 메서드에 전달할 수 있습니다.
19. super 와일드카드는 추가에 강하다
<? super Integer>는 Integer 값을 추가하는 데 적합합니다. 하지만 값을 꺼낼 때는 정확한 타입을 알기 어렵기 때문에 보통 Object로 꺼냅니다.
public static void readValues(ArrayList<? super Integer> list) {
Object value = list.get(0);
System.out.println(value);
}
실제 리스트가 ArrayList<Integer>인지, ArrayList<Number>인지, ArrayList<Object>인지 알 수 없기 때문에 꺼낼 때는 Object로 받는 것이 안전합니다.
20. extends와 super 비교
두 와일드카드는 방향이 반대입니다.
| 구분 | <? extends 타입> | <? super 타입> |
| 방향 | 타입 또는 자식 타입 | 타입 또는 부모 타입 |
| 예시 | <? extends Number> |
<? super Integer> |
| 강한 쪽 | 읽기 | 추가 |
| 읽을 때 | 상한 타입으로 읽기 가능 | 보통 Object로 읽음 |
| 추가할 때 | 대부분 제한됨 | 기준 타입 값 추가 가능 |
21. PECS로 기억하기
와일드카드 방향은 PECS라는 말로 기억할 수 있습니다.
Producer Extends
Consumer Super
처음에는 영어 표현이 어렵게 느껴질 수 있으므로 아래처럼 이해하면 충분합니다.
- 값을 주로 읽어오기만 한다면
extends - 값을 주로 넣어야 한다면
super
즉, extends는 읽기 중심, super는 추가 중심으로 기억하면 됩니다.
22. 타입 파라미터와 와일드카드 차이
<T>와 <?>는 비슷해 보이지만 목적이 다릅니다.
| 구분 | 타입 파라미터 T | 와일드카드 ? |
| 형태 | <T> |
<?> |
| 의미 | 메서드나 클래스 안에서 타입 이름처럼 사용 | 정확히 모르는 타입을 표현 |
| 타입 재사용 | 가능 | 어렵다 |
| 예시 | public <T> T get(T value) |
printList(List<?> list) |
메서드 안에서 같은 타입을 여러 번 연결해서 사용해야 한다면 T가 더 적합합니다. 단순히 어떤 타입의 리스트든 받아서 읽기만 하면 된다면 ?가 간단할 수 있습니다.
23. 전체 예제
이번에는 제한된 제네릭과 와일드카드를 함께 사용하는 예제를 보겠습니다.
import java.util.ArrayList;
public class Main {
public static void main(String[] args) {
ArrayList<Integer> intList = new ArrayList<>();
intList.add(10);
intList.add(20);
ArrayList<Double> doubleList = new ArrayList<>();
doubleList.add(1.5);
doubleList.add(2.5);
printAnyList(intList);
printAnyList(doubleList);
System.out.println("정수 합계: " + sumNumbers(intList));
System.out.println("실수 합계: " + sumNumbers(doubleList));
ArrayList<Number> numberList = new ArrayList<>();
addIntegerValues(numberList);
System.out.println(numberList);
}
public static void printAnyList(ArrayList<?> list) {
for (Object item : list) {
System.out.println(item);
}
}
public static double sumNumbers(ArrayList<? extends Number> list) {
double total = 0;
for (Number number : list) {
total += number.doubleValue();
}
return total;
}
public static void addIntegerValues(ArrayList<? super Integer> list) {
list.add(100);
list.add(200);
}
}
실행 결과는 다음과 같습니다.
10
20
1.5
2.5
정수 합계: 30.0
실수 합계: 4.0
[100, 200]
printAnyList()는 어떤 타입의 리스트든 출력합니다. sumNumbers()는 Number 계열 리스트만 받아서 합계를 구합니다. addIntegerValues()는 Integer를 담을 수 있는 리스트에 값을 추가합니다.
24. 제한된 제네릭과 와일드카드 사용 시 자주 하는 실수
제한된 제네릭과 와일드카드를 처음 배울 때는 아래와 같은 실수를 자주 합니다.
| 실수 | 문제점 | 해결 방법 |
ArrayList<Integer>를 ArrayList<Number>로 생각 |
제네릭 타입 사이에는 상속 관계가 그대로 적용되지 않음 | <? extends Number> 사용 고려 |
<? extends Number>에 값을 추가하려 함 |
실제 타입이 무엇인지 몰라 추가가 제한됨 | 읽기 중심으로 사용 |
<? super Integer>에서 값을 Integer로 바로 꺼내려 함 |
실제 부모 타입이 무엇인지 몰라 Object로 읽는 것이 안전 | 꺼낼 때는 Object 기준으로 생각 |
extends와 super 방향 혼동 |
읽기와 추가 목적이 섞임 | 읽기는 extends, 추가는 super로 기억 |
인터페이스 제한에 implements를 사용하려 함 |
제네릭 제한에서는 인터페이스도 extends 사용 | <T extends Interface> 형태 사용 |
25. 직접 연습해보기
아래 코드를 직접 작성하고 실행해보세요.
import java.util.ArrayList;
public class Main {
public static void main(String[] args) {
ArrayList<Integer> scores = new ArrayList<>();
scores.add(90);
scores.add(80);
scores.add(70);
ArrayList<Double> rates = new ArrayList<>();
rates.add(1.5);
rates.add(2.5);
printList(scores);
printList(rates);
System.out.println("점수 합계: " + sum(scores));
System.out.println("비율 합계: " + sum(rates));
ArrayList<Number> numbers = new ArrayList<>();
addDefaultScores(numbers);
System.out.println("기본 점수 목록: " + numbers);
}
public static void printList(ArrayList<?> list) {
for (Object item : list) {
System.out.println(item);
}
}
public static double sum(ArrayList<? extends Number> list) {
double total = 0;
for (Number number : list) {
total += number.doubleValue();
}
return total;
}
public static void addDefaultScores(ArrayList<? super Integer> list) {
list.add(60);
list.add(70);
list.add(80);
}
}
실행 결과는 다음과 같습니다.
90
80
70
1.5
2.5
점수 합계: 240.0
비율 합계: 4.0
기본 점수 목록: [60, 70, 80]
26. 이번 글 정리
이번 글에서는 제네릭의 타입 범위를 제한하는 제한된 제네릭과 유연한 타입 표현인 와일드카드에 대해 정리했습니다. 핵심 내용은 다음과 같습니다.
- 제한된 제네릭은 타입 파라미터에 사용할 수 있는 타입 범위를 제한한다.
<T extends Number>는 Number 또는 Number의 자식 타입만 허용한다.- 제네릭 제한에서는 클래스와 인터페이스 모두
extends를 사용한다. <?>는 정확한 타입을 모르는 와일드카드이다.ArrayList<Integer>는ArrayList<Number>의 자식 타입이 아니다.<? extends 타입>은 해당 타입 또는 자식 타입을 받을 수 있다.<? extends Number>는 Number 계열 값을 읽을 때 유용하다.<? extends 타입>은 값을 추가하는 데 제한이 크다.<? super 타입>은 해당 타입 또는 부모 타입을 받을 수 있다.<? super Integer>는 Integer 값을 추가할 때 유용하다.- 읽기 중심이면
extends, 추가 중심이면super를 먼저 떠올리면 좋다. - 타입 파라미터
T는 메서드나 클래스 안에서 타입을 재사용할 때 쓰고, 와일드카드?는 정확히 모르는 타입을 표현할 때 쓴다.
한 줄 요약
제한된 제네릭은 사용할 타입의 범위를 정하는 문법이고, 와일드카드는 정확한 타입을 모르거나 여러 제네릭 타입을 유연하게 받기 위한 문법입니다.
다음 글 예고
다음 글에서는 [Java 개념노트 55] 람다식이란 무엇인가?라는 주제로 메서드처럼 동작하는 코드를 더 짧고 간결하게 표현하는 방법을 정리해보겠습니다.
GWDEVELBlog Java 개념노트 시리즈

'Computer Science > Java' 카테고리의 다른 글
| [Java ] 개념노트 56 함수형 인터페이스 이해하기 (0) | 2026.06.18 |
|---|---|
| [Java ] 개념노트 55 람다식이란 무엇인가 (0) | 2026.06.18 |
| [Java ] 개념노트 53 제네릭 클래스와 제네릭 메서드 이해하기 (0) | 2026.06.17 |
| [Java ] 개념노트 52 제네릭이란 무엇인가? (0) | 2026.06.17 |
| [Java] 개념노트 51 Iterator와 컬렉션 반복 이해하기 (0) | 2026.06.17 |