티스토리 뷰

Optional 기초 : NPE에서 벗어나보자.

들어가며

  • WAS log에서 NullPointerException을 발견했던 과거

    • 발생 시점을 추적하기가 어려웠다. 스택 트레이스 보는데도 모르겠음.
  • 메서드를 짜는데 객체가 없을 경우 null을 리턴 x 100(반복)

    • null 체크를 계속 해야 하는 불편. 코드가 거슬린다. (if 널 존재여부...) 까먹었을까봐 불안하다.
  • 그러던 중 스프링 강의에서 객체를 Optional로 감싸 NPE를 막는 걸 발견

    • => 배워야겠다.

    Optional은 람다, 스트림을 알아야 더 사용하기 좋지만...아직 사용하지 못한다. 그저 Optional만 알았을 뿐. 때문에 람다와 스트림을 몰라도 써먹을 수 있는 Optional의 기초만 정리해보도록 한다.

Optional

Optional은 java8부터 등장했다. Optional은 지네릭 클래스로 'T타입의 객체'를 감싸는 래퍼 클래스다. T 타입이기 때문에 Optional 타입의 객체는 모든 타입의 참조변수를 담을 수 있다.

public final class Optional<T> {
    private final T value; // T 타입의 참조 변수
}

객체 생성과 초기화

Opitonal 객체를 생성할 때는 of() 또는 ofNullable()을 사용한다. 참조변수의 값이 null일 가능성이 있으면 ofNullable을 사용해야 한다. of를 쓰면 NullPointerException이 발생하기 때문이다.

  1. 생성
Optional<String> optVal = Optional.of(null); //NPE 발생
Optional<String> optVal = Optional.ofNullable(null) //ok
/* 사용 예: MemberRepository */
private static Map<Long, Member> store = new HashMap<>();

public Optional<Member> findById(Long id) {
    return Optional.ofNullable(store.get(id)); //ok
    // return Optional.of(store.get(id));
}
  • id(key) 값으로 가져온 Member(value) 객체가 없을 수도 있으므로 Optional.of보다 Optional.ofNullable을 사용한다.

이 부분을 정리하면서 '그렇다면 무조건 ofNullable()만 사용하면 되지 않나? of()는 왜 쓰지?'라는 궁금증이 들었다. 스터디원 분이 알려주신 블로그 글에서 그 해답을 찾을 수 있었다. 아래는 블로그 글에 나온 Bad/Good 코드 예시다.

// Bad
return Optional.of(member.getEmail());  // member의 email이 null이면 NPE 발생

// Good
return Optional.ofNullable(member.getEmail());

email같은 경우 null일 가능성이 존재한다. 이렇게 null이 될 가능성이 조금이라도 있으면 ofNullable()을 쓴다.

// Bad
return Optional.ofNullable("READY");

// Good
return Optional.of("READY");

of()는 null이 발생할 리 없는 변수에서 사용한다. "READY"같은 경우 null이 될 리가 없으니 ofNullable()보다 of()를 쓴다.

알고 생각해보니... 메서드명 자체가 그 의미를 이미 내포하고 있더라..

  • nullable한 상황 => ofNullable()
  • null이 없을 상황 => of()
  1. 초기화
Optional<String> optVal = null; // null로 초기화. 할 수는 있으나 권장하지 않음.
Optional<String> optVal = Optional.<String>empty(); // 빈 객체로 초기화

참조변수를 기본값으로 초기화할 때는 null을 그대로 넣는 것보단 empty()로 초기화하는 것이 좋다. NPE로 인한 장애를 피하기 위해 만든게 Optional인데 null로 초기화해서 반환해버리면 도입 의도와 맞지 않기 때문이다.

객체의 값 가져오기

Optional 객체는 값을 감싸고 있다. 그래서 감싼 값을 가져오기 위해서는 get() 또는 orElse()를 사용한다.

  1. get
Optional<String> optVal = Optional.of("abc");
String str1 = optVal.get(); //abc

Optional<String> optVal = Optional.empty();
String str1 = optVal.get(); // NoSuchElementException

of()를 사용해서 Optional을 생성할 경우 값에 null이 들어갈 수도 있다. 이 경우 NoSuchElementException이 발생하기 때문에, orElse()로 대체할 값을 지정할 수 있다.

  1. orElse
String str1 = null; // String str1 = "";
str1 = "";

기존에는 String의 초기값을 null로 세팅하면 NPE를 피하기 위해 ""로 대체했다. 아예 초기값을 ""로 세팅하기도 했다.

Optional<String> optVal = Optional.of("abc");
String str1 = optVal.orElse(""); //abc

Optional<String> optVal = Optional.empty();
String str1 = optVal.orElse(""); // "" Exception 발생 안함

orElse를 사용해 대체값을 지정하면 Optional 객체가 null일 경우 대체값을 리턴한다. 그래서 Exception이 발생하지 않는다.

대체할 값을 람다식으로 지정하는 orElseGet()와 null일 때 지정된 예외를 발생시키는 orElseThrow()도 있다.

객체의 값 null 체크하기

  1. isPresent
if (str != null) {
    System.out.println(str);
}
if (Optional.ofNullable(str).isPresent()) {
    System.out.println(str);
}

isPresent()는 Optional 객체의 값이 있으면 true, null이면 false를 반환한다.

  1. ifPresent
Optional.ofNullable(str).ifPresent(System.out::println);

ifPresent()는 Optional를 반환하는 findAny()나 findFirst()와 같은 최종 연산과 잘 어울린다.

마치며

이제 Optinal이 NPE를 막을 수 있다는 건 대충 알겠다. 그럼 NPE를 막을 수 있는 상황에서 모두 Optional을 써야할까? Java9의 api note에서 Optional을 만든 의도가 적혀있다.

Optional is primarily intended for use as a method return type where there is a clear need to represent "no result," and where using null is likely to cause errors. A variable whose type is Optional should never itself be null; it should always point to an Optional instance.

  • Optional은 주로 메서드가 반환할 결과값이 '없음'을 명백하게 표현할 필요가 있어서 만들어졌다.
    • null을 반환하면 에러를 유발할 가능성이 높은 상황에서 메서드의 반환 타입으로 Optional을 사용해야 한다.(결과값이 없음을 표현하기 위해!)
    • 메서드 반환 타입이 아닌 필드로서 Optional의 사용은 지양한다.
  • Optional 변수는 그 자체가 null이 되어서는 안되고 Optional 객체를 가리켜야 한다.
    • 이건 Optional 초기화를 null이 아니라 빈 객체로 하라는 말인거 같다.

참고 사이트 & 책

댓글