💡요약
1. 반환 값이 없을 수 도 있고 클라이언트가 이를 특별히 처리해야 하는 경우
Optional 을 활용해보자.
- 성능 저하가 뒤따르니 성능에 민감한 메서드라면 null 반환이나 예외 처리를 고려
2. 반환 값 이외의 용도는 쓰는 경우는 매우 드물고 굳이 쓸 이유가 없다.
1. 개요
자바 8 이전에는 메서드가 특정 조건에서 값을 반환할 수 없을 때
취할 수 있는 선택지가 두 가지 있었다. 바로 예외를 던지거나 null 값을 반환하는 것이다
하지만 두 가지 방법에는 허점이 존재했는데
1️⃣예외를 던진다
- 정말 예외적인 상황에 쓰여야 할 예외 처리가 null 처리에 쓰이게 된다
- 예외를 생성할 때 스택 추적 전체를 캡처하므로 이에 대한 비용 문제가 발생한다.
- Stack Trace란?
더보기Stack Trace- 자바 프로그램이 실행될 때 메서드가 호출될 때마다 "스택 프레임"이 생성되어 스택(Stack) 구조로 쌓인다.
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;
}
예외 처리가 메서드 내에 강하게 결합되어 있기 때문에 여러가지 한계를 맞이하게 된다
- 호출하는 클래스마다 예외 메시지를 다르게 하고 싶을 수도 있다
- 클라이언트는 이 런타임 예외가 발생한다는 것을 알고 있어야 한다
- 이 예외에 대해 외부에서 catch 문을 작성해서 잡아야 한다.
즉, 원치 않는 결합도가 생겨버린다
- 예외를 발생시키고 싶지 않을 수 있다
- 마찬가지로 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 반환을 선택해라
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 |