Optional

후추·2023년 3월 27일

들어가기

자바 8에서 Optional이 추가되었다.

Optional : A container object which may or may not contain a non-null value.
- oracle

공식 문서에 따르면,

Optional은 null이 아닌 값을 담을 수도 있고 담지 않을 수도 있는 컨테이너 객체이다.

java에 Optional이 추가되면서, 메서드가 특정 조건에서 값을 반환하지 않아야 할 때 T 대신 Optional<T>를 반환할 수 있게 되었다.

그렇다면 Optional<T> 을 어떻게 사용하는지 알아보자.

Optional 사용하기

null

메서드가 특정조건에서 값을 반환할 수 없을 때 어떻게 해야할까?

자바 8 이전에는 두 가지 선택지가 있었다.

null을 반환하거나 예외를 발생시키는 것이다.

자바 8부터 Optional이 추가되어 null을 반환하거나 예외를 발생시키지 않아도 된다.

Optional이 갖는 장점을 예제로 살펴보자.

List<Integer> 를 입력 받아서, 최대값을 반환하는 메서드가 있다.

public class Main {
	public static Integer max(List<Integer> collection) {
        Integer result = null;
        for (Integer integer : collection) {
            if (result == null || integer.compareTo(result) > 0) {
                result = Objects.requireNonNull(integer);
            }
        }
        return result;
    }
}

이 메서드는 입력받은 List<Integer>이 비었을 경우 null을 반환한다.

public static void main(String[] args) {
        final Integer maxNumber1 = Main.max(List.of(1, 2, 3));
        final Integer maxNumber2 = Main.max(List.of());

        System.out.println(maxNumber1); // 3
        System.out.println(maxNumber2);	// null
    }

이 메서드를 사용하는 클라이언트는 메서드의 반환값이 null 임을 예상하기 힘들다.

메서드가 반환값이 null일 수도 있다는 사실을 암시하지 않기 때문이다.

이러한 메서드는 NullPointException을 발생시킬 위험이 크다.

예외

null 사용을 피하기 위해 예외를 발생시킬 수 있다.

빈 리스트가 입력되면 예외를 발생시키도록 수정한 코드이다.

    public static Integer max(List<Integer> numbers) {
        if (numbers.isEmpty()) {	//예외 발생 코드
            throw new IllegalArgumentException("빈 리스트 입니다.");
        }
        Integer result = null;
        for (Integer integer : numbers) {
            if (result == null || integer.compareTo(result) > 0) {
                result = Objects.requireNonNull(integer);
            }
        }
        return result;
    }

max 메서드가 더 이상 null을 반환할 일이 없어졌다.

그러나 메서드의 유연함이 덜어졌다.

또한 프로그램에 사소한 예외사항이 하나 추가되었다.

이것은 과도한 예외의 사용일 수 있다. 예외는 발생 비용이 크므로 사용 시 주의해야 한다.

Optional

자바 8부터는 Optional을 적용할 수 있다.

public class Main {
    public static Optional<Integer> max(List<Integer> numbers) {
        if (numbers.isEmpty()) {
            return Optional.empty();
        }
        
        Integer result = null;
        for (Integer integer : numbers) {
            if (result == null || integer.compareTo(result) > 0) {
                result = Objects.requireNonNull(integer);
            }
        }
        return Optional.of(result);
    }
}

Optional.empty()는 빈 Optional을 생성한다.

Optional.of(value)는 값이 든 Optional을 생성한다.

겉으로 보기에는 큰 차이가 없어 보이지만, 클라이언트 측에서 차이가 난다.

메서드의 반환 타입이 Optional<T>이 되었기 때문이다.

public static void main(String[] args) {
        final Optional<Integer> maxNumberOptional1 = Main.max(List.of(1, 2, 3));
        final Optional<Integer> maxNumberOptional2 = Main.max(List.of());
    }

메서드의 반환 타입이 Optional<T>이므로 API 사용자는 반환 값이 null 일 수 있음을 명확히 알게 됐다.

(Optional은 Checked Exception과 취지가 비슷하다.)

따라서 Optional을 반환하는 메서드는 null을 반환하는 메서드보다 NullPointerException을 피하기 쉽다.

또한 특정 상황에서 예외를 발생시키는 것보다 메서드가 유연해진다.

Optional 생성하기

Optional은 객체 생성을 위한 세 가지 정적 팩터리 메서드를 제공한다.

Optional.empty()

빈 Optional을 반환한다.

Optional.of(T value)

value를 포함하는 Optional을 반환한다.

만약 value 가 null이면 NullPointerException이 발생한다.

Optional.ofNullable(T value)

value가 존재하면 value를 포함하는 Optional을 반환한다.

value가 존재하지 않으면 빈 Optional을 반환한다.

Optional의 값 사용하기

Optional.get()

Optional에 값이 존재하면 값을 반환한다.
값이 존재하지 않는다면, NoSuchElementException이 발생한다.

값이 존재하지 않는 경우 예외를 피하기 위해 Optional.isPresent()을 사용할 수 있다.

Optional.isPresent()는 값이 존재하면 true를 반환한다. 값이 존재하지 않으면 false를 반환한다.

하지만 isPresent() - get() 조합은 추천되는 방법이 아니다.

orElse(), orElseGet(), orElseThrow() 등의 방법으로 더 짧고 명확한 코드를 작성할 수 있기 때문이다.

Optional.orElse(T other)

Optional에 값이 존재하지 않는 경우, 기본값 혹은 기본값을 반환하는 메서드를 설정할 수 있는 방법이다.

Optional에 값이 존재하면 해당 값을 반환한다.

값이 존재하지 않는다면, other를 반환한다.

Optional.orElseGet(Supplier<? extends T> other)

Optional에 값이 존재하지 않는 경우, 실행할 디폴트 메서드를 설정할 수 있는 방법이다.

Optional에 값이 존재하면 해당 값을 반환한다.

값이 존재하지 않는다면, Supplier에서 제공하는 값을 반환한다.

orElse와 orElseGet의 차이

orElse() 메서드는 Optional에 값이 존재하는 경우에도 호출된다.

반면, orElseGet() 메서드는 Optional에 값이 존재하지 않는 경우에만 호출된다.

따라서 디폴트 메서드를 실행하는데 비용이 크거나, Optional이 비어있을 때만 기본값을 생성하고 싶을 때는 orElseGet()을 사용하는 것이 좋다.

Optional.orElseThrow(Supplier<? extends X> exceptionSupplier)

Optional에 값이 존재하지 않는 경우, 실행할 예외 메서드를 설정할 수 있는 방법이다.

Optional에 값이 존재하면 해당 값을 반환한다.

값이 존재하지 않는다면, Supplier에서 생성한 예외를 발생시킨다.

Optional.ifPresent(Consumer<? super T> consumer)

Optional에 값이 존재하는 경우, 실행할 consumer 메서드를 설정할 수 있는 방법이다.

Optional에 값이 존재하는 경우 consumer가 실행된다.

Optional에 값이 존재하지 않는 경우 아무 일도 일어나지 않는다.

Optional.map(Function<? super T, ? extends U> mapper)

Optional에 값이 존재하는 경우, 실행할 Function 메서드를 설정할 수 있는 방법이다.

Optional에 값이 존재하는 경우 map의 인수로 제공된 함수가 해당 값을 바꾼다.

이때 map 연산의 결과는 Optional에 담겨서 반환된다.

Optional에 값이 존재하지 않는 경우 아무 일도 일어나지 않는다.

Optional.flatMap(Function<? super T,Optional<U>> mapper)

Optional에 값이 존재하는 경우, 실행할 Function 메서드를 설정할 수 있는 방법이다.

이 메서드는 map(Function)과 유사하다.

그러나 map은 Function 메서드의 결과가 Optional인 경우, 연산의 결과가 Optional<Optional<T>> 이 된다.

이러한 중첩 Optional을 막기 위해 flatMap(Function)을 사용할 수 있다.

flatMap은 Function 메서드의 결과가 Optional인 경우, 그대로 반환한다.

Optional에 값이 존재하지 않는 경우 아무 일도 일어나지 않는다.

Optional 사용 시 주의사항

Optional에 null을 초기화하지 말자.

// 잘못된 방법
public Optional<Integer> max() {
	Optional<Integer> result = null;
    //...
}

// 적절한 방법
public Optional<Integer> max() {
	Optional<Integer> result = Optional.empty();
    //...
}

Optional은 값을 담지 않을 수 있는 컨테이너 객체이다.

Optional을 직접적으로 null로 초기화하는 것은 Optional을 도입한 취지에 완전히 어긋나는 행위이다.

빈 Optional이 필요하다면 Optional.empty() 메서드를 사용하자.

컬렉션, 스트림, 배열, 옵셔널 같은 컨테이너 타입을 Optional로 감싸지 말자.

// 잘못된 방법
List<Integer> numbers = users.getUserNumbers();
return Optional.ofNullable(numbers);

// 적절한 방법
List<Integer> numbers = users.getUserNumbers();
if (numbers == null) {
	return Collections.emptyList();
}
return numbers;

빈 컬렉션을 반환하는 것만으로도 충분히 의도를 전달할 수 있다.

만일 Optional로 감싸서 반환한다면, Optional 처리 코드를 불필요하게 작성해야 한다.

Optional을 인스턴스 필드로 사용하지 말자.

자바 9 공식 문서에 Optinal에 관한 API note 가 추가되었다.

Optional is primarily intended for use as a method return type where there is a clear need to represent "no result," and where using null is likely to cause errors. A variable whose type is Optional should never itself be null; it should always point to an Optional instance. -oracle

결국 Optional은 메서드의 반환 타입 외에 사용할 목적으로 만들어진 것이 아니다.

Optional을 다른 방식으로 사용하는 것은 만들어진 의도와는 다르게 사용하는 것이다.

다음과 같은 코드를 보자.

// 잘못 사용된 방식
public class Homin {
    private Optional<Hair> hairOptional; //null 일 수 있는 필드

    public Homin(final Hair hair) {
        this.hairOptional = Optional.ofNullable(hair);
    }

    public Optional<Hair> getHair() {
        return hairOptional;
    }
}

// 적절한 방식
public class Homin {
    private Hair hair;	//null 일 수 있는 필드

    public Homin(final Hair hair) {
        this.hair = hair;
    }

    public Optional<Hair> getHair() {
        return Optional.ofNullable(hair);
    }
}

Homin 클래스를 사용하는 클라이언트는 getHair()의 반환값이 Optional인 것 만으로도 Hair가 null 일 수 있음을 알 수 있다.

만약 필드에 Optional 타입이 있다면 클래스 내부에서 필드를 사용하는 모든 경우에 Optional에 관한 처리를 따로 해주어야 한다.

또한 Optional 이 Serializable 을 구현하지 않았기 때문에, 직렬화 시 문제가 발생할 수 있다.

0개의 댓글