자바의 컬렉션 프레임워크(ArrayList, HashMap 등)를 처음 다루다 보면 <int> 대신 <Integer>라는 Wrapper 클래스를 강제로 사용해야 하는 상황을 마주하게 된다.
왜 이런 번거로운 규칙이 존재하는 것일까? 결론부터 말하자면, 오직 ‘객체(Object)’만 취급하도록 설계된 컬렉션의 내부 구조와, 객체가 아닌 ‘순수 데이터’인 기본형(Primitive) 타입이 구조적으로 충돌하기 때문이다.
1. 자바의 이원화된 타입 시스템 (기본형 vs 참조형)
자바의 데이터 타입은 메모리에 저장되는 방식과 ‘객체 지향적 특징’을 가지는지에 따라 두 가지로 완벽하게 나뉜다. 이를 이해하려면 먼저 클래스(Class)와 객체(Object)의 기술적 의미를 명확히 알아야 한다.
- 클래스 (Class): 데이터(변수)와 그 데이터를 조작하는 기능(메서드)을 하나로 묶어놓은 ‘구조적 템플릿(틀)’이다.
- 객체 (Object): 클래스를 바탕으로 컴퓨터 메모리(Heap)에 실제 공간을 할당받아 생성된 ‘실체(인스턴스)’다. 즉, 데이터뿐만 아니라 동작(기능)까지 스스로 수행할 수 있는 거대한 덩어리다.
이러한 개념을 바탕으로 자바의 데이터 타입을 나누면 다음과 같다.
① 기본형 타입 (Primitive Type)
int, double, boolean 등은 클래스(Class)를 통해 만들어진 ‘객체’가 아니다.
- 메소드나 속성(상태)을 가질 수 없으며, 메모리의 스택(Stack) 영역에 실제 데이터 값(예:
10,3.14) 자체가 직접 저장된다. - 내부에 데이터를 조작하는 어떠한 기능(메서드) 없이 오로지 연산의 재료가 되는 순수한 데이터 덩어리다. (예:
10.toString()처럼 값을 스스로 조작하는 코드는 불가능하다.)
② 참조형 타입 (Reference Type)
String, Integer를 포함해 개발자가 직접 만든 모든 클래스는 템플릿을 통해 메모리(Heap)에 생성된 온전한 ‘객체(Object)’다.
- 데이터 값뿐만 아니라, 그 데이터를 조작할 수 있는 다양한 내장 기능(메서드)을 함께 가지고 있다. (예: 숫자 객체인
Integer는 자신의 값을 문자열로 바꾸는.toString(), 다른 숫자와 비교하는.compareTo()등의 기능을 스스로 가지고 있다.) - ⭐ 핵심 특징: 자바에서 클래스로 만들어진 모든 객체는 자바 시스템의 최상위 조상 클래스인
Object를 자동으로 상속받는다. (기본형은 클래스가 아니므로 상속이란 개념 자체가 없다.)
2. 컬렉션 프레임워크의 특징과 다형성
ArrayList와 같은 컬렉션은 모든 종류의 데이터를 범용적으로 담을 수 있어야 한다. 자바 개발진은 이 범용성을 확보하기 위해 컬렉션의 내부 배열 타입을 최상위 부모 클래스인 Object[]로 설계했다.
- 객체의 종류에 상관없이 하나의 컬렉션에 담을 수 있다.
- 문자열(
String), 학생(Student), 날짜(Date) 등 세상의 모든 객체를 종류를 가리지 않고 하나의 바구니에 다 담을 수 있다.
- 문자열(
- 단, 컬렉션에는 오직 ‘객체’만 담을 수 있다.
이것이 가능한 이유는 ‘모든 클래스는 조상이 같다’는 규칙과, 이전에 배운 ‘다형성(업캐스팅)’ 덕분이다.
- 자바에서 존재하는 모든 참조형 타입(클래스)은 개발자가 명시하지 않아도, 무조건
Object라는 자바의 최고 조상 클래스를 자동으로 상속받고 태어난다. 즉,String도Object의 자손이고, 내가 만든Student클래스도 결국Object의 자손이다. - 우리는 앞선 포스트에서 “부모 타입의 배열에는 모든 자식 객체를 담을 수 있다(다형성)”는 것을 배웠다. (
Student[]배열에 대학생, 중학생을 모두 담았던 것을 떠올려 보자.)
자바 개발진은 이 원리를 그대로 가져왔다. 컬렉션이 내부적으로 데이터를 저장하는 배열의 타입을, 세상 모든 클래스의 공통 부모인 Object[]로 만들어 둔 것이다.
// 컬렉션(ArrayList)이 내부적으로 데이터를 담는 원리
Object[] magicBox = new Object[10];
// 다형성(업캐스팅) 덕분에, 부모인 Object 바구니에 세상 모든 자식 객체들이 들어갈 수 있다!
magicBox[0] = new String("문자열");
magicBox[1] = new Student("홍길동");
결과적으로 다형성 원리에 의해, 어떤 객체든 부모인 Object 타입으로 자연스럽게 변환(업캐스팅)되어 이 만능 배열 안에 쏙 들어갈 수 있게 되는 것이다.
3. 충돌: 기본형 데이터의 한계
여기서 문제가 발생한다. int와 같은 기본형 데이터는 객체가 아니며, 당연히 Object 클래스를 상속받지도 않는다. 따라서 다형성이 성립하지 않으므로 Object 타입으로 변환될 수 없고, 결과적으로 컬렉션 내부의 Object[] 배열에 들어갈 수 없는 것이다.
4. 해결책: Wrapper 클래스
이러한 구조적 한계를 우회하기 위해 도입된 것이 Wrapper 클래스다.
순수한 기본형 데이터를 class로 감싸서(Boxing) 객체로 만들어주는 역할을 한다. int를 감싼 Integer 클래스는 어엿한 참조형 타입이므로, Object를 상속받게 되어 무사히 컬렉션에 담길 수 있게 된다.
import java.util.ArrayList;
public class GenericsLimit {
public static void main(String[] args) {
// ArrayList<int> list1 = new ArrayList<int>();
// 💥 컴파일 에러! Type argument cannot be of primitive type
// 기본형 데이터를 객체로 감싼 Wrapper 클래스(Integer)를 사용해야 한다.
ArrayList<Integer> list2 = new ArrayList<Integer>();
// 내부적으로 자바 컴파일러가 10을 new Integer(10)으로 자동 변환(Auto-boxing) 해준다.
list2.add(10);
}
}