자바에서 오버로딩이 왜 이상하게 동작할까?
(feat. 오토박싱, 제네릭, 타입소거)
며칠 전에 정처기 기출 문제 중 자바 코드를 보다가 정말 헷갈리는 상황을 하나 만났다. 분명히 Integer로 값을 넘겼는데, 의도했던 오버로딩 메서드가 안 불리는 거다. 당연히 print(Integer a)가 불릴 줄 알았는데, 웬걸 print(Object a)가 호출된다.
"내가 뭘 잘못했나?" 싶어서 코드를 찬찬히 뜯어보다가, 자바가 제네릭을 처리하는 방식 + 오버로딩 결정 시점 + 오토박싱까지 엮여서 이런 일이 생긴 거였다.
Java를 사용하다 보면 메서드 오버로딩이 직관과 다르게 동작하는 상황을 종종 마주하게 된다. 특히 제네릭과 함께 사용할 때, 어떤 메서드가 호출되는지 예상하기 어려운 경우가 있다.
처음 썼던 코드
class Printer {
void print(Integer a) {
System.out.print("A" + a);
}
void print(Object a) {
System.out.print("B" + a);
}
void print(Number a) {
System.out.print("C" + a);
}
}
class Collection<T> {
T value;
public Collection(T t) {
value = t;
}
public void print() {
new Printer().print(value);
}
}
public class Main {
public static void main(String[] args) {
new Collection<>(0).print();
}
}
솔직히 당연히 "A0"이 출력될 줄 알았다. 왜냐면 0은 int고, 오토박싱 돼서 Integer가 될 테니까. 그럼 Printer에서 print(Integer a)가 호출되는 게 맞는 거 아닌가?
근데 결과는 B0. print(Object a)가 호출이 되었다.
처음 이걸 이해하려면 오토박싱이 뭔지부터 정확히 알아야 했다.
오토박싱(AutoBoxing)이란?
오토박싱(AutoBoxing)은 자바가 기본형(primitive type)을 자동으로 객체(wrapper class)로 변환하는 기능이다.
예를 들어 int ↔ Integer, double ↔ Double 이런 식이다.
오토박싱은 말 그대로 기본형을 자동으로 객체로 "포장"해주는 과정이다.
즉,
Integer x = 10; // 자동으로 int → Integer로 변환
반대로 Integer를 int로 꺼내 쓸 때는 언박싱(unboxing)이라고 부른다.
그래서 new Collection<>(0)처럼 0을 넘기면 int지만, 오토박싱돼서 Integer로 들어간다. 여기까지는 맞다.
근데 왜 print(Object a)가 호출되지?
여기서 중요한 포인트는 자바의 오버로딩은 "컴파일 타임"에 결정된다는 거다.
그럼 new Collection<>(0)에서 T는 Integer가 맞으니까, value도 Integer일 텐데 왜 print(Integer a)가 안 불릴까?
그 이유는 제네릭과 타입 소거(type erasure) 때문이다.
자바에서 제네릭은 컴파일 타임에만 타입 정보를 갖고, 실제 컴파일된 클래스에는 T가 지워진다. 그래서 Collection<Integer>는 런타임에는 그냥 Collection<Object>처럼 동작하게 된다. 즉, value의 타입이 Object로 바뀌는 것이다.
그러다 보니 new Printer().print(value)를 호출할 때, 컴파일러는 value의 타입을 Object로 본다. 그러면 print(Object a)가 선택된다.
Integer 타입으로 추론됐지만, 결국 컴파일 타임에 T는 날아가기 때문에 발생한 현상이다.
만약 이렇게 썼다면?
new Printer().print(0);
이 코드는 다르다. 0은 int고, 오토박싱 돼서 Integer가 되며, 그 타입은 컴파일러가 명확히 알고 있다.
그래서 이 경우는 print(Integer a)가 정확히 호출된다. 출력은 "A0"이 된다.
정리하며
자바에서는 오버로딩이 **런타임 다형성(polymorphism)**이 아니라 컴파일 타임 다형성이라는 걸 명확히 알아둬야 한다.
그리고 제네릭을 쓸 때는 실제로는 타입이 소거돼서 Object처럼 처리된다는 점도 꼭 기억해야 한다.
이 조합 때문에 예상치 못한 메서드가 호출되는 일이 생길 수 있다. 나도 처음엔 한참 삽질했다. 오토박싱, 오버로딩, 제네릭이 만나면 "보는 것과 실제로 동작하는 것"이 다를 수 있다.
아직 자바의 이런 디테일은 계속 겪어봐야 감이 잡히나 보다.. 😅
'CS' 카테고리의 다른 글
| [CS] Java 기초 (배열) (0) | 2025.02.14 |
|---|---|
| [CS] Java 기초 (형 변환, 문자열 기능) (0) | 2025.02.14 |
| [CS] 소프트웨어 설계 -소프트웨어 아키텍처 패턴 (0) | 2025.01.21 |
| [CS] 소프트웨어 설계 - 분석 모델 확인(모델링/분석 자동화) (0) | 2025.01.20 |
| [CS] 소프트웨어 설계 - 애자일 방법론 (0) | 2025.01.19 |