티스토리 뷰
이전에 근무 도중 점심 시간에 발생한 이슈였다.
개발한 앱의 특성상 클라이언트에서 호출 시점이 최우선인, 다시 말해 가장 먼저 호출되어야 하는 메소드가 있었다.
그 기능이 제대로 수행되지 않으면 시퀀셜하게 호출되는 API의 특성상 뒤에 호출되는 것들에도 영향도가 갈 수 밖에 없다.
그런데, 하필 API 업데이트를 진행한 후 앱이 기동이 되지 않는 것이었다.
클라이언트는 업데이트를 한 것이 없기에 이건 분명 서버의 문제였고, 내가 수정한 소스에서 일어나고 있는 문제였다.
실제로 그런 문제는 의외로 너무 단순한 문제였지만 중요한 문제기도 하였다.
여기서 알게된 것이 바로 Exception 처리의 중요성이고, 그 중 하나로 선택한 것이 NPE이다.
NPE
NullPointerException의 약자로 선언한 객체의 값이 없거나 null일 때,
그 객체에서 호출 가능한 메소드를 호출하였을 때 발생하는 증상
호출하는 클래스에서 (가령 String Class) throws Exception이 발생하지 않는 경우가 많다. 왜냐면 String 클래스는 개발을 알게된 시점부터 다루었던 클래스였고, 워낙 눈에 익다보니 자연스럽고 당연하게 사용하기 때문이다.
근데 String은 primitive type이 아닌 Object의 상속을 받는 reference type 형식의 클래스이다.
primitive type은 null이 떨어지면 메모리 주소에 기록되지 않으므로 바로 에러가 발생하게 된다.
하지만 String은 약간 다르다. (정확하게는 Primitive type 8개의 클래스 말고는 모두 다르다.)
String의 .toString()만을 사용한다 하더라도 String에 null을 할당해버리면 exception이 떨어지게 된다.
이렇게 두개의 메소드가 존재한다 가정하자. 리턴값은 둘 다 null을 반환하는 것을 쉽게 확인할 수 있다.
하지만 b() 메소드를 호출하게 되면 str이라는 변수가 null을 가지고 있으므로 String이라는 클래스의 특성을 사용할 수 없게된다.
즉, null.toString()은 null이라는 것에서 제공하지 않는 메소드이기 때문에 문제가 발생한다.
하지만 컴파일러는 이를 알아볼 수 없다. (사실 위에서는 너무 명시적으로 null이라는 값을 str object에 할당해주었기 때문에 사람은 쉽게 확인이 가능하다.) 컴파일 에러가 발생하지 않고, 런타임 에러가 발생하게 된다.
물론, 이를 해결하기 위하여 아파치(apache, 'common-io'라는 DI를 받으면 사용 가능)에서 제공하는 StringUtils의 isNull, isBlank 함수라던가 Objects.isNull 이라는 함수가 존재하기도 한다. 보통 사용 방법은 아래와 같다.
삼항연산자를 사용하여 축약이 가능하지만, 그래도 if문을 사용해서 사용하는 것이 정말 수고스러울수 밖에 없는 코드이다.
게다가 apache DI를 받아서 사용하기에는... 일이 너무 커진다.
아예 따로 Util을 하나 만들어서 아래와 같이 사용해도 괜찮다.
하지만 우리는 String만 사용하지 않는다. primitive type을 객체화시킨 클래스들(Integer, Double, Long, Float, Char, Boolean)도 있고, 이런 클래스들에 대해 isBlank 함수를 만들게 되면, 개발하는데 너무 시간이 아깝다는 생각이 들 것이다.
Java에서는 이를 캐치하여 Optional이라는 클래스를 따로 제공하고 있다.
이것이 무엇이냐면, null값을 가질 가능성이 있는 객체를 입력받아서 실제로 null값을 입력받으면 내가 설정한 기본값의 객체를 리턴하는 클래스이다. 말로만 하는 것은 다소 어려워서 코드를 보며 쉽게 이해를 하는 것이 좋은 방법일 듯하다.
이렇게 사용하게 되면 파라미터에 들어가는 param 객체가 null을 보낼지어도 ""을 리턴하게 된다.
위에 케이스인 ofNullable, orElse 말고도 Optional에서는 다양한 함수를 제공하고 있다.
equals는 모든 object에 제공되는 함수이므로 패스하도록 하겠다.
메소드 | 반환 타입 | 설명 | 사용 예제 |
of | Optional | non-null을 파라미터로 받아 Optional 클래스에서 제공하는 함수를 사용가능하게 해줌 | Optional.of("string").isPresent() |
ofNullable | Optional<T> | 파라미터에 변수를 넣어 null이면 emptry()를, 그렇지 않으면 Optional을 이어서 사용 | Optional.ofNullable("str") |
isPresent | boolean | 앞에 연결된 변수가 null이 아닌지 체크 | boolean is = Optional.of("").isPresent() |
ifPresent | void | 파라미터에 Consumer를 넣어 앞에서 전달받은 변수가 null이 아닐 경우 수행 | Optional.of("str").ifPresent(String::toString) |
get | T | Optional에서 마지막으로 정제된 값을 가져옴, null 인 경우 NoSuchException 발생 | String str = Optioanl.of("string").get(); |
empty | Optional<T> | T에 들어간 빈 객체를 생성 | Optional<String> s = Optional.empty(); |
orElse, orElseGet | T | of, ofNullable에 이어서 사용 가능하고 null인 경우 기본값을 할당해주거나, 할당해서 가져옴 | Optional.of(null).orElse("default").get() Optional.of(null).orElseGet("default") |
orElseThrow | T | 위와 유사하지만 파라미터에 Supplier를 넣어 특정 Exception을 호출하도록 해줌 | String s = Optional.of("").orElseThrow(IllegalStateException::new); |
map, flatMap | Optional<U> | 앞에서 받은 Optioanl 의 Function을 수행 | Optional<byte[]> str = Optional.of("str").map(s -> s.getBytes()); |
filter | Optional<T> | 앞에서 받은 Optional의 Predicate를 수행 | Optional<String> str = Optioanl.of("string").filter(s -> s.length() == 6); |
(Consumer, Predicate등 알 수 없는 용어들은 여기 글에 설명해두었습니다.)
현재까지 사용해본 NullPointerException을 처리하는 클래스 중 다분하게 사용하는 클래스에 속하고 있고, 앞으로도 애용하게 되는 클래스일듯 하다.
'Server' 카테고리의 다른 글
[Java] 날짜 생성/변환 (0) | 2019.12.06 |
---|---|
[Java] 엑셀 변환 (0) | 2019.12.03 |
[Java] ORM과 JPA, 그리고 Hibernate (0) | 2019.11.28 |
[Java] Lambda와 Stream(2) (0) | 2019.10.15 |
[Java] Lambda와 Stream(1) (2) | 2019.10.15 |
REST의 정체는? (1) | 2019.10.15 |