본문 바로가기

Backend

NPE는 왜 런타임에 터질까? — Modern Null Safety와 가드 패턴(Guard Pattern)

개발을 하다 보면 가장 흔하게 만나는 예외 중 하나가 바로 NullPointerException(이하 NPE)입니다.

자바(Java)를 주요 언어로 사용하는 백엔드 개발자 입장에서, 최신 언어들(Dart, Kotlin, TypeScript 등)을 둘러보다 보면 아주 흥미로운 차이점을 발견하게 됩니다. 바로 "Null 때문에 터지는 에러를 컴파일 시점에 미리 막아버린다"는 점입니다.

이번 글에서는 NPE가 발생하는 본질적인 이유와 최신 언어들의 Modern Null Safety 메커니즘, 그리고 자바 환경에서도 안전한 코드를 작성하기 위한 가드 패턴(Guard Pattern)에 대해 다뤄보겠습니다.

1. NPE는 왜 컴파일 에러가 아니라 '런타임 에러'일까?

자바에서 아래와 같은 코드는 컴파일 단계에서 아무런 경고도 발생하지 않고 완벽하게 빌드됩니다.

 

Java
public Element getFifthElement(List<Element> elements) {
    if (elements.size() < 5) {
        return null; // ⭕ 컴파일 통과! null 반환 허용
    }
    return elements.get(4);
}

문제는 이 메서드를 호출하는 쪽에서 일어납니다.

Java
Element element = getFifthElement(list);
element.doSomething(); // 💣 만약 null이 넘어왔다면 런타임에 NPE 발생!

자바 타입 시스템의 한계

기존 자바의 타입 시스템에서 Element라는 타입은 "실제 Element 객체"일 수도 있고 "null"일 수도 있습니다. 즉, 컴파일러 입장에서는 element.doSomething()을 호출할 때 이 안에 실물 객체가 들어있는지 빈 상자(null)인지 알 길이 없습니다.

결국 코드가 실제 실행되어 해당 라인을 지나는 순간 프로그램이 강제 종료되면서 NPE가 발생합니다. 비유하자면 "비어있는 상자인 줄 모르고 선물이 들어있을 거라 생각해서 손을 푹 넣었다가 베이는 사고"가 터지는 셈입니다.

 

2. Modern Language의 혁신: ? 연산자와 Type System

Dart나 Kotlin 같은 최신 언어들은 이 문제를 타입 시스템(Type System) 자체를 개편하여 해결했습니다.

Dart
// ❌ 컴파일 에러 발생!
Element getFifthElement(List<Element> elements) {
  if (elements.length < 5) {
    return null; // Error: Element 타입은 null을 반환할 수 없음
  }
  return elements[4];
}

위 코드는 빌드조차 되지 않습니다. Element라는 타입 선언 자체가 "절대 null일 수 없음(Non-nullable)"을 의미하기 때문입니다.

? (Nullable)의 진짜 의미

null을 반환하고 싶다면 타입 뒤에 물음표(?)를 명시해야 합니다.

Dart
// ⭕ 컴파일 성공
Element? getFifthElement(List<Element> elements) {
  if (elements.length < 5) {
    return null; // Element? 타입이므로 null 반환 허용
  }
  return elements[4];
}

여기서 중요한 점은, Element?로 선언하는 순간 컴파일러가 '흐름 분석(Flow Analysis)'을 시작한다는 것입니다.

이 함수를 호출한 곳에서 if (element != null) 같은 널 체크 코드를 작성하지 않거나 element?.doSomething() 같은 안전 연산자를 쓰지 않으면, 컴파일러가 빨간 줄을 띄우며 실행 파일 생성을 차단합니다.

핵심: ?는 단순히 null을 허용하는 표기법이 아니라, "이 값은 null일 수 있으니 호출부에서 반드시 예외 처리를 하라"는 컴파일러와의 강제적 계약입니다.

3. 치트키 연산자 ! (Bang Operator)의 위험성

Dart나 TypeScript 등에서는 널 체크가 귀찮을 때 강제로 경고를 끄는 ! (Null Assertion) 연산자를 제공합니다.

Dart
Element? element = getFifthElement(list);
element!.doSomething(); // "내가 보증하는데 이거 null 아니니까 강제로 실행해!"

하지만 ! 연산자는 컴파일러의 안전망을 수동으로 끄는 행위입니다. 만약 개발자의 착각으로 실제 null이 들어온다면 런타임에 즉시 에러가 터지게 됩니다. 테스트 코드가 아닌 실제 서비스 로직에서는 가급적 지양해야 하는 안티 패턴입니다.

4. 자바(Java) 개발자가 NPE를 방어하는 현실적인 전략

자바 환경에서는 타입 뒤에 ?를 붙일 수는 없지만, 두 가지 핵심 전략으로 Null 안전성을 확보할 수 있습니다.

 

전략 1. 리턴 타입에 Optional<T> 활용하여 계약 명시하기

자바 8부터 제공되는 Optional<T>은 Dart의 Element?처럼 "이 메서드의 결과가 null일 수 있다"는 것을 호출자에게 명시적으로 알리는 도구입니다.

Java
// ⭕ null 대신 Optional을 반환하여 호출부에 예외 처리를 강제함
public Optional<Element> getFifthElement(List<Element> elements) {
    if (elements == null || elements.size() < 5) {
        return Optional.empty(); // null 대신 빈 Optional 반환
    }
    return Optional.ofNullable(elements.get(4));
}

// 호출하는 쪽에서도 처리 방식을 명확히 선택해야 함 (NPE 방지)
Element element = getFifthElement(list).orElse(defaultElement);

 

 

전략 2. 가드 패턴 (Guard Pattern) 적용으로 Early Return

메서드 내부나 비즈니스 로직 진입점에서는 예외 조건(Null 또는 유효하지 않은 값)을 코드 입구에서 즉시 리턴(Early Exit)시켜 본문 로직을 보호하는 가드 패턴을 적용합니다.

❌ 중첩 if문 (가드가 없는 코드)

Java
public String getElementName(List<Element> elements) {
    if (elements != null) {
        if (elements.size() >= 5) {
            Element element = elements.get(4);
            if (element != null) {
                return element.getName();
            }
        }
    }
    return "EMPTY";
}

들여쓰기가 깊어져 가독성이 떨어지고, 어느 지점에서 null 방어가 빠졌는지 한눈에 파악하기 어렵습니다.

⭕ 가드 패턴 적용 코드

Java
public String getElementName(List<Element> elements) {
    // 1. 입구에서 예외 조건(null, 크기 부족)을 즉시 걸러냄 (Guard)
    if (elements == null || elements.size() < 5) {
        return "EMPTY";
    }

    Element element = elements.get(4);
    if (element == null) {
        return "EMPTY";
    }

    // 2. 이 하단은 완전하게 안전한 비즈니스 로직만 남음
    return element.getName();
}

가드 패턴을 사용하면 코드가 읽기 쉬워질 뿐만 아니라, 비즈니스 로직이 실행되기 전에 안전망을 확보할 수 있습니다. 추가로 자바 8 이상이라면 Optional<T>과 @NonNull / @Nullable 어노테이션을 정적 분석 도구(SonarQube 등)와 조합해 적극 활용하는 것이 좋습니다.

5. 마치며: 비즈니스 관점에서의 Null Safety

소프트웨어 공학에서 버그 수정 비용은 [컴파일 단계 -> QA 단계 -> 프로덕션 운영 단계]로 뒤로 밀릴수록 폭증합니다.

운영 중인 서비스에서 NPE가 발생하는 것은 단순한 개발자의 실수를 넘어 유저 이탈, 데이터 정합성 깨짐, 긴급 핫픽스 배포라는 파급 비용으로 이어집니다.

언어 차원의 Modern Null Safety 기능을 이해하고, 자바 환경에서도 가드 패턴과 방어적 코딩을 컨벤션으로 가져가는 것은 개발 생산성을 높이고 서비스의 가동률(SLA)을 지키는 가장 확실한 투자입니다.