개발을 하다 보면 삼항 연산자를 사용하는 경우가 많다.
필자도 많이 사용하는 편인데, If-else 문을 간결하게 표현할 수 있어 간단한 조건문에는 삼항 연산자를 사용한다.
그런데,, 당연하게 사용하던 삼항 연산자에서 NullPointerException이 발생했다.
왜 그럴까? 원인을 하나씩 파헤쳐보자..!
* 참고: 과정이 다소 길 수 있으므로, 결론이 궁금한 분들은 👇👇👇
삼항 연산자에서 primitive와 wrapper가 섞이면 wrapper가 auto-unboxing 되어 null일 경우, NullPointerException이 발생할 수 있다.
문제상황
public static final int DEFAULT_JOIN_LIMIT = 1;
public void ... {
Integer userJoinLimit = user.getJoinLimit();
Integer joinLimit = "GUEST".equals(user.getRole()) ? DEFAULT_JOIN_LIMIT : userJoinLimit;
}
로그인한 계정의 역할에 따라 서브계정 가입 횟수를 제한하는 로직이 있다고 가정해보자.
DB에서 사용자 정보를 조회하여 userJoinLimit 값을 가져오는데,
슈퍼어드민인 경우 userJoinLimit 가 null 일 수 있다.
반면, 정의되지 않은 GUEST는 반드시 서브 계정을 1개만 가입 가능 하도록 제한해야 한다. 그래서 위와 같은 로직을 작성했다.
그런데,, 슈퍼어드민으로 테스트를 했을 때,

NullPointerException이 발생했다. 삼항연산자가 원인인 줄은 꿈에도 모르고 계속 이상한 곳에서 이유를 찾고 있었다. (실제로 30분의 삽질을 했다.)
단서를 찾았다.

오랜만에 뭔가를 해결하기 위해 시간을 쓰는 게 재밌다. 그렇게 생각하고 있을 때, 단서를 하나 찾았다.
까먹고 있었다.. 자바는 Exception이 정말 친절하다는 걸,,
🚨 Cannot invoke "java.lang.Integer.intValue()" because userJoinLimit is Null
영어를 해석해 보자면, null인 userJoinLimit에서 intValue()를 호출하려다 보니, NullPointerException이 발생했다는 뜻이다.
흠... 근데 왜, intValue()를 호출하려 했을까? 나는 Wrapper Class를 Primitive Type으로 unboxing을 시도한 적이 없는데?!
그래서 삼항연산자에 대해 공식문서 기반으로 살펴보았다.

위 설명에서 두 번째 문장이 가장 중요한데, 한쪽이 primitive고 한쪽이 boxing 된 reference type(ex. Wrapper Class)라면 primitive 타입으로 자동 변환된다. 그리고 마지막 문장을 보면, 이 과정에서 unboxing이 포함될 수 있다고 되어 있다.
즉, 위 예제에서 문제가 되었던 부분은
Integer joinLimit = 조건문 ? primitive : reference
이 과정에서 null로 할당된 reference type을 unboxing (reference.intValue()) 하려다 보니 NullPointerException이 발생하게 되었다.
해결방안
해결방안은 2가지이다.
1️⃣ Reference Type으로 통일
public static final Integer DEFAULT_JOIN_LIMIT = 1;
public void ... {
Integer userJoinLimit = user.getJoinLimit();
Integer joinLimit = "GUEST".equals(user.getRole()) ? DEFAULT_JOIN_LIMIT : userJoinLimit;
}
2️⃣ Null Check 로직 추가
Integer joinLimit;
if (userJoinLimit == null) {
joinLimit = null;
} else {
joinLimit = "GUEST".equals(getRole()) ? DEFAULT_JOIN_LIMIT : userJoinLimit;
}
필자는 고민 없이 1번 방법을 선택했다.
일반적으로 Wrapper Class를 사용하는 것은 Primitive Type에 비해 메모리를 더 사용하고, 연산 시 unboxing 과정을 거쳐 속도 측면에서 불리하다. 하지만, 해당 로직은 대량 연산이 반복되는 구조도 아니고 단순 참조 값이기에 이런 걸 고려하는 게 오히려 Overengineering이라 판단했다.
오늘의 결론
오늘은 삼항연산자의 함정에 대해 알아보았다.
실무에서 복잡한 로직보다 오히려 이런 사소한 부분이 예상치 못한 버그로 이어지는 경우를 많이 보았다.
오늘처럼 어려운 내용은 아니지만, 한 번쯤은 경험해 볼 이슈도 계속 기록하며
많은 분들이 비슷한 문제를 겪을 때, 조금이나마 도움이 되었으면 한다.
'Server > Java' 카테고리의 다른 글
| GC Overhead limit exceeded 로 시작하여 JVM ... (생략) (3) | 2025.01.23 |
|---|---|
| [JAVA] Enum "equals" vs "==" (4) | 2024.08.21 |
| Java에서 Stack Class는 함정카드다. (4) | 2024.06.13 |
| Java 8 Functional Interface에 대해 알아보자. (0) | 2023.10.31 |
