Java 제네릭과 타입 소거 (Generic & Type Erasure)
요약
Java 제네릭(Generic)의 개념과 타입 소거(Type Erasure)를 알아봅니다. 제네릭 클래스 안에서 T 타입 변수를 넘기면 print(Object x)가, 클래스 밖에서 넘기면 print(Integer x)가 선택되는 이유를 호출 지점 기준으로 이해하여 정보처리기사 실기 문제를 풀어봅니다.
제네릭과 타입 소거 핵심 정리
| 개념 | 설명 | 예시 |
|---|---|---|
| 제네릭 | 타입을 파라미터로 받는 클래스/메서드 | Collection<Integer> |
| 타입 소거 | 컴파일 후 T가 구체적인 타입(Object 또는 제한 타입)으로 대체됨 | <T> → Object, <T extends Number> → Number |
| 오버로딩 결정 시점 | 컴파일 타임에, 컴파일러가 호출 지점 코드만 보고 알 수 있는 인자 타입으로 결정 | 제네릭 클래스 안에서 T 변수를 넘기면 extends 뒤의 타입 기준으로 선택 |
| 핵심 결론 | 같은 변수라도 호출 위치에 따라 선택되는 메서드가 달라짐 | 클래스 안이면 print(Object x), 클래스 밖이면 print(Integer x) (아래 심화 섹션에서 설명) |
제네릭이란? 쌩기초
제네릭(Generic) 은 클래스나 메서드를 정의할 때 타입을 미리 정하지 않고, 사용할 때 지정하는 기능입니다. 왜 그런 게 필요한지부터 봅시다.
제네릭이 없으면
정수를 담는 상자를 만든다고 합시다.
이제 문자열도 담고 싶습니다. IntBox에는 못 넣습니다. value가 int니까요. 그래서 똑같이 생긴 클래스를 하나 더 만듭니다.
실수를 담으려면 DoubleBox, 감자 객체를 담으려면 GamjaBox. 하는 일은 완전히 같은데 타입 이름만 바꾼 클래스를 끝없이 복사하게 됩니다. 담을 타입이 늘어날 때마다 클래스가 하나씩 늘어납니다.
그냥 Object로 하면 안 되나요?
Object는 모든 클래스의 부모라 뭐든 담깁니다. 그러면 클래스 하나로 끝날 것 같습니다.
담을 때는 편합니다. 그런데 꺼낼 때가 문제입니다.
분명 Integer를 넣었는데 Integer 변수로 못 꺼냅니다. value의 타입이 Object라서, 컴파일러 입장에서는 그 안에 Integer가 들었는지 String이 들었는지 알 방법이 없기 때문입니다.
그래서 캐스팅을 씁니다.
(Integer)는 컴파일러에게 "이 값은 Integer가 맞다, 내가 책임진다" 고 알려주는 표시입니다. 컴파일러는 이 말을 그냥 믿고 통과시킵니다. 믿을 뿐, 확인하지는 않습니다.
그래서 거짓말을 하면 이렇게 됩니다.
이 코드는 컴파일이 그냥 통과합니다. 실행해야 터집니다.
클래스 복사는 사라졌는데, 대신 타입 검사가 통째로 사라졌습니다. 실수가 실행 중에야 발견됩니다. 사용자가 화면을 보고 있을 때 터지는 셈입니다.
제네릭은 둘 다 해결합니다
클래스는 하나만 만들되, 쓸 때 타입을 지정하게 하면 됩니다. 그게 제네릭입니다.
쉽게 말하면 "어떤 타입이든 담을 수 있는 상자" 를 만드는 것입니다. 여기서 T는 타입 파라미터라고 부릅니다. 실제로 사용할 때 구체적인 타입을 지정합니다.
IntBox·StringBox처럼 클래스를 복사하지 않아도 되고, 아까 ObjBox에서 터졌던 그 실수는 이제 실행 전에 잡힙니다.
| 방식 | 타입마다 클래스 복사 | 잘못된 타입을 언제 잡나 |
|---|---|---|
IntBox, StringBox, ... | 필요 | 컴파일 시점 |
ObjBox (Object 하나로) | 불필요 | 실행할 때 — 이미 늦음 |
Box<T> (제네릭) | 불필요 | 컴파일 시점 |
제네릭의 목적은 여기 오른쪽 열에 있습니다. 클래스 하나로 여러 타입을 쓰면서도 컴파일 시점 타입 검사를 잃지 않는 것. 이 목적을 기억해 두면 뒤에 나올 타입 소거가 훨씬 잘 이해됩니다.
T는 그냥 이름입니다
T는 자바가 아는 특별한 키워드가 아닙니다. 변수 이름 짓듯이 그냥 지은 이름이라 아무거나 써도 됩니다.
셋 다 똑같이 동작합니다. 다만 아무렇게나 지으면 읽기 어려우니, 자바에서는 관례적으로 아래 글자를 씁니다. T는 Type(타입)의 줄임말입니다.
| 글자 | 줄임말 | 주로 쓰는 곳 |
|---|---|---|
T | Type | 가장 일반적인 타입 하나 |
E | Element | 여러 값을 담는 클래스의 원소 (List<E> 등) |
K, V | Key, Value | Map<K, V>의 키와 값 |
N | Number | 숫자 타입 |
시험 문제도 대부분 T를 쓰지만, E나 다른 글자가 나와도 당황할 필요 없습니다. 글자만 다를 뿐 하는 일은 똑같습니다. <E>라고 적혀 있으면 이 페이지의 T 자리에 E를 넣어 읽으면 됩니다.
| 코드 | T의 타입 | value의 타입 |
|---|---|---|
Box<Integer> | Integer | Integer |
Box<String> | String | String |
Box<Double> | Double | Double |
잠깐, 컴파일과 런타임이 뭔가요? 기초
타입 소거를 이해하려면 컴파일과 런타임의 차이를 먼저 알아야 합니다. 앞에서 ObjBox가 "실행 중에야" 터졌다고 했는데, 그 시점을 부르는 이름이 바로 런타임입니다. 자세한 설명은 컴파일과 런타임의 차이를 참고하세요.
간단히 정리하면:
- 컴파일:
.java파일을.class파일로 변환하는 과정 - 런타임: 변환된
.class파일이 실제로 실행되는 시점
타입 소거는 컴파일 과정에서 일어납니다. 아래에서 자세히 알아보겠습니다.
타입 소거란? 기초
타입 소거(Type Erasure) 는 Java의 제네릭이 컴파일 후에 타입 정보가 제거되는 것을 말합니다.
핵심: 컴파일 전 vs 컴파일 후
컴파일러가 제네릭 타입 T를 모두 Object로 바꿔버립니다. 이것이 타입 소거입니다.
| 시점 | T의 타입 | value의 타입 |
|---|---|---|
| 컴파일 전 (소스 코드) | Integer (지정한 타입) | Integer |
| 컴파일 후 (실행 코드) | 제거됨 | Object |
그런데 왜 지워버릴까? 호기심
"검사가 끝났으니 지운다"는 건 지워도 되는 이유입니다. 그러면 왜 굳이 지워야 했을까요? 시험에 나오는 내용은 아니지만, 알아두면 타입 소거가 훨씬 납득됩니다.
자바에 제네릭이 처음부터 있었던 게 아니기 때문입니다. 제네릭은 자바가 나오고 한참 뒤인 Java 5(2004년)에서야 추가됐습니다.
그전에도 자바에는 여러 값을 담아두는 List 같은 기본 클래스들이 있었습니다. 그런데 제네릭이 없으니 담을 타입을 정할 방법이 없었죠. 방금 만든 ObjBox와 똑같은 처지였습니다 — 뭐든 담으려고 전부 Object로 받았고, 꺼낼 때 캐스팅했고, 틀리면 런타임에 터졌습니다. 그리고 그런 코드가 이미 세상에 산더미처럼 쌓여 있었습니다.
Java 5에서 제네릭을 넣으면서 이 산더미를 어떻게 할지 정해야 했습니다. 뭐가 문제인지 구체적으로 보겠습니다.
제네릭이 없던 시절에 누군가 만들어 배포한 라이브러리에 이런 메서드가 있다고 합시다. 그때는 제네릭이 없었으니 Box를 그냥 Box라고 받았습니다.
이제 제네릭이 생겼고, 내 코드는 이렇습니다.
소거 덕분에 그냥 됩니다. Box<String>은 컴파일이 끝나면 그냥 Box가 되니까요. 옛 라이브러리는 다시 컴파일할 필요도, 소스를 구할 필요도 없습니다.
만약 자바가 소거를 안 하고 제네릭을 런타임까지 진짜 타입으로 만들었다면, Box<String>과 Box는 서로 다른 것이라 OldLib.use(mine)은 컴파일조차 안 됩니다. 이미 세상에 배포된 라이브러리를 전부 제네릭 버전으로 새로 만들 때까지, 자바 5는 아무도 못 쓰는 셈이 됩니다. JVM도 제네릭을 이해하도록 뜯어고쳐야 하고요.
그래서 자바는 다른 길을 골랐습니다. 컴파일이 끝나면 제네릭 정보를 지워서, Box<String>이 예전 Box와 똑같은 바이트코드가 되게 한 것입니다. 그러면 옛 코드와 새 코드를 섞어 써도 그대로 돌아가고, JVM은 제네릭을 몰라도 됩니다.
우리가 만든 Box로도 확인됩니다. 자바의 모든 객체는 getClass()로 "너 실제로 무슨 클래스야?" 라고 물어볼 수 있습니다. 런타임에 답하는 메서드라, 소거가 정말 일어났는지 확인하기에 딱 좋습니다. 소거는 "컴파일이 끝나면 지워진다"는 주장이니, 런타임에 물어봐야 확인이 되니까요.

분명 <String>과 <Integer>로 다르게 만들었는데, 런타임에 물어보니 둘 다 그냥 Box 라고 답합니다. Box<String>과 Box<Integer>는 런타임에 완전히 같은 클래스입니다. <String>, <Integer>는 컴파일러가 검사에만 쓰고 버린 메모일 뿐입니다.
즉 타입 소거는 자바의 실수가 아니라 "기존 코드를 안 깨고 제네릭을 끼워 넣기 위한 선택" 이었습니다.
대가도 있습니다. 런타임에 T가 뭐였는지 정말로 남아 있지 않아서, 이런 것들이 안 됩니다.
o instanceof T가 안 되는 이유
instanceof는 런타임에 "이 객체가 이 타입이 맞냐?"고 물어보는 연산자입니다. 구체적인 타입을 물으면 잘 됩니다.
그런데 T는 런타임에 없습니다. 방금 getClass()로 확인했듯이 지워졌으니까요. 물어볼 대상 자체가 사라진 겁니다. 게다가 소거하고 나면 o instanceof T는 o instanceof Object가 되어 뭘 넣든 항상 true가 됩니다. 아무 의미가 없죠. 그래서 컴파일러가 아예 못 쓰게 막습니다.
new T[10]이 안 되는 이유
배열은 제네릭과 달리 원소 타입을 런타임까지 가지고 갑니다. getClass()로 물어보면 이렇게 답합니다.
[Ljava.lang.String;은 "String을 담는 배열"이라는 뜻입니다. 배열은 자기가 무슨 타입을 담는지 런타임에도 알고 있습니다. 반면 Box는 <String>이었는지 <Integer>였는지 답하지 못하죠.
그래서 new T[10]은 모순입니다. 배열을 만들려면 무슨 타입을 담을 배열인지 정해야 하는데, T는 런타임에 없어서 정할 수가 없습니다. 무슨 배열을 만들지 결정할 수 없으니 컴파일러가 거부합니다.
잠깐, 타입에도 위아래가 있습니다 기초
여기서 상속에서 나온 이야기 하나를 짚고 가야 합니다. 제네릭과 상관없는 내용이지만, 이게 없으면 뒤의 설명이 통째로 막힙니다.
자바의 타입들은 부모-자식으로 줄이 서 있습니다. 지금까지 나온 타입들을 세워보면 이렇습니다.

Integer의 부모는Number,Number의 부모는Object입니다.String도Object의 자식이지만Number줄에는 없습니다.Object는 맨 위입니다. 그래서 "모든 클래스의 부모"라고 부릅니다.
여기서 딱 하나만 기억하면 됩니다. 자식은 부모이기도 하지만, 부모는 자식이 아닙니다. Integer는 Number이기도 하고 Object이기도 합니다. 하지만 Object가 전부 Integer는 아닙니다. String일 수도 있으니까요.
그래서 Object로만 알려진 값은 Number를 요구하는 자리에 못 들어갑니다. Number를 달라는데 Number라는 보장이 없는 걸 내미는 셈이니까요. 컴파일러도 이렇게 거절합니다.
변수 자리든 메서드 매개변수 자리든 똑같습니다. 앞으로 이 페이지에 계속 나올 "Object라서 Number 자리에 못 넣는다"는 말은 전부 이 위계 하나에서 나옵니다. 뒤에서 볼 extends 제한도, "가장 구체적인 메서드 선택"도 전부 이 나무 위에서 벌어지는 일입니다.
타입 소거와 오버로딩 기초
이 개념이 시험에 출제되는 핵심입니다. 오버로딩과 타입 소거가 만나면 예상과 다른 결과가 나올 수 있습니다. 오버로드 선택이 컴파일 시점에 어떤 기준으로 결정되는지는 Java 컴파일타임 바인딩 페이지에서 자세히 다룹니다.
문제 상황
Printer 클래스에는 같은 이름의 print 메서드가 3개 있습니다. 매개변수 타입만 다릅니다.
오버로딩에서 배운 대로, Java는 전달하는 인자의 타입에 따라 어떤 메서드를 호출할지 결정합니다. 컴파일러는 두 단계로 고릅니다.
- 후보 추리기: 그 인자를 받을 수 있는 메서드만 남깁니다.
- 가장 구체적인 것 선택: 후보가 여럿이면 그중 가장 구체적인 메서드를 고릅니다. 앞에서 본 타입 위계에서 아래쪽(자식 쪽) 이 더 구체적입니다.
| 호출 코드 | 인자 타입 | 호출되는 메서드 |
|---|---|---|
print(new Integer(5)) | Integer | print(Integer x) -> "A5" |
print(new Object()) | Object | print(Object x) -> "B..." |
print(3.14) | Double (Number의 하위) | print(Number x) -> "C3.14" |
마지막 행이 2단계가 필요한 이유를 보여줍니다. 3.14는 Double이므로 print(Number x)도 print(Object x)도 받을 수 있어서 1단계에서 둘 다 후보로 남습니다. 이때 Number가 Object보다 구체적이므로 print(Number x)가 선택되어 "C3.14"가 출력됩니다.
제네릭 클래스 안에서 호출하면?
Printer를 위에 같이 두고 보겠습니다. 판정을 따라가려면 둘이 한눈에 보여야 합니다.
이 클래스를 다음과 같이 사용한다고 합시다.
Collection<Integer>이니까 value는 Integer입니다. 그러면 new Printer().print(value)에서 Printer의 print(Integer x)가 호출될 것 같습니다.
하지만 실제로는 Printer의 print(Object x) 가 호출됩니다.
왜 print(Object x) 가 호출될까?
오버로딩의 메서드 결정은 컴파일 타임에 이루어집니다. 여기서 순서가 중요합니다. 컴파일러는 타입 소거를 하기 전에 먼저 메서드를 고릅니다.
메서드를 고르는 시점에 new Printer().print(value)의 value는 아직 타입이 T입니다. 그런데 Collection 클래스 안에는 <Integer>라고 적힌 곳이 없습니다. 그냥 T value라고만 돼 있죠. 그래서 컴파일러가 value에 대해 아는 것은 T가 보장하는 타입, 즉 Object까지입니다.
| Printer의 메서드 | value(타입 T)를 넣을 수 있나? |
|---|---|
print(Integer x) | ❌ value가 Integer라는 보장이 없음 |
print(Number x) | ❌ value가 Number라는 보장이 없음 |
print(Object x) | ✅ value는 최소한 Object |
앞의 위계에서 본 그대로입니다. 부모(Object)는 자식(Integer, Number) 자리에 못 들어갑니다. 후보가 print(Object x) 하나만 남아 그것이 선택되고, "B" + 0 이 출력되어 답은 B0입니다.
메서드가 정해진 뒤에야 소거가 일어납니다. 이제 T를 Object로 지우면 필드는 이렇게 됩니다.
이미 print(Object x)로 정해졌고 필드도 Object라, 그대로 들어맞습니다.
여기까지가 시험에 나오는 형태입니다. 그리고 이 경우엔 지름길이 하나 있습니다. 클래스 안에서는 T가 보장하는 타입(Object)과 소거 후 타입(Object)이 값이 같아서, "소거되어 Object가 되니까 print(Object x)" 라고 순서를 반대로 외워도 답이 맞습니다. 기출은 전부 이 형태라 그렇게 외워도 됩니다. 다만 진짜 순서는 선택이 먼저, 소거가 나중이라는 것만 알아두면 됩니다. 곧 볼 클래스 밖 호출에서는 이 차이가 답을 가릅니다.
타입에 제한이 있으면? 심화
위에서 <T>는 Object로 소거된다고 배웠습니다. 그런데 <T extends Number>처럼 타입에 제한이 있으면 결과가 달라집니다. 이렇게 제한이 걸린 경우의 소거를 제한된 타입 소거(bounded type erasure)라고 부릅니다.
사실 <T>는 <T extends Object>입니다
타입 소거 규칙은 하나입니다: T는 항상 extends 뒤의 타입으로 소거됩니다.
extends 뒤에 적는 타입은 "T 자리에는 이 타입이나 그 자식만 온다"는 제한입니다. 뒤집어 말하면 컴파일러 입장에서는 T에 뭐가 오든 최소한 이 타입이기는 하다는 보장이 됩니다. <T extends Number>면 T에 Integer가 오든 Double이 오든 Number인 건 확실하니까요.
그래서 이 글에서는 extends 뒤의 타입을 T가 보장하는 타입 이라고 부르겠습니다. 정식 용어로는 T의 상한1이라고 합니다. 아래 셋은 모두 같은 말입니다.
extends뒤의 타입- T가 보장하는 타입 (이 글에서 주로 쓸 표현)
- 상한 (정식 용어)
| 선언 | 실제 의미 | T가 보장하는 타입 (상한) |
|---|---|---|
<T> | <T extends Object> | Object |
<T extends Number> | <T extends Number> | Number |
<T extends Comparable> | <T extends Comparable> | Comparable2 |
코드로 비교
앞에서 본 그 나무를 다시 가져와 보겠습니다. 두 경우를 위계 위에 올려놓으면 extends 뒤의 타입이 하는 일이 한눈에 보입니다. extends 뒤에 적은 타입은 이 나무에 그어지는 한계선입니다. 그 선보다 위로는 T에 못 오고, 선 아래만 올 수 있습니다.

extends 하나로 답이 갈립니다
소거 타깃이 Object냐 Number냐가 왜 중요한지는 오버로딩과 만나야 드러납니다. 앞의 Collection + Printer와 같은 구조인데, 메서드가 2개뿐이라 더 단순합니다.
Number를 받는 것과 Object를 받는 것, 딱 두 개입니다. 여기에 제네릭 클래스 안에서 data를 전달해 보겠습니다. extends 유무만 다른 두 클래스를 각각 만들어 비교합니다.
① 제한 없는 제네릭 — <T>
Box<Integer>로 만들었으니 data에 든 값은 Integer입니다. Integer는 Number의 자식이니 check(Number n)이 불릴 것 같습니다. 하지만 <T>에는 제한이 없으니, 컴파일하면 data는 Object로 소거됩니다.
Object를 넘기는 호출이 되었으니 check(Number n)에는 못 들어갑니다. 부모(Object)는 자식(Number) 자리에 못 가니까요. 남는 후보는 check(Object o) 하나뿐이라 결과는 "O" 입니다.

② Number로 제한한 제네릭 — <T extends Number>
inside()의 코드는 Box와 한 글자도 다르지 않습니다. 다른 것은 클래스 선언의 extends Number뿐입니다. 그런데 이 한 마디가 소거 타깃을 바꿉니다.
이번엔 Number를 넘기는 호출입니다. check(Number n)에 그대로 들어가므로 그것이 선택되어 결과는 "N" 입니다. inside() 본문은 손도 안 댔는데 extends Number 하나로 답이 바뀌었습니다.

| 제네릭 선언 | 호출 위치 | data가 보장하는 타입 (상한) | 호출되는 메서드 | 반환값 |
|---|---|---|---|---|
<T> | 클래스 안 (inside()) | Object | check(Object o) | "O" |
<T extends Number> | 클래스 안 (inside()) | Number | check(Number n) | "N" |
inside() 본문은 같은데 extends Number 유무만으로 "O"와 "N"으로 갈립니다.
이 표는 inside()처럼 클래스 안에서 호출할 때만 성립합니다. 클래스 밖에서 꺼내 넘기면 결과가 달라집니다.
클래스 밖에서 호출하면? 심화
지금까지의 규칙에는 "제네릭 클래스 안에서 호출할 때"라는 전제가 붙어 있었습니다. 이 전제를 빼고 규칙만 외우면 정반대 답이 나옵니다.
정처기 실기 기출은 모두 클래스 안에서 호출하는 형태라, 이 섹션의 코드가 그대로 출제되지는 않습니다. 다만 앞의 규칙을 "무조건"으로 외우면 변형된 문제에서 무너지기 때문에, 왜 전제가 필요한지 한 번은 보고 넘어가는 것이 좋습니다.
아래 코드는 똑같은 value 필드 를 클래스 안과 밖에서 각각 넘깁니다. Printer도 같이 두었습니다.
먼저 지금까지 배운 타입 소거로 두 줄을 예측해 봅시다.
첫 줄 c.printInside()는 앞에서 본 그대로입니다. <T>니까 컴파일하면 value는 Object로 소거되고, print(Object x)가 선택되어 B0입니다.
둘째 줄 new Printer().print(c.value)는 어떨까요? 넘기는 건 똑같은 value 필드입니다. 그리고 소거는 이 필드에 이미 일어났습니다.
value는 Object입니다. 그러니 소거로 풀면 여기도 print(Object x)가 선택되어 B0이어야 합니다.
실행해 보면 이렇습니다.
둘째 줄이 A0입니다. print(Integer x)가 호출된 겁니다. 소거된 필드는 분명 Object인데 Integer를 받는 메서드가 불렸습니다. 소거로 푸는 방법이 여기서 깨집니다.
왜 밖에서는 A0일까?
"필드는 이미 Object로 소거됐는데, 어떻게 Integer 메서드가 불리지?"라는 의문이 당연합니다. 맞습니다. 필드는 Object입니다. 핵심은 컴파일러가 두 단계로 일한다는 점입니다.
1단계 — 아직 소거 전, 타입을 검사하고 메서드를 고르는 단계. 이때는 <Integer>가 아직 안 지워졌습니다. main에는 바로 위에 이렇게 적혀 있습니다.
컴파일러는 c의 타입이 Collection<Integer>인 걸 보고, c.value의 타입을 Integer로 계산합니다. 그래서 이 시점에 print(c.value)를 print(Integer x) 호출로 확정합니다. 메서드 선택은 여기서 이미 끝납니다.
여기서 한 번 더 막힐 수 있습니다. "필드는 소거돼서 Object인데, c.value가 어떻게 Integer야?"
필드의 타입과 c.value의 타입은 다릅니다. 필드는 저장 그릇이라 소거되어 Object 하나뿐입니다. 하지만 c.value라는 접근식의 타입은 필드 선언이 아니라 c가 무슨 타입이냐로 계산됩니다. c가 Collection<Integer>니까 T 자리에 Integer가 들어가, c.value는 Integer입니다.

2단계 — 그 다음에 소거. 이제 <Integer>를 지웁니다. 그래도 필드는 원래 Object라 달라질 게 없고, 메서드는 1단계에서 이미 print(Integer x)로 정해졌습니다. 남은 문제는 하나입니다. 필드에서 값을 꺼내면 Object로 나오는데, 정해진 메서드는 Integer를 받습니다. 그래서 컴파일러가 그 사이에 캐스팅을 자동으로 넣어 Integer로 맞춰줍니다. 우리가 쓴 코드는 캐스팅이 없지만, 컴파일러는 이렇게 채워 넣은 셈입니다.
순서가 핵심입니다. 소거해서 Object가 된 다음 메서드를 고르는 게 아니라, 메서드를 먼저 고르고 소거는 그 뒤에 캐스팅으로 뒤처리합니다. 그래서 필드가 Object여도 print(Integer x)가 호출되어 A0이 나옵니다.

클래스 안에서 왜 달랐는지도 이제 분명합니다. printInside()를 컴파일할 때는 1단계에서 value의 타입이 아직 T입니다. Collection 클래스 안에는 <Integer>라고 적힌 곳이 없어서, 컴파일러가 Integer로 계산할 근거가 없습니다. <Integer>는 main에만 적혀 있고 클래스 안에는 없다 — 이 차이가 전부입니다.
| 호출 위치 | <Integer>가 보이나? | 1단계에서 계산한 인자 타입 | 선택된 메서드 | 출력 |
|---|---|---|---|---|
클래스 안 (printInside) | 안 보임 (main에만 있음) | T → Object까지만 | print(Object x) | B0 |
클래스 밖 (main) | 바로 위에 보임 | Integer | print(Integer x) | A0 |

그러면 지금까지 배운 타입 소거는 뭐였나 싶을 수 있습니다. 쓸모없는 지식이 아닙니다. 클래스 안에서 value가 왜 Object까지밖에 못 가는지, 컴파일된 Collection 클래스 하나가 왜 Collection<Integer>와 Collection<String>에 동시에 쓰일 수 있는지를 설명해 주는 것이 타입 소거입니다. 그리고 클래스 안 호출에서는 소거 후 타입과 컴파일러가 아는 타입이 마침 같아서, 소거로 풀어도 답이 맞습니다. 기출이 전부 그 경우고요. 다만 답을 고르는 진짜 기준은 호출 위치라는 것만 알고 있으면 변형 문제에도 흔들리지 않습니다.
이것은 제네릭만의 이야기가 아닙니다. Java 컴파일타임 바인딩에서 다루는 "오버로드 후보는 호출 코드가 쓰인 위치에서 컴파일러가 볼 수 있는 것으로 결정된다"는 규칙이, 제네릭에도 그대로 적용된 것뿐입니다. 그 페이지에서 말하는 "선언 타입"이 이 페이지의 "컴파일러가 보는 타입"과 같은 개념입니다.
정보처리기사 실기 기출 문제
Footnotes
-
상한(bound): 타입 파라미터에 올 수 있는 타입의 위쪽 한계.
<T extends Number>면 T 자리에 올 수 있는 것은Number와 그 자식(Integer,Double등)뿐이고,Number보다 위(부모)인Object는 못 옵니다. 그래서 "위쪽 한계"라고 부릅니다. 한계가Number라는 것은 곧 "T는 최소한Number이기는 하다"는 보장과 같은 말입니다.<T>의 상한은Object입니다. ↩ -
Comparable은 Java에서 비교 가능한 타입임을 나타내는 인터페이스입니다.compareTo()메서드를 제공하며,Integer,String등이 이를 구현합니다. ↩