[Java] 66. Optional

2025. 7. 4. 23:56·Java/Modern Java(8~)

 

 

Optional

#Java/adv3


옵셔널이 필요한 이유

NPE

  • 자바에서 null은 값이 없음을 표현하는 기본적인 방법이다.
  • 하지만, null을 잘못 사용하거나 null 참조에 대해 메서드를 호출하면 NullPointerException (NPE)이 발생하여 프로그램이 예기치 않게 종료될 수 있다.
    • 메서드 뎁스가 깊고, null 체크를 누락하면 추척하기 어렵고, 디버깅 비용이 증가한다.

가독성 저하

  • null을 반환하거나 사용하는 경우, 조건문을 통해 객체가 null인지 계속 확인해야 한다. 이러한 null 체크 로직이 누적되면 코드가 복잡해지고 가독성이 저하된다.

의도가 드러나지 않음

  • 메서드 시그니처(String findNameById(Long id))만 보고서는 이 메서드가 null을 반환할 수도 있다는 사실을 명확히 알기 어렵다.
  • 메서드를 호출하는 입장에서는 "반드시 값이 존재할 것"이라고 가정했다가, 런타임에 null 이 나와서 문제가 생길 수 있다.
    • ex) String findNameById(Long id)보고findNameById(id).toUpperCase()를 호출했는데, 알고보니 null을 반환해서 NPE가 발생해버렸다.

Optional의 등장

  • 이러한 문제를 해결하고자 자바 8부터 Optional 클래스를 도입했다.
  • Optional은 "값이 있을 수도 있고 없을 수도 있음"을 명시적으로 표현해주어, 메서드의 계약(Contract)이나 호출 의도를 좀 더 분명하게 드러낸다.
  • Optional을 사용하면 "빈 값"을 표현할 때, 더 이상 null 자체를 넘겨주지 않고 Optional.empty()처럼 의도를 드러내는 객체를 사용할 수 있다.
  • Optional을 사용함으로써 얻을 수 있는 이점
    • null 체크 로직이 간결해지고, NPE가 발생할 수 있는 부분을 컴파일러, IDE, 코드 리뷰어가 쉽게 파악할 수 있다.

null을 직접 반환하는 경우

  • String name = findNameById(id); // 결과 존재X 시 null 반환
  • System.out.println("name = " + name.toUpperCase());
    • NPE 발생!!
  • if (name != null) System.out.println(id + ": " + name.toUpperCase()); else {...}
    • null 체크 로직을 빠트리면 NPE가 발생한다.

Optional을 반환하는 경우

// 이름이 있으면 이름을 대문자로 출력, 없으면 "UNKNOWN"을 출력해라.
static void findAndPrint(Long id) {
    Optional<String> optName = findNameById(id);
	// 다른 개발자, 클라이언트 입장에서는 반환타입이 Optional임을 보고 값이 있을 지 없을 지 명시적으로 알 수 있다.
    String name = optName.orElse("UNKNOWN");
    System.out.println(id + ": " + name.toUpperCase());
}

static Optional<String> findNameById(Long id) {
    String findName = map.get(id);
    Optional<String> optName = Optional.ofNullable(findName);
    return optName;
}
  • Optional.ofNullable(findId)를 통해 null 이 될 수도 있는 값을 Optional 로 감싼다.
  • 메서드를 사용하는 클라이언트는 메서드 시그니처(Optional<String> findNameById(Long id))만 보고도 "반환 결과가 있을 수도, 없을 수도 있겠구나"라고 명시적으로 인지할 수 있다.
  • Optional.orElse("대체값"): 옵셔널에 값이 있으면 해당 값을 반환하고, 값이 없다면 대체값을 반환한다.
  • 이 방식은 "값이 없을 수도 있다"는 점을 호출하는 측에 명확히 전달하므로, 놓치기 쉬운 null 체크를 강제하고 코드의 안정성을 높인다.

Optional 소개

  • java.util.Optional<T> 는 "존재할 수도 있고 존재하지 않을 수도 있는" 값을 감싸는 일종의 컨테이너 클래스
  • null을 내부적으로 다루는 대신, Optional 객체에 감싸서 Optional.empty() 또는 Optional.of(value) 형태로 다룬다.
  • Optional 등장 이유
    • "값이 없을 수 있다"는 상황을 프로그래머가 명시적으로 처리하도록 유도
    • 런타임 NPE를 사전에 예방하기 위해 도입됨.
  • Optional은 언제 사용하면 좋을까?
    • Optional은 "값이 없을 수도 있다"는 상황을 반환할 때 주로 사용된다.
    • "항상 값이 있어야 하는 상황"에서는 Optional을 사용할 필요 없이 그냥 해당 타입을 바로 사용하거나 예외를 던지는 방식이 더 좋을 수 있다.

Optional의 생성과 값 획득

Optional 생성 : 3가지 방법

  • Optional.of(T value)
    • 내부 값이 확실히 null이 아닐 때 사용. null 을 전달하면 NullPointerException 발생
  • Optional.ofNullable(T value)
    • 값이 null 일 수도 있고 아닐 수도 있을 때 사용. null이면 Optional.empty()를 반환한다.
    • Optional<String> opt2 = Optional.ofNullable("Hello!");
    • Optional<String> opt3 = Optional.ofNullable(null);
  • Optional.empty()
    • 명시적으로 "값이 없음"을 표현할 때 사용
    • Optional<String> opt4 = Optional.empty();

Optional 값 획득: Optional에 들어있는 값을 꺼내는 방법

  1. isPresent()
    • 값이 있으면 true, 없으면 false
    • isEmpty(): 자바 11 이상에서 사용 가능, 값이 비어있으면 true, 값이 있으면 false 를 반환
  2. get()
    • 값이 있는 경우 그 값을 반환, 값이 없으면 NoSuchElementException 발생.
    • 직접 사용 시 주의해야 하며, 가급적이면 orElse, orElseXxx 계열 메서드를 사용하는 것이 안전.
  3. orElse(T other)
    • 값이 있으면 그 값을 반환, 값이 없으면 other 를 반환.
  4. orElseGet(Supplier<? extends T> supplier)
    • 값이 있으면 그 값을 반환, 값이 없으면 supplier 호출하여 생성된 값을 반환.
  5. orElseThrow(...)
    • 값이 있으면 그 값을 반환, 값이 없으면 지정한 예외를 던짐.
  6. or(Supplier<? extends Optional<? extends T>> supplier)
    • 값이 있으면 해당 값의 Optional 을 그대로 반환, 값이 없으면 supplier 가 제공하는 다른 Optional 반환
    • 값 대신 Optional을 반환한다는 특징

Optional<String> optValue = Optional.of("Hello");
Optional<String> optEmpty = Optional.empty();

// get() : 값이 없으면 예외 발생. 가급적 사용을 피하자.
String getValue = optValue.get();
//String getValue2 = optEmpty.get(); // 예외 발생 -> 주석 처리

// orElse() : 값이 없으면 지정된 기본값 사용
String value1 = optValue.orElse("기본값"); // Hello
String empty1 = optEmpty.orElse("기본값"); // 기본값

// orElseGet() : 값이 없으면 람다를 실행하여 기본값 생성
String value2 = optValue.orElseGet(() -> {
    System.out.println("람다 호출 - optValue"); // 호출 안됨
    return "New Value";
});
String empty2 = optEmpty.orElseGet(() -> {
    System.out.println("람다 호출 - optEmpty"); // 호출됨
    return "New Value";
});

// orElseThrow() : 값이 없으면 예외 발생
String value3 = optValue.orElseThrow(() -> new RuntimeException("값이 없습니다!"));
try {
    // optEmpty는 값이 없으므로 예외 발생
    String empty3 = optEmpty.orElseThrow(() -> new RuntimeException("값이 없습니다!"));
    System.out.println("empty3 = " + empty3); // 실행되지 않음
} catch (Exception e) {
    System.out.println("예외 발생: " + e.getMessage());
}

// or() : Optional 반환
Optional<String> result1 = optValue.or(() -> Optional.of("Fallback"));
System.out.println(result1); // Optional[Hello], 값이 이미 존재 -> 원본 그대로

Optional<String> result2 = optEmpty.or(() -> Optional.of("Fallback"));
System.out.println(result2); // Optional[Fallback], 비어있으므로 대체 Optional 반환

Optional 값 처리:

Optional 에서는 값이 존재할 때와 존재하지 않을 때를 처리하기 위한 다양한 메서드들을 제공한다

  • ifPresent(Consumer<? super T> action)
    • 값이 존재하면 action 실행, 값이 없으면 아무것도 안 함
    • ifPresentOrElse(Consumer<? super T> action, Runnable emptyAction)
      • 값이 존재하면 action 실행, 값이 없으면 emptyAction 실행
    • map(Function<? super T, ? extends U> mapper)
      • 값이 있으면 mapper 를 적용한 결과(Optional<U>) 반환
        • Optional<String> 문자열 길이로 매핑 Optional<Integer>
      • 값이 없으면 Optional.empty() 반환
    • flatMap(Function<? super T, ? extends Optional<? extends U>> mapper)
      • map과 유사하지만, Optional을 반환할 때 중첩되지 않고 평탄화(flat)해서 반환
      • Optional 안의 Optional을 하나의 Optional로 평탄화한다~
    • filter(Predicate<? super T> predicate)
      • 값이 있고 조건을 만족하면 그대로 반환,
      • 조건 불만족이거나 비어있으면 Optional.empty() 반환
    • stream()
      • 값이 있으면 단일 요소를 담은 Stream<T> 반환, 값이 없으면 빈 스트림 반환

Optional<String> optValue = Optional.of("Hello");
Optional<String> optEmpty = Optional.empty();

// ifPresent(): 값이 존재하면 Consumer 실행, 없으면 아무 일도 하지 않음
optValue.ifPresent(v -> System.out.println("optValue 값: " + v));
optEmpty.ifPresent(v -> System.out.println("optEmpty 값: " + v)); // 실행 X

// ifPresentOrElse():값이 있으면 Consumer 실행, 없으면 Runnable 실행
optValue.ifPresentOrElse(
        v -> System.out.println("optValue 값: " + v),
        () -> System.out.println("optValue는 비어있음")
);
optEmpty.ifPresentOrElse(
        v -> System.out.println("optEmpty 값: " + v), // 실행 X
        () -> System.out.println("optEmpty는 비어있음")
);

// map(): 값이 있으면 Function 적용 후 Optional로 반환, 없으면 Optional.empty()
Optional<Integer> lengthOpt1 = optValue.map(String::length); // Optional[5]
Optional<Integer> lengthOpt2 = optEmpty.map(String::length); // Optional.empty

// flatMap(): map()과 유사하나, 이미 Optional을 반환하는 경우 중첩을 제거
System.out.println("=== 4. flatMap() ===");
System.out.println("[map]");
// Optional[Hello] -> Optional[Optional[HELLO]] (중첩됨)
Optional<Optional<String>> nestedOpt = optValue.map(s -> Optional.of(s.toUpperCase()));

System.out.println("[flatMap]"); //flatMap을 사용하면 한 번에 평탄화
// Optional[Hello] -> Optional[HELLO]
Optional<String> flattenedOpt = optValue.flatMap(s -> Optional.of(s.toUpperCase()));

// filter(): 값이 있고 조건을 만족하면 그 값을 그대로, 불만족시 Optional.empty()
Optional<String> filtered1 = optValue.filter(s -> s.startsWith("H")); //Optional[Hello]
Optional<String> filtered2 = optValue.filter(s -> s.startsWith("X")); //Optional.empty

// stream(): 값이 있으면 단일 요소 스트림, 없으면 빈 스트림
optValue.stream().forEach(s -> System.out.println("optValue.stream() -> " + s));// 실행O
optEmpty.stream().forEach(s -> System.out.println("optEmpty.stream() -> " + s));// 실행X
  • Optional 의 다양한 메서드를 활용하면, 값이 존재할 때와 존재하지 않을 때의 로직을 명확하고 간결하게 구현할 수 있다.

즉시 평가와 지연 평가1

orElse()와 orElseGet()의 차이를 이해하기 위해서는 즉시 평가와 지연 평가를 이해해야 한다.

  • 즉시 평가(eager evaluation):
    • 값(혹은 객체)을 바로 생성하거나 계산해 버리는 것
  • 지연 평가(lazy evaluation):
    • 값이 실제로 필요할 때(즉, 사용될 때)까지 계산을 미루는 것

로거 예제: 일반적인 상황에서는 로그를 남기지 않다가, 디버깅이 필요한 경우에만 디버깅용 로그를 추가로 출력

public class Logger {

    private boolean isDebug = false;
	// Getter, Setter
    // DEBUG로 설정한 경우만 출력 - 데이터를 받음
    public void debug(Object message) {
        if (isDebug) {
            System.out.println("[DEBUG] " + message);
        }
    }
}
  • debug()에 전달한 메시지는 isDebug 값을 true로 설정한 경우에만 메시지를 출력한다.

public class LogMain1 {

    public static void main(String[] args) {
        Logger logger = new Logger();
        logger.setDebug(true);
        logger.debug(10 + 20); // 디버그 켜짐, 연산 결과 출력

        System.out.println("=== 디버그 모드 끄기 ===");
        logger.setDebug(false);
        logger.debug(100 + 200); // 디버그 꺼짐, 연산 결과 출력 안됨
    }
}

디버그 모드가 꺼져있는 경우

// 자바 언어의 연산자 우선순위상 메서드를 호출하기 전에 괄호 안의 내용이 먼저 계산된다.
logger.debug(100 + 200); // 1. 여기서는 100 + 200이 즉시 평가된다.
logger.debug(300);       // 2. 100 + 200 연산의 평가 결과는 300이 된다.
debug(300)               // 3. 메서드를 호출한다. 이때 계산된 300의 값이 인자로 전달된다.
public void debug(Object message = 300) { // 4. message에 계산된 300이 할당된다.
    if (isDebug) { // 5. debug 모드가 꺼져있으므로 false이다.
        System.out.println("[DEBUG] " + message); // 6. 실행되지 않는다.
    }
}

앞서 계산한 100 + 200 연산은 어디에도 사용되지 않는다. 결과적으로 연산은 계산된 후에 버려진다.


즉시 평가와 지연 평가2

100 + 200이 실제로 연산되었는지 확인하기 위해 메서드로 분리하여 호출해보자.

public class LogMain2 {

    public static void main(String[] args) {
        Logger logger = new Logger();
        logger.setDebug(true);
        logger.debug(value100() + value200()); // 디버그 켜짐

        System.out.println("=== 디버그 모드 끄기 ===");
        logger.setDebug(false);
        logger.debug(value100() + value200()); // 디버그 꺼짐
    }

    static int value100() {
        System.out.println("value100 호출");
        return 100;
    }

    static int value200() {
        System.out.println("value200 호출");
        return 200;
    }
}

로그를 보면 디버그 모드를 끈 경우에도 value100(), value200() 이 실행된 것을 확인할 수 있다. debug() 메서드를 호출하기 전에 괄호 안의 내용이 먼저 평가(계산)되는 것을 확인할 수 있다.


그렇다면 debug 모드가 켜져있을 때는 해당 연산을 처리하고, debug 모드가 꺼져있을 때는 해당 연산을 처리하지 않으려면 어떻게 해야 할까? 가장 간단한 방법은 debug모드인 경우에만 debug()메서드를 호출하도록 if문을 사용하는 것이다.

if (logger.isDebug()) {
    logger.debug(value100() + value200());
}

이렇게 하면 디버그 모드를 체크한 이후에 아무런 로그가 남지 않는다. debug()메서드가 실행되지 않은 것을 확인할 수 있다.
하지만 이렇게 하려면 디버그를 출력할 때 마다 계속 if 문을 사용해야 한다. 코드 한 줄을 작성하는데 코드 2줄이 더 필요하다! (if 문을 닫는 괄호 포함)


// 1. if 문을 통한 확실한 체크, 코드는 지저분해지지만, 필요 없는 연산 수행X
if (logger.isDebug()) {
    logger.debug(value100() + value200());
}

// 2. 필요없는 연산이 추가되지만 코드는 깔끔
logger.debug(value100() + value200());

두 마리 토끼를 모두 잡을 수는 없을까? (2번의 코드의 깔끔함과 1번의 필요 없는 연산은 수행하지 않는 것)


이렇게 하려면 100 + 200, value100() + value200() 같은 연산을 정의하는 시점과 해당 연산을 실행하는 시점을 분리해야 한다. 그래서 연산의 실행을 최대한 지연해서 평가(계산)해야 한다.


즉시 평가와 지연 평가3

자바 언어에서 연산을 정의하는 시점과 해당 연산을 실행하는 시점을 분리하는 방법은 여러 가지가 있다.

  • 익명 클래스를 만들고, 메서드를 나중에 호출
  • 람다를 만들고, 해당 람다를 나중에 호출

람다를 사용해서 연산을 정의하는 시점과 실행하는 시점을 분리해서 문제를 해결해보자.
Logger에 람다(Supplier)를 받는 debug 메서드를 하나 추가하자

// supplier에 람다가 전달된다. (람다는 아직 실행되지 않았다.)
public void debug(Supplier<?> supplier = lambda) {
    if (isDebug) { // 4. 디버그 모드이면 if 문이 수행된다. 디버그 모드가 아니면 람다는 실행되지 않는다.

       // 5. supplier.get()을 실행하는 시점에 람다에 있는 value100() + value200()이 평가(계산)된다.
        // 6. 평가 결과인 300을 반환하고 출력한다.
        System.out.println("[DEBUG] " + supplier.get());
    }
}

public class LogMain3 {

    public static void main(String[] args) {
        Logger logger = new Logger();
        logger.setDebug(true);
        logger.debug(() -> "Operation 1: " + (value100() + value200())); // 람다 전달

        System.out.println("=== 디버그 모드 끄기 ===");
        logger.setDebug(false);
        logger.debug(() -> "Operation 2: " + (value100() + value200())); // 람다 전달 (실행 안됨)
    }

람다를 사용해서 연산을 정의하는 시점과 실행(평가)하는 시점을 분리할 수 있고, 값이 실제로 필요할 때 까지 계산을 미룰 수 있다.


orElse() vs orElseGet()

이제 orElse()와 orElseGet()의 차이를 이해할 수 있을 것이다.
orElse()는 보통 데이터를 받아서 인자가 즉시 평가되고, orElseGet()은 람다를 받아서 인자가 지연 평가된다.


public class OrElseGetMain {
    public static void main(String[] args) {
        Optional<Integer> optValue = Optional.of(100);
        Optional<Integer> optEmpty = Optional.empty();

        System.out.println("단순 계산");
		// 10 + 20이 이미 계산된 후 orElse()가 호출된다. (즉시 연산)
        Integer i1 = optValue.orElse(10 + 20); // 10 + 20 계산 후 버림
        Integer i2 = optEmpty.orElse(10 + 20); // 10 + 20 계산 후 사용

        // 값이 있으면 그 값, 없으면 지정된 기본값 사용 (orElse)
        System.out.println("값이 있는 경우");
        Integer value1 = optValue.orElse(createData()); // createData() 즉시 호출됨
        System.out.println("value1 = " + value1);

        System.out.println("값이 없는 경우");
        Integer empty1 = optEmpty.orElse(createData()); // createData() 즉시 호출됨
        System.out.println("empty1 = " + empty1);

        // 값이 있으면 그 값, 없으면 지정된 람다 사용 (orElseGet)
        System.out.println("값이 있는 경우");
        Integer value2 = optValue.orElseGet(() -> createData()); // createData() 호출 안됨

        System.out.println("값이 없는 경우");
        Integer empty2 = optEmpty.orElseGet(() -> createData()); // createData() 호출됨
    }

    public static int createData() {
        System.out.println("데이터를 생성합니다...");
        try {
            Thread.sleep(1000); // 데이터 생성에 1초 걸린다고 가정
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        int createValue = new Random().nextInt(100);
        System.out.println("데이터 생성이 완료되었습니다. 생성 값: " + createValue);
        return createValue;
    }
}
  • orElse(createData())
    • Optional 에 값이 있어도 createData()가 즉시 호출된다. 호출된 값은 버려진다.
    • 자바 연산 순서상 createData()를 호출해야 그 결과를 orElse()에 인자로 전달할 수 있다.
  • orElseGet(() -> createData())
    • Optional 값이 있으면 createData()가 호출되지 않는다.
    • orElseGet()에 람다를 전달한다. 해당 람다는 이후에 orElseGet()안에서 실행될 수 있다.
    • Optional 내부에 값이 있다면, 인자로 전달한 람다를 내부에서 실행하지 않는다.
    • Optional 내부에 값이 없다면, 인자로 전달한 람다를 내부에서 실행하고, 그 결과를 반환한다.

두 메서드의 차이

  • orElse(T other)는 "빈 값이면 other 를 반환"하는데, other 를 "항상" 미리 계산한다.
    • 따라서 other를 생성하는 비용이 큰 경우, 실제로 값이 있을 때도 쓸데없이 생성 로직이 실행될 수 있다.
    • orElse()에 넘기는 표현식은 호출 즉시 평가하므로 즉시 평가(eager evaluation)가 적용된다.
  • orElseGet(Supplier supplier)은 빈 값이면 supplier를 통해 값을 생성하기 때문에, 값이 있을 때는 supplier 가 호출되지 않는다.
    • 생성 비용이 높은 객체를 다룰 때는 orElseGet()이 더 효율적이다.
    • orElseGet()에 넘기는 표현식은 필요할 때만 평가하므로 지연 평가(lazy evaluation)가 적용된다.

사용 용도
orElse(T other)

  • 값이 이미 존재할 가능성이 높거나, 혹은 orElse()에 넘기는 객체(또는 메서드)가 생성 비용이 크지 않은 경우 사용해도 괜찮다.
  • 연산이 없는 상수나 변수의 경우 사용해도 괜찮다.

orElseGet(Supplier supplier)

  • 주로 orElse()에 넘길 값의 생성 비용이 큰 경우, 혹은 값이 들어있을 확률이 높아 굳이 매번 대체 값을 계산할 필요가 없는 경우에 사용한다.

정리하면, 단순한 대체 값을 전달하거나 코드가 매우 간단하다면 orElse()를 사용하고, 객체 생성 비용이 큰 로직이 들어있고, Optional에 값이 이미 존재할 가능성이 높다면 orElseGet()을 고려해볼 수 있다.


실전 활용1 - 주소 찾기

public class User {
    private String name;
    private Address address; // 주소가 없을 수 있음
	// 생성자, getter() 생략
package optional.model;

public class Address {
    private String street;
	// 생성자, getter() 생략

null을 직접 다루는 방식

// 사용자의 주소(street) 출력, 없으면 "Unknown"
static void printStreet(User user) {
    String userStreet = getUserStreet(user);
    if (userStreet != null) {
        System.out.println(userStreet);
    } else {
        System.out.println("Unknown");
    }
}

// 사용자의 주소(street) 반환, 없으면 null 반환
static String getUserStreet(User user) {
    if (user == null) {
        return null;
    }
    Address address = user.getAddress(); // Address가 null일 수 있음
    if (address == null) {
        return null;
    }
    return address.getStreet(); // Street 반환
}
  • null 체크 구문이 자주 등장하고 복잡하다.

Optional로 개선

private static void printStreet(User user) {
    getUserStreet(user).ifPresentOrElse(
            System.out::println, // 값이 있을 때 출력
            () -> System.out.println("Unknown") // 값이 없을 때 "Unknown" 출력
    );
}

static Optional<String> getUserStreet(User user) {
    return Optional.ofNullable(user) // user가 null일 수 있으므로 ofNullable 사용
            .map(User::getAddress)    // user.getAddress() 처리
            .map(Address::getStreet); // address.getStreet() 처리 
    // 여기서 map 체이닝 중간에 null이면 Optional.empty()를 반환
}
  • null 체크 구문이 사라지고 의도가 명확해졌다.
  • getUserStreet() 메서드는 이제 Optional<String> 을 반환하므로, 호출 측에서 ifPresentOrElse(), orElse(), orElseGet() 등을 통해 안전하게 처리할 수 있다.
  • 여러 map() 체이닝을 통해 내부에서 null이 발생하면 자동으로 Optional.empty()로 전환된다.
  • 참고로 Optional.ofNullable(user).map(u -> u.getAddress().getStreet()); 이건 불가능하다. Address가 null일 수도 있기 때문이다. (null.getStreet())

실전 활용2 - 배송

public class Order {
    private Long id;
    private Delivery delivery; // 배송 정보가 없을 수 있음
	// 생성자, getter() 생략
public class Delivery {
    private String status;
    private boolean canceled; // 배송 취소 여부
	// 생성자, getter() 생략

orderRepository 라는 맵에서 주문 정보를 찾은 다음, 배송 정보를 조회하여 출력하는 코드를 작성해보자.

  • 배송 정보가 null 이거나, canceled == true 인 경우에는 "배송X"를 출력한다.

public class DeliveryMain {

    static Map<Long, Order> orderRepository = new HashMap<>();
    static {
        orderRepository.put(1L, new Order(1L, new Delivery("배송완료", false)));
        orderRepository.put(2L, new Order(2L, new Delivery("배송중", false)));
        orderRepository.put(3L, new Order(3L, new Delivery("배송중", true))); //취소됨.배송X
        orderRepository.put(4L, new Order(4L, null)); // 배송 정보 없음. 배송X
    }

    public static void main(String[] args) {
        System.out.println("1 = " + getDeliveryStatus(1L));
        System.out.println("2 = " + getDeliveryStatus(2L));
        System.out.println("3 = " + getDeliveryStatus(3L)); // 취소됨 -> 배송X
        System.out.println("4 = " + getDeliveryStatus(4L)); // 배송 정보 없음 -> 배송X
    }

    static String getDeliveryStatus(Long orderId) {
        return findOrder(orderId)                     
                .map(Order::getDelivery)               // Order -> Delivery
                .filter(delivery -> !delivery.isCanceled())
                .map(Delivery::getStatus)              // Delivery -> String
                .orElse("배송X");                      // 값이 없으면 "배송X"
    }

    static Optional<Order> findOrder(Long orderId) {
        return Optional.ofNullable(orderRepository.get(orderId));
    }
}
  • 주문 정보가 null이면 Optional.empty()
  • 배송이 없거나 취소된 경우(filter에서 걸러짐)에도 Optional.empty() 체이닝
  • 최종적으로 .orElse("배송X") 처리

옵셔널 - 베스트 프랙티스

  • Optional 이 좋아보여도 무분별하게 사용하면 오히려 코드 가독성과 유지보수에 도움이 되지 않을 수 있다.
  • Optional은 주로 메서드의 반환값에 대해 값이 없을 수도 있음을 표현하기 위해 도입되었다.
    • 여기서 핵심은 메서드의 반환값에 Optional을 사용하라는 것이다.

1. 반환 타입으로만 사용하고, 필드에는 가급적 쓰지 말기

  • Optional은 주로 메서드의 반환값에 대해 "값이 없을 수도 있음"을 표현하기 위해 도입되었다.
  • 클래스의 필드(멤버 변수)에 Optional을 직접 두는 것은 권장하지 않는다.
    • Optional 자체도 참조 타입이기 때문에, 혹시라도 개발자가 부주의로 Optional 필드에 null 을 할당하면, 그 자체가 NullPointerException을 발생시킬 여지를 남긴다.
  • 값이 없음을 명시하기 위해 사용하는 것이 Optional 인데, 정작 필드 자체가 null 이면 혼란이 가중된다.
  • 만약 Optional로 필드 값을 받고 싶다면, 필드는 Optional을 사용하지 않고, 반환하는 시점에 Optional 로 감싸주는 것이 일반적으로 더 나은 방법이다.

2. 메서드 매개변수로 Optional 을 사용하지 말기

  • 자바 공식 문서에 Optional은 메서드의 반환값으로 사용하기를 권장하며, 매개변수로 사용하지 말라고 명시되어 있다.
  • 호출하는 측에서는 단순히 null 전달 대신 Optional.empty()를 전달해야 하는 부담이 생기며, 결국 null을 사용하든 Optional.empty()를 사용하든 큰 차이가 없어 가독성만 떨어진다.
  • 권장 예시
    • 오버로드된 메서드를 만들거나,
    • 명시적으로 null 허용 여부를 문서화하는 방식을 택합니다.

3. 컬렉션(Collection)이나 배열 타입을 Optional 로 감싸지 말기

  • List<T>, Set<T>, Map<K, V> 등 컬렉션(Collection) 자체는 비어있는 상태(empty)를 표현할 수 있으므로, Optional로 컬렉션을 감싸면 Optional.empty()와 "빈 리스트"(Collections.emptyList()) 요렇게 이중 표현이 되어 혼란을 야기한다.
  • 컬렉션이 비어있는 경우를 고려한다면, Optional로 반환하는게 아니라 빈 컬렉션을 반환하자.

4. isPresent()와 get() 조합을 직접 사용하지 않기

  • Optional의 get() 메서드는 가급적 사용하지 않아야 한다.
  • if (opt.isPresent()) { ... opt.get() ... } else { ... }는 사실상 null 체크와 다를 바 없으며, 깜빡하면 NoSuchElementException 같은 예외가 발생할 위험이 있다.
  • 대신 orElse, orElseGet, orElseThrow, ifPresentOrElse, map, filter 등의 메서드를 활용하면 간결하고 안전하게 처리할 수 있다.
    • get() 메서드는 가급적 사용하지 말고, 예제 코드나, 간단한 테스트에서만 사용하는 것을 권장한다.
  • 어쩔 수 없이 get() 메서드를 사용해야 하는 상황이라면, 이럴 때는 반드시 isPresent()와 함께 사용하자.

5. orElseGet() vs orElse()차이를 분명히 이해하기

  • 비용이 크지 않은(또는 간단한 상수 정도) 대체값이라면 간단하게 orElse()를 사용하자.
  • 복잡하고 비용이 큰 객체 생성이 필요한 경우, 그리고 Optional 값이 이미 존재할 가능성이 높다면 orElseGet()를 사용하자.

6. 무조건 Optional 이 좋은 것은 아니다

  • Optional은 분명히 편의성과 안전성을 높여주지만, 모든 곳에서 "무조건" 사용하는 것은 오히려 코드 복잡성을 증가시킬 수 있다.
  • 다음과 같은 경우 Optional 사용이 오히려 불필요할 수 있다.
    1. "항상 값이 있는" 상황
      • 비즈니스 로직상 null이 될 수 없는 경우, 그냥 일반 타입을 사용하거나, 방어적 코드로 예외를 던지는 편이 낫다.
    2. "값이 없으면 예외를 던지는 것"이 더 자연스러운 상황
      • 예를 들어, ID 기반으로 무조건 존재하는 DB 엔티티를 찾아야 하는 경우, Optional 대신 예외를 던지는 게 API 설계상 명확할 수 있다. 물론 이런 부분은 비즈니스 상황에 따라 다를 수 있다.
    3. "흔히 비는 경우"가 아니라 "흔히 채워져 있는" 경우
      • Optional 을 쓰면 매번 .get(), orElse(), orElseThrow() 등 처리가 강제되므로 오히려 코드가 장황해질 수 있다.
    4. "성능이 극도로 중요한" 로우레벨 코드
      • Optional은 래퍼 객체를 생성하므로, 수많은 객체가 단기간에 생겨나는 영역(예: 루프 내부)에서는 성능 영향을 줄 수 있다. (일반적인 비즈니스 로직에서는 문제가 되지 않는다. 극한 최적화가 필요한 코드라면 고려 대상)

클라이언트 메서드 vs 서버 메서드
사실 Optional 을 고려할 때 가장 중요한 핵심은 Optional을 생성하고 반환하는 서버쪽 메서드가 아니라, Optional 을 반환하는 코드를 호출하는 클라이언트 메서드에 있다. 결과적으로 Optional 을 반환받는 클라이언트의 입장을 고려해서 선택하자.

  • "이 로직은 null 을 반환할 수 있는가?"
  • "null 이 가능하다면, 호출하는 사람 입장에서 '값이 없을 수도 있다'는 사실을 명시적으로 인지할 필요가 있는가?"
  • "null 이 적절하지 않고, 예외를 던지는 게 더 맞진 않은가?"

Optional 기본형 타입 지원

다음과 같은 이유로 잘 안 쓴다.

  • Optional<T> 와 달리 map(), flatMap() 등의 다양한 연산 메서드를 제공하지 않는다. 그래서 범용적으로 활용하기보다는 특정 메서드(isPresent(), getAsInt() 등)만 사용하게 되어, 일반 Optional<T> 처럼 메서드 체인을 이어 가며 코드를 간결하게 작성하기 어렵다.
  • 기존에 이미 Optional<T> 를 많이 사용하고 있는 코드베이스에서, 특정 상황만을 위해 OptionalInt 등을 섞어 쓰면 오히려 가독성을 떨어뜨린다.

원시 타입 Optional을 고려해볼 만한 경우

  • 예외적으로 미세한 성능을 극도로 추구하거나, 기본형 타입 스트림을 직접 다루면서 중간에 OptionalInt, OptionalLong, OptionalDouble 을 자연스럽게 얻는 상황이라면 이를 사용하는 것도 괜찮다.

Ref) 김영한의 실전 자바 - 고급 3편, 람다, 스트림, 함수형 프로그래밍 강의 | 김영한 - 인프런

'Java > Modern Java(8~)' 카테고리의 다른 글

[Java] 68. 병렬 스트림  (0) 2025.07.04
[Java] 67. 디폴트 메서드  (0) 2025.07.04
[Java] 65. 스트림 API3 - 컬렉터  (0) 2025.07.04
[Java] 64. 스트림 API2 - 기능  (0) 2025.07.04
[Java] 63. 스트림 API1 - 기본  (0) 2025.07.04
'Java/Modern Java(8~)' 카테고리의 다른 글
  • [Java] 68. 병렬 스트림
  • [Java] 67. 디폴트 메서드
  • [Java] 65. 스트림 API3 - 컬렉터
  • [Java] 64. 스트림 API2 - 기능
lumana
lumana
배움을 나누는 공간 https://github.com/bebeis
  • lumana
    Brute force Study
    lumana
  • 전체
    오늘
    어제
    • 분류 전체보기 (457)
      • Software Development (27)
        • Performance (0)
        • TroubleShooting (1)
        • Refactoring (0)
        • Test (8)
        • Code Style, Convetion (0)
        • DDD (0)
        • Software Engineering (18)
      • Java (71)
        • Basic (5)
        • Core (21)
        • Collection (7)
        • 멀티스레드&동시성 (13)
        • IO, Network (8)
        • Reflection, Annotation (3)
        • Modern Java(8~) (12)
        • JVM (2)
      • Spring (53)
        • Framework (12)
        • MVC (23)
        • Transaction (3)
        • AOP (11)
        • Boot (0)
        • AI (0)
      • DB Access (1)
        • Jdbc (1)
        • JdbcTemplate (0)
        • JPA (14)
        • Spring Data JPA (0)
        • QueryDSL (0)
      • Computer Science (130)
        • Data Structure (27)
        • OS (14)
        • Database (10)
        • Network (21)
        • 컴퓨터구조 (6)
        • 시스템 프로그래밍 (23)
        • Algorithm (29)
      • HTTP (8)
      • Infra (1)
        • Docker (1)
      • 프로그래밍언어론 (15)
      • Programming Language(Sub) (77)
        • Kotlin (1)
        • Python (25)
        • C++ (51)
        • JavaScript (0)
      • FE (11)
        • HTML (1)
        • CSS (9)
        • React (0)
        • Application (1)
      • Unix_Linux (0)
        • Common (0)
      • PS (13)
        • BOJ (7)
        • Tip (3)
        • 프로그래머스 (0)
        • CodeForce (0)
      • Book Review (4)
        • Clean Code (4)
      • Math (3)
        • Linear Algebra (3)
      • AI (7)
        • DL (0)
        • ML (0)
        • DA (0)
        • Concepts (7)
      • 프리코스 (4)
      • Project Review (6)
      • LegacyPosts (11)
      • 모니터 (0)
      • Diary (0)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.4
lumana
[Java] 66. Optional
상단으로

티스토리툴바