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() 를 부를 수가 없다. 상자에는 그런 메서드가 없기 때문이다.
찾는 책이 없는 경우
- 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() | 쓰지 않는다 — 아래 참고 |
orElse 와 orElseGet 의 차이는 실무에서 종종 문제가 된다. 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 은 만병통치약이 아니다. 자리를 가린다.
쓴다
메서드가 돌려주는 값
- 찾지 못할 수 있는 조회의 반환 타입
findByTitle·findFirst- 부르는 쪽이 검사를 잊을 수 없다
- 생김새만 봐도 없을 수 있다는 걸 안다
안 쓴다
그 밖의 자리
- 필드 — 객체가 쓸데없이 무거워진다
- 매개변수 — 부르는 쪽이 감싸느라 번거롭다
- 컬렉션 — 비었으면
Optional말고 빈 목록을 준다 Optional<List<Book>>는 거의 언제나 잘못이다
목록을 돌려주는 메서드는 결과가 없어도 null 이나 Optional 이 아니라 빈 List 를 준다. 부르는 쪽은 for 를 돌리면 그만이다. 아무 일도 일어나지 않는다.
Optional 자체를 null 로 두는 것은 최악이다. 상자를 만든 이유가 사라진다.
null 갈래를 닫는다
이 시리즈에서 null 을 네 번 만났다.
- 4강참조형의 기본값은 null
- 16강map.get() 은 없으면 null
- 18강NullPointerException 을 붙잡는다
- 20강애초에 없을 수 있음을 타입에 적는다
null 은 자바를 만든 사람들도 실수였다고 말하는 물건이다. 하지만 언어에서 지울 수는 없었다. 지우면 세상의 모든 자바 코드가 깨진다.
그래서 지우는 대신, 지우지 않고도 안전해지는 길을 열었다. Optional 은 그 길이다.
정리하면
Optional 은 "없을 수도 있다"는 사실을 타입에 적는 방법이다.
- 01
찾지 못할 수 있으면
Optional을 돌려준다메서드의 생김새가 사실을 말하게 된다. 부르는 쪽은 검사를 잊을 수 없다.
static Optional<Book> findByTitle(String title) - 02
상자를 열지 말고 상자째 다룬다
map 으로 바꾸고, orElse 로 기본값을 정한다.
findByTitle("코틀린") .map(Book::getTitle) .orElse("없음"); - 03
없으면 안 되는 자리에서는
orElseThrow18강의 예외와 이어진다. 같은 메서드를 쓰는 곳마다 다르게 판단할 수 있다.
- 04
get()은 쓰지 않는다isPresent + get 조합도 마찬가지다. null 검사를 길게 쓴 것뿐이다.
- 05
반환 타입에만 쓴다
필드·매개변수·컬렉션에는 쓰지 않는다. 빈 목록은 빈 List 로 돌려준다.
Part 6의 마지막 강의가 하나 남았다.
14강에서 @Override 를 붙였을 때, 그것이 "실행되지 않는 표시"인데 컴파일러가 읽는다고 했다. 그리고 21강에서 다시 보자고 했다.
이제 그 약속을 지킬 차례다. 그 이야기는 1강의 첫 질문으로 돌아간다. 바이트코드는 JVM 이 읽는다. 그럼 @ 로 시작하는 저것은 누가 읽는가.