반응형

1. 개요

 자바 8 이전에는 빈 값에 대한 처리 선택지가 '예외' 혹은 'null 반환'이었다. 이 둘 모두 허점이 있다.

예외는 정말 예외적인 상황에서만 사용해야한다는 것과(빈 값이라고 예외를 날려서는 안될 케이스도 있다. ex. 검색결과)

예외를 생성할 때 스택 추적을 하므로 이에 대한 비용 문제가 있다. null을 반환하면 위 문제가 생기지 않지만 언젠가, 그리고 어디선가 NullPointException 이 발생하여 시스템 버그를 초래할 수 있다. 그런데 자바 8 이후 또 하나의 선택지가 생겼다. 그게 바로 Optional<T> 이다. 

 


2. Optional<T> 이란?

 옵셔널은 1개의 null이 아닌 T 타입 객체를 담거나, 아무것도 담지 않을 수 있는 불변 컬렉션이다. 옵셔널을 사용하지 않는다면 보통 T를 반환할테지만 상황에 따라 아무것도 반환하지 않아야 할때가 있다면 T 대신 Optional<T> 를 반환하도록 선언하면 된다. 옵셔널을 반환하는 메서드는 예외를 던지는 메서드보다 유연하고 사용하기 쉬우며, null을 반환하는 메서드보다 오류 가능성이 낮다.

 


3. max 값을 구하는 예제

 

최댓값을 구하는 메서드이다. 파라미터로 들어온 Collection의 요소가 없을 경우 예외를 발생시키고 있다.

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(); // 빈 옵셔널을 반환함으로써 클래스 외부에서 예외를 발생시킬 수도, 시키지 않을 수도 있다. Null도 아니다!

    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.empty(), 값이 든 옵셔널은 Optional.of(value)로 처리한게 끝이다. 이 방식은 위 방식과는 다르게 호출한 클라이언트에서 Optional 값에 대한 처리가 가능하다. 원하는 예외메시지를 발생시킬수도, 발생시키지 않을수도 있고, 더 나아가 기본 값을 넣을 수도 있다. 앞선 코드보다 훨씬 유연하고 깔끔한 것을 볼 수 있다.

Integer maxValue = max2(list).orElse(0); // 빈 옵셔널일 경우 0으로 설정

//클라이언트에 따른 예외 메시지 변경
Integer maxValue2 = max2(list).orElseThrow(() -> new IllegalArgumentException("요청 리스트가 비어있습니다."));

 

 

참고로 Optional.of(value)에 null을 넣으면 NPE가 발생하므로 주의해야한다. 또한 옵셔널을 반환하는 메서드는 절대 null을 반환하면 안된다. 옵셔널을 도입한 취지를 저버리는 행위이기 때문이다.

 


4. Optional 을 사용해야하는 기준

그렇다면 null, 예외를 던지는 대신 옵셔널을 선택하는 기준은 뭘까? 옵셔널은 검사 예외와 취지가 비슷하다. 즉, 반환 값이 없을 수도 있음을 API 사용자에게 명확히 알려준다. 반환 값이 없을 경우에 대한 처리를 사용자가 작성해야하므로 null 보다 안전하고 깔끔하게 처리할 수 있다.

 

클라이언트 입장에서 이를 사용한다면 클라이언트는 옵셔널에 대한 처리를 클라이언트 코드에서 선택하면 된다.

1) 기본 값 설정

int maxValue = max2(list).orElse(0);  // 기본 값 설정

 

2) 원하는 예외 설정

// 원하는 예외 설정
String maxString = max2(wordList).orElseThrow(() -> new RuntimeException("단어 리스트가 비어있습니다. 리스트를 확인해주세요"));

 

3) 항상 값이 유효하다고 가정할때 설정

int value = max2(list).get(); // 항상 값이 있다고 가정하고 get!

 

 

이 외에도 filter, map, ifPresent 등 다양한 메서드들을 지원하고 있다. 앞선 기본 메서드로 처리가 힘들다면 API 문서를 참조해 문제를 해결해줄 수 있는 메서드가 있는지 찾아보자. 만약 적합한 메서드를 찾지 못했다면 isPresent 메서드를 활용할 수 있다. isPresent 메서드는 가스레인지의 안전벨브 역할로, 옵셔널 값이 채워져있다면 true, 비어있다면 false를 리턴한다. 

 

자바 9 버전부터 지원하는 Optional.map 메서드를 사용해 원하는 타입을 갖는 Optional 객체로 변환할 수도 있다. 아래는 Integer 타입이 들어있는 최대값을 받아 String 타입으로 변환한다. 값이 없을 경우에 대해 "N/A" 값이 반환되도록 orElse 체인 메서드로 처리했다.

String res = max2(list).map(val -> Integer.toString(val)).orElse("N/A");

 

 

참고로 이를 지원하지 않는 자바 8 버전의 경우 아래와 같이 작성할 수 있다. isPresent로 필터링 후 get으로 빼온다.

streamOfOptionals
    .filter(Optional::isPresent)
    .map(Optional::get)

 

 

자바 9에서는 Optional 에 stream() 메서드가 추가되었다. Optional을 stream으로 변환해주는 어댑터다. 옵셔널에 값이 있으면 그 값을 원소로 담은 스트림으로 한단계 벗겨주는 것이다. 어찌됐든 이런 저런 메서드들을 많이 지원하니 찾아보는 것을 권장한다.

 


5. 반환값으로 옵셔널을 사용했을때의 단점

결합도를 낮추고 유연성과 코드의 깔끔함을 더해주지만 구조상 단점이 있다. 객체를 박싱하는 옵셔널 특성 상 박싱하고자 하는 객체가 많을수록 Optional 객체를 생성하고 박싱하는 비용이 발생한다. 언박싱할때도 비용이 발생하는 건 마찬가지이다. 때문에 리스트, 스트림, 배열 등에 사용할 요소들에 대해 별 생각없이 Optional 을 사용한다면 성능 측면에서 문제가 발생할 수 있다. 성능이 중요한 상황에서는 옵셔널을 사용하지 않는 것이 좋다.

 


6. 기본 타입을 담는 옵셔널도 있다.

int, long, double 과 같은 기본 타입에 대해 Optional 을 사용할 땐 Integer, Long, Double 과 같은 참조 타입으로 박싱하여 사용하지 말고 OptionalInt, OptionalLong, OptionalDouble 과 같은 옵셔널 타입을 사용하자. 박싱된 기본 타입을 담은 옵셔널을 반환하는 일은 없도록 해야한다.

 


7. 정리

반환 값이 없을 수도 있는 메서드라면 옵셔널 사용을 고려해야한다. 하지만 성능 저하가 뒤따르니, 성능에 민감한 메서드라면 null을 반환하거나 예외를 던지는 편이 나을 수 있다.

반응형

+ Recent posts