학습 자료

Optional — null 을 상자에 담는다


Article

책을 제목으로 찾는 메서드를 만든다. 없으면 null 을 돌려준다.

static Book findByTitle(String title) {
    for (Book b : books) {
        if (b.getTitle().equals(title)) return b;
    }
    return null;
}

부르는 쪽은 이렇게 쓴다.

Book b = findByTitle("코틀린");
System.out.println(b.getPrice());
터미널
  • Exception in thread "main" java.lang.NullPointerException

4강에서 처음 만나고, 9강 배열에서 만나고, 11강 객체에서 만나고, 16강 map.get() 에서 또 만난 그 예외다.

문제는 null 을 돌려준 게 아니다. 메서드의 생김새 어디에도 "없을 수 있다"고 적혀 있지 않다는 것이다.

static Book findByTitle(String title)

Book 을 준다고 되어 있다. 부르는 쪽은 Book 을 받았다고 믿는다. 검사를 잊는다. 컴파일러도 아무 말이 없다.

상자에 담아서 돌려준다

Optional<Book> 을 돌려준다.

static Optional<Book> findByTitle(String title) {
    return books.stream()
        .filter(b -> b.getTitle().equals(title))
        .findFirst();
}

19강의 findFirst() 가 이미 Optional 을 준다.

System.out.println(findByTitle("자바"));
System.out.println(findByTitle("코틀린"));
콘솔
  • Optional[자바]
  • Optional.empty

값이 있으면 Optional[자바], 없으면 Optional.empty 다. 둘 다 상자다. 상자 자체는 절대 null 이 아니다.

이제 메서드의 생김새가 사실을 말한다. Optional<Book> 을 받은 사람은 상자를 열어야 한다는 것을 안다. 그냥 b.getPrice() 를 부를 수가 없다. 상자에는 그런 메서드가 없기 때문이다.

null 을 돌려줄 때 vs Optional 을 돌려줄 때

찾는 책이 없는 경우

Book b
null
→ 아무 곳도 안 가리킨다. 건드리면 NPE
Optional<Book> o
Optional.empty
→ 비어 있는 상자. 상자는 실재한다
타입이 달라졌다. 컴파일러가 "없을 수도 있음"을 알게 됐다.

상자를 여는 법

가장 많이 쓰는 것부터 본다.

String title = findByTitle("코틀린")
    .map(Book::getTitle)
    .orElse("없음");

System.out.println(title);
콘솔
  • 없음

map 은 19강의 스트림에서 본 그것과 같은 뜻이다. 상자 안에 값이 있으면 바꾸고, 비어 있으면 아무것도 안 한다.

orElse 는 "비어 있으면 이걸 대신 써라"다.

값이 있을 때는 이렇게 흐른다.

int price = findByTitle("자바")
    .map(Book::getPrice)
    .orElse(0);
콘솔
  • 20000

if (b != null) 이 한 번도 안 나왔다. 검사를 잊을 수가 없다. 잊으면 컴파일이 안 된다.

상자를 여는 방법들
메서드하는 일
orElse(기본값)비었으면 기본값. 기본값은 항상 계산된다
orElseGet(() -> ...)비었을 때만 람다를 실행해 기본값을 만든다
orElseThrow(() -> ...)비었으면 예외를 던진다
ifPresent(v -> ...)있으면 그 값으로 무언가 한다. 없으면 아무 일도 없다
map(...) · filter(...)상자를 유지한 채 안의 값을 바꾸거나 거른다
isPresent() · isEmpty()있는지 · 비었는지 묻는다
get()쓰지 않는다 — 아래 참고

orElseorElseGet 의 차이는 실무에서 종종 문제가 된다. orElse(계산해서만든기본값()) 이라고 쓰면, 값이 있어도 그 계산이 실행된다. 인자니까 먼저 평가된다. 계산이 무거우면 orElseGet 을 쓴다.

orElseThrow — 18강과 만난다

찾지 못한 것이 정말 잘못된 상황이라면, 기본값 대신 예외를 던진다.

Book b = findByTitle("코틀린")
    .orElseThrow(() -> new BookNotFoundException("코틀린"));
터미널
  • Exception in thread "main" BookNotFoundException: 코틀린 을(를) 찾을 수 없다

18강에서 만든 그 예외다. 이제 두 강의가 이어진다.

Optional 은 "없어도 괜찮다"는 뜻이고, orElseThrow 는 "여기서는 없으면 안 된다"고 선언하는 자리다. 같은 메서드를 쓰는 두 곳이 각자 판단할 수 있다. 목록 화면에서는 그냥 넘어가고, 상세 화면에서는 404 를 내는 식이다.

null 을 돌려주던 시절에는 이 판단을 findByTitle 안에서 미리 해야 했다.

get() 을 쓰지 않는 이유

Book b = findByTitle("코틀린").get();
터미널
  • Exception in thread "main" java.util.NoSuchElementException: No value present

터진다. 그리고 null 을 돌려주던 시절과 똑같아졌다.

상자를 만드는 법

직접 만들 일도 있다.

Optional.of("자바")          // 값이 확실히 있을 때. null 을 넣으면 NPE
Optional.ofNullable(value)   // null 일 수도 있을 때
Optional.empty()             // 빈 상자

Optional.of(null) 은 그 자리에서 NullPointerException 이다. 일부러 그렇게 만들었다. "확실히 있다"고 해놓고 null 을 넣었으니 코드가 틀린 것이다.

16강의 map.get() 을 감쌀 때 ofNullable 이 요긴하다.

Optional.ofNullable(prices.get("코틀린")).orElse(0);

어디에 쓰고 어디에 안 쓰나

Optional 은 만병통치약이 아니다. 자리를 가린다.

Optional 의 자리

쓴다

메서드가 돌려주는 값

  • 찾지 못할 수 있는 조회의 반환 타입
  • findByTitle · findFirst
  • 부르는 쪽이 검사를 잊을 수 없다
  • 생김새만 봐도 없을 수 있다는 걸 안다

안 쓴다

그 밖의 자리

  • 필드 — 객체가 쓸데없이 무거워진다
  • 매개변수 — 부르는 쪽이 감싸느라 번거롭다
  • 컬렉션 — 비었으면 Optional 말고 빈 목록을 준다
  • Optional<List<Book>> 는 거의 언제나 잘못이다

목록을 돌려주는 메서드는 결과가 없어도 null 이나 Optional 이 아니라 List 를 준다. 부르는 쪽은 for 를 돌리면 그만이다. 아무 일도 일어나지 않는다.

Optional 자체를 null 로 두는 것은 최악이다. 상자를 만든 이유가 사라진다.

null 갈래를 닫는다

이 시리즈에서 null 을 네 번 만났다.

4강에서 시작한 이야기
  1. 4강참조형의 기본값은 null
  2. 16강map.get() 은 없으면 null
  3. 18강NullPointerException 을 붙잡는다
  4. 20강애초에 없을 수 있음을 타입에 적는다

null 은 자바를 만든 사람들도 실수였다고 말하는 물건이다. 하지만 언어에서 지울 수는 없었다. 지우면 세상의 모든 자바 코드가 깨진다.

그래서 지우는 대신, 지우지 않고도 안전해지는 길을 열었다. Optional 은 그 길이다.

정리하면

Optional 은 "없을 수도 있다"는 사실을 타입에 적는 방법이다.

다시 짚어보기
  1. 01

    찾지 못할 수 있으면 Optional 을 돌려준다

    메서드의 생김새가 사실을 말하게 된다. 부르는 쪽은 검사를 잊을 수 없다.

    static Optional<Book> findByTitle(String title)
  2. 02

    상자를 열지 말고 상자째 다룬다

    map 으로 바꾸고, orElse 로 기본값을 정한다.

    findByTitle("코틀린")
        .map(Book::getTitle)
        .orElse("없음");
  3. 03

    없으면 안 되는 자리에서는 orElseThrow

    18강의 예외와 이어진다. 같은 메서드를 쓰는 곳마다 다르게 판단할 수 있다.

  4. 04

    get() 은 쓰지 않는다

    isPresent + get 조합도 마찬가지다. null 검사를 길게 쓴 것뿐이다.

  5. 05

    반환 타입에만 쓴다

    필드·매개변수·컬렉션에는 쓰지 않는다. 빈 목록은 빈 List 로 돌려준다.

Part 6의 마지막 강의가 하나 남았다.

14강에서 @Override 를 붙였을 때, 그것이 "실행되지 않는 표시"인데 컴파일러가 읽는다고 했다. 그리고 21강에서 다시 보자고 했다.

이제 그 약속을 지킬 차례다. 그 이야기는 1강의 첫 질문으로 돌아간다. 바이트코드는 JVM 이 읽는다. 그럼 @ 로 시작하는 저것은 누가 읽는가.

Optional — null 을 상자에 담는다 — 디코드랩(DCODELAB)