55. 옵셔널 반환은 신중히 하라

값의 부재(Absence)를 어떻게 다룰 것인가?

자바 8 이전 : 값 부재 시 주로 예외 또는 null 반환 방식을 사용했습니다

예외(Exception) 던지기

// 예시: 사용자를 못 찾으면 예외 던지기
public User findUserOrThrow(String id) throws UserNotFoundException {
    User user = // ... ID로 사용자 찾는 로직 ...
    if (user == null) {
        throw new UserNotFoundException(id + " 사용자를 찾을 수 없음");
    }
    return user;
}
try {
    User user = findUserOrThrow("someId");
} catch (UserNotFoundException e) {
    System.err.println(e.getMessage());
}
  • 장점

    • 값 부재 상황을 호출자에게 강제로 알릴 수 있습니다 (특히 검사 예외)

  • 단점

    • 예외는 진짜 '예외적' 상황에만 사용해야 합니다 (아이템 69)

    • 성능 비용

      • 스택 추적(stack trace) 전체를 캡처

null 반환

// 예시: 사용자를 못 찾으면 null 반환
public User findUserOrNull(String id) {
    User user = // ... ID로 사용자 찾는 로직 ...
    return user; // 찾으면 User 객체, 못 찾으면 null 반환
}
User user = findUserOrNull("someId");
if (user != null) { // 매번 null 체크 필요!
    System.out.println("사용자 이름: " + user.getName());
} else {
    System.out.println("사용자를 찾을 수 없습니다.");
}
  • 장점

    • 예외 처리에 비해 추가적인 성능 비용이 거의 없습니다

  • 단점

    • NullPointerException(NPE) 위험

    • 코드 오염

      • 호출하는 코드마다 방어적인 null 체크 코드를 명시적으로 추가


자바 8의 대안: Optional

개념

  • Optional<T>불변(immutable) 컨테이너 객체

  • 이 컨테이너는 내부에 T 타입의 값을 최대 한 개 담거나 (null이 아닌 값), 혹은 아무것도 담지 않을 수 있습니다

    • 값이 있으면 '존재한다(present)' 또는 '비어있지 않다(not empty)'

    • 값이 없으면 '비어있다(empty)'

목적 및 장점

  • 명시적 의사 전달

    • 메서드 반환 타입이 Optional라는 것 자체로 "이 메서드는 값을 반환하지 않을 수도 있습니다"라는 사실을 API 사용자(호출자)에게 명확히 알립니다

  • NPE 방지

    • null을 직접 다루는 대신 Optional 객체를 통해 값의 존재 여부를 확인하고 안전하게 값을 추출하도록 유도함으로써 NPE 발생 가능성을 크게 줄입니다

  • 처리 강제

    • Optional API를 사용해야만 내부 값에 접근할 수 있으므로, 개발자가 값 부재 상황을 인지하고 명시적으로 처리하도록 자연스럽게 유도합니다

  • 유연성 및 가독성

    • 예외를 사용하는 것보다 유연하고(값 부재가 오류가 아님을 표현), null을 반환하는 것보다 API 사용 오류 가능성이 작으며 코드의 의도가 더 명확해집니다


Optional 사용법: 생성 및 반환

Optional 객체를 만들고 반환하는 것은 간단합니다. 주로 정적 팩토리 메서드를 사용합니다.

Optional 객체 생성 방법

  • Optional.empty()

    • 비어있는 Optional 객체를 생성

    • 값이 없는 경우 사용

  • Optional.of(value)

    • value를 담고 있는 Optional 객체를 생성

    • 주의: valuenull이면 NPE 발생!

    • null이 아님을 확신할 때 사용

  • Optional.ofNullable(value)

    • valuenull이 아니면 그 값을 담은 Optional을 생성

    • null이면 비어있는 Optional 객체를 생성

    • null일 가능성이 있는 값을 Optional로 변환할 때 안전하게 사용

예시: 컬렉션 최댓값 구하기

  • 예외 처리 방식

    public static <E extends Comparable<E>> E max(Collection<E> c) {
        if (c.isEmpty())
            throw new IllegalArgumentException("빈 컬렉션");
    
        E result = null;
        // ... 최댓값 계산 ...
    
        return result;
    }

  • Optional 반환 방식

    public static <E extends Comparable<E>> Optional<E> max(Collection<E> c) {
        if (c.isEmpty())
            return Optional.empty();
    
        E result = null;
        // ... 최댓값 계산 ...
    
        return Optional.of(result);
    }
    • Optional 반환 구현이 어렵지 않다.

  • Stream 버전 Optional 반환 방식

    // 스트림 API 활용 시 더 간결
    public static <E extends Comparable<E>> Optional<E> maxStream(Collection<E> c) {
        return c.stream().max(Comparator.naturalOrder());
    }
    • 트림의 max() 같은 종단 연산 중 상당수가 옵셔널 반환

  • 주의

    • Optional을 반환 타입으로 사용하는 메서드에서 null을 반환하는 것은 절대 금물

    • Optional 도입 취지를 완전히 무시하는 행위


Optional을 반환해야 하는 경우

핵심 원칙 : 메서드 실행 결과 값이 없을 가능성이 있고, 그 상태를 호출자가 명시적으로 인지하고 처리하도록 강제하고 싶을 때 Optional<T> 반환을 고려

  • 이는 검사 예외(Checked Exception)의 취지와 유사

  • 즉, 이 메서드는 값을 안 줄 수도 있으니 대비하라고 API 레벨에서 알려주는 것

  • '오류'가 아닌 '정상적일 수 있는 값의 부재'를 다룬다는 차이

반환된 Optional 처리 방법

기본값 지정 : orElse(defaultValue)

String lastWordInLexicon = max(words).orElse("단어 없음...");
  • Optional이 비어있으면 미리 제공된 defaultValue를 반환

예외 던지기 : orElseThrow(exceptionSupplier)

Toy myToy = max(toys).orElseThrow(TemperTrantrumException::new);
  • Optional이 비어있으면 지정된 예외를 발생

  • 예외가 발생할 때만 예외 생성 비용 추가

값 직접 얻기 (주의!) : get()

Element lastNobleGas = max(elements.NOBLE_GASES).get();
  • Optional에 값이 있으면 그 값을 반환하고, 잘못 판단한 것이라면 NoSuchElementException

  • 값이 있다고 확신하는 경우가 아니라면 사용을 지양하고, orElse 등 다른 안전한 메서드를 추천

기본값 지연 계산 : orElseGet(supplier)

  • Optional이 비어있을 때만 supplier를 호출하여 기본값을 생성

  • 기본값 생성 비용이 클 때 유용

값 필터링 : filter(predicate)

  • Optional에 값이 있고, 주어진 조건(predicate)을 만족하면 그 Optional을 그대로 반환

  • 만족하지 않는다면 빈 Optional을 반환

값 변환 : map(function)

Optional<String> parentPid =
    ph.parent().map(h -> String.valueOf(h.pid()));
  • Optional에 값이 있으면 주어진 함수(function)를 적용하고, 그 결과를 담은 새로운 Optional을 반환

  • 비어있으면 빈 Optional을 반환

중첩된 Optional 처리 : flatMap(function)

streamOfOptionals.flatMap(Optional::stream);
  • map과 유사하지만, 주어진 함수가 Optional을 반환할 때 사용

  • 결과가 중첩된 Optional<Optional<T>>가 되지 않도록 한 단계 펼쳐줍니다

값이 있을 때만 동작 수행 : ifPresent(consumer)

user.getEmail().ifPresent(email ->
    System.out.println("이메일: " + email));
  • Optional에 값이 있을 때만 주어진 동작(consumer)을 수행

존재 여부 확인 (주의!) : isPresent()

// 부모 프로세스의 프로세스 ID를 출력하거나, 부모가 없다면 "N/A"를 출력
Optinal<ProcessHandel> parentProcess = ph.parent();
System.out.println("부모 PID: " +
    (parentProcess.isPresent() ?
        String.valueOf(parentProcess.get().pid()) : "N/A"));
System.out.println("부모 PID: " +
    ph.parent()
    .map(h -> String.valueOf(h.pid()))
    .orElse("N/A"));
  • 값이 있으면 true, 없으면 false를 반환

  • if (opt.isPresent()) { opt.get()... } 패턴보다는 ifPresent, map, orElse 등을 사용하는 것이 더 간결하고 권장됩니다

Optional을 사용하면 안 되는 경우

성능이 극도로 중요한 경우

  • Optional 객체 생성 및 메서드 호출에는 약간의 오버헤드

  • 성능 병목 지점에서는 사용에 신중해야 하며, 필요시 성능 측정을 통해(아이템 67) null 반환이나 예외 방식이 더 나을지 판단

컨테이너 타입 래핑

  • 컬렉션, 스트림, 배열 등 다른 컨테이너 타입을 Optional로 감싸서 반환 금지

    • Optional<List<T>>, Optional<Map<K, V>>, Optional<Stream<T>>, Optional<T[]>

  • 대신 빈 컨테이너(empty list, empty map 등)를 반환

    • 아이템 54. null이 아닌, 빈 컬렉션이나 배열을 반환하라

  • 옵셔널 처리 코드를 생략하여 클라이언트 코드 간결함

박싱된 기본 타입 래핑

  • Optional<Integer>, Optional<Long>, Optional<Double>

  • 이중 포장(값 + Optional 객체)으로 비효율적

  • 전용 클래스인 OptionalInt, OptionalLong, OptionalDouble을 사용

  • Boolean, Byte, Character, Short, Float 용은 없으므로 이 경우는 Optional<T> 사용 가능

컬렉션의 요소/키/값, 배열 요소

  • 컬렉션이나 배열의 원소로 Optional을 사용하는 것은 거의 항상 좋지 않습니다

  • 예를 들어 Map<K, Optional<V>>는 키가 없는 경우와, 키는 있지만 값이 빈 Optional인 경우

  • 쓸데없이 높아지는 복잡성으로 혼란과 오류 가능성

옵셔널은 컬렉션의 키, 값, 원소나 배열의 원소로 사용하는 게 적절한 상황은 거의 없다

인스턴스 필드

  • 필드 타입으로 Optional을 사용하는 것은 일반적으로 권장되지 않습니다

  • 필드가 Optional이라는 것은 그 필드가 선택적(optional)임을 나타내는데, 이런 필드가 많다면 클래스 설계 자체를 재검토해야 할 가능성

    • 해당 필드를 갖는 하위 클래스를 만들거나, 다른 디자인 패턴 적용 고려

  • 예외적 허용 (예: 빌더 패턴)

    • 다수의 선택적 필드

    • 기본 타입 필드(int, byte, ..)의 부재를 명확히 나타내야 할 때는 유용

    • 선택적 필드의 게터 메서드들이 옵셔널을 반환


신중한 Optional 반환 실천 체크리스트

DO

  • [O] 메서드가 값을 반환하지 못할 '정상적인' 가능성이 있고, 호출자가 이 상황을 반드시 인지하고 대처해야 한다면 Optional<T> 반환을 고려

  • [O] 반환된 Optional을 처리할 때는 풍부한 API를 적극 활용하여 코드를 간결하고 안전하게 작성

  • [O] int, double 값을 Optional로 반환해야 한다면, Optional<Integer>, Optional<Double> 대신 성능이 좋은 OptionalInt, OptionalDouble 사용을 우선 고려

  • [O] null이 절대 아니라고 확신하는 값, Optional.of(value)를 사용

  • [O] null일 수도 있는 값, Optional.ofNullable(value)을 사용

DON'T


핵심 요약

Optional<T>는 자바 8 이후 값의 부재 가능성을 명확히 알리고 NPE를 방지하는 데 도움을 주는 강력하고 유용한 도구

주된 용도는 메서드의 반환 타입으로, 호출자에게 '값이 없을 수 있음'을 명시하고 관련 처리를 강제하고자 할 때 효과적

Optional API(orElse, map, ifPresent 등)를 잘 활용하면 코드를 더 선언적이고 안전하며 간결하게 작성 가능

성능, 컨테이너 래핑, 박싱된 기본 타입, 컬렉션/배열 요소, 필드 등에는 사용을 신중히 하거나 피해야 합니다

Optional은 만병통치약이 아니므로, 상황에 맞게 적절히 사용해야 합니다

Last updated