Java 제네릭: "이 바구니에는 사과를 담을 수 없습니다"
1. 들어가며
자바 제네릭을 공부하다 보면 가장 당혹스러운 순간이 찾아옵니다. "분명히 과일 바구니라고 선언했는데, 왜 사과를 못 담게 하지?"
이 글은 ? extends T라는 외계어 같은 문법 뒤에 숨겨진, 자바 컴파일러의 "안전 과민증"에 대한 이야기입니다.
2. 환경
- IDE: VS Code
- JDK: Java 17
Phase 1. 상식의 배신 (왜 사과 바구니는 과일 바구니가 아닐까?)
현실 세계에서 "사과는 과일이다"는 참입니다. 그래서 친구가 "과일 좀 담아올 바구니 줘"라고 했을 때, 집에 있던 "사과 바구니"를 줘도 아무 문제가 없습니다. 어쨌든 과일을 담을 수 있으니까요.
하지만 자바는 아주 고지식합니다. 자바에게 사과 바구니(List<Apple>)와 과일 바구니(List<Fruit>)는 완전히 다른 물건입니다.
List<Fruit> basket = new ArrayList<Apple>(); // ❌ 컴파일 에러!
왜 이렇게 깐깐하게 굴까요? 만약 이걸 허용해주면, 사과 바구니로 둔갑한 과일 바구니에 누군가 바나나를 슬쩍 넣을 수 있기 때문입니다. 자바는 이런 오염(Pollution)을 끔찍하게 싫어해서, 아예 처음부터 "타입이 정확히 일치하지 않으면 연결하지 마!"라고 막아버립니다.
Phase 2. 타협점: "내용물은 묻지 않을게" (와일드카드)
개발자들이 불만을 터뜨립니다. "아니, 뭘 넣으려는 게 아니라 그냥 꺼내서 보기만 할 건데도 안 받아줘요?"
그래서 나온 타협책이 바로 와일드카드(?)입니다.
// "이 안에 뭐가 들었는진 정확히 모르지만, 적어도 Fruit의 일종인 건 확실해."
List<? extends Fruit> unknownBasket = new ArrayList<Apple>(); // ✅ OK!
이제 Apple 바구니든 Banana 바구니든 다 연결할 수 있습니다. 마치 바구니 겉면에 "과일(Fruit)류 포함됨"이라는 스티커를 붙인 것과 같습니다.
Phase 3. 대가: "대신 아무것도 못 넣습니다" (Read Only)
하지만 이 스티커(? extends Fruit)를 붙이는 순간, 자바는 아주 강력한 제약을 겁니다. 바로 "쓰기 금지(Write Ban)"입니다.
1. 지금
unknownBasket은 사과 바구니일 수도, 바나나 바구니일 수도 있습니다.2. 당신이 여기에
Apple을 넣으라고 명령합니다.3. 하지만 만약 실제 연결된 게 바나나 바구니라면? 바나나 바구니에 사과가 들어가는 사고가 터집니다.
4. 컴파일러: "실체가 뭔지 확신할 수 없으니(Unknown), 그냥 아예 뚜껑을 닫아버리자(Read Only)."
List<? extends Fruit> basket = ...;
Fruit f = basket.get(0); // ✅ 꺼내는 건 OK (뭐가 나오든 과일일 테니까)
basket.add(new Apple()); // ❌ 넣는 건 절대 금지! (실체가 바나나 바구니면 어쩔래?)

위의 출력 결과는 뭘까?
결과는 아래 더보기에
Basket Class: java.util.ArrayList
빨간 밑줄이 그어지는 이 순간이 바로 자바가 "안전"을 위해 문을 걸어 잠그는 장면입니다.
이것이 바로 제네릭의 역설입니다. "더 많은 종류를 받아들이려(유연함) 했더니, 정작 데이터를 넣을 능력(쓰기)을 잃어버렸다."
4. 결론: 언제 써야 할까?
코드를 외우는 건 의미가 없습니다. 이 원칙 하나만 기억하면 됩니다.
소비(Consume)할 때는 와일드카드를 써라. 생산(Produce)할 때는 쓰지 마라.
데이터를 꺼내서 읽기만 하는(Read) 조회용 메서드나 라이브러리(addAll 같은)를 만들 때는 <? extends T>가 최고의 선택입니다. 자바의 표준 라이브러리도 이 원칙을 철저히 지키고 있습니다.

"다양한 타입을 받아들이겠다"는 관용이 필요한 곳에는 어김없이 와일드카드가 쓰입니다.
하지만 데이터를 변경하고 추가해야 하는(Write) 곳에는 절대 쓰면 안 됩니다.
'Dev Study > Java' 카테고리의 다른 글
| Java ArrayList와 HashMap 완벽 정리: VS Code 디버깅으로 보는 내부 동작 (0) | 2026.02.09 |
|---|---|
| [JAVA & Antigravity] 인터페이스: 강한 결합을 끊고 유연함을 얻다 (3) | 2026.02.05 |
| [Java] 다형성과 추상 클래스: if문의 늪에서 탈출하기 (3) | 2026.01.21 |
| [Java] 책임은 누구에게 있는가: 상속과 super의 본질 (0) | 2026.01.21 |
| [Java] 객체지향 설계와 메모리 구조: 싱글턴, 빌더 패턴, 그리고 불변성 (0) | 2026.01.15 |