-
Generic과 raw-type 그리고 비검사 경고CS/Java (with Effective) 2022. 4. 28. 16:25
해당 글은 Effective Java item26, item27을 기반으로 합니다.
로 타입은 사용하지 말라.
비검사 경고를 제거하라.Generic 파트는 엄청 긴 것도 아니거니와.. 객체를 선언하고 Generic을 통해 타입 선언 등에 있어서 주의할 점이 많을 것 같아 웬만하면 싹다 훑고 가려합니다.
item 26. raw 타입은 사용하지 말라.
우선 Generic class, Generic Interface란, 아래와 같이 선언에 있어 타입 매개 변수를 사용한 경우를 의미합니다.
public interface List<E> extends Collection<E> { ... }
해당 코드는 대표적인 제네릭 인터페이스 List<E>의 선언부입니다.
원래는 List<E>로 표현하는게 맞지만, 단순히 List라고도 선언할 수 있습니다. 이를 raw 타입이라고 부릅니다.
이렇게 선언하는 경우 컴파일에는 문제가 없지만 런타임에 에러를 발생시키게 됩니다.
정수형 데이터를 넣을 리스트를 선언하고 실수로 문자열 값을 넣고 사용하는 경우를 가정해보겠습니다.
import java.util.ArrayList; import java.util.List; public class Main { public static void main(String[] args) throws Exception { List intList = new ArrayList<>(); intList.add(12); intList.add("string is strong"); int a = (int) intList.get(0); int b = (int) intList.get(1); System.out.println(a + " " + b); } }
코드는 위와 같이 구성할 수 있겠네요.
의도대로 문자열을 넣고 int 변수에 get(0)을 했지만, compile time엔 아무런 에러가 발생하지 않습니다.
하지만, runtime엔 아래처럼 ClassCastException이 발생합니다.
엌ㅋㅋ 코드 막 짜다가 개같이 멸망!위의 코드가 runtime에 에러가 발생하는 이유는 raw-type으로 선언한 경우 해당 값이 Object 타입으로 저장되기 때문입니다.
String은 Object 클래스를 상속하는 하위 클래스죠.
int는 auto boxing으로 인해 Integer wrapper class로 변경될 수 있는데요. 해당 클래스는 Number class를 상속하고, 이 Number 클래스는 Object 클래스를 상속합니다.
즉, 두 타입 모두 Object 클래스를 상속하기 때문에 compile 땐 문제없이 코드가 구성이 되지만, 실제 실행시엔 String -> Integer 변경이 불가능하기 때문에 위와 같은 Exception이 발생하게됩니다. (line 9)
raw-type이 이렇게나 위험합니다!특히, 개발을 진행할 땐 이러한 Runtime error의 위험성을 최대한 줄여야합니다.
예를 들어 의존성 주입을 할 때 생성자 주입을 많이 쓰는 이유는 여러 장점이 있지만, 순환참조 문제를 실행시 바로 캐치해낼 수 있기 때문이죠. 생성자 주입을 사용하지 않으면 해당 파트로 접근하기 전까진 문제를 알 수 없으니 모르고 배포하는 경우 서비스를 제공하는 도중에 문제가 발생할 위험이 존재하게 됩니다.
위의 코드를 예시로 보자면, 9~10번째 line을 다른 서비스에서 호출해 사용하는 경우 비슷한 문제가 발생할 수 있겠죠.
이렇게 raw-type은 웬만하면 사용하지 않습니다만, raw-type과 같이 실제 타입 매개변수를 신경쓰고 싶지 않다면 비한정적 와일드 카드 '?'를 사용합니다. 비한정적 와일드 카드는 타입이 확정되지 않은 만큼 null외에 다른 타입의 데이터를 저장할 수 없습니다. 즉, 해당 컬렉션에 대한 불변성을 유지해줄 수 있습니다.
위와 같이 비한정적 와일드 카드를 사용한 경우 null외엔 이전의 컬렉션과 같은 타입의 데이터를 저장하려한다 해도 저장할 수 없습니다.
출력 결과는 13번째 줄을 주석처리하고 진행한 결과입니다.
raw-type를 사용할 수 있는 예외 상황은 2가지가 존재합니다.
- class 리터럴
- instanceof 연산자
class 리터럴이란 'List.class, String[].class, int.class' 등의 표현을 의미합니다. 자바 명세에선 기본 타입과 배열 타입에 대한 클래스 리터럴만 허용하도록 쓰여있습니다. 따라서, List<Integer>.class 등의 표현은 사용할 수 없습니다.
제네릭 정보는 런타임에 지워지는데, 이에 따라 instanceof 연산자에는 비한정적 와일드 카드 타입만 사용할 수 있습니다. 당연하게도 raw-type도 사용 가능한데, 두 타입 모두 동일하게 동작하므로 단순하게 raw-type을 사용해도 되겠습니다.
앞에 쓴 코드를 아래와 같이 변경해 줄 수 있겠죠.
import java.util.ArrayList; import java.util.List; public class Main { public static void main(String[] args) throws Exception { List<Integer> data = new ArrayList<>(); data.add(1); data.add(2); data.add(3); if(data instanceof List) { List<?> intList = data; intList.stream().forEach(d -> System.out.println(d)); } } }
Collection을 사용하는 경우는 잘 빼먹지 않으니 괜찮은 것 같은데, 제가 직접 정의한 제네릭 클래스의 타입 매개 변수를 까먹는 경우가 종종 있었습니다. 이러한 실수를 조심하면 될 것 같고, 일부러 raw-type을 쓰려고 의도하지만 않는다면 큰 문제는 없을 것으로 보입니다.
item 27. 비검사 경고를 제거하라.
raw-type 파트에서 이야기했던 타입 안정성 내용이 공통적인 부분이 있어 함께 기술합니다.
비검사 경고란 일반적으로 아래와 같은 경우 발생합니다.
보시는 바와 같이 컴파일타임에 컴파일러가 해당 선언에서 타입 매개변수를 파악할 수 없어 발생하는 경고입니다.
이렇게 선언된 경우 간단하게 해당 해시셋을 사용하는데 있어 큰 문제는 없지만, ClassCastException이 발생할 여지를 주게됩니다.
이 문제는 간단히 <> 연산자를 통해 해당 경고를 없애고 타입 안정성을 보장해줄 수 있습니다.
그런데 실행하는데 큰 문제가 없는 비검사 경고를 왜 굳이 수정해줘야할까요? 그냥 두고 쓰면 안될까요?
당연히 안 됩니다. 위의 예시는 raw 타입 선언으로 인한 문제점이기도 하지만, 이와 같이 비검사 경고가 발생합니다.
rawTypeSet 메서드에서 rawTypes에 두개의 타입이 저장되는 현상을 볼 수 있습니다. 일반적인 코드에선 당연히 compile-error가 발생해야하지만, 문제없이 실행까지 진행할 수 있음을 볼수 있습니다.
저 같은 사람의 경우엔 눈 자체가 컴파일러이므로 이런 실수를 눈치채지 못할리 없지만요. 후후즉, 이전까지 계속해서 강조드렸던 Runtime-error의 가능성을 줄이고 Compile-error를 통해 문제를 수정하는 습관에 또 다시 반대되는 예시라 할 수 있습니다. 실컷 코드를 구현하고 실행하려하니 에러가 발생한다면, 어디서 에러가 발생했는지 파악하기도 어려울 뿐 더러 어떤 문제인지도 알기 힘들 수 있으니까요.
따라서, 그림 6의 문제는 이와 같이 명시적인 타입 선언과 <> 연산자를 통해 Compile-time에 문제점을 조기에 발견하고 수정할 수 있도록 습관을 들이는 것이 좋겠습니다.
물론, 비검사 경고를 무조건 제거하기 어렵지만, 타입의 안정성을 보장할 수 있다면 @SuppressWarnings("unchecked") 어노테이션을 선언해주면 됩니다. 해당 어노테이션은 지역 변수부터 메서드 클래스 전체까지 선언 가능합니다.
클래스 전체에 걸어두고 사용한다면, 번거롭게 하나씩 설정할 필요 없으니 선언하는 데 있어 조금은 편리합니다만, 클래스 내에서도 비검사 경고를 무시해도되는 경우와 반드시 수정해야하는 경우 등 여러가지 상황이 연출될 수 있습니다. 이에따라 너무 넓은 범위에 적용하게되면 예외 상황을 놓칠 수 있으니, 최대한 좁은 범위에서 사용하는 것이 좋습니다.
@SuppressWarnings에 사용할 수 있는 파라미터는 여러가지가 있고, "unchecked"의 예시도 소개하면 좋겠지만, 전자의 경우 현 포스팅과는 조금 결이 다르고 "unchecked"를 사용하는 예시는 여러 상황에 따라 달라질 수 있을 것 같아 간단히 책의 예시를 참고하시길 바라며 생략하겠습니다.
마지막으로 @SuppressWarnings("unchecked")를 사용하시는 경우 반드시 주석으로 왜 타입 안정성을 보장할 수 있는지 남겨주시길 바랍니다. 그러한 근거를 표기함으로써 코드의 가독성을 크게 높여주는 효과와 더불어 잘못된 코드 수정을 통해 기존의 안정성을 잃도록하는 상황을 방지할 수 있도록 말이죠.
참고 자료
Effective Java - 조슈아 블로크
반응형'CS > Java (with Effective)' 카테고리의 다른 글
Composition vs extends (0) 2022.03.02 HashCode (0) 2022.02.27 Singleton (0) 2022.02.08 Builder (0) 2022.02.06 Static Factory Method (0) 2022.01.13