-
Notifications
You must be signed in to change notification settings - Fork 93
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[#27][B팀] 비검사 경고를 제거하라
- Loading branch information
Showing
1 changed file
with
193 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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")`을 붙이는 이유에 대해서 주석으로 설명이 필요하다. 이를 통해 해당 코드의 이해를 돕고 다른 사람이 그 코드를 수정하여 타입 안정성을 잃지 않도록 할 수 있다. |