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 객체를 생성주의:
value
가null
이면 NPE 발생!null이 아님을 확신할 때 사용
Optional.ofNullable(value)
value
가null
이 아니면 그 값을 담은 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)
orElse(defaultValue)
String lastWordInLexicon = max(words).orElse("단어 없음...");
Optional이 비어있으면 미리 제공된
defaultValue
를 반환
예외 던지기 : orElseThrow(exceptionSupplier)
orElseThrow(exceptionSupplier)
Toy myToy = max(toys).orElseThrow(TemperTrantrumException::new);
Optional이 비어있으면 지정된 예외를 발생
예외가 발생할 때만 예외 생성 비용 추가
값 직접 얻기 (주의!) : get()
get()
Element lastNobleGas = max(elements.NOBLE_GASES).get();
Optional에 값이 있으면 그 값을 반환하고, 잘못 판단한 것이라면
NoSuchElementException
값이 있다고 확신하는 경우가 아니라면 사용을 지양하고,
orElse
등 다른 안전한 메서드를 추천
기본값 지연 계산 : orElseGet(supplier)
orElseGet(supplier)
Optional이 비어있을 때만
supplier
를 호출하여 기본값을 생성기본값 생성 비용이 클 때 유용
값 필터링 : filter(predicate)
filter(predicate)
Optional에 값이 있고, 주어진 조건(predicate)을 만족하면 그 Optional을 그대로 반환
만족하지 않는다면 빈 Optional을 반환
값 변환 : map(function)
map(function)
Optional<String> parentPid =
ph.parent().map(h -> String.valueOf(h.pid()));
Optional에 값이 있으면 주어진 함수(function)를 적용하고, 그 결과를 담은 새로운 Optional을 반환
비어있으면 빈 Optional을 반환
중첩된 Optional 처리 : flatMap(function)
flatMap(function)
streamOfOptionals.flatMap(Optional::stream);
map
과 유사하지만, 주어진 함수가 Optional을 반환할 때 사용결과가 중첩된
Optional<Optional<T>>
가 되지 않도록 한 단계 펼쳐줍니다
값이 있을 때만 동작 수행 : ifPresent(consumer)
ifPresent(consumer)
user.getEmail().ifPresent(email ->
System.out.println("이메일: " + email));
Optional에 값이 있을 때만 주어진 동작(consumer)을 수행
존재 여부 확인 (주의!) : isPresent()
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