Item 32
제네릭과 가변인수를 함께 쓸 때는 신중하라
🟧 가변인수 (Varargs)
가변인수는 필요에 따라 매개변수의 개수를 동적으로 조정할 수 있는 기능입니다.
덕분에 varargs 메서드를 n번 오버로딩 하지않아도 됩니다.
가변인수 사용하는 방법은 타입... 매개변수명 으로 사용하면 됩니다.
가변인수는 파라미터로 넘겨진 값들을 컴파일러가 컴파일 타임에 배열을 만들어 처리해주는 방식입니다.
🟧 제네릭과 가변인수
위에서 요구사항이 들어왔습니다.
"같은 타입의 데이터를 List로 반환하는 메서드를 만들어라."
기존에 만들어져 있는 Collection의 Api를 이용해도 되겠지만
직접 차근차근 구현 해보면서 제네릭과 가변인수를 사용하게 되는 과정을 보겠습니다.
- 버전 01. asListV1
- 버전 02. asListV2
- 버전 03. asListV3
- 버전 04. asListV4
- 버전 05. asListV5
- 버전 06. asListV6
- 버전 07. asListV7
버전 01. asListV1
01. 각 String 타입의 데이터 3개를 List로 묶어 반환한다.
버전 01. (asListV1)은 RawType List를 반환하고 있습니다.
asListV1 메서드에 매개변수 타입이 모두 String입니다.
asListV1 메서드를 호출함으로서 반환되는 List로 부터 String 타입의 데이터를 꺼낸다고 해도 형변환 오류가 발생하지 않습니다.
버전 01. - 단점 01. RawType List는 타입에 불안전하다.
- asListV1 메서드에서 반환된 RawType List는 외부에서 다양한 값을 넣을 수 있습니다.
다양한 타입의 데이터를 가지고 있는 List에서 데이터를 사용하기 위해서는 형 검사, 형 변환 로직이 필요합니다.
번거로우면서 타입이 불안전하여 런타임 에러 (ClassCastException)이 발생 가능성이 증가합니다.
버전 01. - 단점 02. asListV1 메서드 인자로 String 타입의 데이터만 들어갈 수 있다.
- asListV1 메서드의 매개변수 타입이 모두 String으로 다른 타입은 들어갈 수 없습니다.
- asListV1 메서드를 호출하면 항상 String 타입 데이터를 지닌 RawType List를 반환합니다.
버전 01. - 단점 03. asListV1 메서드의 매개변수 개수는 항상 3개이다.
- asListV1 메서드의 매개변수 개수가 오직 3개로만 정해져 있습니다.
버전 01. asListV1 메서드의 단점을 정리하면 다음과 같습니다.
- 단점 01. RawType List는 타입 불안전합니다.
- 단점 02. asListV1 메서드의 인자로 String 타입의 데이터만 들어갈 수 있습니다.
- 단점 03. asListV2 메서드의 매개변수 개수는 항상 3개이어야만 합니다.
🟧 제네릭을 이용하여 타입 안전을 보장한다.
버전 02. - "단점 01, 단점 02"를 해결한다.
단점 01, 단점 02를 해결해보겠습니다.
- 단점 01. RawType List는 타입 불안전합니다.
- 단점 02. asListV1 메서드의 인자로 String 타입의 데이터만 들어갈 수 있습니다.
먼저 현재 상황에 대해서 조금 더 알아보도록 하겠습니다.
버전 01. - 단점 01. RawType List는 타입에 불안전하다.
- asListV1 메서드를 호출하여 반환된 List에 다양한 타입이 들어갈 수 있습니다.
데이터를 추가하는 것은 문제가 없으나
데이터를 받아올 때 런타임 에러가 발생할 수 있는데
List의 4번째 원소 Character 타입을 String 타입으로 형 변환을 시도하고 결국 ClassCastException이 발생합니다.
가장 큰 문제점은 컴파일 시 에러를 잡을 수 없다는 점입니다.
Heap 영역에 올라간 List에 저희는 Character 타입의 'a'을 추가합니다.
런타임 시에 List에 있는 원소를 꺼내오기 전까지는 문제가 없겠지만 꺼내오는 순간 예외가 발생합니다.
힙(Heap) 영역에 타입 안전성이 깨진 List가 올라가 있는 상황을 힙 오염(Heap Pollution)이라고 합니다.
버전 02. - 해결 01. 제네릭 타입을 사용한다.
- 제네릭 타입 <T> 을 사용하면 Collection에 저장되는 데이터의 타입 안전성을 보장할 수 있습니다.
- "단점 01. RawType List는 타입에 불안전하다." 를 해결한다.
- "단점 02. 메서드의 인자로 String 타입의 데이터만 들어갈 수 있다." 를 해결한다.
[해결] 단점 01. RawType List는 타입에 불안전하다.
- 제네릭 타입을 이용하여 다른 타입이 들어올 경우 컴파일 시점에 에러를 발생시켜 타입 안전을 지킬 수 있습니다.
[해결] 단점 02. 메서드 인자로 String 타입의 데이터만 들어갈 수 있다.
- 제네릭 타입을 이용함으로서 타입 안전하고 다양한 매개변수화 타입 반환이 가능합니다.
[미해결] 단점 03. 메서드 인자로 데이터가 3개만 들어갈 수 있다.
- 아직 매개변수 개수는 3개로 고정되어 있는 상태로 단점을 해결하지 못했습니다.
🟧 가변인수를 이용하여 메서드 매개변수 개수를 동적으로 조절한다.
버전 03. - "단점 03"을 해결한다.
"단점 03. 메서드 인자로 데이터가 3개만 들어갈 수 있다." 을 해결하기 위해서는 동적으로 데이터 개수를 받을 수 있어야 합니다.
두 가지 방법을 비교하고 장단점을 보겠습니다.
- 배열(Array)을 이용한다.
- 가변인수(Varargs)를 이용한다.
[해결] 단점 03. 메서드 인자로 데이터가 3개만 들어갈 수 있다. - 배열(Array)
배열을 이용하게 되면 단점 01, 단점 02, 단점 03을 모두 해결할 수 있습니다.
이 때의 장단점은 다음과 같습니다.
- [장점] 데이터 개수를 동적으로 조정할 수 있습니다.
- [단점] 계속해서 배열로 만들어서 넘겨야 합니다.
[해결] 단점 03. 메서드 인자로 데이터가 3개만 들어갈 수 있다. - 가변인수(Varargs)
가변인수도 단점 01, 단점 02, 단점 03을 모두 해결할 수 있습니다.
컴파일시에 자동으로 배열을 만들어 원소를 넣어주기 때문에 배열을 이용했을 때의 단점도 보완할 수 있습니다.
- [장점] 데이터 개수를 동적으로 조정할 수 있습니다.
[단점] 계속해서 배열로 만들어서 넘겨야 합니다.
🟧 제네릭과 가변인수를 같이 사용하여 문제를 해결한다.
단점 01, 단점 02, 단점 03을 해결하기 위해서 사용한 것.
제네릭, 가변인수
"버전 01" 단점 세 가지를 해결하기 위해서 "제네릭", "가변인수"를 활용 했습니다.
제네릭을 이용하여 타입 안전성을 높이고 가변인수를 이용하여 매개변수 개수를 동적으로 처리할 수 있게 됐습니다.
결과적으로 "버전 04"으로 메서드를 수정했습니다.
private <T> List<T> asListV4(T... ts) {
List<T> values = new ArrayList<>();
for (T t : ts) {
values.add(t);
}
return values;
}
🟧 제네릭과 가변인수를 같이 사용할 때 힙 오염 될 수 있다.
가변인수 배열이 다른 객체의 참조 했을 때 ClassCastException가 발생할 수도 있다.
이전까지 "같은 타입의 데이터를 List로 반환하는 메서드를 만들어라." 라는 요구사항을 구현 했습니다.
여기서 새로운 요구사항이 또 추가됐습니다.
"데이터가 몇 번째 데이터인지 알 수 있게 번호를 넣어주세요. 다음은 예시입니다."
["헤나01", "헤나02", "헤나03"], [1, 2, 3]
요구사항을 추가하기 위해 기존에 "버전 04" 메서드를 수정해서 "버전 05"를 작성했습니다.
"버전 05"를 작성하기 위해서 제네릭과 가변인수를 이용했습니다.
메서드 작동 확인을 위해 실행 했을 때 "157 라인"에서 ClassCastException 예외를 볼 수 있습니다.
asListV5 메서드 내부에서 List<Object>에 count를 넣어줄 때 힙 오염(Heap Pollution)이 발생됩니다.
힙 오염은 컴파일 시에 확인되지 않고 런타임 에러로 발생되어 큰 문제를 야기할 수 있습니다.
가변인수 배열 자신을 반환 했을 때 ClassCastException가 발생할 수도 있다.
위에서 "버전 05"는 해결하지 못하였기에 직접 같은 타입의 번호를 넣어 반환하려 합니다.
asListV6는 제네릭 타입 매개변수 두 개를 받아 convertBy 메서드에게 넘겨줍니다.
convertBy 메서드는 가변인수를 이용하고 있어 받은 매개변수로 컴파일 시점에 만들어진 배열 자신을 반환합니다.
String 배열로 받으려고 했지만 이 때 ClassCastException 이 발생합니다.
예외 발생 원인은 convertBy에서 컴파일 시점에 만들어진 T[] 자신을 직접 반환하고
반환된 Object[] 을 다른 메서드가 직접 사용하여 String[]으로 형변환 할 때 실패합니다.
제네릭과 가변인수을 사용할 때 무조건적으로 타입 안전하지는 않다.
"버전 05", "버전 06"을 봤을 때 ClassCastException이 발생한 것을 보아
제네릭과 가변인수를 같이 사용한다고 했을 때 타입이 안전하다는 것을 보장할 수 없습니다.
정리하면 다음과 같은 상황에서 형변환 예외가 발생했습니다.
- 버전 05, 가변인수 배열이 다른 객체의 참조 했을 때
- 버전 06, 가변인수 배열 자신을 반환 했을 때
🟧 제네릭과 가변인수를 같이 안전하게 사용하는 법
"버전 05", "버전 06" 상황이 나오지 않게 하면 됩니다.
즉, 제네릭과 가변인수를 같이 사용할 때 두 가지를 지키면 됩니다.
- 가변인수 배열이 다른 객체를 참조하지 않게 한다.
- 가변인수 배열 자신을 반환하지 않게 한다.
"버전 07" asListV7 메서드가 성공적으로 실행되고 있습니다.
실행 결과를 보면 원하는 대로 출력되고 있지만 컴파일 경고를 볼 수 있습니다.
컴파일 경고
"uses unchecked or unsafe operations."
@SafeVarargs를 사용하여 안전을 약속하고 경고를 없앤다.
제네릭과 가변인수를 같이 사용하면 힙 오염(Heap Pollution)이 발생할 수 있다고 경고해줍니다.
asListV7 메서드는 나올 수 있는 형변환 위험을 없앴기에 타입 안전한 메서드입니다.
힙 오염이 발생할 이유가 없으므로 "@SafeVarargs" 어노테이션을 asListV7 메서드 위에 달아줍니다.
컴파일 경고를 없애고 타입 안전한 메서드라는 의미를 갖게 됩니다.
실행하면 이전과 같은 결과에 컴파일 경고가 사라진 것을 알 수 있습니다.
🟧 제네릭과 가변인수는 위험할 수도 있는데 왜 사용할까 ?
제네릭과 가변인수를 같이 사용하는 이유는 실무에서 유용하기 때문입니다.
버전 07. asListV7 은 사실 이미 만들어진 기능입니다.
Arrays.asList() 와 같은 기능입니다.
이외로도 이미 제네릭과 가변인수를 같이 사용하는 유용한 API들이 존재합니다.
Collections.addAll 메서드 코드를 한 번 봐볼까요 ?
T... elements로 제네릭과 가변인수를 같이 사용하여 매개변수로 이용했습니다.
컴파일 타임에 T 원소를 담기 위한 T[]이 만들어졌지만
T[]에 아무것도 저장하지 않고 외부로 노출되지도 않았습니다.
🟧 정리하면
1. 가변인수 매개변수를 담는 제네릭 배열에 아무것도 저장하지 않는다.
2. 가변인수 매개변수를 담는 제네릭 배열이 외부로 노출되지 않는다.
라는 조건에 만족해서 Collections.addAll 메서드는 안전하다고 할 수 있습니다.
🟧 제네릭과 가변인수 정리
정리하자면 가변인수와 제네릭의 조합은 좋지 않습니다.
배열과 제네릭은 타입 규칙이 다르고
가변인수는 배열을 노출시키기 때문에 타입 안전하지 않습니다.
제네릭과 가변인수를 같이 사용하는 것이 위험할 수도 있습니다.
하지만 실무에서의 편리함 덕분에 막아두지 않았습니다.
안전하게 사용하기 위해서는 항상 메서드가 타입 안전한지 확인하고 @SafeVarargs 어노테이션을 이용합시다.
'💬 언어 > 이펙티브 자바' 카테고리의 다른 글
[이펙티브 자바] Item 23 - 태그 달린 클래스보다는 클래스 계층구조를 활용하라 (0) | 2023.04.14 |
---|---|
[이펙티브 자바] Item 22 - 인터페이스 타입을 정의하는 용도로만 사용하라 (1) | 2023.03.20 |
[이펙티브 자바] Item 20 - 추상 클래스보다는 인터페이스를 우선하라 (0) | 2023.03.13 |
[이펙티브 자바] Item 49 - 매개변수가 유효한지 검사하라 (0) | 2023.03.06 |
[이펙티브 자바] Item 26 - Raw Type은 사용하지 말라 (0) | 2023.02.22 |