자바에서 여러 데이터를 한번에 저장하기 위해서는 배열이나 제네릭스(Generics)를 사용한다. 그런데 제네릭스를 공부하다보면 중간에 컬렉션(Collection)과 컬렉션 프레임워크라는 개념이 나오는데 이 때부터 개념들이 헷갈리기 시작한다.

배열(Array), 컬렉션(Collection), 컬렉션 프레임워크, Wrapper 클래스, 제네릭스(Generics)라는 5가지 개념들은 서로 독립적으로 존재하는 것이 아니다. “기존 기술의 치명적인 단점을 극복하기 위해 다음 기술이 탄생했다”는 꼬리에 꼬리를 무는 완벽한 인과관계를 가지고 있다. 자바가 데이터를 다뤄온 발전 흐름을 따라가며 이들의 역할을 하나씩 조망해 본다.

1. 배열 (Array) : 자바의 원시적인 데이터 저장소

가장 먼저 등장한 것은 자바가 태어날 때부터 가지고 있던 기본적인 자료구조인 배열이다.

특징: int[], String[]처럼 기본형 데이터와 참조형(객체) 데이터를 가리지 않고 모두 담을 수 있으며, 구조가 단순해 속도가 가장 빠르다.
문제점: 메모리를 할당받을 때 크기가 고정되어 버린다. 중간에 데이터를 추가하거나 삭제하면 알아서 공간이 늘어나거나 줄어들지 않으므로, 개발자가 일일이 배열을 복사하고 당기는 복잡한 코드를 짜야 한다.

2. 컬렉션 프레임워크와 컬렉션 : 똑똑한 동적 배열의 등장

배열의 한계를 극복하기 위해 자바 개발진이 내놓은 해결책이 바로 컬렉션 프레임워크(Collection Framework)다.

개념: 다수의 데이터를 쉽고 효과적으로 관리할 수 있도록 자바가 공식적으로 제공하는 ‘데이터 구조 라이브러리(클래스들의 아키텍처)’ 전체를 컬렉션 프레임워크라고 부른다. 그리고 그 규약에 맞춰 만들어진 ArrayList, HashSet 같은 개별 구현체 클래스들을 컬렉션(Collection)이라고 부른다.
특징: 내부적으로 배열을 숨겨놓고, 데이터가 꽉 차면 알아서 배열 크기를 늘려주며(add), 중간 데이터를 지우면 빈칸을 알아서 당겨주는(remove) 강력한 내장 메서드를 제공한다.
문제점: 이 만능 바구니는 어떤 종류의 객체든 다 담을 수 있도록 최상위 부모인 Object[] 배열로 내부를 설계했다. 즉, 오직 ‘객체(Object)’만 들어갈 수 있다.

3. Wrapper 클래스 : 기본형 데이터를 객체로 둔갑시키는 포장지

컬렉션의 편리함을 맛본 개발자들은 int, double 같은 숫자 데이터도 컬렉션에 넣고 싶어졌다. 하지만 기본형 데이터는 ‘객체’가 아니므로 컬렉션의 Object 배열에 들어갈 수 없었다.

해결책: 기본형 데이터를 예쁘게 포장해서 객체로 만들어주는 Wrapper 클래스(Integer, Double 등)가 등장했다. intInteger라는 껍데기로 감싸(Boxing) 어엿한 객체로 취급받게 함으로써, 무사히 컬렉션에 들어갈 수 있는 연결고리가 완성되었다.

4. 제네릭스 (Generics <T>) : 만능 컬렉션의 위험성을 통제하는 안전장치

컬렉션에 모든 객체(Wrapper 포함)를 담을 수 있게 되었지만, 또 다른 대참사가 발생했다. 컬렉션은 기본적으로 Object 타입으로 모든 걸 다 받다 보니, 하나의 리스트에 문자열(String)과 숫자(Integer)가 무분별하게 섞여 들어가는 문제가 생긴 것이다.
이 데이터를 꺼내 쓰려면 매번 원래 타입으로 형변환(다운캐스팅)을 해야 했고, 잘못 변환하면 프로그램이 실행 도중 강제 종료(ClassCastException)되어 버렸다.

해결책: 컬렉션을 생성할 때 <String>, <Integer>처럼 명확한 이름표를 붙여 단일 타입만 허용하도록 강제하는 문법인 제네릭스가 도입되었다. 이를 통해 불필요한 형변환을 없애고, 잘못된 타입이 들어오는 것을 코드 작성(컴파일) 단계에서 완벽히 차단하게 되었다.

💡 결론: 모든 역사가 담긴 단 한 줄의 코드

결국 우리가 실무에서 매일같이 작성하는 아래의 코드는, 자바 데이터 구조의 역사적 한계를 모두 극복한 최종 완성본이다.

ArrayList<Integer> list = new ArrayList<>();