배열의 공변성
이번 포스트에서는 이펙티브 자바(Effective Java)에서 다루는 주제 중 하나인 배열의 공변성(Covariance)과 그로 인해 발생하는 문제점에 대해 알아보겠습니다. 자바를 사용하면서 한 번쯤은 겪었거나 들어봤을 “배열 공변성”에 대한 이야기를 통해 왜 이 설계가 위험할 수 있는지, 그리고 어떻게 대처할 수 있는지 살펴보도록 하겠습니다.
1. 배열의 공변성이란?
자바에서 배열은 공변적(covariant)입니다.
즉, 만약 Integer가 Number의 하위 타입(subtype)이라면, Integer[]는 Number[]의 하위 타입이 됩니다.
Integer[] intArray = new Integer[10];
Number[] numArray = intArray; // 배열의 공변성 때문에 컴파일 성공
이처럼 배열의 공변성 덕분에, 하위 타입의 배열을 상위 타입 배열로 취급할 수 있습니다. 표면적으로는 편리해 보이지만, 이 설계에는 치명적인 문제점이 숨어 있습니다.
2. 공변성이 가져오는 문제점
배열의 공변성 때문에 발생하는 주요 문제는 런타임 타입 안전성(runtime type safety) 입니다. 위 예제를 확장해 보겠습니다.
Integer[] intArray = new Integer[10];
Number[] numArray = intArray; // Integer[]는 Number[]의 하위 타입
numArray[0] = 3.14; // Double 타입의 값 할당 시도
코드를 보면, numArray는 Number[] 타입으로 선언되어 있으며, 3.14 (Double 타입) 할당이 컴파일러 입장에서는 문제가 없어 보입니다.
그러나 실제로 numArray는 Integer[]를 참조하고 있으므로, Double 값을 할당하는 순간 ArrayStoreException이 발생합니다.
이처럼 컴파일 타임에는 문제가 발견되지 않고, 런타임에 에러가 발생하는 현상은 디버깅을 어렵게 만들며, 시스템의 안정성을 해칠 수 있습니다.
3. 왜 배열은 공변적으로 설계되었나?
배열은 자바 초창기부터 존재했던 자료구조입니다. 당시 자바의 타입 시스템에서는 배열의 공변성이 자연스럽게 받아들여졌습니다.
또한, 배열은 런타임에 reifiable하기 때문에, 실행 시점에 정확한 타입 정보를 가지고 있어 잘못된 타입의 삽입 시점에 예외를 던질 수 있습니다.
하지만, 이러한 런타임 검사 방식은 컴파일 타임에 문제를 잡을 수 있는 제네릭스(generics)와는 대조적입니다.
즉, 배열은 유연하지만 안전성 면에서 취약한 반면, 제네릭스는 엄격한 타입 검사를 통해 컴파일 타임에 오류를 방지합니다.
4. 제네릭스로 안전한 대안 사용하기
자바 5부터 도입된 제네릭스는 불공변(invariant)입니다.
예를 들어, List<Integer>는 List<Number>의 하위 타입이 아니기 때문에, 컴파일 타임에 타입 불일치가 발생합니다.
List<Integer> intList = new ArrayList<>();
// List<Number> numList = intList; // 컴파일 에러: List<Integer>는 List<Number>의 하위 타입이 아님
// 안전한 방법: 제네릭 메서드나 와일드카드 타입을 사용하여 타입 계층을 관리할 수 있음
List<? extends Number> numList = intList;
이런 방식은 타입 불일치를 컴파일 단계에서 잡아주기 때문에, 런타임 에러를 예방하는 데 큰 도움이 됩니다.
이펙티브 자바에서는 이러한 이유로 배열보다는 제네릭 컬렉션 사용을 권장하고 있습니다.
5. 정리 및 결론
- 배열의 공변성: 자바 배열은 하위 타입의 배열을 상위 타입 배열로 취급할 수 있어 편리하지만, 잘못된 타입의 값이 할당될 가능성이 있어 런타임 에러(예: ArrayStoreException)가 발생할 수 있습니다.
- 런타임 안전성: 배열은 실행 시점에 타입 체크를 하지만, 이는 개발자가 실수를 발견하기 어렵게 만듭니다.
- 제네릭의 강점: 제네릭은 컴파일 타임에 엄격한 타입 검사를 제공하여, 배열의 공변성 문제와 같은 위험을 미연에 방지합니다.
- 이펙티브 자바의 권장 사항: 가능한 경우 배열 대신 제네릭 컬렉션을 사용하여 타입 안전성을 확보하고, 런타임 예외를 줄이도록 합니다.
배열의 공변성은 자바의 역사적 유산이라 볼 수 있으나, 현대의 개발 환경에서는 제네릭을 활용한 타입 안전한 코드를 작성하는 것이 바람직합니다.