ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 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이 발생합니다.

    엌ㅋㅋ 코드 막 짜다가 개같이 멸망!

    그림 1. raw-type ClassCastException

    위의 코드가 runtime에 에러가 발생하는 이유는 raw-type으로 선언한 경우 해당 값이 Object 타입으로 저장되기 때문입니다.

    StringObject 클래스를 상속하는 하위 클래스죠.

    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외에 다른 타입의 데이터를 저장할 수 없습니다. 즉, 해당 컬렉션에 대한 불변성을 유지해줄 수 있습니다.

    그림 2. 한정정 와일드 카드

    위와 같이 비한정적 와일드 카드를 사용한 경우 null외엔 이전의 컬렉션과 같은 타입의 데이터를 저장하려한다 해도 저장할 수 없습니다.

    출력 결과는 13번째 줄을 주석처리하고 진행한 결과입니다.

     

     

    raw-type를 사용할 수 있는 예외 상황은 2가지가 존재합니다.

    1. class 리터럴
    2. 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));
            }
        }
    }

    그림 3. instanceof raw-type 결과

     

    Collection을 사용하는 경우는 잘 빼먹지 않으니 괜찮은 것 같은데, 제가 직접 정의한 제네릭 클래스의 타입 매개 변수를 까먹는 경우가 종종 있었습니다. 이러한 실수를 조심하면 될 것 같고, 일부러 raw-type을 쓰려고 의도하지만 않는다면 큰 문제는 없을 것으로 보입니다.

     

     

    item 27.  비검사 경고를 제거하라.

    raw-type 파트에서 이야기했던 타입 안정성 내용이 공통적인 부분이 있어 함께 기술합니다.

    비검사 경고란 일반적으로 아래와 같은 경우 발생합니다.

     

    그림 4. 비검사 경고 예시

    보시는 바와 같이 컴파일타임에 컴파일러가 해당 선언에서 타입 매개변수를 파악할 수 없어 발생하는 경고입니다.

    이렇게 선언된 경우 간단하게 해당 해시셋을 사용하는데 있어 큰 문제는 없지만, ClassCastException이 발생할 여지를 주게됩니다.

     

    그림 5. 비검사 경고 제거

    이 문제는 간단히 <> 연산자를 통해 해당 경고를 없애고 타입 안정성을 보장해줄 수 있습니다.

    그런데 실행하는데 큰 문제가 없는 비검사 경고를 왜 굳이 수정해줘야할까요? 그냥 두고 쓰면 안될까요?

     

     

    그림 6. 비검사 경고로 인한 Runtime-error

    당연히 안 됩니다. 위의 예시는 raw 타입 선언으로 인한 문제점이기도 하지만, 이와 같이 비검사 경고가 발생합니다.

    rawTypeSet 메서드에서 rawTypes에 두개의 타입이 저장되는 현상을 볼 수 있습니다. 일반적인 코드에선 당연히 compile-error가 발생해야하지만, 문제없이 실행까지 진행할 수 있음을 볼수 있습니다.

    저 같은 사람의 경우엔 눈 자체가 컴파일러이므로 이런 실수를 눈치채지 못할리 없지만요. 후후

     

    즉, 이전까지 계속해서 강조드렸던 Runtime-error의 가능성을 줄이고 Compile-error를 통해 문제를 수정하는 습관에 또 다시 반대되는 예시라 할 수 있습니다. 실컷 코드를 구현하고 실행하려하니 에러가 발생한다면, 어디서 에러가 발생했는지 파악하기도 어려울 뿐 더러 어떤 문제인지도 알기 힘들 수 있으니까요.

     

     

    그림 7. Compile-time에 문제점을 찾고 수정 가능

    따라서, 그림 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

    댓글

Designed by minchoba.