24.08.03 TIL - Effective Java Item 55
옵셔녈 반환은 신중히 하라
자바 8 이전 값 반환할 수 없을 때
자바 8 전에는 메서드가 특정 조건에서 값을 반환할 수 없을 때 취할 수 있는 선택지가 2가지 있었다.
- 예외를 던진다.
- null을 반환한다.
두 방법 모두 허점이 있다.
예외 던지는 경우 허점
- 예외는 진짜 예외적인 상황에서만 사용해야 한다.
- 예외를 생성할 때 stack trace 전체를 캡처해 비용 크다.
null 반환하는 경우 허점
- 별도의 null 처리 코드 추가해야 한다.
- null 처리 무시하고 반환하면 NullPointException 발생할 수 있다.
(null을 반환하게 한 실제 근본적인 원인과 전혀 상관 없는 코드에서 발생 가능)
다른 선택지
자바 8로 가면서 메서드가 특정 조건에서 값을 반환할 수 없을 때 취할 수 있는 선택지가 하나 더 생겼다.
Optional<T>라는 선택지가 생겼다.
Optional<T>는 2가지를 담을 수 있다.
- null이 아닌 T타입 참조 하나를 담는다.
- 아무것도 담지 않는다.
Optional을 반환하는 메서드는 예외, null을 반환하는 메서드보다 아래와 같은 장점이 있다.
- 유연하고 사용하기 쉽다.
- 오류 가능성이 작다.
옵셔널 생성 방법
옵셔널을 반환하도록 구현하는 것은 어렵지 않다.
적절한 정적 팩터리를 사용해 옵셔널을 생성해주면 된다.
Optional.empty()
빈 옵셔널은 Optional.empty()로 만든다.
Optional.of(값)
값이 든 옵셔널은 Optional.of(값)으로 만든다.
값에 null을 넣으면 NullPointerException이 발생한다.
Optional.ofNullable(값)
null 값도 허용하는 옵셔널을 만들려면 Optional.ofNullable(값)로 만들면 된다.
하지만 옵셔널을 반환하는 메서드에서는 절대 null을 반환하지 말라고 한다.
왜냐하면 이렇게 되면 옵셔널을 도입한 취지를 완전히 무시하는 것이기 때문이다.
스트림의 종단 연산
스트림의 종단 연산 중 상당수가 옵셔널을 반환한다.
스트림의 종단 연산에는 match(), sum(), count(), max() 등이 있다.
클라이언트가 옵셔널 받았을 때 취할 행동
옵셔널에 항상 값이 채워져 있다고 확신한다면 곧바로 값을 꺼내 사용할 수 있다.
.get()으로 옵셔널 안에 있는 값을 꺼내서 사용하면 된다.
다만 값이 없다면 NoSuchElementException이 발생한다.
Element lastNobleGas = max(Elements.NOBLE_GASES).get();
위처럼 메서드가 옵셔널을 반환한다면 클라이언트는 값을 받지 못할 수도 있다.
따라서 클라이언트는 값을 받지 못했을 때 취할 행동을 선택해야 한다.
여기에는 여러가지 행동이 있다.
첫번째 행동
첫번째 행동은 기본값을 설정하는 방법이다.
.orElse(기본값)으로 값이 없으면 기본 값을 받도록 하는 것이다.
String lastWordInLexion = max(words).orElse("기본값");
두번째 행동
두번째 행동은 상황에 맞는 예외를 던지는 것이다.
.orElseThrow(IllegalArgumentException ::new);로 값이 없으면 예외를 던지게 하는 것이다.
Toy myToy = max(toys).orElseThrow(TemperTantrumException::new);
세번째 행동
세번째 행동은 값이 필요할 때 Supplier<T> 사용해 값 생성하는 것이다.
기본 값을 설정하는 비용이 아주 커서 부담이 될 때가 있다.
그럴 때는 Supplier<T>를 인수로 받는 .orElseGet을 사용하면 된다.
값이 없다면 Supplier의 코드 실행되지 않아서 불필요한 계산 비용 줄일 수 있다.
// Supplier를 사용하여 기본값을 제공
Supplier<String> defaultSupplier = () -> expensiveComputation();
// orElseGet 사용: Supplier를 사용하여 필요할 때만 기본값 생성
String result = optionalValue.orElseGet(defaultSupplier);
네번째 행동
네번째 행동은 값이 있는지 없는지 먼저 확인해보는 것이다.
.isPresent()는 안전 밸브 역할의 메서드다.
옵셔널에 값이 있다면 true, 값이 없다면 false를 반환한다.
// Optional이 값을 가지고 있는지 확인
if (optionalValue.isPresent()) {
// 값이 존재하는 경우 그 값을 사용
System.out.println("Value is present: " + optionalValue.get());
} else {
// 값이 존재하지 않는 경우
System.out.println("Value is not present");
}
다섯번째 행동
다섯번째 행동은 Stream 사용하는 것이다.
자바 9에서 Optional에 stream()이 추가되었다.
옵셔널에 값이 있으면 그 값을 원소로 담은 스트림으로 변환한다.
옵셔널에 값이 없으면 빈 스트림으로 변환한다.
streamOptionals.flatMap(Optional::stream)
옵셔널 언제 사용?
null을 반환하거나 예외를 던지는 대신 옵셔널 반환을 선택해야 하는 기준은 무엇인가?
옵셔널은 검사 예외와 취지가 비슷하다.
즉 반환 값이 없을 수도 있음을 API 사용자에게 명확히 알려주는 것이다.
그래서 옵셔널도 클라이언트가 값을 받지 못했을 때 반드시 이에 대처하는 코드 작성해야 한다.
그리고 옵셔널 반환 선택하는 기본 규칙이 있다.
기본 규칙은 ‘결과가 없을 수 있으며, 클라이언트가 이 상황을 특별하게 처리해야 한다’ 이다.
이런 경우엔 Optional<T>를 반환하면 된다.
옵셔널 사용 시 주의점
반환으로 옵셔널을 사용한다고 무조건 이득이 되는 것은 아니다.
옵셔널도 엄연히 새로 할당하고 초기화해야 하는 객체다.
그리고 그 안에서 값을 꺼내려면 메서드를 호출해야 하니 한 단계를 더 거쳐야 한다.
그래서 성능이 중요한 상황에서는 옵셔널이 맞지 않을 수 있다.
그리고 옵셔널을 사용하는데 주의점이 있는데 살펴보자.
첫번째 주의점
첫번째 주의점은 ‘컬렉션, 스트림, 배열, 옵셔널 같은 컨테이너 타입은 옵셔널로 감싸면 안된다’는 것이다.
빈 컨테이너를 그대로 반환하면 클라이언트에 옵셔널 처리 코드를 넣지 않아도 되기 때문이다.
두번째 주의점
두번째 주의점은 ‘박싱된 기본 타입을 담은 옵셔널을 반환하는 일은 없도록 하자’는 것이다.
박싱된 기본 타입을 담는 옵셔널은 기본 타입 자체보다 무거울 수 밖에 없다.
그래서 int, long, double 전용 옵셔널 클래스들인 OptionalInt, OptionalLong, OptionalDouble을 사용하자는 것이다.
단 덜 중요한 기본 타입의 박싱된 기본 타입인 Boolean, Byte, Character, Short, Float는 전용 옵셔널 클래스가 없다.
세번째 주의점
세번째 주의점은 ‘옵셔널을 컬렉션의 키, 값, 원소 or 배열의 원소로 사용하는게 적절한 상황은 거의 없다’는 것이다.
옵셔널을 반환하는 이외의 다른 쓰임에는 대부분 적절하지 않기 때문이다.
왜냐하면 예를 들어 맵의 키로 옵셔널을 쓰면 맵 안에 키가 없다는 사실을 나타내는 방법이 2가지가 된다.
- 키 자체가 없는 경우
- 키는 있지만 키가 빈 옵셔널인 경우
이렇게 쓸데없이 복잡해지고 혼란과 오류 가능성을 키운다.
네번째 주의점
네번째 주의점은 ‘옵셔널을 인스턴스 필드에 저장해두는 것은 대부분 좋지 않다’는 것이다.
옵셔널을 인스턴스에 필드에 저장해두는 상황이 있다.
이런 상황 대부분은 아래의 2가지 클래스를 따로 만들어야 함을 암시하는 안 좋은 코드다.
- 필수 필드를 갖는 클래스
- 위의 클래스 확장해 선택적 필드를 추가한 하위 클래스
결론
- 값을 반환하지 못할 가능성이 있다.
- 호출할 때마다 반환값이 없을 가능성을 염두해 둬야 한다.
이런 메서드라면 옵셔널을 반환해야 할 상황일 수 있다.
하지만 옵셔널 반환에는 성능 저하가 뒤따른다.
따라서 성능에 민감한 메서드라면 null을 반환하거나 예외 던지는 편이 나을 수 있다.
그리고 옵셔널을 반환값 이외의 용도로 쓰는 경우는 드물다.