1. 개요

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

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

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

2. NullPointerException 이란 무엇입니까 ?

NullPointerException 에 대한 Javadoc에 따르면 다음 과 같이 객체가 필요한 경우 응용 프로그램이 null 을 사용하려고 할 때 throw됩니다 .

  • 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;
    }
}

인쇄 () 메서드 호출은 인쇄 할 것 "널 (null)을" 그러나 예외가 발생하지 않습니다. 마찬가지로  process () 응답에서 null 반환하지 않습니다  . 오히려 예외가 발생합니다.

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

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

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

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

4. API 계약 자동화

4.1. 정적 코드 분석 사용

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

FindBugs @Nullable  및  @NonNull  어노테이션을 통해 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. 거짓  복구 할 수없는 것입니다 검사되지 않은 오류가 주장 결과

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

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

6.1. 전제 조건

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

예를 들어 두 가지 방법을 살펴 보겠습니다. 하나는 조기에 실패하고 다른 하나는 실패합니다.

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 vs 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은 새로운 Objects  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에 비해 선택적 값을 처리하기위한 더 나은 계약을 제공합니다 Optional 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;
    }
}

반환하여  선택 사항,  위 그림과 같이, 처리  방법은 응답이 비어있을 수 및 컴파일 시간에 처리해야하는 호출자에게 취소합니다.

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

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

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

이 API의 호출자에 대한 null  검사 의 필요성을 제거했지만  이를 사용하여 빈 응답을 반환했습니다. 이를 방지하기 위해  Optional지정된 값으로 Optional  을 반환 하는 ofNullable  메서드를  제공  하거나 값이 null 인 경우 empty 를 제공합니다  .

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

8.2. 컬렉션과 함께 Optional 사용

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

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

이 함수는 List의 첫 번째 항목을 반환합니다. 스트림 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 예제를 살펴 보겠습니다  . 무엇은 반환한다면 옵션 List, 또는 포장 방법으로 포장 할 수 있었다 널 (null)선택적 사용 ofNullable를 ?

우리 로 findFirst의 방법은 반환하고자하는  옵션 의 첫 번째 요소  옵션 List :

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

사용하여  flatMap의 온 기능 선택 사항 에서 반환  getOptional을  우리는 반환 내부 식의 결과 압축을 풀 수있는 선택 사항 . flatMap이 없으면  결과는  Optional <Optional <String >> 입니다. flatMap의 때 작업 만 수행  옵션이 비어 있지 않습니다.

9. 도서관

9.1. Lombok 사용

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

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

몇 가지 예를보기 전에 Lombok에 대한 Maven 의존성을 추가해 보겠습니다 .

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.6</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);
    }
}

경우  PARAM이  있다 null의 경우, 이 방법은 발생 NullPointerException이. 메서드는 계약에서이를 명시해야하며 클라이언트 코드는 예외를 처리해야합니다.

9.2. StringUtils 사용

일반적으로 문자열 유효성 검사에는 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.11</version>
</dependency>

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

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

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

10. 결론

이 기사에서는 NullPointerException  의 다양한 이유 와 식별하기 어려운 이유를 살펴 보았습니다  . 그런 다음 매개 변수, 반환 유형 및 기타 변수를 사용 하여 null확인하는 코드에서 중복을 피하는 다양한 방법을 보았습니다 .

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