카테고리 없음

Java에서 NullPointerException 피하기

ghyeong 2024. 11. 16. 22:07
반응형

NullPointerException(NPE)는 Java 개발자라면 누구나 한 번쯤 겪어본 골칫거리입니다. 이 글에서는 NPE를 예방하기 위한 Java의 주요 도구인 Optional 클래스를 중심으로, null 안전성을 향상시키는 다양한 기법을 비교하고 활용법을 소개합니다.


NullPointerException: 왜 발생할까?

NPE는 다음과 같은 상황에서 주로 발생합니다:

  • null 값을 참조하려고 할 때
    String name = null;
    System.out.println(name.length()); // NPE 발생
  • 메서드의 반환값이 null인데 이를 처리하지 않을 때
     
public String findName() {
    return null;
}
System.out.println(findName().length()); // NPE 발생

NPE는 디버깅을 어렵게 만들고, 생산성 저하와 런타임 오류를 유발할 수 있습니다.


Optional 클래스란?

Optional 클래스는 Java 8에서 도입된 기능으로, null 값을 직접 다루지 않고 대신 명시적인 처리를 강제합니다.

Optional을 사용하는 이유

  1. 명시적 의도 표현: 값이 null일 수 있음을 코드에서 나타냄.
  2. 안전한 null 처리: null 여부를 검사하는 로직을 줄이고, 메서드 체이닝을 안전하게 지원.

Optional 사용법

반응형
  • Optional 생성하기
    Optional<String> optionalName = Optional.of("John"); // null이 아닌 값
    Optional<String> emptyOptional = Optional.empty();  // 빈 값
    Optional<String> nullableOptional = Optional.ofNullable(null); // null 허용


  • Optional 값 가져오기
    optionalName.get();  // 값 반환 (값이 없으면 예외 발생)
    optionalName.orElse("Default"); // 값이 없으면 기본값 반환
    optionalName.orElseThrow(() -> new IllegalArgumentException("Value is missing"));


  • Optional 체이닝
optionalName.map(String::toUpperCase).ifPresent(System.out::println);

 

 


Null 안전성을 향상시키는 기법

@NonNull 어노테이션 사용

Lombok 또는 IDE 플러그인을 통해 변수가 null이 될 수 없음을 명시합니다.

 
public void printName(@NonNull String name) {
    System.out.println(name.toUpperCase());
}

Java 14의 NullPointerException 메시지 개선

Java 14부터 NPE 메시지가 개선되어, 어떤 변수에서 문제가 발생했는지 명확하게 알 수 있습니다.

String name = null;
System.out.println(name.toUpperCase()); 
// 메시지: Cannot invoke "String.toUpperCase()" because "name" is null​

Kotlin의 Null 안전성 비교

Java에서의 null 처리보다 Kotlin은 언어 자체에서 null을 엄격히 구분합니다.

var name: String? = null // null 허용
name?.length // 안전 호출​

Optional 활용 예시

Optional로 메서드 반환값 처리

public Optional<String> findNameById(int id) {
    return id == 1 ? Optional.of("John") : Optional.empty();
}
Optional<String> name = findNameById(2);
name.ifPresentOrElse(
    System.out::println,
    () -> System.out.println("Name not found")
);​

Stream과 Optional 결합

List<String> names = List.of("Alice", "Bob", "Charlie");
Optional<String> foundName = names.stream()
    .filter(name -> name.startsWith("B"))
    .findFirst();
foundName.ifPresent(System.out::println);​

중첩 객체에서 NullPointerException 방지

class User {
    private Address address;
    public Optional<Address> getAddress() {
        return Optional.ofNullable(address);
    }
}
class Address {
    private String city;
    public Optional<String> getCity() {
        return Optional.ofNullable(city);
    }
}

Optional<String> city = user.getAddress()
    .flatMap(Address::getCity)
    .or(() -> Optional.of("Default City"));
System.out.println(city.orElse("Unknown"));​

Optional vs Null 직접 처리

특징OptionalNull 직접 처리

코드 가독성 더 명확하고 직관적 null 체크 코드 반복
런타임 안정성 NullPointerException 방지 null 체크를 누락하면 오류
추가 메모리 사용량 약간 증가 없음
API 설계의 명확성 명시적으로 null 가능성 표현 모호함

Optional의 한계

  • 모든 경우에 적합하지 않음: 필드나 컬렉션에서 Optional 사용은 권장되지 않습니다.
  • 과도한 체이닝은 가독성 저하: 복잡한 체이닝은 이해하기 어려울 수 있습니다.

상황별 Optional 활용 

  • 메서드 반환값: 반환값이 없거나 null일 가능성이 있으면 Optional을 사용.
  • 필드에서는 사용하지 말기: 클래스 필드에 Optional을 사용하는 것은 메모리 낭비와 가독성 저하를 초래.
  • 단순한 연산에는 사용하지 않기: 값이 null일 가능성이 낮거나 간단한 null 체크만 필요한 경우, Optional 사용은 과도할 수 있음.
반응형