ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 고대 자바 개발자들은 완전히 멘탈이 나가버렸습니다 : NPE
    백엔드 : 서버공부/Spring 2026. 2. 18. 16:27
    728x90

    요즘 내 상황을 요약하는 그림이다.

    OO님 500에러 봐주세요

     

    백엔드 개발자로서 자바를 쓰다 보면 NullPointerException을 정말 자주 마주하게된다.

    이게 “가끔 만나는 버그”라기보다는, 어딘가 항상 숨어 있는 존재 같은 느낌이다.

    NULL의 역사: “편의 기능”이 “빚”이 되기까지

    NullPointerException 을 이해하려면 null을 먼저이해해야한다.

    쉽게말해서 Null 은 값이 없는 것을 표현하기위해 존재하는 값이다. 

    개념적으로는 숫자 0 으로 정의되어있지만, Null은 숫자인지 글인지 형식 조차 없는 것이다.

     

    null은 프로그래밍 언어 ALGOL W의 창시자 중 한 명인 토니 호어가 의도적으로 넣은 값이라고한다.
    왜 넣었을까?

    이유는 꽤 합리적이었다.

    • “나중에 채워질 값”을 표현하기 쉬움 (인정 ㅇㅇ)
    • 초기화되지 않은 상태를 표기하기 쉬움 (인정 ㅇㅇ)

    NPE는 언제 터질까?

    다시 돌아와서 NullPointerException (NPE) 은 언제 터지는 것일까?

    자바에서 NPE는 한 문장으로 설명할 수 있다.
    “객체가 필요한 곳에서 null을 사용했을 때.”
    조금 더 기술적으로 말하면, “null 레퍼런스를 dereference(역참조)하려는 순간” 이다. 

     

    자바에서 “null 레퍼런스를 dereference(역참조)한다”는 말을 풀어서 쓰면, 이런 뜻이다.

     

    자바에서 객체는 보통 변수 안에 "객체 자체"가 들어있는게 아니라, 그 객체를 가리키는 값(주소 비슷한 것)이 들어있다.

    이 가리키는 값을 레퍼런스(reference, 참조)라고 부른다.

    즉, User user 같은 변수 user User 객체 자체가 아니라, "객체가 있는 곳을 가리키는 좌표"를 들고 있는 샘이다.

     

    그런데 null 은 "어떤 객체도 가리키지 않는다"는 특별한 레퍼런스 값이다.

    따라서 null 레퍼런스는 거리키는 대상없이 공중을 가리키는 것이라고 볼 수 있다.

     

    다음으로 dereference(역참조)란 레퍼런스가 가리키는 대상으로 실제로 들어가서 그 객체의 멤버를 사용하려는 행위를 말한다.

    아래 코드를 예로 살펴보자

    user.getName();

    여기서 JVM은 아래 순서대로 작동하려한다.

     

    1. 변수 user 안의 값을 본다. (레퍼런스 확인)
    2. 그 레퍼런스가 가리키는 객체로 “이동”한다. (역참조)
    3. 그 객체의 메서드 getName()을 호출한다.

     

     

    즉 .(점 연산자)뒤에 뭔가를 붙이는 순간이 대부분 "역참조"이다.

    NULL은 왜 참조는 되고, 역참조는 안 되는가

    변수에 어떤 값이든 담을 수 있으니까, null도 담을 수 있다. 따라서 참조(레퍼런스)를 “가지고 있는 것” 자체는 가능하다. 

    하지만 역참조는 "대상이 실제로 존재한다"라는 전제가 필요하기때문에 이야기가 달라진다.

     

    따라서 null은 “대상 없음”이기 때문에, 역참조를 시도하는 순간 JVM 입장에서는 이렇게 된다.

    “객체로 들어가서 메서드를 호출해야 하는데, 들어갈 객체가 없네?” 

     

    그래서 NullPointerException은 "null인지 확인되는 순간"이 아니라, null을 대상으로 뭔가를 하려는 순간,

    역참조를 시도한 지점에서 터진다.

     

    아래는 null 참조 대표 사례이다.

     

    • obj.method() : 메서드 호출을 위해 obj가 가리키는 객체로 들어가야 함
    • obj.field : 필드를 읽기 위해 객체 내부로 들어가야 함
    • arr.length : 배열 객체에 접근해야 함 (arr가 null이면 NPE)
    • arr[i] : 배열 원소를 읽으려면 배열 객체가 필요함
    • s.toString() / s.equals(...) : 호출 주체가 null이면 바로 NPE

     

    이것 말고도 찾아보면 더 많다...

     


    그러나 지금까지 자바는 NPE를 방지하는 것보다는 NPE를 개발자들이 더 빨리 인지하고 고치게 만드는 쪽으로 발전해왔다.
    그래서 개발을 하다 보면 “어떤 변수가 null이었는지”를 좀 더 친절하게 알려주는 메시지를 보게 된다.

     

    자바의 해결책 : Optional

    자바가 그렇다고 이런 개발자들의 고통을 보고만 있지는 않았다.

    Java 8에서는 NPE를 방지하기 위한 한가지 대안으로 Optional<T> 클래스를 도입했다.

     

    기존 자바에서는 메서드가 값을 반환하지 못하는 상황일 때, 관례적으로 null을 반환했다.

    하지만 이 방식은 호출하는 쪽에서 반드시 null체크를 해줘야 한다는 전제를 깔고있으며,

    그 전제를 코드 상에서 강제하는 장치가 없다는 문제가 있었다.

     

    User user = userRepository.findById(id);
    String name = user.getName(); // user가 null이면 NPE

     

    findById()가 null을 반환할 수 있다는 사실은 메서드 시그니처만 보고는 명확히 드러나지 않는다.

    따라서 호출자는 “값이 항상 존재한다”는 가정을 하기 쉽고, 그 가정이 깨지는 순간 NPE가 발생한다.

     

    Optional이 등장한 이유

    Optional<T>의 핵심 목적은 단순히 “NPE를 막는 것”이 아니라 "이 값은 없을 수도 있다"는 사실을

    타입 시스템 수준에서 명시하기 위함이다. 

    그러니까 null 을 암묵적으로 반환하는 대신, "값이 있을 수도 있고 없을 수도 있다"는 상태를 명시적인 객체로 표현하자는 것이다.

     

    Optional<User> user = userRepository.findById(id);

     

    예를 들어 위와 같이 User 타입을 Optional로 감싸게되면

    호출하는 쪽에서 “아, 이건 비어 있을 수도 있구나”라는 걸 컴파일 단계에서 인지하게 된다.

     

    Optional은 어떻게 NPE를 줄이는가

    Optional은 값을 바로 꺼내 쓰지 못하게 설계되어 있다. 내부에 접근하려면 반드시 다음 중 하나를 거쳐야한다.

    내부 값에 접근하려면 반드시 다음 중 하나를 거쳐야 한다.

    • isPresent() 확인
    • ifPresent() 사용
    • orElse(), orElseGet() 같은 대체값 처리
    • map(), flatMap()을 통한 안전한 체이닝

    즉, “역참조를 바로 못 하게 막아둔 래퍼”라고 볼 수 있다.

     

    보통 개발할때는 "orElse()"를 통해 명시적으로 값이 없을 경우를 예외처리해주고, "map()"을 통해 일반 객체로 변환한다.

     

    중요한 점

    Optional<T>는 null이 올 수 있는 값을 감싸는 Wrapper 클래스로, 참조하더라도 바로 NPE가 발생하지 않도록 도와준다.

    하지만 또 NPE로 부터 안전하게 하겠다고 신나서 Optional을 남발하면,
    NullPointerException 대신 NoSuchElementException 이라는 또 다른 차원의 지옥을 맛보게 된다.

    Optional<User> optionalUser = userRepository.findById(id);
    
    // optionalUser가 값을 가지고 있지 않으면 NoSuchElementException 발생
    User user = optionalUser.get();

     

    Optional 은 만능 해결책이 아니다. Optional 의 본질은

    • null을 직접 다루지 않게 하고
    • 값의 존재 여부를 명시적으로 처리하게 만들고
    • 코드의 의도를 더 분명하게 드러내는 것

    에 있다.

    결국 “없을 수도 있음”을 명시적으로 표현해주는 도구일 뿐이다.

    마무리

    Optional이 NPE로 부터 안전한 개발을 하게해주고 어쩌고해도.. 결국 

    서비스를 개발하다보면 Optional의 보호에서 결국 벗어나는 코드를 작성하게된다.

    Optional을 사용하더라도, 결국 Controller나 DTO 조합 단계에서는 그 값을 꺼내야 하고,
    외부 시스템, DB, Web 요청, 직렬화/역직렬화 과정 이러한 경계에서는 null이 자연스럽게 발생한다.
    그 순간 우리는 다시 null을 마주하게 된다.

    결국 자바에서 NPE는 “없애야 할 존재”라기보다는, null이라는 설계를 안고 가는 언어를 쓰는 이상 함께 관리해야 할 동료(?)에 가깝다.

    중요한 건 어디서 들어오고 어디서 터질 수 있는지를 항상 의식하면서 경계에서 정리하고, 내부 로직에서는 계약을 명확히 하는 것이다. 
    예를 들면 아래처럼..

    Sting name = user == null ? null : user.name();

     

    이 한 줄은 단순한 삼항 연산자가 아니라, “이 참조는 비어 있을 수 있다”는 사실을 의식하고 있다는 표현이다.

     

    자바로 스프링 서버를 개발한다는 건, 결국 이 . 하나의 무게를 이해하고 코드를 쓰는 일인 것 같다.

     

    추가적으로..

    솔직히 말해서 아직 “null을 관리하는 능력”이 부족하다는 것을 느끼는 요즘이다.

    • 어디까지는 null을 허용할 것인가
    • 어디부터는 null을 금지할 것인가
    • 어디서 예외를 던질 것인가

    이걸 의식적으로 설계하는 사람이, 단순히 NPE를 안 터뜨리는 개발자가 아니라

    “안전한 시스템을 만드는 개발자” 라고 생각한다.

Designed by Tistory.