Java 삼항연산자 사용시 주의점(boxing, unboxing, NullPointerException)

회사업무에서 특정 요건을 처리한 후 서버에 배포하였다.

 

그런데 내가 배포한 이후에 QA 담당자께서 되던 기능이 안된다고 하시는데... 불길한 맘으로 서버 로그를 확인한 결과 잉?? NullPointerException이 떨어져 있었다...

 

아무리 생각해도 해당 라인에서는 NullPointerException이 발생할 여지가 전혀 없는 코드였는데 배포가 잘 안된건가? 하고 팀장님께 해당 라인의 코드와 배포상황을 말씀드렸고 배포는 정상적으로 진행되었음을 확인했다. 

 

그렇다면 실제로 해당 코드에서 NullPointerException이 발생했다는 건데.. 예제 코드로 상황을 재현해 보겠다.

 

삼항연산자 NullPointerException 예제
class Scratch {
    public static void main(String[] args) {
        Boolean 특정조건 = true;
        Integer buildingType = null;

        // 해당 코드에서 NullPointerException이 발생한다.
        // 왜그럴까?
        buildingType = Boolean.TRUE.equals(특정조건) ? buildingType : getNum();
    }
    
    public static int getNum() {
        return 1;
    }
}

아무리 봐도 NullPointerException을 유발할만한 코드는 없어보였다.

 


그렇다면 예상해볼 수 있는 점은 형변환과 관련된 부분인데 Integer에서 int로 int에서 Integer로 형변환 되는 부분은 맞췄다고 생각했다. 즉, 컴파일러의 검사는 통과했다는 뜻.

 

근데 왜 런타임시 NullPointerException을 냈을까?

 

이는 Boxing과 UnBoxing이 어떻게 일어나는지를 보면 확인할 수 있다.

 


실제로 Integer 타입에서 int 타입으로 unboxing이 진행될때는 Integer클래스의 intValue() 메소드의 호출에 의해서 진행된다.

 

실제 바이트 코드로 이를 확인해봐야겠다.

 

	LINENUMBER 71 L14
    GETSTATIC java/lang/Boolean.TRUE : Ljava/lang/Boolean;
    ALOAD 1
    INVOKEVIRTUAL java/lang/Boolean.equals (Ljava/lang/Object;)Z
    IFEQ L15
    ALOAD 2
    INVOKEVIRTUAL java/lang/Integer.intValue ()I
    GOTO L16

이는 해당 라인(실제 코드에서 71번 라인이었음)에서 Boolean.equals 메소드를 호출하는 것과 이후에 Integer.intValue() 의 호출이 이루어지는 것을 볼 수 있다.

 

즉, 위 코드는 buildingType 이라는 변수의 intValue() 메소드의 호출을 시도하다가 NullPointerException을 뱉어낸 것이다.

 

실제로 인텔리제이에서 다음과 같은 코드를 확인했을 때, 다음과 같은 경고 드래그(?) 를 띄웠다.

언박싱을 시도할 때 NullPointerException을 내뱉을 수 있다는 것이다.

 


현재까지 확인한 내용은 

 

boolean 조건 ? Integer : int

해당 케이스에서 반환되는 타입은 int로 확인된다. 그렇다면 인텔리제이의 타입추론 기능을 활용하여 다른 기본 타입들도 해당 케이스에 primitive 타입으로 정해질까?? 확인해보겠다.

 

Byte _byte = null;
Short _short = null;
Integer _int = null;
Long _long = null;
Float _float = null;
Double _double = null;
Boolean _boolean = null;
Character _char = null;
        
byte b = 특정조건 ? _byte : Byte.MAX_VALUE;
short i1 = 특정조건 ? _short : Short.MAX_VALUE;
int i = 특정조건 ? _int : Integer.MAX_VALUE;
long l = 특정조건 ? _long : Long.MAX_VALUE;
float v = 특정조건 ? _float : Float.MAX_VALUE;
double v1 = 특정조건 ? _double : Double.MAX_VALUE;
boolean b1 = 특정조건 ? _boolean : true;
char c = 특정조건 ? _char : '1';

인텔리제이의 타입추론으로 확인해본 결과 삼항연산자 조건에서 Wrapper 타입과 primitive 타입이 선언된 결과 primitive 타입으로 타입이 추론되었다.

 


이를 해결하기 위해서는 삼항연산자를 사용할 때 해당 케이스를 모두 판단해야할텐데.. 숫자타입, 소수타입, 문자타입 이 세가지는 각각의 타입도 다양할 뿐 아니라 문자타입은 다음과 같이 허용되는 케이스도 존재한다.

char c = 71;

 

이에 대한 지식을 안다면 삼항연산자를 쓸때 타입추론기능을 활용하여 실제 정해지는  타입을 확인하거나 콜론 사이에 존재하는 두 값의 타입을 일치시켜주는게 제일 안전한 것 같다.

 

아니면 if 문을 쓰는건데.. 별로 맘에들지는 않는 결론이라 애매하다..ㅠㅠ

 


이러한 여러가지 경우의 수를 정리해주는 표가 있다.

 

즉, 

특정조건 ? Integer.MAX_VALUE : Long.MAX_VALUE;

해당 코드에서 반환할 타입을 결정할 때, 타입은 long타입으로 결정된다. 왜냐하면 Integer 타입은 Long으로 표현될 수 있지만 2의 32승을 넘어가는 숫자값은 Integer 타입으로 표현될 수 없는 값일 것이다.

 

그러므로 타입의 결정을 long 타입으로 결정짓는 것이다.

 

아래의 링크를 통해 삼항연산시 발생할 수 있는 형변환 가능한 타입들간의 경우의수가 존재하는데 이는 아래의 링크를 통해 한번 살펴보면 좋을 것 같다.

 

https://johngrib.github.io/wiki/ternary-operator-and-null-pointer-exception/

 

Java의 삼항 연산자와 Null Pointer Exception

언박싱하다 NPE가 터지는 것이 원인

johngrib.github.io

 

 

댓글

Designed by JB FACTORY