
빈 상자를 건네는 것과, 아무것도 주지 않는 것은 다릅니다.
5편에서 findFirst()의 결과가 그냥 값이 아니라 Optional로 돌아왔습니다. 조건에 맞는 값이 없을 수도 있기 때문이라고 했죠. 오늘은 그 상자의 정체를 봅니다.
null은 아무 말도 해 주지 않습니다
자바를 쓰다 보면 가장 자주 만나는 오류가 NullPointerException입니다. 값이 있어야 할 자리에 null이 들어 있는데 그걸 쓰려다 터지는 것이죠.
String name = findName(1004); // 없으면 null을 돌려준다고 치면
System.out.println(name.length()); // 여기서 NullPointerException
문제는 findName의 생김새만 봐서는 null이 올 수 있는지 알 수 없다는 것입니다. 반환형은 그냥 String이니까요. 없을 수 있다는 사실은 주석이나 문서에만 적혀 있고, 읽지 않으면 그대로 지나칩니다.
그래서 방어적으로 이렇게 씁니다.
if (name != null) {
System.out.println(name.length());
}
틀린 코드는 아닙니다. 다만 깜빡하면 아무도 안 알려 준다는 점이 문제입니다.
Optional은 상자입니다
자바 8이 내놓은 답이 Optional입니다. 값을 그대로 주는 대신 상자에 담아 줍니다. 상자 안에 값이 들어 있을 수도 있고, 비어 있을 수도 있습니다.
Optional<String> name = findName(1004);
이렇게 되면 받는 쪽이 모를 수가 없습니다. 반환형에 "없을 수도 있음"이 적혀 있으니까요. 그리고 값을 쓰려면 상자를 열어야 하는데, 그 과정에서 자연스럽게 "없으면 어떻게 할지"를 정하게 됩니다.
쉽게 — 택배로 비유하면 이렇습니다. null은 택배가 아예 안 오는 것이고,
Optional은 빈 상자라도 오는 것입니다. 빈 상자를 받으면 "아, 물건이 없구나" 하고 알 수 있죠. 안 오면 언제까지 기다려야 할지도 모릅니다.
상자 만들기
세 가지를 씁니다.
Optional<String> a = Optional.of("포켓코딩"); // 값이 확실히 있을 때
Optional<String> b = Optional.ofNullable(maybeNull); // null일 수도 있을 때
Optional<String> c = Optional.empty(); // 빈 상자
주의할 점 하나. Optional.of(null)은 그 자리에서 예외가 납니다. null이 올 수 있는 값이라면 반드시 ofNullable을 써야 합니다. 이름이 헷갈리기 쉬운데, of는 "확실히 있다", ofNullable은 "없을 수도 있다"로 기억하면 됩니다.
꺼내는 방법이 여럿입니다

그림 1. 꺼내기 전에 "없으면 어떻게 할지"를 먼저 정하게 됩니다.
orElse — 없으면 이 값으로
String result = name.orElse("이름 없음");
가장 많이 씁니다. 값이 있으면 그것을, 없으면 괄호 안의 것을 돌려줍니다.
orElseGet — 없을 때만 만들기
String result = name.orElseGet(() -> makeDefaultName());
orElse와 결과는 같지만 동작이 다릅니다. orElse는 괄호 안을 항상 먼저 계산합니다. 값이 있어서 쓰지도 않을 기본값을 만들어 버리는 것이죠. 기본값을 만드는 데 시간이 걸린다면(DB 조회 같은) orElseGet을 써야 합니다. 2편에서 배운 Supplier가 여기 들어갑니다.
orElseThrow — 없으면 예외
String result = name.orElseThrow(() -> new RuntimeException("회원 없음"));
값이 반드시 있어야 하는 상황에서 씁니다. 없다는 사실 자체가 오류인 경우죠.
ifPresent — 있을 때만 실행
name.ifPresent(n -> System.out.println("안녕하세요, " + n));
값을 꺼내 변수에 담지 않고 있을 때만 무언가를 하고 끝냅니다. 2편의 Consumer가 들어가는 자리입니다.
상자를 열지 않고 안을 바꾸기
Optional도 스트림처럼 map과 filter를 씁니다. 상자를 연 다음 처리하는 게 아니라, 상자째로 이어 붙입니다.
String len = findName(1004)
.filter(n -> n.length() >= 2) // 두 글자 이상만
.map(n -> n + "님") // 뒤에 붙이기
.orElse("해당 없음");
중간에 값이 없어지면 뒤의 연산은 그냥 건너뜁니다. null 검사를 단계마다 넣을 필요가 없어지는 것이 이 방식의 이점입니다.
단골 함정
첫째, get()을 바로 부르는 것. Optional에는 get()이 있지만, 값이 없으면 예외가 납니다. 결국 NullPointerException을 다른 예외로 바꾼 것뿐이죠. get()은 쓰지 않는다고 생각하는 편이 안전합니다.
둘째, isPresent()로 검사하고 get()으로 꺼내는 것.
if (name.isPresent()) { // 이러면
System.out.println(name.get());
}
동작은 합니다. 그런데 이건 if (name != null)과 모양이 똑같아서, Optional을 쓴 의미가 거의 없습니다. ifPresent나 orElse로 바꾸면 한 줄로 끝납니다.
셋째, 필드나 매개변수에 쓰는 것. Optional은 메서드 반환값을 위해 만들어진 도구입니다. 클래스 필드로 두거나 매개변수로 받으면 오히려 코드가 번잡해집니다. "돌려줄 때 없을 수 있음을 알리는 용도"로 한정해 쓰는 것이 일반적인 권장입니다.
넷째, orElse 안에 무거운 계산을 넣는 것. 앞에서 본 대로 값이 있어도 실행됩니다. 무거우면 orElseGet으로 바꾸세요.
직접 실행해 보기
orElse와 orElseGet의 차이는 눈으로 봐야 확실합니다. 출력 순서를 눈여겨보세요.
import java.util.*;
public class Main {
// 회원 번호로 이름 찾기 — 1004번만 있다고 가정
static Optional<String> findName(int id) {
if (id == 1004) return Optional.of("홍길동");
return Optional.empty();
}
static String makeDefault() {
System.out.println(" (기본값 만드는 중... 오래 걸린다고 치죠)");
return "이름 없음";
}
public static void main(String[] args) {
// 1) 있을 때와 없을 때
System.out.println("1004번: " + findName(1004).orElse("없음"));
System.out.println("9999번: " + findName(9999).orElse("없음"));
System.out.println();
// 2) orElse는 값이 있어도 기본값을 만든다
System.out.println("--- orElse (값 있음) ---");
String a = findName(1004).orElse(makeDefault());
System.out.println("결과: " + a);
System.out.println();
System.out.println("--- orElseGet (값 있음) ---");
String b = findName(1004).orElseGet(() -> makeDefault());
System.out.println("결과: " + b);
System.out.println("→ orElseGet은 기본값을 만들지 않았습니다");
// 3) ifPresent — 있을 때만 실행
System.out.println();
findName(1004).ifPresent(n -> System.out.println("인사: " + n + "님 반갑습니다"));
findName(9999).ifPresent(n -> System.out.println("이 줄은 안 나옵니다"));
// 4) filter와 map으로 이어 쓰기
System.out.println();
String r1 = findName(1004).filter(n -> n.length() >= 2).map(n -> n + "님").orElse("해당 없음");
String r2 = findName(1004).filter(n -> n.length() >= 5).map(n -> n + "님").orElse("해당 없음");
System.out.println("두 글자 이상: " + r1);
System.out.println("다섯 글자 이상: " + r2);
// 5) 없는데 꺼내려 하면
System.out.println();
try {
findName(9999).orElseThrow(() -> new RuntimeException("회원을 찾을 수 없습니다"));
} catch (RuntimeException e) {
System.out.println("orElseThrow → " + e.getMessage());
}
}
}📱 코드 실행(실습)은 태블릿·PC에서 이용할 수 있어요. 모바일에서는 위 코드를 눈으로 따라가며 확인하세요.
makeDefault()가 언제 실행되는지가 핵심입니다. orElse 쪽에서는 값이 있는데도 실행되고, orElseGet 쪽에서는 실행되지 않습니다.
한 장 정리
| 항목 | 내용 |
|---|---|
| 왜 쓰나 | "없을 수 있음"을 타입으로 알리기 위해 |
| 만들기 | of(확실히 있음) · ofNullable(없을 수도) · empty |
| 기본값 | orElse(항상 계산) · orElseGet(없을 때만) |
| 없으면 오류 | orElseThrow |
| 있을 때만 | ifPresent |
| 이어 쓰기 | filter · map — 없으면 건너뜀 |
| 피할 것 | get(), isPresent()+get(), 필드·매개변수 사용 |
이것만 기억하면 됩니다. Optional은 null을 없애는 도구가 아니라 없을 수 있다는 사실을 드러내는 도구입니다. 그래서 값을 꺼낼 때마다 "없으면 무엇으로 대신할지"를 정하게 만들고, 그 과정에서 NullPointerException이 줄어듭니다. get()만 쓰지 않으면 절반은 성공입니다.
미니 퀴즈
개념을 제대로 잡았는지 직접 풀어 봅니다. 정답은 눌러야 나옵니다.
미니 퀴즈
포켓코딩(JAVA4) 6편 · 예제 출처: 강사 개인 GitHub 저장소. 다음 편: 메서드 참조 — 람다를 더 짧게 쓰기.