콘텐츠로 이동

Optional

Optional

Optional이란?

Optional\<T>: 값이 있거나 없을 수 있는 컨테이너 객체. Java 8에서 도입됐으며, 메서드 반환 타입으로 사용해 "값이 없을 수 있음"을 타입 수준에서 표현한다.

Stream 최종 연산 중 findFirst(), findAny(), min(), max(), reduce() 등이 Optional을 반환한다.


왜 쓰는가?

null 직접 반환 vs Optional 반환

구분 null 직접 반환 Optional 반환
NPE 가능성 호출자가 null 체크를 잊으면 즉시 NPE 컴파일러가 명시적 처리를 유도
의도 표현 반환값이 없을 수 있음을 타입으로 표현 못함 Optional<T> 타입 자체가 "없을 수 있음"을 선언
체이닝 단계마다 null 체크 필요 map, flatMap, filter로 안전하게 체이닝
// Before: 다단계 null 체크
String city = null;
if (user != null && user.getAddress() != null) {
    city = user.getAddress().getCity();
}

// After: Optional 체이닝
String city = Optional.ofNullable(user)
    .map(User::getAddress)
    .map(Address::getCity)
    .orElse("미입력");

언제 쓰는가

적합 부적합
메서드 반환 타입 (DB 조회, 컬렉션 검색) 필드 타입 (직렬화 불가)
값이 없을 수 있음을 명시적으로 표현할 때 메서드 파라미터
Stream 최종 연산 결과 처리 컬렉션 원소 타입

Optional 생성

메서드 설명 null 허용
Optional.of(value) 값이 반드시 존재할 때 X — null이면 NPE
Optional.ofNullable(value) 값이 있을 수도 없을 수도 있을 때 O — null이면 empty 반환
Optional.empty() "결과 없음"을 명시적으로 반환할 때
Optional<String> a = Optional.of("hello");           // 값이 확실히 있음
Optional<String> b = Optional.ofNullable(findUser()); // DB 조회 결과
Optional<String> c = Optional.empty();               // 빈 결과 명시 반환

값 존재 여부 확인

메서드 설명 Java 버전
isPresent() 값이 있으면 true Java 8
isEmpty() 값이 없으면 true Java 11
Optional<String> opt = Optional.of("hello");

if (opt.isPresent()) { ... }  // 값이 있을 때
if (opt.isEmpty()) { ... }    // 값이 없을 때

주의: isPresent() + get() 조합은 null 체크와 다를 바 없어 Optional의 장점을 살리지 못한다. map, orElse, ifPresent 계열을 사용하자.


값 추출

메서드 값 없을 때 언제 사용
get() NoSuchElementException 값이 반드시 있음을 확신할 때만 (지양)
orElse(T) 인자로 준 기본값 반환 기본값이 상수·단순 값일 때
orElseGet(Supplier<T>) Supplier 실행 결과 반환 기본값 생성 비용이 클 때
orElseThrow() NoSuchElementException 없으면 반드시 에러 (Java 10+)
orElseThrow(Supplier) Supplier가 던지는 예외 커스텀 예외 지정
opt.orElse("기본값");
opt.orElseGet(() -> expensiveDefault());
opt.orElseThrow(() -> new EntityNotFoundException("사용자 없음"));

orElse vs orElseGet

구분 orElse(T) orElseGet(Supplier<T>)
인자 평가 시점 항상 즉시 평가 (값이 있어도 실행) 값이 없을 때만 실행 (Lazy)
적합한 경우 "N/A", 0 등 상수 new Object(), DB 쿼리, 파일 I/O
Optional<String> opt = Optional.of("있음");

opt.orElse(expensiveMethod());          // expensiveMethod()가 호출됨!
opt.orElseGet(() -> expensiveMethod()); // expensiveMethod() 호출 안 됨

값 변환

메서드 인자 설명 반환
map(Function<T,U>) T → U 값 변환 (값 없으면 empty 유지) Optional<U>
flatMap(Function<T, Optional<U>>) T → Optional<U> 중첩 Optional 방지 Optional<U>
filter(Predicate<T>) T → boolean 조건 불충족이면 empty Optional<T>

map — null 체크 없이 안전한 체이닝

String city = Optional.ofNullable(user)
    .map(User::getAddress)
    .map(Address::getCity)
    .orElse("미입력");

flatMap — 반환 타입이 이미 Optional인 메서드와 연결

map을 쓰면 Optional<Optional<String>>이 중첩되므로 flatMap을 사용한다.

// getEmail()이 Optional<String>을 반환하는 경우
Optional<String> email = Optional.ofNullable(user)
    .flatMap(User::getEmail);  // map이면 Optional<Optional<String>>이 됨

filter — 조건 불일치 시 empty

Optional.of(age)
    .filter(a -> a >= 19)
    .orElseThrow(() -> new IllegalArgumentException("미성년자"));

소비 (값 사용)

메서드 설명 Java 버전
ifPresent(Consumer<T>) 값이 있으면 Consumer 실행 Java 8
ifPresentOrElse(Consumer, Runnable) 있으면 Consumer, 없으면 Runnable 실행 Java 9
// 값이 있을 때만 처리
opt.ifPresent(user -> log.info("로그인: {}", user.getName()));

// 있을 때 / 없을 때 분기
opt.ifPresentOrElse(
    user -> log.info("로그인: {}", user.getName()),
    () -> log.warn("사용자 없음")
);

주의: ifPresent 내부에서 외부 변수에 값을 대입하려는 패턴(ifPresent(v -> result = v))은 effectively final 제약으로 컴파일 오류다. 이 경우 orElse 계열을 사용한다.


Stream과의 연계

상황 코드 패턴
Stream 최종 연산 결과 처리 findFirst(), findAny(), min(), max() → Optional
Optional<T>Stream<T> optional.stream() (Java 9+) — 0개 또는 1개 요소 스트림
List<Optional<T>>에서 값만 추출 flatMap(Optional::stream)
// findFirst 결과를 Optional로 처리
String name = users.stream()
    .filter(u -> u.getId() == targetId)
    .findFirst()
    .map(User::getName)
    .orElse("미등록 사용자");

// Optional::stream — 값 있는 것만 추출
List<Optional<String>> optionals = List.of(Optional.of("A"), Optional.empty(), Optional.of("B"));
List<String> present = optionals.stream()
    .flatMap(Optional::stream)  // empty는 제거, 값 있는 것만
    .toList();  // ["A", "B"]

단점 / 주의할 점

안티패턴 문제 대안
Optional을 필드 타입으로 사용 Serializable 미구현 — 직렬화 불가 반환 타입으로만 사용
Optional을 메서드 파라미터로 사용 호출부 불편, 코드 복잡도 증가 오버로딩 또는 null 허용 파라미터
isPresent() + get() 조합 null 체크와 동일, Optional 도입 효과 없음 map, orElse, ifPresent 사용
orElse(expensiveOp()) 값이 있어도 항상 실행됨 orElseGet(() -> expensiveOp())
get() 남용 값 없으면 NoSuchElementException orElseThrow() 또는 isPresent() 선검사
List<Optional<T>> 불필요하게 복잡 빈 컬렉션 반환으로 대체
Optional.of(null) 즉시 NPE null 가능성이 있으면 ofNullable 사용
Optional을 반환하는 메서드가 null 반환 Optional의 의미가 사라짐 반드시 Optional.empty() 반환