코드리뷰를 할 때 배열을 사용할 것인가 리스트를 사용할 것인가는 의외로 자주 등장하는 주제이다.
배열과 리스트의 차이점을 설명하기 전에 우선 배열과 제너릭 타입의 중요한 차이를 알아야 한다.
배열은 Sub가 Super의 하위 타입이라면 Sub 배열은 Super배열의 하위 타입이 된다. 이것을 공변이라고 하는데 함께 변한다는 뜻이다.
반면에 제너릭은 불공변이다. 즉, 서로 다른 타입 Type1, Type2가 있을 때, List, List은 서로 연관성이 없다. 여기서 중요한 차이가 나타나게 된다. 배열은 타입 에러가 발생하는 실수를 런타임에야 알게 되지만, 리스트를 사용하면 컴파일할 때 바로 알 수 있다. 또 다른 주요한 차이점으로 배열은 실체화가 된다. 이것은 배열은 런타임에도 자신이 담기로 한 원소의 타입을 인지하고 확인한다는 뜻이다. 반면에 제너릭은 타입 정보가 런타임에는 소거된다. 소거는 제너릭이 지원되기 전의 레거시 코드와 제너릭 타입을 함께 사용할 수 있게 해주는 매커니즘으로, 자바 5가 제너릭으로 순조롭게 전환될 수 있도록 해줬다. 이상의 주요 차이로 인해 배열과 제너릭은 잘 어우러지지 못한다. 예컨대 배열은 제네릭 타입, 매개변수화 타입, 타입 매개변수로 사용할 수 없다. 즉, 코드를 new List[] 와 같은 식으로 작성하면 컴파일할 때 제네릭 배열 생성 오류를 일으킨다. 제네릭 배열을 만들지 못하게 막은 이유는 무엇일까? 타입 안전하지 않기 때문이다. 이를 허용한다면 컴파일러가 자동 생성한 형변환 코드에서 런타임에 ClassCastException이 발생할 수 있다. 런타임에 ClassCastException이 발생하는 일을 막아주겠다는 제네릭 타입 시스템의 취지에 어긋나는 것이다.
배열을 제네릭으로 만들 수 없어 귀찮을 때도 있다. 예컨대 제네릭 컬렉션에서는 자신의 원소 타입을 담은 배열을 반환하는게 보통은 불가능하다. 또한 제네릭 타입과 가변인수 메서드를 함께 쓰면 해석하기 어려운 경고 메세지를 받게 된다. 가변인수 메서드를 호출할 때마다 가변인수 매개변수를 담을 배열이 하나 만들어지는데, 이때 그 배열의 원소가 실체화 불가 타입이라면 경고가 발생하는 것이다. 이 문제는 @SafeVarargs 애너테이션으로 대처할 수 있다. 배열로 형변환할 때 제네릭 배열 생성 오류나 비검사 형변환 경고가 뜨는 경우 대부분은 배열인 E[] 대신 컬렉션인 List를 사용하면 해결된다. 코드가 조금 복잡해지고 성능이 살짝 나빠질 수도 있지만, 그 대신 타입 안전성과 상호운용성은 좋아진다. 생성자에서 컬렉션을 받는 Chooser 클래스를 예로 살펴보자. 이 클래스는 컬렉션 안의 원소 중 하나를 무작위로 선택해 반환하는 choose 메서드를 제공한다. 생성자에 어떤 컬렉션을 넘기느냐에 따라 이 클래스를 주사위판, 매직 8볼, 몬테카를로 시뮬레이션용 데이터 소스 등으로 사용할 수 있다. choose 메서드를 호출할 때마다 반환된 Object를 원하는 타입으로 형변환해야 한다. 혹시나 아팁이 다른 원소가 들어 있었다면 런타임에 형변환 오류가 날 것이다. 만약 우리가 제너릭 타입을 사용할 때, 해당 타입을 T로 쓴다고 가정해보자. 이 T가 무슨 타입인지 알 수 없으니 컴파일러는 이 형변환이 런타임에도 안전한지 보장할 수 없다는 메시지가 나타나게 된다. 제네릭에서는 원소의 타입 정보가 소거되어 런타임에는 무슨 타입인지 알 수 없음을 기억하자. 그렇다면 프로그램은 동작하게 될까? 동작한다. 단지 컴파일러가 안전을 보장하지 못할 뿐이다. 코드를 작성하는 사람이 안전하다고 확신한다면 주석을 남기고 애너테이션을 달아 경고를 숨겨도 된다. 하지만 애초에 경고의 원인을 제거하는 편이 훨씬 낫다. 비검사 형변환 경고를 제거하려면 배열 대신 리스트를 쓰면 된다. 그러면 어떠한 경우에는 코드양이 조금 늘고 속도도 조금 더 느릴 수 있지만, 런타임 에러 ClassCastException을 만날 일은 없으니 훨씬 이득이다. 배열과 리스트 중 리스트를 사용해야 하는 이유를 제네릭과 연결 지어서 설명하고 있다. 여기와 연관해서 Object타입보다는 제네릭 타입을 써야 하는 이유를 몇 가지 추가해 보자. 우선 일반 클래스를 제네릭 클래스로 만드는 첫 단계는 클래스 선언에 타입 매개변수를 추가하는 일이다. 스택이 담을 원소의 타입 하나만 추가하면 된다. 이때 타입 이름으로는 보통 E를 사용한다. 그런 다음 코드에 쓰인 Object를 적절한 타입 매개변수로 바꾸고 컴파일해 보자. 여기까지 하면, 대체로 하나 이상의 오류나 경고가 발생하는데 대부분 E와 같은 실체화 불가 타입으로는 배열을 만들 수 없다는 것이다. 이를 해결하는 방법 중 첫 번째는 제네릭 배열 생성을 금지하는 제약을 대놓고 우회하는 방법이다. Object 배열을 생성한 다음 제네릭 배열로 형변환해보자. 이제 컴파일러는 오류 대신 경고를 내보낼 것이다. 이렇게도 할 수는 있지만 타입 안전하지 않다. 컴파일러는 타입 안전한지 증명할 방법이 없지만 개발자는 할 수 있다. 따라서 이 비검사 형변환이 프로그램의 타입 안전성을 해치지 않음을 스스로 확인해야 한다. 비검사 형변환이 안전함을 직접 증명했다면 범위를 최소로 좁혀 @SuppressWarnings 애너테이션으로 해당 경고를 숨기자. 그러면 깔끔히 컴파일되고, 명시적으로 형 변환하지 않아도 ClassCastException 걱정 없이 사용할 수 있게 된다. 두 번째 방법은 elements 필드의 타입을 E[]에서 Object[]로 바꾸는 것이다. 이러면 오류 대신 경고가 뜰 텐데 아까 이야기한 것처럼 직접 안전함을 증명하고 범위를 최소로 좁혀서 경고를 해결하자.
요약하자면,
배열과 제네릭에는 매우 다른 타입 규칙이 적용된다. 배열은 공변이고 실체화되는 반면, 제네릭은 불공변이고 타입 정보가 소거된다. 그 결과 배열은 런타임에는 타입 안전하지만 컴파일 타임에는 그렇지 않다. 제네릭은 반대다. 그래서 둘을 섞어 쓰기란 쉽지 않다. 둘을 섞어 쓰다가 컴파일 오류가 경고를 만나면, 가장 먼저 배열을 리스트로 대체하는 방법을 적용해보자.
'IT 개발 > Effective Java' 카테고리의 다른 글
메서드 시그니처를 신중히 설계하기 (0) | 2023.02.22 |
---|---|
적시에 방어적 복사본 만들기 (0) | 2023.02.21 |
비검사 경고 제거하기 (0) | 2023.02.19 |
인터페이스는 구현하는 쪽을 생각해서 설계하기 (0) | 2023.02.19 |
추상 클래스보다는 인터페이스 우선하기 (0) | 2023.02.19 |