[EffectiveJava] Item 55. 옵셔널 반환은 신중히 하라

2025. 7. 1. 20:58·Backend/Java
💡요약
1. 반환 값이 없을 수 도 있고 클라이언트가 이를 특별히 처리해야 하는 경우
Optional 을 활용해보자.
  - 성능 저하가 뒤따르니 성능에 민감한 메서드라면 null 반환이나 예외 처리를 고려
2. 반환 값 이외의 용도는 쓰는 경우는 매우 드물고 굳이 쓸 이유가 없다.

 

1. 개요

자바 8 이전에는 메서드가 특정 조건에서 값을 반환할 수 없을 때
취할 수 있는 선택지가 두 가지 있었다. 바로 예외를 던지거나 null 값을 반환하는 것이다

하지만 두 가지 방법에는 허점이 존재했는데

1️⃣예외를 던진다

  • 정말 예외적인 상황에 쓰여야 할 예외 처리가 null 처리에 쓰이게 된다
  • 예외를 생성할 때 스택 추적 전체를 캡처하므로 이에 대한 비용 문제가 발생한다.
  • Stack Trace란?
    더보기
    Stack Trace
    1. 자바 프로그램이 실행될 때 메서드가 호출될 때마다 "스택 프레임"이 생성되어 스택(Stack) 구조로 쌓인다.
    2. 예외가 발생하면 JVM은 스택을 따라 올라가며 예외 정보를 수집한다.예외가 발생한 시점에서 메서드 호출 경로를 역순으로 기록하는 것이 스택 트레이스(Stack Trace)예외 객체가 생성될 때 현재 스택을 모두 스캔해야 하므로 CPU 연산 비용이 크다.예외를 생성할 때 스택 추적 전체를 캡처하므로 이에 대한 비용 문제가 발생한다.

2️⃣null 값을 반환

  • 1의 문제점은 발생하지 않지만 별도의 null 처리 코드를 추가해야 한다.
  • null 처리를 무시하고 반환 된 null 값은 언젠가, 그리고 어디선가 NullPointException 이 발생하여 시스템 버그를 초래할 수 있다.
이를 해결하기 위해 자바 8 이후 또 하나의 선택지가 생겼다.
그게 바로 Optional 이다.

2. Optional 이란?

  • 1개의 null이 아닌 T 타입 객체를 담거나, 아무것도 담지 않는다.
  • Optional<T> 는 원소를 최대 1개만 가질 수 있는 '불변’ 컬렉션 이다.
    • 예외를 던지는 메서드보다 유연하고 사용하기 쉽다.
    • null 을 반환하는 메서드보다 오류 가능성이 적다.
💡특정 조건에서는 아무것도 반환하지 않아야 할 때

T 대신 Optional를 반환하도록 선언하면 된다


3. max 값을 구하는 예제

Optional이 적용되지 않은 함수

public static <E extends Comparable<E>> E max(Collection<E> c){ // 재귀적 타입 한정
    if(c.isEmpty())                  
        throw new IllegalArgumentException("빈 컬렉션");
        // 클래스 내부에서 예외를 던지고 있다.

    E result = null;
    for(E e : c)
        if (result == null || e.compareTo(result) > 0)
            result = Objects.requireNonNull(e);
    
    return result;
}

예외 처리가 메서드 내에 강하게 결합되어 있기 때문에 여러가지 한계를 맞이하게 된다

  1. 호출하는 클래스마다 예외 메시지를 다르게 하고 싶을 수도 있다
    • 클라이언트는 이 런타임 예외가 발생한다는 것을 알고 있어야 한다
    • 이 예외에 대해 외부에서 catch 문을 작성해서 잡아야 한다.
      즉, 원치 않는 결합도가 생겨버린다
  2. 예외를 발생시키고 싶지 않을 수 있다
    • 마찬가지로 catch 문에서 예외를 잡고 아무것도 수행하지 않도록 설정해야 한다. 이상하다.

이를 Optional 로 구현해보면 어떨까?

public static <E extends Comparable<E>> Optional<E> max2(Collection<E> c){
    if(c.isEmpty())
        return Optional.empty();
        // 빈 옵셔널을 반환함으로써 클래스 외부에서 예외를 발생시킬 수도, 시키지 않을 수도 있다.

    E result = null;
    for(E e: c)
        if(result == null || e.compareTo(result) > 0)
            result = Objects.requireNonNull(e);

    return Optional.of(result); // result 를 참조하는 Optional 타입을 반환한다.
    // Optional.of에 null이 들어가게 되면 NPE가 발생하므로 주의해야한다. 
}
  • 위 방식과는 다르게 호출한 클라이언트에서 Optional 값에 대한 처리가 가능하다.

1) 항상 값이 채워져 있다고 가정

Integer maxValue = max2(list).get(); // 항상 값이 있다고 가정하고 get!

2) 기본 값 설정

Integer maxValue2 = max2(list).orElse(0);  // 빈 옵셔널일 경우 0으로 설정

3) 원하는 예외 던지기

//클라이언트에 따른 예외 메시지 변경
Integer maxValue3 = max2(list)
                    .orElseThrow(() -> new IllegalArgumentException("요청 리스트가 비어있습니다."));

원하는 예외 메시지를 발생시킬수도, 발생시키지 않을수도 있고,
더 나아가 기본 값을 넣을 수도 있다.
앞선 코드보다 훨씬 유연하고 깔끔한 것을 볼 수 있다.

예외, null 반환 대신 Optional 반환을 선택하는 기준

  • 검사 예외와 취지가 비슷하다. 즉 반환 값이 없을 수도 있음을 API 사용자에게 명확히 알려준다.
  • 예외를 던지거나 null을 반환한다면 API 사용자가 그 사실을 인지하지 못해 끔찍한 결과로 이어질 수 있다.
  • 하지만 Optional을 반환하면 (마치 검사 예외를 던지면) 클라이언트에서는 반드시 이에 대처하는 코드를 작성해 넣어야 한다

⭐ 호출할 때마다 반환 값이 없을 가능성을 염두에 둬야 하는 메서드라면 Optional 반환을 선택해라

🚨

옵셔널을 반환하는 메서드는 절대 null을 반환하면 안된다.
옵셔널을 도입한 취지를 저버리는 행위이기 때문이다.


4. 더 다양한 Optional 메서드

filter, map, ifPresent 등 다양한 메서드들을 지원하고 있다.

✅ Optional<T>에서 자주 사용되는 메서드 목록

메서드 이름 설명
isPresent 값이 존재하는지 확인
isEmpty 값이 비어있는지 확인 (Java 11+)
get 값이 존재하면 반환, 없으면 NoSuchElementException 발생
orElse 값이 존재하면 반환, 없으면 기본값 반환
orElseGet 값이 존재하면 반환, 없으면 함수 실행 결과 반환
orElseThrow 값이 존재하면 반환, 없으면 예외 발생
ifPresent 값이 존재하면 주어진 Consumer 실행
ifPresentOrElse 값이 존재하면 Consumer 실행, 없으면 Runnable 실행 (Java 9+)
map 값이 존재하면 함수 적용 후 Optional로 반환
flatMap 값이 존재하면 함수 적용 후 반환된 Optional을 그대로 반환
filter 조건을 만족하는 값만 유지, 만족하지 않으면 Optional.empty() 반환

 

예제

Optional.of("Hello").isPresent(); // true

Optional.empty().isEmpty(); // true

Optional.of("Hello").get(); // "Hello"

Optional.empty().orElse("Default"); // "Default"

Optional.empty().orElseGet(() -> "Generated"); // "Generated"

Optional.empty().orElseThrow(() -> new RuntimeException("예외 발생")); // RuntimeException 발생

Optional.of("Hello").ifPresent(System.out::println); // 출력: Hello

Optional.empty().ifPresentOrElse(
                    System.out::println,
                    () -> System.out.println("값 없음")
); // 출력: 값 없음

Optional.of("Hello").map(String::length); // Optional[5]

Optional.of("Hello").flatMap(s -> Optional.of(s.length())); // Optional[5]

Optional.of("Hello").filter(s -> s.length() > 5); // Optional.empty()

isPresent

  • Optional 값이 있다면 true, 없다면 false 반환
    • 사실 이 메서드를 통해 작성할 수 있는 로직은 orElse()로도 보통 가능하다.

orElse() 로 대체가 가능하기 때문에 단독으로는 사용하지 않는 것이 권장된다.


orElseGet

  • 초기 생성 비용이 아주 클 때 유용
public class Main {
    public static void main(String[] args) {
        Optional<String> optionalValue = Optional.of("Hello");

                // orElse: 항상 기본값 생성 (불필요한 연산 발생 가능)
                String result1 = optionalValue.orElse(getExpensiveDefault());
                // orElseGet: 값이 없을 때만 기본값 생성 (불필요한 연산 방지)
                String result2 = optionalValue.orElseGet(() -> getExpensiveDefault());
    }

    public static String getExpensiveDefault() {
        System.out.println("기본값을 생성 중...");
        return "기본값";
    }
}

// 출력 : 기본값을 생성 중...  (orElse만 실행됨)
  • 초기 생성 비용이 아주 큰데 값의 여부에 따라 생성할 수도 있고 생성하지 않을 수도 있는 경우 orElseGet() 을 사용한다.
    • 값이 처음 필요할 때 Supplier 를 이용해 생성하므로 초기 생성 비용을 낮춘다.

Supplier는 인자를 받지 않고 T 타입의 값을 반환하는 함수형 인터페이스
값을 필요할 때만 생성하는 역할 Supplier<String> supplier = () -> "Hello";


Optional을 남용하면 안 되는 경우

1️⃣ 컬렉션(List, Set, Map), 배열, 스트림 같은 컨테이너 타입을 Optional로 감싸면 안 됨

→ Optional<List<T>> 대신 그냥 List<T> 반환이 더 적절함

🔹 왜 그럴까?

  • 빈 컨테이너를 반환하면 Optional로 감싸지 않아도 자연스럽게 처리 가능
  • → 클라이언트가 Optional을 추가로 다룰 필요 없음
  • Optional도 결국 객체이므로 불필요한 메모리 할당 및 초기화 비용이 발생
  • → Optional.get()을 호출해야 값에 접근 가능하므로 여러 단계를 거쳐야 함 - 성능 문제 발생

2️⃣ 기본 타입 (int, long, double …)

  • 기본 타입 (int, long, double …)에 Optional을 사용할 경우
    int → Integer → Optional 값을 두 겹이나 감싸기 때문에 기본 타입 보다 무거울 수 밖에 없다.
  • 필요하다면 전용 Optional 클래스인 OptionalInt, OptionalLong, OptionalDouble을 사용

'Backend > Java' 카테고리의 다른 글

[Java] 정렬 알고리즘 정리  (1) 2025.07.02
[EffectiveJava] Item 14. Comparable을 구현할지 고려하라  (1) 2025.07.01
[EffectiveJava] item 61. 박싱된 기본 타입보다는 기본 타입을 사용하라  (0) 2025.07.01
[EffectiveJava] item 64. 객체는 클래스가 아닌 인터페이스로 참조하라  (0) 2025.07.01
[EffectiveJava] Item 7. 다 쓴 객체 참조를 해제하라  (0) 2025.07.01
'Backend/Java' 카테고리의 다른 글
  • [Java] 정렬 알고리즘 정리
  • [EffectiveJava] Item 14. Comparable을 구현할지 고려하라
  • [EffectiveJava] item 61. 박싱된 기본 타입보다는 기본 타입을 사용하라
  • [EffectiveJava] item 64. 객체는 클래스가 아닌 인터페이스로 참조하라
devoks
devoks
꾸준히 작성해보자!
  • devoks
    ok's 개발 블로그
    devoks
  • 전체
    오늘
    어제
    • 분류 전체보기 (112) N
      • Backend (15)
        • SpringBoot (0)
        • Java (15)
      • Cs (18) N
      • Infra (0)
        • AWS (0)
        • Docker (0)
      • CodingTest (79)
        • Programmers (79)
  • 링크

    • My GitHub
  • 인기 글

  • 태그

    StringTokenizer
    codingtest
    json
    effectivejava
    switch
    programmers
    BufferedWriter
    java
    BufferedReader
    CS
  • 최근 글

  • hELLO· Designed By정상우.v4.10.4
devoks
[EffectiveJava] Item 55. 옵셔널 반환은 신중히 하라
상단으로

티스토리툴바