Guide to Objects.requireNonNull() in Java
1. 소개
NullPointerException은 자바에서 가장 일반적인 예외 중 하나입니다. 이는 null을 가리키는 참조 변수를 접근하거나 상호작용할 때 발생합니다. 객체를 검증하는 것은 중요하며, 특히 메서드나 생성자의 매개변수일 경우 더욱 그렇습니다. 이 검증을 통해 견고하고 신뢰할 수 있는 코드를 작성할 수 있습니다. 우리는 이를 직접 null 체크 코드를 작성하거나, 서드파티 라이브러리를 사용하거나, 더 편리한 접근 방식을 선택함으로써 수행할 수 있습니다.
이 튜토리얼에서는 후자의 방법, 즉 자바가 제공하는 유연하고 내장된 솔루션인 Objects.requireNonNull() 메서드에 대해 살펴보겠습니다.
2. Null 값 처리
간단히 리뷰하자면, 수동 null 체크를 피할 수 있는 여러 대안이 존재합니다. 코드에 if 문을 감싸는 대신, 더 많은 라이브러리 중에서 선택하는 것을 고려할 수 있습니다. Spring, Lombok (@NonNull), 그리고 Uber의 NullAway는 그 예 중 일부입니다.
반면, 일관성을 유지하거나 코드 베이스에 추가 라이브러리를 도입하지 않거나 바닐라 자바를 사용하려는 경우, Optional 클래스 또는 Objects 메서드 사이에서 선택할 수 있습니다. Optional은 null 값 처리를 단순화하지만, 경우에 따라 코드 오버헤드를 추가하고 간단한 사용 사례에서 코드를 복잡하게 만들 수 있습니다. 또한 별도의 객체이므로 메모리를 추가로 소비합니다. 더불어 Optional은 NullPointerException을 NoSuchElementException으로 전환할 뿐, 결함을 해결하지는 않습니다.
반면, Objects 클래스는 객체를 보다 효율적으로 작업하기 위한 정적 유틸리티 메서드를 제공합니다. 자바 7에서 도입된 이 클래스는 자바 8과 9에서 여러 번 업데이트되어, 작업을 수행하기 전에 조건을 검증하는 메서드를 제공합니다.
3. Objects.requireNonNull()의 장점
Objects 클래스는 null 값 체크 및 처리를 단순화하는 requireNonNull() 정적 메서드 세트를 제공합니다. 이 메서드들은 사용 전에 null 객체를 체크하는 간단하고 간결한 접근 방식을 제공합니다.
requireNonNull() 메서드의 주요 목적은 객체 참조가 null인지 체크하고, null일 경우 NullPointerException을 발생시키는 것입니다. 예외를 명시적으로 발생시킴으로써, 우리는 체크의 의도를 전달하고, 코드가 이해하고 유지보수하기 쉽게 만들 수 있습니다. 또한 이는 개발자와 유지보수자에게 오류가 의도된 것임을 알리며, 시간이 지남에 따라 행동이 잘못 변경되는 것을 방지합니다.
Objects 클래스는 java.util 패키지의 일부로, 외부 라이브러리나 의존성 없이 쉽게 접근할 수 있습니다. 이 메서드는 잘 문서화되어 있어, 개발자가 올바른 사용법에 대한 명확한 지침을 제공합니다.
4. 사용 사례 및 오버로드된 메서드
이제 requireNonNull() 메서드의 다양한 사용 사례와 오버로드된 변형에 대해 살펴보겠습니다.
이 메서드는 간단한 null 체크부터 사용자 정의 메시지를 통한 복잡한 검증까지 다양한 시나리오에서 null 값을 처리할 수 있게 해줍니다.
또한, 기본값을 지원하는 두 개의 추가 메서드인 requireNonNullElse() 및 requireNonNullElseGet()도 있습니다. 이러한 사용 사례를 이해함으로써, 우리는 코드베이스에 requireNonNull()을 효과적으로 포함할 수 있습니다.
4.1. 메서드와 생성자에서 단일 매개변수 검증
먼저 가장 간단한 구현인 단일 매개변수를 가진 requireNonNull()을 살펴보겠습니다:
public static <T> T requireNonNull(T obj) {
if (obj == null) {
throw new NullPointerException();
} else {
return obj;
}
}
이 메서드는 제공된 객체 참조가 null인지 체크합니다. null인 경우 NullPointerException을 발생시키고, 그렇지 않으면 변경되지 않은 객체를 반환합니다.
이 메서드는 주로 메서드와 생성자에서 매개변수를 검증하는 데 사용됩니다. 예를 들어, greet() 메서드에 전달된 name이 null이 아닌지 확인하고자 할 때는 다음과 같은 접근 방식을 사용할 수 있습니다:
void greet(String name) {
Objects.requireNonNull(name, "Name cannot be null");
logger.info("Hello, {}!", name);
}
이 예제를 통해 우리는 메서드가 유효한 인자를 받도록 만들 수 있으며, 객체가 사용되기 전에 null 객체를 감지하여 접근 시 오류를 방지할 수 있습니다.
4.2. 사용자 정의 오류 메시지를 통한 다중 매개변수 검증
다음으로, 우리의 메서드나 생성자가 여러 인수를 받을 경우의 처리 방법을 살펴보겠습니다. Objects.requireNonNull()의 변형 중 하나는 null 체크를 위한 객체 참조 외에 String 메시지를 추가로 받을 수 있습니다. 이것은 객체 참조가 null일 경우 사용자 정의 오류 메시지를 포함한 NullPointerException을 발생시킬 수 있도록 합니다:
public static <T> T requireNonNull(T obj, String message) {
if (obj == null)
throw new NullPointerException(message);
return obj;
}
여러 매개변수를 포함하는 생성자에서 검증을 위해 이 메서드를 사용할 수 있습니다:
static class User {
private final String username;
private final String password;
public User(String username, String password) {
this.username = Objects.requireNonNull(username, "Username is null!");
this.password = Objects.requireNonNull(password, "Password is null!");
}
// getters
}
사용자 정의 메시지가 포함된 NullPointerException은 무엇이 잘못되었는지와 어떤 매개변수가 문제를 일으켰는지에 대한 더 많은 맥락을 제공합니다. 이는 오류 보고를 개선하고 디버깅을 쉽게 만들어줍니다.
4.3. Supplier를 사용한 메시지 생성 지연
마지막으로, 또 다른 오버로드된 메서드를 사용하여 NullPointerException을 사용자 정의할 수 있습니다:
public static <T> T requireNonNull(T obj, Supplier<String> messageSupplier) {
if (obj == null) {
throw new NullPointerException(messageSupplier == null ? null : messageSupplier.get());
} else {
return obj;
}
}
두 번째 파라미터는 오류 메시지를 위한 Supplier입니다. 이는 null 체크 후 메시지 생성을 지연시켜 성능을 개선할 수 있게 하며, 특히 문자열 연결이나 복잡한 계산과 같은 비용이 많이 드는 작업에서 유용합니다:
void processOrder(UUID orderId) {
Objects.requireNonNull(orderId, () -> {
String message = "Order ID cannot be null! Current timestamp: " + getProcessTimestamp();
message = message.concat("Total number of invalid orders: " + getOrderAmount());
message = message.concat("Please provide a valid order.");
return message;
});
logger.info("Processing order with id: {}", orderId);
}
위 예제에서 getOrderAmount()와 getProcessTimestamp()는 데이터베이스 쿼리나 외부 API 호출과 같은 시간 소모적인 작업을 수반할 수 있습니다. 메시지 생성을 지연함으로써, 이 접근 방식은 orderId가 null이 아닐 때 불필요한 성능 비용을 방지합니다.
하지만 메시지 Supplier를 생성하는 오버헤드가 직접적으로 메시지를 생성하는 것보다 낮아야 한다는 점에 유의해야 합니다.
5. 모범 사례
앞서 언급했듯, 메서드와 생성자를 설계할 때 매개변수 제한을 강화하여 코드의 예측 가능한 동작을 보장해야 합니다. 메서드나 생성자의 시작 부분에 Objects.requireNonNull()을 사용하는 것은 잘못된 인자를 조기에 포착하는 데 도움이 됩니다. 이 방식은 또한 코드의 가독성을 유지하고, 유지보수를 쉽게 하며, 디버깅을 단순화하는 데 기여합니다.
이 또한 requireNonNull()*은 실패-빠른 시스템을 구축하는 데 중요한 역할을 할 수 있습니다. 실패-빠른 원칙은 오류가 즉시 감지되도록 하여 Cascading failures를 방지하고 디버깅의 복잡성을 줄이는 것입니다. 메서드가 매개변수를 사전 검증하면, 명확한 예외로 신속하게 실패하며 문제의 원인을 분명히 할 수 있습니다. 이러한 검증은 시스템이 혼란스러운 오류, 부정확한 결과를 생성하거나 심지어 코드의 무관한 부분에서 문제를 일으키지 않도록 하는 데 필요합니다.
또한, 공식 또는 보호된 메서드에 대해 매개변수 제한을 명시적으로 문서화하는 것도 좋은 관행입니다. Javadoc의 @throws 태그를 사용하여 매개변수 제한이 위반될 경우 던지는 예외를 명시할 수 있습니다. 여러 메서드가 NullPointerException을 던진다면, 각 메서드마다 반복하는 것보다 클래스 수준 Javadoc에서 다룰 수 있습니다.
6. 결론
이 튜토리얼에서는 Objects.requireNonNull() 및 그 오버로드된 변형을 사용하여 메서드 및 생성자 매개변수를 효율적으로 검증하는 방법을 보여주었습니다. 이러한 메서드는 자바에서 null 체크를 처리하는 간단하면서도 강력한 방법을 제공합니다. 사용자 정의 오류 메시지 및 메시지 생성을 지연시키는 내장 지원은 유연성을 추가하며, 다양한 사용 사례에 적합하게 만듭니다.
또한 매개변수 제한을 강화하고, 실패-빠른 원칙을 사용하며, 예외를 문서화하는 것과 같은 모범 사례를 채택하는 것은 우리 코드베이스의 전반적인 품질과 유지보수성을 향상시킵니다.
항상 그랬듯이, 완전한 소스 코드는 GitHub에서 확인할 수 있습니다.