1. 개요

일반적으로 null 변수, 참조 및 컬렉션은 Java 코드에서 처리하기 까다롭습니다. 식별하기 어려울 뿐만 아니라 다루기도 복잡합니다.

실제로 null을 처리할 때 누락된 부분은 컴파일 타임에 식별할 수 없으며 런타임에 NullPointerException이 발생합니다 .

이 사용방법(예제)에서는 Java에서 null을 확인해야 하는 필요성과 코드에서 null 확인을 방지하는 데 도움이 되는 다양한 대안을 살펴보겠습니다 .

2. NullPointerException 이란 무엇입니까 ?

NullPointerException 에 대한 Javadoc 에 따르면 다음과 같이 객체가 필요한 경우 애플리케이션이 null을 사용하려고 시도할 때 발생합니다.

  • null 객체 의 인스턴스 메서드 호출
  • null 개체 의 필드 액세스 또는 수정
  • 배열인 것처럼 null 의 길이를 사용합니다.
  • 배열인 것처럼 null 슬롯 액세스 또는 수정
  • Throwable 값인 것처럼 null 던지기

이 예외를 발생시키는 Java 코드의 몇 가지 예를 빠르게 살펴보겠습니다.

public void doSomething() {
    String result = doSomethingElse();
    if (result.equalsIgnoreCase("Success")) 
        // success
    }
}

private String doSomethingElse() {
    return null;
}

여기서는 null 참조 에 대한 메서드 호출을 시도했습니다 . 이로 인해 NullPointerException 이 발생합니다 .

또 다른 일반적인 예는 null 배열 에 액세스하려는 경우입니다 .

public static void main(String[] args) {
    findMax(null);
}

private static void findMax(int[] arr) {
    int max = arr[0];
    //check other elements in loop
}

이로 인해 6행에서 NullPointerException 이 발생합니다.

따라서 null 개체의 필드, 메서드 또는 인덱스에 액세스하면 위의 예제에서 볼 수 있듯이 NullPointerException 이 발생합니다 .

NullPointerException을 피하는 일반적인 방법은 null을 확인하는 것입니다 .

public void doSomething() {
    String result = doSomethingElse();
    if (result != null && result.equalsIgnoreCase("Success")) {
        // success
    }
    else
        // failure
}

private String doSomethingElse() {
    return null;
}

실제 세계에서 프로그래머는 어떤 개체가 null일 수 있는지 식별하기 어렵습니다 . 적극적으로 안전한 전략은 모든 개체에 대해 null을 확인하는 것일 수 있습니다 . 그러나 이로 인해 불필요한 null 검사가 많이 발생 하고 코드 가독성이 떨어집니다.

다음 몇 섹션에서는 이러한 중복을 방지하는 Java의 일부 대안을 살펴보겠습니다.

3. API 계약을 통한 null 처리

마지막 섹션에서 설명한 것처럼 null 개체 의 메서드 또는 변수에 액세스하면 NullPointerException 이 발생합니다 . 또한 개체에 액세스하기 전에 개체에 null 검사를 수행하면 NullPointerException 가능성이 제거된다는 점에 대해서도 논의했습니다 .

그러나 종종 null 값을 처리할 수 있는 API가 있습니다 .

public void print(Object param) {
    System.out.println("Printing " + param);
}

public Object process() throws Exception {
    Object result = doSomething();
    if (result == null) {
        throw new Exception("Processing fail. Got a null response");
    } else {
        return result;
    }
}

print () 메소드 호출은 "null"을 출력하지만 예외를 발생시키지는 않습니다. 마찬가지로 process()는 응답에서 null을 반환하지 않습니다 . 오히려 Exception 을 던집니다 .

따라서 위의 API에 액세스하는 클라이언트 코드의 경우 null 검사 가 필요하지 않습니다 .

그러나 이러한 API는 계약에서 이를 명시해야 합니다. API가 그러한 계약을 게시하는 일반적인 장소는 Javadoc입니다.

그러나 이것은 API 계약에 대한 명확한 표시를 제공하지 않으므로 준수를 보장하기 위해 클라이언트 코드 개발자에 의존합니다.

다음 섹션에서는 몇 가지 IDE 및 기타 개발 도구가 개발자에게 어떻게 도움이 되는지 살펴보겠습니다.

4. API 계약 자동화

4.1. 정적 코드 분석 사용

정적 코드 분석 도구는 코드 품질을 크게 향상시키는 데 도움이 됩니다. 그리고 이러한 도구를 통해 개발자는 null 계약을 유지할 수 있습니다 . 한 가지 예는 FindBugs 입니다 .

FindBugs는 @Nullable@NonNull 어노테이션을 통해 null 계약을 관리하는 데 도움이 됩니다 . 모든 메서드, 필드, 지역 변수 또는 매개변수에 대해 이러한 어노테이션을 사용할 수 있습니다. 이렇게 하면 어노테이션이 달린 유형이 null 일 수 있는지 여부를 클라이언트 코드에 명시적으로 알 수 있습니다 .

예를 보자:

public void accept(@NonNull Object param) {
    System.out.println(param.toString());
}

여기서 @NonNull은 인수가 null 일 수 없음을 분명히 합니다 . 클라이언트 코드가 null 에 대한 인수를 확인하지 않고 이 메서드를 호출하면 FindBugs는 컴파일 시 경고를 생성합니다.

4.2. IDE 지원 사용

개발자는 일반적으로 Java 코드 작성을 위해 IDE에 의존합니다. 예를 들어 변수가 할당되지 않았을 때 스마트 코드 완성 및 유용한 경고와 같은 기능은 확실히 많은 도움이 됩니다.

일부 IDE에서는 개발자가 API 계약을 관리할 수 있으므로 정적 코드 분석 도구가 필요하지 않습니다. IntelliJ IDEA는 @NonNull@Nullable 어노테이션을 제공합니다.

IntelliJ에서 이러한 어노테이션에 대한 지원을 추가하려면 다음 Maven 의존성을 추가해야 합니다.

<dependency>
    <groupId>org.jetbrains</groupId>
    <artifactId>annotations</artifactId>
    <version>16.0.2</version>
</dependency>

이제 IntelliJ는 마지막 예에서와 같이 null 검사가 누락된 경우 경고를 생성합니다 .

IntelliJ는 복잡한 API 계약을 처리하기 위한 계약 어노테이션 도 제공합니다 .

5. 주장

지금까지는 클라이언트 코드에서 null 검사의 필요성을 제거하는 것에 대해서만 이야기했습니다. 그러나 실제 응용 프로그램에는 거의 적용되지 않습니다.

이제 null 매개변수를 허용할 수 없거나 클라이언트가 처리해야 하는 null 응답을 반환할 수 있는 API로 작업하고 있다고 가정해 보겠습니다 . 이는 null 값 에 대한 응답 또는 매개 변수를 확인해야 할 필요성을 나타냅니다 .

여기서는 기존의 null 검사 조건문 대신 Java Assertions를 사용할 수 있습니다.

public void accept(Object param){
    assert param != null;
    doSomething(param);
}

2행에서 null 매개변수를 확인합니다. 어설션이 활성화되면 AssertionError 가 발생합니다 .

null 이 아닌 매개 변수 와 같은 전제 조건을 주장하는 좋은 방법이지만 이 접근 방식에는 두 가지 주요 문제가 있습니다 .

  1. 어설션은 일반적으로 JVM에서 비활성화됩니다.
  2. 잘못된 어설션 은 복구할 수 없는 확인되지 않은 오류를 초래합니다.

따라서 프로그래머가 조건을 확인하기 위해 어설션을 사용하는 것은 권장되지 않습니다. 다음 섹션에서는 null 유효성 검사를 처리하는 다른 방법에 대해 설명합니다.

6. 코딩 관행을 통한 Null 검사 방지

6.1. 전제조건

일반적으로 일찍 실패하는 코드를 작성하는 것이 좋습니다. 따라서 API가 null 이 될 수 없는 여러 매개변수를 허용하는 경우 API 의 전제 조건으로 null 이 아닌 모든 매개변수를 확인하는 것이 좋습니다 .

초기에 실패하는 방법과 실패하지 않는 방법의 두 가지 방법을 살펴보겠습니다.

public void goodAccept(String one, String two, String three) {
    if (one == null || two == null || three == null) {
        throw new IllegalArgumentException();
    }

    process(one);
    process(two);
    process(three);
}

public void badAccept(String one, String two, String three) {
    if (one == null) {
        throw new IllegalArgumentException();
    } else {
        process(one);
    }

    if (two == null) {
        throw new IllegalArgumentException();
    } else {
        process(two);
    }

    if (three == null) {
        throw new IllegalArgumentException();
    } else {
        process(three);
    }
}

분명히 우리는 badAccept() 보다 goodAccept () 를 선호해야 합니다 .

대안으로 API 매개변수의 유효성을 검사하기 위해 Guava의 사전 조건을 사용할 수도 있습니다 .

6.2. 래퍼 클래스 대신 프리미티브 사용

null은 int 와 같은 프리미티브에 허용되는 값이 아니므 로 가능하면 Integer 와 같은 래퍼 대응 항목보다 선호해야 합니다 .

두 정수를 합산하는 메서드의 두 가지 구현을 고려하십시오.

public static int primitiveSum(int a, int b) {
    return a + b;
}

public static Integer wrapperSum(Integer a, Integer b) {
    return a + b;
}

이제 클라이언트 코드에서 이러한 API를 호출해 보겠습니다.

int sum = primitiveSum(null, 2);

null 은 int 에 유효한 값이 아니므 로 컴파일 타임 오류가 발생합니다 .

래퍼 클래스와 함께 API를 사용하면 NullPointerException이 발생 합니다 .

assertThrows(NullPointerException.class, () -> wrapperSum(null, 2));

다른 예제인 Java Primitives Versus Objects 에서 다룬 것처럼 래퍼보다 프리미티브를 사용하는 다른 요소도 있습니다 .

6.3. 빈 컬렉션

경우에 따라 메서드의 응답으로 컬렉션을 반환해야 합니다. 이러한 메서드의 경우 항상 null 대신 빈 컬렉션을 반환 해야 합니다 .

public List<String> names() {
    if (userExists()) {
        return Stream.of(readName()).collect(Collectors.toList());
    } else {
        return Collections.emptyList();
    }
}

이렇게 하면 클라이언트가 이 메서드를 호출할 때 null 검사 를 수행할 필요가 없습니다 .

7. 객체 사용

Java 7은 새로운 객체 API를 도입했습니다. 이 API에는 많은 중복 코드를 제거하는 몇 가지 정적 유틸리티 메서드가 있습니다.

그러한 메서드 중 하나 인 requireNonNull()을 살펴보겠습니다 .

public void accept(Object param) {
    Objects.requireNonNull(param);
    // doSomething()
}

이제 accept() 메서드를 테스트해 보겠습니다 .

assertThrows(NullPointerException.class, () -> accept(null));

따라서 null이 인수로 전달되면 accept()는 NullPointerException을 발생시킵니다 .

이 클래스에는 개체에서 null 을 확인하는 조건자로 사용할 수 있는 isNull()nonNull() 메서드도 있습니다 .

8. 옵션 사용

8.1. orElseThrow 사용

Java 8은 언어에 새로운 선택적 API를 도입했습니다. 이는 null 에 비해 선택적 값을 처리하기 위한 더 나은 계약을 제공합니다 .

선택적이 null 검사 의 필요성을 제거하는 방법을 살펴보겠습니다 .

public Optional<Object> process(boolean processed) {
    String response = doSomething(processed);

    if (response == null) {
        return Optional.empty();
    }

    return Optional.of(response);
}

private String doSomething(boolean processed) {
    if (processed) {
        return "passed";
    } else {
        return null;
    }
}

위에 표시된 대로 Optional을 반환함으로써 프로세스 메서드는 응답이 비어 있을 수 있으며 컴파일 타임에 처리되어야 함을 호출자에게 분명히 합니다.

이것은 특히 클라이언트 코드에서 null 검사 의 필요성을 제거합니다 . 선택적 API 의 선언적 스타일을 사용하여 빈 응답을 다르게 처리할 수 있습니다 .

assertThrows(Exception.class, () -> process(false).orElseThrow(() -> new Exception()));

또한 API가 빈 응답을 반환할 수 있음을 클라이언트에 알리기 위해 API 개발자에게 더 나은 계약을 제공합니다.

이 API 호출자에 대한 null 검사 의 필요성을 없앴지만 이를 사용하여 빈 응답을 반환했습니다.

이를 방지하기 위해 Optional은 지정된 값 또는 값이 null 인 경우 비어 있는 Optional을 반환하는 ofNullable 메서드를 제공합니다 .

public Optional<Object> process(boolean processed) {
    String response = doSomething(processed);
    return Optional.ofNullable(response);
}

8.2. 콜렉션과 함께 선택사항 사용

빈 컬렉션을 처리하는 동안 Optional이 유용합니다.

public String findFirst() {
    return getList().stream()
      .findFirst()
      .orElse(DEFAULT_VALUE);
}

이 함수는 List의 첫 번째 항목을 반환하도록 되어 있습니다. Stream API의 findFirst 함수 는 데이터가 없을 때 옵션을 반환합니다. 여기서는 orElse를 사용하여 대신 기본값을 제공했습니다.

이렇게 하면 빈 List이나 Stream 라이브러리의 필터 메서드 를 사용한 후 공급할 항목이 없는 List을 처리할 수 있습니다.

또는 이 메서드에서 Optional을 반환하여 클라이언트가 처리 방법을 결정하도록 할 수도 있습니다 .

public Optional<String> findOptionalFirst() {
    return getList().stream()
      .findFirst();
}

따라서 getList 의 결과가 비어 있으면 이 메서드는 클라이언트에 빈 Optional을 반환합니다 .

컬렉션과 함께 Optional을 사용하면 null이 아닌 값을 반환하는 API를 설계할 수 있으므로 클라이언트에서 명시적인 null 검사를 피할 수 있습니다.

여기서 이 구현은 null을 반환하지 않는 getList 에 의존한다는 점에 유의하는 것이 중요합니다 . 그러나 이전 섹션에서 논의한 것처럼 null 보다는 빈 List을 반환하는 것이 더 나은 경우가 많습니다 .

8.3. 옵션 결합

함수가 Optional 을 반환하도록 만들기 시작하면 결과를 단일 값으로 결합하는 방법이 필요합니다.

이전의 getList 예제를 살펴보겠습니다 . Optional List을 반환하거나 ofNullable을 사용하여 Optionalnull을 래핑하는 메서드로 래핑하는 경우 어떻게 됩니까 ?

우리의 findFirst 메소드는 선택적 List 의 선택적 첫 번째 요소를 반환하려고 합니다 .

public Optional<String> optionalListFirst() {
   return getOptionalList()
      .flatMap(list -> list.stream().findFirst());
}

getOptional 에서 반환된 Optional 에 flatMap 함수를 사용하여 Optional 을 반환하는 내부 표현식의 결과를 압축 해제할 수 있습니다 . flatMap 이 없으면 결과는 Optional<Optional<String>> 이 됩니다 . flatMap 작업 Optional이 비어 있지 않은 경우에만 수행됩니다 .

9. 도서관

9.1. 롬복 사용

Lombok은 프로젝트에서 상용구 코드의 양을 줄여주는 훌륭한 라이브러리입니다. getter, setter 및 toString() 과 같은 Java 애플리케이션에서 자주 작성하는 코드의 공통 부분을 대신하는 일련의 어노테이션이 함께 제공됩니다 .

또 다른 어노테이션은 @NonNull 입니다 . 따라서 프로젝트에서 이미 Lombok을 사용하여 상용구 코드를 제거한 경우 @NonNull이 null 검사 의 필요성을 대체할 수 있습니다 .

몇 가지 예제로 이동하기 전에 Lombok에 대한 Maven 의존성 을 추가해 보겠습니다 .

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.20</version>
</dependency>

이제 null 검사가 필요한 모든 곳에서 @NonNull을 사용할 수 있습니다 .

public void accept(@NonNull Object param){
    System.out.println(param);
}

따라서 null 검사 가 필요한 객체에 간단히 어노테이션을 달고 Lombok에서 컴파일된 클래스를 생성합니다.

public void accept(@NonNull Object param) {
    if (param == null) {
        throw new NullPointerException("param");
    } else {
        System.out.println(param);
    }
}

paramnull 인 경우 이 메서드는 NullPointerException 을 throw합니다 . 메서드는 계약에서 이를 명시적으로 만들어야 하며 클라이언트 코드는 예외를 처리해야 합니다.

9.2. StringUtil 사용

일반적으로 문자열 유효성 검사에는 null 값 외에 빈 값에 대한 검사가 포함됩니다 .

따라서 이것은 일반적인 유효성 검사 문입니다.

public void accept(String param){
    if (null != param && !param.isEmpty())
        System.out.println(param);
}

많은 문자열 유형을 처리해야 하는 경우 이는 빠르게 중복됩니다. 여기에서 StringUtils가 유용합니다.

실제로 작동하는 것을 보기 전에 commons-lang3 에 대한 Maven 의존성을 추가해 보겠습니다 .

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.12.0</version>
</dependency>

이제 StringUtils를 사용하여 위의 코드를 리팩터링해 보겠습니다 .

public void accept(String param) {
    if (StringUtils.isNotEmpty(param))
        System.out.println(param);
}

따라서 우리는 null 또는 빈 검사를 정적 유틸리티 메서드 isNotEmpty() 로 대체했습니다 . 이 API는 일반적인 String 함수를 처리하기 위한 다른 강력한 유틸리티 메서드를 제공합니다 .

10. 결론

이 기사에서는 NullPointerException이 발생하는 다양한 이유 와 식별하기 어려운 이유를 살펴보았습니다 .

그런 다음 매개 변수, 반환 유형 및 기타 변수를 사용하여 null을 확인하는 코드의 중복을 방지하는 다양한 방법을 살펴보았습니다 .

모든 예제는 GitHub에서 사용할 수 있습니다 .

Generic footer banner