제네릭 배열을 직접 만들 수 없는 이유

yoondgu·2023년 3월 12일
1

Java 

목록 보기
18/18
post-thumbnail

우테코 5기 백엔드 과정 중 작성한 글입니다.
학습하는 과정인 만큼 잘못된 내용이 있다면 얼마든지 피드백 부탁드립니다 !!


우테코 학습 과정 중, 직접 String 타입의 ArrayList를 구현해본 뒤, 해당 구현 클래스를 실제 ArrayList처럼 제네릭으로 만들어 보는 미션이 있었다.

제네릭에 대해 책을 펼쳐 학습하기 전에
일단 무작정 기존 메서드들의 파라미터, 리턴 타입을 모두 타입 파라미터로 교체해보는 방식으로 진행했다.
그래서 원소 데이터를 가지고 있는 elementData 의 선언 타입을 제네릭 배열로 바꾸려고 했지만,
컴파일 에러가 떴다.

이를 통해 제네릭 배열을 직접 만들 수 없다는 것을 알게 되었다.

실제 ArrayList 클래스의 구현 코드를 다시 찾아보니,
원소 데이터를 저장하는 elementData 의 타입이 제네릭 배열이 아닌 Object 타입의 배열임을 확인할 수 있었다.

왜 제네릭 배열은 직접 선언해서 만들 수 없는 걸까?
그 전에 제네릭이 무엇인지부터 짚고 넘어가자.

제네릭이란

  • 제네릭은 데이터 타입을 일반화한다.
    • 클래스, 메서드에서 사용할 데이터 타입을 인스턴스를 생성할 때나 메서드를 호출할 때 정한다.
  • 클래스, 메소드에서 사용하는 내부 데이터 타입을 컴파일 시 미리 지정한다.
  • 이는 미리 타입 검사를 수행한다는 뜻이기도 하다.
  • Object 타입을 사용하면 어떤 하위 타입도 저장할 수 있지만, 매번 형변환을 해주어야 하며 타입 안전성 문제가 생긴다.
    • 반면 Generic은 컴파일 시 미리 타입 검사를 수행하기 때문에 이 문제를 해결할 수 있다.
  • Collection 프레임워크에서 사용 예시를 잘 살펴볼 수 있다.

인스턴스 생성, 메서드 호출과 같은 말 때문에 런타임 시점에 타입 체크가 진행되는 것이 아닌가? 헷갈릴 수도 있는데, 그게 아니다!!
인스턴스 생성, 메서드 호출 과 같은 코드를 작성할 때 개발자가 데이터 타입을 정해줄 수 있다는 뜻이다.

  1. 인스턴스를 생성할 때 제네릭 클래스의 데이터 타입을 정한다.
// 아래와 같은 코드를 작성하여 타입을 지정하면 컴파일 타임에서 타입 체크를 해준다.
// 생성 시 new 연산에서 데이터 타입이 생략 가능하다. 변수 타입 선언에서 해당 타입을 알고 있기 때문이다.
final SimpleList<Printer> printers = new SimpleArrayList<>();
  1. 메서드를 호출할 때 제네릭 메서드의 데이터 타입을 정한다.
// 파라미터의 타입이 SimpleList<E extends Number>일 때, 메서드 호출 코드 작성 시 전달한 파라미터값에 따라 타입을 정한다.
SimpleList.sum(new SimpleArrayList<>(1, 2, 3));

// 제네릭 타입이 SimpleList<E extends Number>이기 때문에 아래 코드에 대해서 컴파일 에러가 발생한다.
// 컴파일 타임에서 타입 체크를 할 때, Number 클래스에 대한 상속 여부까지 확인하기 때문이다.
SimpleList.sum(new SimpleArrayList<>("a", "b", "c"));

제네릭 배열을 생성할 수 없다?

배열과 제네릭의 차이점에는 두 가지가 있다.
1. 공변 vs 불공변
2. 런타임 실체화 vs 런타임 소거

결론부터 말하자면 런타임에 실체화되는 배열의 타입을 제네릭으로 하기 때문에 타입 안전성을 보장할 수 없게 된다.

공변(covariant) vs 불공변

공변은 사전적 의미로 함께 변하는 이라는 뜻이다.
Sub 타입이 Super 타입의 하위 타입이라고 했을 때,

  • 배열의 경우 Sub[]은 배열 Super[]의 하위 타입이다. ⇒ 공변
  • 제네릭 타입의 경우 ArrayList<Sub>ArrayList<Super>의 하위 타입이 아니다. ⇒ 불공변

런타임 실체화 vs 런타임 소거

  • 배열의 타입은 런타임에 실체화된다.
  • 반면 제네릭 타입은, 이미 컴파일 시점에 타입 체크를 한 후 런타임에는 소거된다.

런타임에 소거된다는 것은, 컴파일에서만 타입에 대한 제약을 정의하고 런타임에는 타입에 대해 알 수 없게 된다는 뜻이다.
언바운드 타입(<?>, <T>)은 Object로 변환되고
바운드 타입은 제한하는 상위 타입으로 변환된다.
예를 들어 <E extends Comparable>Comparable로 변환된다.

만약 제네릭 배열이 가능하다고 가정해보자.

// 실제로는 아래처럼 SimpleList<Printer>라는 제네릭 타입의 배열을 직접 생성하려고 하면 컴파일 에러가 발생한다.
final SimpleList<Printer>[] printerLists = new SimpleArrayList<Printer>[1];
// 만약 에러가 발생하지 않는다면,
// 배열은 공변하므로 printerLists라는 배열은 모든 클래스가 상속하는 Object 타입의 배열로 형변환이 가능하다.
final Object[] objects = printerLists;
// Object 배열에 SimpleList<Printer> 외의 전혀 다른 타입을 저장할 수 있게 된다.
objects[0] = new SimpleArrayList<>(1);

// printer 타입의 값을 꺼내기를 기대하지만, Integer 타입의 값 1을 꺼내게 된다.
// 런타임 시점에 형변환 오류가 발생한다. 즉 타입 안전성을 보장하지 못했다.
final Printer printer = objects[0].get(0);


(실제로는 IDE에서 Generic array creation을 할 수 없다고 컴파일 경고가 뜬다.)

따라서 제네릭 배열은 타입 안전성을 보장할 수 없기 때문에, 직접 생성이 불가능하도록 만들어둔 것이다.

  • 그래서 ArrayList의 원소를 저장하는 내부 배열인 elementData는 Object 타입의 배열이고,
    제네릭을 통해 타입 체크가 된 값을 저장할 때 형변환을 해주고 있다.

엄밀히 말하자면 직접 생성이 불가능한 것이지 자바에서 제네릭 배열이 지원되지 않는 것은 아니다.
Object 배열을 E[]로 강제 형변환할 수 있다.
그러나 타입 안전성을 보장해줄 수 없기 때문에 컴파일러에서 경고를 표시한다.

결론

제네릭 배열을 직접 생성할 수 없으며,
강제 형변환으로 만드는 것 또한 타입 안전성을 보장하기 위해서는 권장하지 않는다.
ArrayList처럼 클래스 내부에서 자체적으로 형 안전성을 보장할 수 있도록 구현한 상황이 아니라면,
제네릭 타입에 대해서는 웬만하면 배열이 아닌 List를 사용하자!


(번외) try to generify 경고와 @SuppressWarnings("unchecked")

내가 직접 만든 ArrayList에 제네릭 타입을 적용하다 보니 아래와 같은 경고 메시지가 생겼다.
확인되지 않은 형변환이라는 것이다.

실제 ArrayList가 구현된 내용을 보면,
아래처럼 @SuppressWarnings("unchecked") 어노테이션을 붙여 경고를 억제하는 것을 확인할 수 있다.

이펙티브 자바에서는 무점검 경고(unchecked warning)를 제거하라고 한다.
그러나 만약 이를 사용한다면, 그 대신 왜 형 안전성을 위반하지 않는지 주석을 통해 정확히 밝히라고 말한다.

ArrayList에서 Object 배열에 담는 값은 컴파일 시에 타입 체크를 해서 타입 안전성이 지원되는 제네릭 타입임이 보장되므로, 형변환 시 오류가 일어날 위험이 없기 때문에 문제가 없어보인다.

0개의 댓글