Objects.requireNonNull을 왜 사용할까?
Objects.requireNonNull()
자바에서 null
을 처리하는 것은 굉장히 중요하다. 잠재적인 null
을 처리해주지 않으면 런타임환경에서 예상치 못한 예외가 발생하여 서비스에 치명적일 수 있다. 이러한 자바 + 스프링을 사용하면서 null
을 처리하는 방법은 다양한데, 이 중 Objects.requireNonNull()
에 대해 알아보려고 한다.
Objects.requireNonNull()
은 자바 7부터 생긴 유틸 메서드이다. 이 메서드는 총 3개의 오버로딩 메서드가 있다. null
을 체크할 객체와 예외 메시지를 담는다.
1
2
3
public void test(String text) {
Objects.requireNonNull(text);
}
1
2
3
public void test(String text) {
Objects.requireNonNull(text, "Text is null");
}
1
2
3
public void test(String text) {
Objects.requireNonNull(text, () -> "Text is null");
}
자바를 다루는 책이나 오픈소스 등을 보면 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());
}}
null
인 String
을 method3()
까지 가지고 들어가서 toUpperCase()
를 호출해주는 코드이다. 다들 예상하다시피 toUpperCase()
를 호출하는 시점에 NPE
가 발생한다.
매우 간단한 메서드이기 떄문에 금방 예외를 확인할 수 있었는데, 만약 각각의 메서드가 일정 작업 시간이 소요되면 어떻게 될까?
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초 뒤에 발생할 것이다.
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));
}
Objects.requireNonNull
을 사용하자 main
메서드에서 바로 NPE
가 발생했다. 이제 더 이상 NPE
를 보기 위해 2초를 기다리지 않아도 된다. Fail Fast는 이러한 이유로 중요한 것이다.
그런데 if 로 null 체크하는 거랑 똑같지 않나?
맞다. 똑같다. 내가 이 글을 작성하는 이유도 거기서 오는 궁금증이었다. 사실 if
로 null
체크를 하면 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);
}
}
심지어 if
를 사용하면 다양한 예외로 변경해서 던져줄 수도 있지만 Objects.requireNonNull
은 NPE
밖에 못 뱉는다. 그럼에도 사용하는 이유는 뭘까?
진짜로 왜 쓰는거지?
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()
이 너무 많이 보인다. 볼 때마다 도대체 왜 쓰는 건가 싶었는데 내가 살펴본 이유가 전부라면 그닥 사용할만한 매력을 느끼지 못하겠다. 혹시 사용하는 것이 좋다는 의견이라면 적극적으로 알려주길 바란다. 나도 궁금하다.