JAVA/Effective Java

[아이템 28] 배열보다는 리스트를 사용하라

꾸준함. 2024. 2. 18. 03:02

배열 vs 제너릭

배열과 제네릭 타입에는 다음과 같이 두 가지 차이점이 존재합니다.

  • 배열은 공변(covariant)인 반면 제네릭은 불공변
  • 배열은 실체화(reify) 되는 반면 제네릭은 실체화되지 않음

 

1. 배열은 공변인 반면 제네릭은 불공변

 

공변은 간단히 말하면 함께 변한다는 의미이며, 반면에 불공변은 함께 변하지 않는다는 뜻입니다.

 

1.1 배열은 공변

자바에서 Object는 최상위 계층이므로 String과 Object 클래스는 호환이 됩니다.

따라서 Object[] 배열에 String을 담을 수 있으며 위 코드에서 anything을 Object[] 배열로 선언했지만 실제 인스턴스는 String 배열입니다.

해당 코드는 문법적으로 문제가 없기 때문에 컴파일은 되지만 한 가지 문제가 존재합니다.

배열은 공변이기 대문에 String 배열에 다른 타입을 넣더라도 컴파일 타임에 잡아낼 수 없고 런타임 시점에 ArrayStoreException이 발생합니다.

특정 인덱스에 접근할 때 배열을 사용할 경우 시간 복잡도가 O(1)이기 때문에 성능적으로 유리하지만 이처럼 안전성이 떨어진다는 것이 배열의 단점입니다.

 

 

 

1.2. 제네릭은 불공변

 

반면에 제네릭을 사용하면 타입 간의 계층 구조가 의미 없어집니다.

앞서 언급했다시피 Object와 String은 상하관계가 존재하지만 List<String> 인스턴스를 List<Object> 타입으로 선언하려고 하면 컴파일 에러가 발생합니다.

이와 같이 제네릭을 사용하면 컴파일 시점에 타입 확인을 하기 때문에 런타임이 아닌 시점에서 안전성을 보장할 수 있습니다.

 

 

 

2. 배열은 실체화가 되는 반면 제네릭은 실체화 되지 않음

 

배열은 런타임에도 자신이 담기로 한 원소의 타입을 인지하고 확인하며 이를 저자는 실체화가 된다고 표현합니다.

앞서 차이점 1에서 설명했다시피 배열의 경우 다른 타입 원소를 넣으려고 하면 ArrayStoreException이 발생합니다.

 

반면 제네릭의 경우 아이템 26에서 언급했다시피 컴파일 과정에서 타입이 벗겨지고 Object로 받은 뒤 제네릭일 경우 컴파일러가 자체적으로 타입 체킹을 합니다.

즉, 런타임 시점에는 선언한 타입이 소거되기 때문에 실체화가 되지 않는다고 표현합니다.

실제로 바이트 코드를 확인해보면 컴파일 과정에서 타입이 소거되고 선언한 타입을 기반으로 타입 확인하는 것을 볼 수 있습니다. 

 

바이트 코드

 

3. 배열과 제네릭을 같이 사용 못하는 이유

앞서 언급한 두 가지 주요 차이 때문에 배열과 제네릭은 잘 어우러지지 못합니다.

이 때문에 자바 컴파일러는 제네릭 배열을 만들지 못하게 막으며 주요 이유는 타입 안전성을 보장하지 못하기 때문입니다.

 

 

 

제네릭 배열을 생성하는 코드가 가정할 경우

  • 배열은 공변이기 때문에 Object[] 배열에 stringLists를 할당해도 아무 문제없음
  • 또한 Object가 최상위 계층이고 List<Integer>는 제네릭이고 컴파일 과정에서 타입이 소거되기 때문에 저장하는데 문제없음
  • 다음 줄이 문제인데 List<String> 인스턴스만 담겠다고 선언한 stringLists 배열에는 지금 List<Integer> 인스턴스가 저장되어 있고 컴파일러가 꺼낸 원소를 자동으로 String으로 캐스팅하는 과정에서 ClassCastException 발생

 

이처럼 제네릭 배열을 허용할 경우 위와 같이 타입 안전성 관련 이슈가 충분히 발생할 수 있기 때문에 자바에서는 배열과 제네릭을 함께 사용하지 못하도록 막습니다.

 

 

구체화된 예시

저자는 구체화된 예시로 생성자에서 컬렉션을 받는 Chooser 클래스 코드를 제시했습니다.

  • Chooser 클래스는 컬렉션 내 원소 중 하나를 무작위로 선택해 반환하는 choose 메서드를 제공하며
  • 생성자에 어떤 컬렉션을 넘기느냐에 따라 이 클래스를 범용적으로 사용 가능

 

제네릭을 사용하지 않고 구현한 Chooser 클래스

 

 

 

위 클래스의 경우 생성자에 넘겨받는 컬렉션 내 타입 안전성을 보장할 수 없습니다.

만약 넘겨받는 컬렉션 내 Number로 타입 캐스팅이 불가한 타입이 존재할 경우 ClassCastException이 발생할 것입니다.

 

타입 안전성 보장을 위한 첫 시도

 

타입 안전성을 보장하기 위해 아이템 29 개념에 따라 제네릭으로 리팩토링하면 아래와 같을 것입니다.

 

 

 

하지만 위 코드의 경우 toArray()의 반환 값은 Object[] 타입인 반면 choiceArray는 T[] 타입이기 때문에 타입 호환성이 안 맞아 컴파일 에러가 발생합니다.

 

타입 안전성 보장을 위한 두 번째 시도

 

타입 호환성을 맞추기 위해 choices.toArray()를 T[] 타입으로 캐스팅해줍니다.

 

 

 

하지만 이렇게 변경할 경우 T가 무슨 타입인지 알 수 없어 타입 안전성을 보장할 수 없다는 비검사 경고가 발생합니다.

물론 unchecked warning은 컴파일에 영향을 미치지 않으며, 아이템 27에서 다룬 @SuppressWarnings 어노테이션을 사용하면 경고를 제거할 수 있습니다.

그러나 여전히 컴파일러가 타입 안전성을 완전히 보장하지 못하는 것은 변하지 않습니다.

 

배열 대신 리스트 사용

 

choiceArray 대신 제네릭 리스트인 choiceList를 사용하면 이전에 발생했던 타입 안전성 문제를 해결할 수 있습니다.

인덱스를 참조할 때 시간 복잡도가 배열 대비 느려지겠지만 런타임에 ClassCastException 발생하는 상황을 방지해 주기 때문에 조회 성능이 극도로 민감한 상황이 아니고서야 배열보다는 리스트를 사용하는 것을 권장합니다.

 

 

 

 

정리

배열은 공변이고 실체화가 되는 반면 제네릭은 불공변이고 실체화가 되지 않습니다.

배열의 경우 문제가 생기더라도 런타임 시점에서야 잡을 수 있지만 리스트의 경우 컴파일 타임에서 문제를 잡아낼 수 있습니다.

조회 성능이 극도로 민감한 상황이 아니라면 배열보다는 제네릭 리스트 사용을 권장합니다.

이는 코드의 안정성과 유지보수성을 향상할 수 있습니다.

 

참고

이펙티브 자바
이펙티브 자바 완벽 공략 2부 - 백기선 강사님

 

반응형