Post

Objects.requireNonNull을 왜 사용할까?

Objects.requireNonNull을 왜 사용할까?

Objects.requireNonNull()

자바에서 null을 처리하는 것은 굉장히 중요하다. 잠재적인 null을 처리해주지 않으면 런타임환경에서 예상치 못한 예외가 발생하여 서비스에 치명적일 수 있다. 이러한 자바 + 스프링을 사용하면서 null을 처리하는 방법은 다양한데, 이 중 Objects.requireNonNull()에 대해 알아보려고 한다.

image

Objects.requireNonNull()은 자바 7부터 생긴 유틸 메서드이다. 이 메서드는 총 3개의 오버로딩 메서드가 있다. null을 체크할 객체와 예외 메시지를 담는다.

1
2
3
public void test(String text) {  
    Objects.requireNonNull(text);
}

image

1
2
3
public void test(String text) {  
    Objects.requireNonNull(text, "Text is null");
}

image

1
2
3
public void test(String text) {  
    Objects.requireNonNull(text, () -> "Text is null");  
}

image

자바를 다루는 책이나 오픈소스 등을 보면 Objects.requireNonNull()이 자주 보인다. 이 메서드는 객체가 null인 지 확인하고 null이라면 NullPointerException(이하 NPE) 을 던진다. 그런데 생각해보면 굳이 이런 메서드를 사용하지 않더라도, null을 가리키는 객체를 참조하면 NPE를 던지는데 굳이 이 메서드를 사용하는 이유가 뭘까?

가장 큰 이유는 Fail Fast 이다.

Fail Fast

빠른 실패라는 뜻인데, Objects.requireNonNull()을 사용하는 게 뭐가 왜 빠르다는 건 지 알아보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static void main(String[] args) {  
    NonNullTest nonNullTest = new NonNullTest();  
    nonNullTest.method1(null);  
}  
  
static class NonNullTest {  
  
    public void method1(String text) {  
        method2(text);  
    }  
    private void method2(String text) {  
        method3(text);  
    }  
    private void method3(String text) {  
        System.out.println(text.toUpperCase());  
    }}

nullStringmethod3()까지 가지고 들어가서 toUpperCase()를 호출해주는 코드이다. 다들 예상하다시피 toUpperCase()를 호출하는 시점에 NPE가 발생한다.

image

매우 간단한 메서드이기 떄문에 금방 예외를 확인할 수 있었는데, 만약 각각의 메서드가 일정 작업 시간이 소요되면 어떻게 될까?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public static void main(String[] args) throws InterruptedException {  
    System.out.println("start >>> " + getNowTime());  
    NonNullTest nonNullTest = new NonNullTest();  
    nonNullTest.method1(null);  
}  
  
static class NonNullTest {  
  
    public void method1(String text) throws InterruptedException {  
        Thread.sleep(1000);  
        method2(text);  
    }  
    private void method2(String text) throws InterruptedException {  
        Thread.sleep(1000);  
        method3(text);  
    }  
    private void method3(String text) {  
        System.out.println("invoke >>> " + getNowTime());  
        System.out.println(text.toUpperCase());  
    }}  
  
private static String getNowTime() {  
    return LocalDateTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss"));  
}

이번엔 method3 이 호출되기 까지 2초가 소요된다. 그렇다면 NPE도 2초 뒤에 발생할 것이다.

image

main 메서드에서부터 null이었는데, 한참 지난 후에야 참조를 하면서 예외가 발생한다. 우리는 null에 예민하기 때문에 최대한 빨리 예외를 처리하고 싶고, 불필요한 로직을 수행하지 않도록 하고 싶다. 그렇다면 이번에는 실패를 빨리 확인할 수 있다는 Objects.requireNonNull을 사용해보자.

1
2
3
4
5
public static void main(String[] args) throws InterruptedException {  
    System.out.println("start >>> " + getNowTime());  
    NonNullTest nonNullTest = new NonNullTest();  
    nonNullTest.method1(Objects.requireNonNull(null));  
}  

image

Objects.requireNonNull을 사용하자 main메서드에서 바로 NPE가 발생했다. 이제 더 이상 NPE를 보기 위해 2초를 기다리지 않아도 된다. Fail Fast는 이러한 이유로 중요한 것이다.

그런데 if 로 null 체크하는 거랑 똑같지 않나?

맞다. 똑같다. 내가 이 글을 작성하는 이유도 거기서 오는 궁금증이었다. 사실 ifnull 체크를 하면 NPE 자체를 피하고 다른 로직으로 처리할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static void main(String[] args) throws InterruptedException {  
    System.out.println("start >>> " + getNowTime());  
    NonNullTest nonNullTest = new NonNullTest();  
    nonNullTest.method1(null);  
}  
  
static class NonNullTest {  
  
    public void method1(String text) throws InterruptedException {  
        if (text == null) {  
            System.out.println("Text는 null일 수 없습니다. 다시 시도 해주세요.");  
            return;  
        }  
  
        Thread.sleep(1000);  
        method2(text);  
    }  
 }  

image

심지어 if를 사용하면 다양한 예외로 변경해서 던져줄 수도 있지만 Objects.requireNonNullNPE밖에 못 뱉는다. 그럼에도 사용하는 이유는 뭘까?

진짜로 왜 쓰는거지?

StackOverflow나 다른 블로그를 보면 가독성와 Fail Fast가 주된 이유이다. 가독성이 좋다는 것은 Objects.requireNonNull()이 명시적이라는 것이다. 그런데 개인적으로는 읽어도 잘 공감이 되지 않는다.

Fail Fast

앞서 얘기했듯이 Fail Fast는 if문으로도 충분히 가능하고, 좀 더 유연하게 사용할 수 있다. Fail Fast를 위해 NPE만을 뱉어내는 Objects.requireNonNull()을 실무에서 사용할 일이 있을까? 의문이다.

가독성

1
2
3
4
5
if (text == null) {
	throw new NullPointerException();
}

Objects.requireNonNull(text);

이 둘 중 뭐가 더 직관적이고 명시적인가? 나는 if를 사용하는 게 더 명시적이라고 생각한다. 물론 전자가 라인 수가 3배나 많고 추상화의 관점이나 객체지향의 관점에서 보면 Objects.requireNonNull()가 좋은코드(?)라고 볼 수 있겠으나, 해당 메서드에 대한 이해가 있어야 한다. 내가 이 글을 작성하는 이유도 이 메서드를 이해할 수 없어서 작성하는 것이다.

유연함

나는 실무에서 NullPointerException을 명시해서 던져주는 경우가 단 한 번도 없었다. 항상 다른 RuntimeException으로 감싸거나 커스텀 예외를 던져주었다. Objects.requireNonNull()을 다른 예외로 치환해주려면 다음과 같이 작성해야할 것이다.

1
2
3
4
5
try {
	Objects.requireNonNull(text);
} catch(NullPointerException e) {
	throw new CustomException(e, "Text should not be null")
}

이번에는 간단하게 if로 예외를 처리해볼까?

1
2
3
if (text == null) {
	throw new CustomException("Text should not be null");
}

개인적으로 try-catch문을 많이 사용하는 것을 싫어한다. 어떻게 할 수 없는 depth가 생겨 코드가 난잡해지고, try문 내외부의 변수관리하는 것도 귀찮아진다. 이런 이유로, 나는 후자의 방법으로 null을 처리하는 것이 좀 더 낫다는 의견이다.

결론

if를 사용해서 null을 처리를 하든, Objects.requireNonNull()을 사용하든 개발자의 마음이다. 뭐가 더 좋고 뭐가 더 나은 선택인 지는 정해져 있지않다. 어찌됐든 각자의 개발환경에 맞는 선택을 하는 것이 중요한 것 같다. 자바 코드 레퍼런스들을 보면 Objects.requireNonNull()이 너무 많이 보인다. 볼 때마다 도대체 왜 쓰는 건가 싶었는데 내가 살펴본 이유가 전부라면 그닥 사용할만한 매력을 느끼지 못하겠다. 혹시 사용하는 것이 좋다는 의견이라면 적극적으로 알려주길 바란다. 나도 궁금하다.

Reference

  1. 자바8 공식문서
  2. 스택오버플로우
  3. hudi님 블로그
This post is licensed under CC BY 4.0 by the author.