들어가며
Java/Spring 을 사용하던 중 항상 직면하는 문제인 Null 처리(NPE) 에 관한 이야기를 해보려고 합니다.
여러분들은 Java 에서 Null 처리를 어떻게 하고 계시나요?
코틀린을 사용하시는 분들은 정확히는 잘 모르지만 Null 처리를 자동으로 해주기 때문에 딱히 신경을 쓰지 않아도 된다고 합니다.
(부럽습니다..)
하지만 Java 를 사용하시는 분들은 보통 직접 Null 처리를 해야합니다
보통 어떠한 Parameter 를 통하여 값을 전달받을 때 NPE 방지를 위해서 Null 처리를 하고는 합니다.
저는 위 Parameter 값에서 null 을 체크하기 위해 보통 아래와 같은 코드를 작성하고는 했습니다
if(id==null) {
throw new RuntimeException("Null ~");
}
본론
근본적으로 NPE 는 왜 발생할까? 라는 고민을 해봤습니다.
결론부터 말하자면
Java 는 값이 없는 상태 -> null 로 표현한다.
보통 '프론트 -> 백엔드' 로 값을 전달할 때 프론트에서 Null 을 전달할 수도 있고 빈값을 전달할 수도 있다.
그리고 Java 에는 위 값들을 정의하기 위해 '자료구조' 를 사용하여 변수를 사용합니다.
class PrimitiveType {
// 객체가 아닌 것들 자료구조
int a;
double b;
char c;
float d;
byte e;
short f;
boolean g;
long h;
}
class WrapperType {
// 객체 들은 WrapperType 이다
String s;
Integer i;
Double d;
Character c;
Person person;
}
변수를 정의하는 타입은 크게 2가지로 나뉘어 있다. 위와 같은 타입들이 Java 에서 대표적인 타입들이다.
아래의 그림에서 Type 들에 대한 큰 그림을 볼 수 있다.
그리고 위 타입들에게는 중요한 차이점이 있다.
바로 Null 값으로 초기화가 되냐, 안돼냐의 차이이다.
왜? 위와 같은 내용이 나올까? 라는 고민을 하신다면 '링크' 를 클릭하여 글을 읽어보기를 추천합니다.
위 내용을 다루는 글이 아니기 때문에 이유를 설명하지는 않겠습니다.
NullPointerException은 데이터가 null 일 때 발생하는 runtime 예외상황으로 내가 원하는 어플리케이션 플로우 가 진행되는 중에
프로그램이 비정상 종료가 될 수 있으므로 따로 꼭 처리를 해줘야 한다.
즉 Null 이 발생한 후에 try~catch 든 적절한 처리가 없다면 어플리케이션이 종료되는 상황이 발생한다는 뜻이다.
(보통 Null 체크를 할 때 버그에 대한 내용을 throw 를 발생시키지 않고 log 로 찍긴한다, 아니면 커스텀 Exception 을 사용하든..)
많은 Null 관련 테스트를 하지 않고 운영서버에 어플리케이션을 배포했다가, NPE 가 떨어지게 되면
어플리케이션이 종료되어 서비스가 다운되는 장애상황을 직면할 수도 있고, 그것은 회사으 손실로 까지 이어질 수 있다.
즉 Java 는 타입안정성을 중요시 하기 때문에 Null 처리가 중요하게 여겨집니다.
그렇다면 어떻게 하면 Null 처리를 효율적으로 할 수 있을까요?
0) 직관적인 분기 처리
if(id==null)
log.debug("Null...");
대표적인 방식이긴 하다. 위 방식은 나름 직관적이고 효율? 적이다.
하지만 위 코드가 많아질 경우 '가독성 저하' 이유로 잘 사용하지 않는다고 한다.
물론 null 처리가 조금 밖에 없다면 사용해도 무방할 것 같긴하다
1) 라이브러리를 사용
보통 Null 처리를 진행할 때 저는 빈값 처리 및 공백처리를 같이 하는 편 입니다.
그리고 위 빈값 및 공백 처리를 할 때는 apache.commons 라이브러리를 사용하고는 합니다.
implementation group: 'org.apache.commons', name: 'commons-lang3', version: '3.17.0'
위 라이브러리를 사용하여 StringUtils 객체를 사용하여 빈값 & 공백 처리를 진행하고는 합니다.
@Test
void NullCheck() {
Person person = new Person();
person.setName(null);
if(StringUtils.isEmpty(person.getName())) {
// true
}
if(StringUtils.isBlank(person.getName())) {
// true
}
if(person.getAge() == null) {
// true
}
}
사실 String 객체의 자체 메소드인 isEmpty(), isBlank() 를 사용할 수도 있다.
하지만 사용하지 않는 이유는 String 객체의 메소드들은 값이 Null 인 경우 NPE 를 발생시킨다.
하지만 StringUtils.isEmpty() 는 값을 정상적으로 처리하고 true 를 반환한다.
위 고질적인 문제들 때문에 나 포함 여러 사람들이 위 라이브러리를 사용한다고 생각한다.
그리고 추가적으로 빈값까지 없는 값인 null 로 처리하기 위해서는 'isBlank()' 메소드를 사용한다.
@Test
void NullTest() {
String str = " ";
String a = null;
boolean isEmpty = StringUtils.isEmpty(str); // true
boolean isBlank = StringUtils.isBlank(str); // true
boolean b = StringUtils.isEmpty(a); // true
boolean e = StringUtils.isBlank(a); // true
}
위 방법을 통하여 NullSafe 를 지킬 수 있다
2) Obejcts 메소드 사용
String a = null;
Objects.isNull(a);
Objects.nonNull(a);
Objects.requireNonNull(a,"");
Objects.requireNonNullElse(a,"Null 발생"); // 예외를 던짐
위 코드에서 isNull(), nonNull() 은 NullSafe 하지 않다
그러므로 아래와 requireNonNull() 메소드를 사용하여 분기 처리를 하는것이 NullSafe 하다.
3) Optional
Optional은 null 값을 다루기 위한 JDK8 이후로 나온 표준화된 방식으로,
null 사용을 줄이고, 명시적으로 값이 없음을 표현하기 위해 사용되고 있습니다.
Optional 은 메서드 반환값처럼 "값이 없을 가능성"이 명확한 경우에는 Optional을 사용하는 것이 권장된다.
위 내용이 잘이해가 되지않는다면 아래 코드를 보자
public Optional<User> findById(Long id) {
// 비즈니스 로직
return Optional.of(
// 로직
);
}
이런식으로 처리를 한다. 반환값이 있을수도 있고, 없을 수도 있는 모호한 값일 때 사용한다.
보통 조회를 하는 로직을 짤 때 많이 사용하는 편이다.
Ex) jpa 의 findById() 메소드
보통 Parameter 값들 까지는 Optional 로 처리하지는 않다.
이유는 간단하다. 귀찮음 + 성능상 좋지 않다.
반환값은 1개지만, 파라미터는 1개가 될수도있고 여러개가 될 수도 있기 때문이다.
그러므로 Parameter Null 체크를 할떄는 위에 방법들을 사용하기를 권장한다.
✅ 알아두면 좋은 내용
Wrapper Type 객체 중에 String 이 아닌 값을 String 으로 변환하기 위해서 보통
'toString()' 메소드 및 'String.valueOf()' 메소드를 보편적으로 사용하고 있다.
결론만 말하자면 toString() 은 NPE가 발생하고 String.valueOf()는 null 스트링 값 자체가 리턴된다.
Integer a = null;
System.out.println(i.toString()); // NPE 발생
System.out.println(String.valueOf(i)); // null 그대로 반환
본인이 잘 판단하여 사용하는게 맞지만, null 관련 오류를 만나기 싫으므로 저는 'toString()' 은 자제하여 사용하는 편 입니다.
추가적으로 맥락에 맞지는 않지만 retrun null 은 Java 에서 안티패턴 이기 때문에 권장하지 않습니다.
결론
null 체크는 어디까지나 "문제가 발생하지 않도록 방어적 코드"일 뿐, 근본적으로 null 발생을 막을 수 없다고 생각한다.
그러므로 근본적인 이유를 없애기 위하여 프론트 및 백엔드 단에서 Null 이 들어오지 않게 Validation 체크를 잘하도록 하자.