아이템55. 옵셔널(Optional)
자바 8 이전 메서드가 특정 조건에서 값을 반환할 수 없을 때 취할 수 있는 선택지
- 예외를 던진다.
null
을 반환한다.
위 두 방법의 문제점.
- 예외는 예외적인 상황에만 사용해야 하며, 예외를 생성할 때 스택 추적 전체를 캡처하므로 비용도 만만치 않다.
- null을 반환하도록 메서드를 구현하면, NPE를 고려해서 클라이언트에서 항상 null체크를 해야함.
NPE(NullPointerException)이 무서운 이유
근본적인 원인 즉, null을 반환한 부분에서 예외가 발생하는 것이 아니라, 원인과 상관 없는 부분에서 그 값을 사용하려할 때 예외가 발생하기 때문에 발생 시점을 추적하기 번거롭다.
지비8 이후 null처리의 구원자 Optional<T>
Optional<T>
은 null이 아닌 클래스 타입 참조를 담거나 혹은 아무것도 담지 않는 것을 나타내는 불변 컨테이너 객체이다. 아무것도 담지 않은 옵셔널은 비었다
고 표현하고, 어떤 값을 담고 있는 옵셔널은 비지 않았다.
고 표현한다.
특정 조건에서 아무 값도 반환하지 말아야 하는 메서드를 만들어야 한다면 옵셔널을 반환하도록 구현하면 된다. 옵셔널을 반환하는 메서드는 예외를 던지는 메서드보다 유연하고 사용하기 쉽고 null을 반환하는 메서드보다 오류 가능성이 작다.
컬렉션을 매개변수로 받아서 최대값을 구하는 메서드 예시
옵셔널을 사용하지 않는 코드
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;
}
컬렉션이 비어있으면 IllegalArgumentException
을 던진다.
옵셔널을 사용한 코드
public static <E extends Comparable<E>> Optional<E> max(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);
}
컬렉션이 비어있으면 빈 옵셔널을 반환하고 아니라면 비지 않은 옵셔널을 반환한다.
옵셔널을 만드는 방법
옵셔널을 만드는 방법은 간단하다. 옵셔널의 정적 팩토리 메서드를 사용해 옵셔널을 생성해주기만 하면 된다.
Optional.empty()
: 빈 옵셔널을 생성한다.Optional.of(value)
: 매개변수로 들어온 객체를 담은 옵셔널을 생성한다. (null을 매개변수로 넘길 경우NullPointerException
을 던진다.)Optional.ofNullable(value)
: of처럼 매개변수로 들어온 객체를 담은 옵셔널을 생성하지만, null이 들어오면 빈 옵셔널을 생성한다.
ps. 스트림 종단 연산 중 상당수가 옵셔널을 반환한다.
위의 max()메서드의 스트림 버전
public static <E extends Comparable<E>> Optional<E> max(Collection<E> c) {
return c.stream().max(Comparator.naturalOrder());
}
옵셔널 활용
옵셔널은 검사 예외와 취지가 비슷하다. 반환값이 있을 수도, 없을 수도 있다는 걸 API 사용자에게 명확히 알려준다. 비검사 예외를 던지거나 null을 반환하면 API사용자가 그 사실을 인지하지 못해 끔찍한 결과가 발생할 수 있지만, 옵셔널과 검사 예외를 사용하면 클라이언트는 반드시 이에 대처하는 코드를 작성해야 한다.
옵셔널 활용 1 : 기본값 세팅
List<String> words = Arrays.asList(args);
String lastWordInLexicon = max(words).orElse("단어 없음...");
위의 max함수를 사용한 예시이다. words라는 컬렉션이 비어있다면, orElse()
메서드의 매개변수로 오는 값을 기본값으로 반환한다.
옵셔널 활용 2 : 원하는 예외를 던짐
List<String> words = Arrays.asList(args);
String lastWordInLexicon = max(words).orElseThrow(() -> new NoSuchElementException("요소가 없어요~~"));
words라는 컬렉션이 비어있다면, orElseThrow()
메서드의 매개변수로 오는 예외 객체 공급자로 예외를 던질 수 있다.
옵셔널 활용 3 : 값이 항상 채워져 있다고 가정한다.
List<String> words = Arrays.asList(args);
String lastWordInLexicon = max(words).get();
쓰지 말자... 이거 쓸거면 옵셔널 왜 쓰나... 그냥 객체 반환하지... 가끔 테스트 코드에서만 허용한다.
옵셔널 활용 고급
옵셔널을 1개의 원소를 갖는 Stream이라고 생각하면 편하다. 스트림과 직접적인 관계는 없지만 사용 방법이나 기본 사상이 매우 유사하다.
map()
List<String> words = Arrays.asList(args);
Optional<Integer> maxLength = max(words).map(String::length);
스트림처럼 map메서드를 호출해서 원하는 형태로 변신할 수 있다!
filter()
List<String> words = Arrays.asList(args);
Optional<String> s = max(words).filter(word -> word.length() > 5);
스트림처럼 옵셔널에 filter를 적용시킬 수 있다! 값이 1개거나 없기 때문에 조건에 충족하지 못하면 무조건 빈 옵셔널이 반환된다.
ifPresent()
이 메서드는 특정 결과를 반환하는 대신에 옵셔널이 감싸고 있는 값이 존재할 경우에만 실행할 로직을 함수형 매개변수로 전달할 수 있다.
List<String> words = Arrays.asList(args);
Optional<String> s = max(words).filter(word -> word.length() > 5);
s.ifPresent(System.out::println);
s가 빈 옵셔널이면 아무 일이 일어나지 않고 빈 옵셔널이 아니면 sout에 의해 출력된다.
옵셔널이 무조건 좋은 건 아니다!
컬렉션, 스트림, 배열, 옵셔널같은 컨테이너 타입은 옵셔널로 감싸면 안된다.
빈 옵셔널Optional<List<T>>
을 반환하는 것 보다 빈List<T>
을 반환하는게 더 좋다.
빈 컨테이너를 그대로 반환하면 클라이언트에 옵셔널 처리 코드를 넣지 않아도 된다.
결과가 없을 수 있으며, 클라이언트가 이 상황을 특별하게 처리해야하는 경우에만 옵셔널을 반환하게 한다
이렇게 하더라도 옵셔널을 반환하려면 대가가 따른다. 엄연히 옵셔널도 메모리를 할당받아야 하는 객체이고, 그 안에 있는 값을 사용하려면 메서드를 거쳐서 사용해야 하니 한 단계 더 거치는 셈이다. 그래서 성능이 중요한 상황에서는 옵셔널 사용이 맞지 않을 수 있다.
박싱된 기본 타입 옵셔널
박싱된 기본 타입 옵셔널은 값을 두 겹이나 감싼 형태이기 때문에 기본 타입 자체보다 무겁다.
그래서 옵셔널도 스트림처럼 기본형 특화 옵셔널이 존재한다.
OptionalInt
, OptionalLong
, OptionalDouble
기본형 특화 옵셔널들은 옵셔널이 가지는 대부분의 메서드를 제공한다. 그러니 박싱된 기본 타입을 담는 옵셔널을 반환하지 말도록 하고 기본형 특화 옵셔널을 사용하자.
옵셔널은 반환 용도로만 사용!
옵셔널을 반환 이외의 용도로 사용하는 것은 대부분 적절치 못하다.
만약 옵셔널을 컬렉션의 키, 값, 원소나 배열의 원소로 쓰면 쓸데없이 복잡성만 늘고 혼란만 야기된다.
필드로 만드는 것도 좋지 못하다.
만약 선택적 필드가 필요하다면 getter에서 옵셔널을 반환하게 할 순 있어도 필드 자체를 옵셔널로 하는 것은 매우 안좋다...
결론
- 값을 반환하지 못할 가능성이 있는 메서드는 옵셔널을 반환하게 만들자.
- 단, 성능상의 대가가 따르니 이 부분 고려할 것.
- 옵셔널은 반환 용도로만 쓸 것.
끗.
참고
'Java' 카테고리의 다른 글
상태 패턴 정리 (0) | 2022.04.12 |
---|---|
타입 안전 이종 컨테이너 (0) | 2022.04.05 |
멤버 클래스는 되도록 static으로 (2) | 2022.03.30 |
Stream.forEach() vs for-each (0) | 2022.03.14 |
표준 함수형 인터페이스 (0) | 2022.03.08 |