Skip to content

Commit

Permalink
[#27][B팀] 비검사 경고를 제거하라
Browse files Browse the repository at this point in the history
[#27][B팀] 비검사 경고를 제거하라
  • Loading branch information
ksy90101 authored Feb 8, 2021
2 parents b716455 + fb7e2b2 commit 87aa70d
Showing 1 changed file with 193 additions and 0 deletions.
193 changes: 193 additions & 0 deletions 5장/27_비검사_경고를_제거하라_박경철.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
# item 27. 비검사 경고를 제거하라

Raw type: [https://docs.oracle.com/javase/tutorial/java/generics/rawTypes.html](https://docs.oracle.com/javase/tutorial/java/generics/rawTypes.html)

제네릭을 사용하면 수많은 컴파일러 경고를 볼 수 있다. 비검사 형변환 경고, 비검사 메서드 호출 경고, 비검사 매개변수화 가변인수 타입 경고, 비검사 변환 경고들이 제네릭을 사용했을때 나타날 수 있는 컴파일러 경고이다.

```java
Set<String> words = new HashSet();
```

위 코드에서는 다음과 같은 경고를 볼 수 있다.

```
Raw use of parameterized class 'HashSet'
Unchecked assignment: 'java.util.HashSet' to 'java.util.Set<java.lang.String>'
```

즉, 인스턴스로 생성한 `HashSet`을 Raw 타입으로 사용했기 때문에 나타난 비검사 경고이다. 이를 해결하기 위해서는 HashSet에도 `HashSet<String>`과 같이 타입 매개변수를 명시하던가 자바 7부터 지원하는 제네릭 타입 추론을 활용하여 다이아몬드 연산자 `<>`를 사용할 수 있다.

위와 같은 비검사 경고를 가능한 모두 제거해야한다. 비검사 경고를 없애면 타입 안정성이 높아지고 ClassCastException이 발생할 여지를 없애준다.

## 비검사 `unchecked`의 의미

비검사`unchecked`는 Java 컴파일러가 type-safe를 보장하기 위해 필요한 타입 정보가 충분히 존재하지 않는다는 의미이다. 기본적으로 Java 컴파일러는 비검사 경고를 활성화하지 않으며 이를 사용하기 위해서는 컴파일러에 `-Xlint:uncheck` 옵션을 주어야한다.

### 비검사 변환 경고 `unchecked conversion`

```java
@Test
void uncheckedCast() {
Map<String, String> map = getMap();
}

private Map getMap() {
return new HashMap<String, String>();
}
```

메서드 `getMap`은 반환 타입이 Raw 타입 Map이다. 때문에 위 `getMap` 호출부에서는 다음과 같은 경고가 나타난다.

```
src/test/java/edu/pkch/generic/GenericTest.java:20: warning: [unchecked] unchecked conversion
Map<String, String> map = getMap();
^
required: Map<String,String>
found: Map
```

즉, map이 `Map<String, String>`으로 선언되었기 때문에 `Map<String, String>`으로 할당되어야하지만 실제로는 Raw 타입인 Map을 할당하기 때문에 발생한다.

### 비검사 형변환 경고 `unchecked cast`

```java
@Test
void uncheckedCast() {
Map<String, String> map = (Map<String, String>) getMap();
}

private Map getMap() {
return new HashMap<String, String>();
}
```

그렇다면 위 비검사 변환 경고에서 발생한 문제를 해결하기위해서 Raw 타입 Map을 `Map<String, String>`으로 강제 형변환하면 어떻게 될까? 이때는 다음과 같은 경고가 나타난다.

```
src/test/java/edu/pkch/generic/GenericTest.java:20: warning: [unchecked] unchecked cast
Map<String, String> map = (Map<String, String>) getMap();
^
required: Map<String,String>
found: Map
```

위 경우 Java 컴파일러는 Map의 제네릭 변수를 String으로 결정할 수 있는 방법이 없다. `아마 type erasure 때문에?` 따라서 비검사 경고를 알려준다.

### 비검사 메서드 호출 경고 `unchecked call`

```java
@Test
void uncheckedCall() {
Set words = new HashSet();
words.add("hello");
words.add(1);

assertThat(words.contains("hello")).isEqualTo(true);
assertThat(words.contains(1)).isEqualTo(true);
}
```

위 Set `words`는 Raw 타입으로 선언되었다. 때문에 위 `words`에 엉뚱한 `1`이 들어가더라도 컴파일에서 잡을 수 없다. 이 경우에는 컴파일러가 다음과 같은 경고를 알려준다.

```
src/test/java/edu/pkch/generic/GenericTest.java:15: warning: [unchecked] unchecked call to add(E) as a member of the raw type Set
words.add("1");
^
where E is a type-variable:
E extends Object declared in interface Set
```

위와 같이 Java가 Raw 타입을 허용하는 이유는 하위 호환성 때문이다. Java의 제네릭은 1.5 이후에 등장했다. 때문에 이전 버전에는 존재하지 않은 개념이기 때문에 이를 호환하기 위해서 허용하는 것이다.

### 비검사 매개변수화 가변인수 타입 경고 `unchecked parameterized vararg type`

비검사 매개변수화 가변인수 타입 경고가 나타날 수 있는 이유 참고: [https://stackoverflow.com/questions/21132692/java-unchecked-unchecked-generic-array-creation-for-varargs-parameter](https://stackoverflow.com/questions/21132692/java-unchecked-unchecked-generic-array-creation-for-varargs-parameter)

제네릭 가변인수가 heap pollution의 가능성이 있는 이유: [https://stackoverflow.com/questions/12462079/possible-heap-pollution-via-varargs-parameter](https://stackoverflow.com/questions/12462079/possible-heap-pollution-via-varargs-parameter)

자바가 제네릭 배열을 지원하지 않는 이유: [https://stackoverflow.com/questions/2927391/whats-the-reason-i-cant-create-generic-array-types-in-java](https://stackoverflow.com/questions/2927391/whats-the-reason-i-cant-create-generic-array-types-in-java)

```java
private <T> List<List<T>> toList(List<T>... elements) {
// ...
}
```

다음과 같이 가변인수를 제네릭과 함께 사용했을때 문제가 될 수 있다.
가변인수는 암묵적으로 배열을 생성한다. 이때 자바는 제네릭 배열의 생성을 허용하지 않는다. 이런 불일치로 다음과 같은 비검사경고를 알려준다.

```
src/test/java/edu/pkch/generic/GenericTest.java:33: warning: [unchecked] Possible heap pollution from parameterized vararg type List<T>
private <T> List<List<T>> toList(List<T>... elements) {
^
where T is a type-variable:
T extends Object declared in method <T>toList(List<T>...)
```

위 경고를 보면 비검사 매개변수화 가변인수 타입 경고가 heap pollution의 가능성이 있다고 알려준다.

#### heap pollution

Java heap pollution이란 제네릭 타입에서 변수화된 타입이 실제 타입과 다를때를 의미한다.

```java
@Test
void heapPollution() {
List list = toList("1", 2);
Iterator<String> iter = list.iterator();

while (iter.hasNext())
{
String str = iter.next(); // ClassCastException
System.out.println(str);
}
}

private <T> List<T> toList(T... elements) {
return Arrays.asList(elements);
}
```

toList를 통해 여러 타입을 가지는 List를 만들 수 있다. 그리고 Iterator를 `String`으로 정한 후에 `next`를 활용하여 반복하게 되면 결국 `Integer` 2 때문에 ClassCastException이 발생하게된다.

즉, 변수화된 Iterator는 `String`을 받도록 선언되었지만 실제 값으로는 `Integer`가 나타난 것이다. 이를 **heap pollution**이라고 한다.

## 비검사 경고 해결방법

위와 같이 비검사 경고의 원인들을 보았을때 대부분의 경우는 Raw 타입을 사용했을때 발생한 것을 알 수 있다. 따라서 대부분은 Raw 타입을 사용하지 않고 제네릭 타입을 명시하면 해결할 수 있다.

그리고 비검사 매개변수화 가변인수 타입 경고에서 확인했던거처럼 제네릭과 가변인수를 사용하지 않도록 주의한다면 거의 모든 비검사 경고를 해결할 수 있을 것이다.

### @SuppressWarnings("unchecked")

```java
package java.lang;

import java.lang.annotation.*;
import static java.lang.annotation.ElementType.*;

@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE, MODULE})
@Retention(RetentionPolicy.SOURCE)
public @interface SuppressWarnings {
String[] value();
}
```

`@SuppressWarnings`는 위와 같은 형태를 띈다. 컴파일러의 경고를 제외하는데 사용한다.

컴파일러가 비검사 경고를 계속 알려주지만 해당 코드가 안전하다고 확신할 수 있다면 `@SuppressWarnings("unchecked")`를 사용할 수 있다.

```java
@SuppressWarnings("unchecked")
private <T> List<T> toList(Class<T> clazz, T... elements) {
return Arrays.asList(elements);
}
```

앞선 `toList` 메서드에 `Class<T>` 타입의 clazz를 파라미터로 추가한 코드이다. 이렇게 추가한다면 제네릭 타입 T를 clazz로 결정할 수 있으므로 elements에 올 수 있는 타입은 clazz로 설정한 타입이 된다. 즉, elements는 컴파일러에 의해서 안전하다고 보장할 수 있다.

위와 같은 경우 안전하다고 보장할 수 있으므로 `@SuppressWarnings("unchecked")`로 비검사 경고를 숨길 수 있다. 단, 타입 안정성을 검증하지 않은 채 경고를 숨기면 여전히 ClassCastException의 여지가 존재하므로 꼭 검증후에 `@SuppressWarnings("unchecked")`를 달도록 한다.

그리고 `@SuppressWarnings("unchecked")`는 최대한 좁은 범위에 적용해야한다. 넓은 범위에 적용한다면 의도하지 않은 비검사 경고를 놓칠 수 있다. `@SuppressWarnings`는 클래스, 필드, 메서드, 변수, 인자 등 모든 선언부에 사용가능하다.

마지막으로 `@SuppressWarnings("unchecked")`을 붙이는 이유에 대해서 주석으로 설명이 필요하다. 이를 통해 해당 코드의 이해를 돕고 다른 사람이 그 코드를 수정하여 타입 안정성을 잃지 않도록 할 수 있다.

0 comments on commit 87aa70d

Please sign in to comment.