Java에서 가장 흔한 오류
NullPointerException(모두가 NPE라고 부릅니다)은 아무것도 가리키지 않는 참조 -null- 를 마치 실제 객체를 가리키는 것처럼 사용하려 할 때 발생합니다. 객체 타입의 Java 변수는 객체를 담고 있거나, "여기에 객체가 없음"을 뜻하는 값인 null을 담고 있습니다. null에게 무언가를 하라고 요청하는 순간 -- 메서드를 호출하거나, 필드 중 하나를 읽거나, 인덱스로 접근하거나 -- 작업할 대상이 없으므로 JVM이 예외를 던집니다.
이전 페이지에서 예상된 실패를 처리하려고 사용하는 try-catch와 달리, NPE는 거의 항상 단순한 버그입니다. 이 페이지의 목적은 그것을 잡는 것이 아니라, 왜 발생하는지 이해하고 애초에 그것을 만들어 내지 않는 코드를 작성하는 것입니다.
length()를 실행할 String이 없으므로 프로그램은 NullPointerException과 함께 멈춥니다.
메시지 읽기
Java 14부터 메시지는 무엇이 null이었는지 정확히 알려줍니다. 이를 Helpful NullPointerException(유용한 NPE)이라고 부릅니다. 무언가를 바꾸기 전에 먼저 읽으세요:
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.length()" because "name" is null
at Main.main(Main.java:4)
두 부분이 중요합니다. Cannot invoke "String.length()"는 실패한 연산이고, because "name" is null은 범인을 지목합니다. at Main.main(Main.java:4) 줄은 정확한 줄을 가리키는 스택 트레이스입니다. 따라서 해결책은 "4번째 줄을 try로 감싸는 것"이 아니라 "왜 4번째 줄에서 name이 null인지 알아내는 것"입니다. 버그는 거의 항상 그보다 앞쪽, 값이 할당되었어야 했지만 되지 않은 곳에 있습니다.
발생시키는 여러 경우
NPE는 몇 가지 연산에서 비롯되며, 모두 "null을 건드리는" 것의 변형입니다:
map 조회는 전형적인 원인입니다. 키가 없으면 get은 null을 반환하고, 그 값을 마침내 사용할 때가 되어서야 몇 줄 뒤에서 NPE가 드러나는 경우가 많습니다. 언박싱은 교묘한 경우인데, null인 Integer를 int에 할당하면 복사할 숫자가 없으므로 예외가 발생합니다.
null 체크로 방어하기
가장 단순한 방어는 참조를 사용하기 전에 null이 아님을 확인하는 if입니다:
변수를 상수 String과 비교할 때는 상수를 앞에 두세요 - answer.equals("yes") 대신 "yes".equals(answer). answer가 null이면 첫 번째 형태는 차분하게 false를 반환하지만, 두 번째는 예외를 던집니다.
Objects.requireNonNull로 빠르게 실패하기
null 체크를 여기저기 흩뿌리면 지저분해집니다. 어떤 값이 결코 null이어서는 안 될 때 -- 생성자 인자처럼 -- 경계에서 Objects.requireNonNull로 검증하세요. 이것은 나중에 코드 깊숙한 곳에서가 아니라, 잘못된 값이 도착하는 지점에서 즉시 명확한 메시지와 함께 예외를 던집니다:
이 "빠르게 실패하기" 습관은 200줄 떨어진 곳의 모호한 NPE를, 원인 지점에서의 정확한 불평으로 바꿔 줍니다. 여기서 예외를 잡는 것은 단지 메시지를 보여 주기 위함입니다 - 실제 코드라면 버그가 고쳐지도록 예외가 그대로 드러나게 둘 것입니다.
애초에 null을 피하기
가장 좋은 NPE는, 부딪힐 null이 아예 없어서 결코 발생할 수 없는 것입니다. 몇 가지 습관이 큰 도움이 됩니다:
null대신 빈 컬렉션이나 빈 문자열을 반환하세요.Collections.emptyList()와""는 반복하거나 메서드를 호출해도 안전합니다.- map에서는
getOrDefault를 사용해, 실패한 조회가null대신 실제 값을 내놓도록 하세요. - 필드를 "나중까지"
null로 두지 말고 선언할 때 초기화하세요.
값이 정말로 선택적일 때 -- 정당하게 아무것도 찾지 못할 수 있는 조회처럼 -- Java는 Optional을 제공합니다. 이것은 조용히 null을 돌려주는 대신, 호출자가 "없음" 경우를 처리하도록 강제하는 컨테이너입니다. API에서 이런 빈틈을 설계 차원에서 완전히 없애고 싶다면, 다음에 읽어야 할 관련 개념이 바로 이것입니다.
마무리
NullPointerException은 null을 담고 있는 참조를 마치 객체를 담고 있는 것처럼 사용했다고 Java가 알려 주는 것입니다. 해결책은 그것을 잡는 경우가 드뭅니다 - 유용한 메시지를 읽고, 값이 설정되었어야 할 곳까지 거슬러 올라가, null이 아님을 보장하거나 그것을 사용하는 지점을 보호하는 것입니다. 경계에서 빠르게 실패하기 위해 Objects.requireNonNull에 기대고, null보다 빈 값과 getOrDefault를 선호하며, 부재가 실제로 예상되는 결과일 때는 Optional에 의지하세요. 이 사고방식을 익히면, Java에서 가장 흔한 오류가 여러분 자신의 코드에서는 가장 드문 것 중 하나가 됩니다.
자주 묻는 질문
Java에서 NullPointerException은 무엇 때문에 발생하나요?
null을 가리키는 참조를 마치 실제 객체를 가리키는 것처럼 사용할 때 발생합니다. 예를 들어 메서드를 호출하거나(name.length()), 필드를 읽거나, 배열 요소에 접근하거나, null인 Integer를 언박싱할 때입니다. 변수가 객체를 담고 있지 않아 작업할 대상이 없으므로 JVM이 NullPointerException을 던집니다.
Java에서 NullPointerException은 어떻게 고치나요?
메시지를 읽으세요 - Java 14부터는 무엇이 null이었는지 정확히 알려줍니다(예: "Cannot invoke "String.length()" because "name" is null"). 그런 다음 그 변수가 왜 null인지 추적하세요: 빠진 초기화, null을 반환한 메서드, 또는 아무것도 찾지 못한 map 조회 등입니다. 값이 결코 null이 되지 않도록 원인을 고치거나, null 체크, Objects.requireNonNull, Optional로 사용 지점을 보호하세요.
null을 검사하는 것과 NullPointerException을 잡는 것 중 무엇이 더 낫나요?
null을 검사하세요. NullPointerException은 예상된 상황이 아니라 로직상의 버그를 알리는 것이므로, 잡기보다는 예방해야 합니다. 그것을 잡으면 진짜 문제가 어디에 있는지 가려집니다. try/catch는 입출력 오류처럼 정말로 예외적인 상황을 위해 아껴 두세요.