본문 바로가기
학습/Java

Optional의 배경지식

코동이 2021. 2. 18.

Optional의 의도된 쓰임새는 이펙티브 자바 3판 [ 아이템 55 ] 옵셔널 반환은 신중히 하라에서 잘 나와있는데, stackoverflow의 Should Java 8 getters return optional type? 글에서 해당 기술의 개발자가 답글한 내용 중에 핵심 부분을 가져왔습니다. (다수 의역)

 

 

Our intention was to provide a limited mechanism for library method return types where there needed to be a clear way to represent "no result", and using null for such was overwhelmingly likely to cause errors. For example, you probably should never use it for something that returns an array of results, or a list of results; instead return an empty array or list. You should almost never use it as a field of something or a method parameter. I think routinely using it as a return value for getters would definitely be over-use.   -Brian Goetz


우리의 의도는 메서드 리턴 타입 라이브러리를 위한 제한된 메커니즘을 제공하는 것이었습니다. 리턴 타입이 "결과 없음" 을 표현하는 명백한 방식이 필요할 때입니다. 그러한 이유로 단순히 null을 사용하는 것은 에러를 유발할 가능성이 압도적으로 높습니다. 결과의 배열을 리턴할 때나, 리스트를 리턴할 때는 절대로 Optional을 사용해서는 안됩니다; 대신에, 빈 배열이나 빈 리스트를 리턴해야합니다. 대부분의 메서드 파라미터나 어떠한 필드로서도 Optional을 사용해서는 안됩니다. getter의 리턴 타입으로 사용하는 것 또한 과하다고 생각합니다.

즉, 리턴타입의 "결과 없음"을 단순히 null로 만드는 것은 많은 에러를 유발할 수 있기 때문에, Optional을 통해 처리를 하도록 만든 것입니다. 

 

오라클 문서에 따르면, Optional은 컨테이너 객체입니다. null일수도 있고 아닐 수도 있습니다. NullPointerException 을 예방하기 위해서 Java 8에서 처음 소개되었습니다. Optional은 다른 객체에 대한 참조를 포함하는 wrapper class입니다. 이러한 맥락에서, 객체는 단순히 메모리 위치를 가리키는 포인터이고, 아무것도 가리키지 않을 수 있습니다.  

 

Optional 이전의 방법

Java 8 이전에는, 프로그래머들은 Optional 대신에 null을 반환해야만 했습니다. 이 접근법에는 몇가지 문제가 있습니다. 첫째는 null 값이 특정한 값을 가질 수도 있지만, 그렇게 표현 할 방법이 없었다는 것입니다. 반대로, Optional을 리턴하는 것은 그 안에 값이 "없을 수도" 있다는 API에서의 명확한 선언입니다. 과거에는 만약 우리가 NullPointerException을 가지지 않도록 "확신"하기 위해서, 각각의 참조에 대해서 명확하게 null 확인을 했어야 했습니다. 다음의 코드에서 확인 할 수 있습니다.

 

    private void getIsoCode( User user){
        if (user != null) {
            Address address = user.getAddress();
            if (address != null) {
                Country country = address.getCountry();
                if (country != null) {
                    String isocode = country.getIsocode();
                    if (isocode != null) {
                        isocode = isocode.toUpperCase();
                    }
                }
            }
        }
    }

이 과정을 쉽게 하기 위해서, Optional의 사용 방법을 확인하겠습니다.

Optional의 생성

1. static <T> Optional<T> empty()

//Creating an empty optional
Optional<String> empty = Optional.emtpy();

비어있는 Optional 객체를 반환합니다. 이 Optional 안에는 어떠한 값도 존재하지 않습니다. 혹시 강한 충동이 있더라도, Option.empty()로 리턴된 객체와 어떠한 객체를 "==" 으로 비교하는 것은 피해야 합니다. singleton이라는 보장이 없기 때문에, 대신에 isPresent()를 사용해야 합니다.

 

2. static <T> Optional<T> of(T value)

//Creating an optional using of
Strin name = "java";
Optional<String> opt = Optional.of(name);

of 메서드는 null이 아닌 매개변수를 기대합니다; 그렇지 않으면(매개변수가 null이라면), NullPointerException을 발생시킬 것입니다. 그래서, 매개변수가 null인지 아닌지를 모른다면, 아래에 나오는 ofNullable을 사용해야 합니다.

 

3. static <T> Optional<T> ofNullable(T value)

//Possible null value
Optional<String> optional = Optional.ofNullable(name());

private String name() {
	String name = "Java";
    return (name.length()>5)? name : null;
}

 null이 아니면 매개변수의 값을 리턴할 것이고, null이라면 empty Optional을 리턴할 것입니다.

ofNullable을 사용한다면, null을 참조하더라도 NullPointerException을 발생시키지 않습니다. 단지 empty Optional 객체를 리턴할 뿐입니다.

 

Optional 값의 존재 확인하기

1. boolean isPresent()

//ispresent
Optional<String> optional1 = Optional.of("javaone");
if(optional1.isPresent()){
	//Do something, normally a get
}

만약 값이 존재한다면 true를 리턴합니다, 값이 존재하지 않는다면 false를 리턴합니다. 즉, 컨테이너화된 객체가 null이 아니라면 true를 리턴하고, null이면 false를 리턴합니다. 이 메서드는 보통 객체에 대한 연산을 하기 이전에 Optional이 처음으로 호출됩니다.

 

2. boolean isEmpty()

//isempty
Optional<String> optional1 = Optional.of("javaone");
if(optional1.isEmpty()){
	//Do something
}

만약 값이 존재한다면 false를 리턴합니다, 값이 존재하지 않는다면 true를 리턴합니다. isPresent와 반대로 사용하면 되며, Java 11 이상의 버전에서만 사용 가능합니다.

 

3. void ifPresent(Consumer<? super T> consumer)

//ifpresent
Optional<String> optional1 = Optional.of("javaone");
optiona1.ifPresent(s -> System.out.println(s.length()));

만약 값이 존재한다면, 값을 이용해 특정한 consumer를 호출합니다; 그렇지 않으면, 아무것도 하지 않습니다.

Java 8을 처음 사용한다면, consumer가 무엇인지 익숙하지 않을 것입니다; 쉽게 말해, consumer는 매개변수를 사용하지만 아무것도 리턴하지 않는 것을 말합니다. ifPresent 사용은 1석 2조의 효과를 가지고 있습니다. 값이 존재하는지도 확인할 수 있고, 하나의 메소드로 다음 연산까지 할 수 있기 때문입니다.

 

Optional 값 조회하기

 

1. Tget()

//get
Optional<String> optional1 = Optional.of("javaone");
if(optional1.isPresent()){
	String value = optional1.get();
}

만약 Optional 안에 값이 존재한다면, value를 리턴합니다. 값이 존재하지 않는다면, NoSuchElementException을 발생시킵니다. 아마도 프로그래머가 원하는 것은 Optional 안에 저장되어 있는 값일 것이고, 단순히 get()을 호출함으로써 조회할 수 있습니다. 그러나, 이 메서드는 값이 null일 때 예외를 던지기 때문에 대안으로 orElse() 메서드가 있습니다.

 

2. TorElse(Tother)

//orElse
String nullName = null;
String name = Optional.ofNullable(nullName).orElse("default_name");

만약 값이 존재하면 value를 리턴합니다, 값이 존재하지 않는다면 other을 리턴합니다. 이 메서드는 Optional 객체 안에 래핑된 value을 조회하기 위해 사용하며 default 값으로 행동하는 하나의 매개변수(other)를 가지고 있습니다. orElse() 메서드는 래핑된 value(존재하는 경우)와 매개변수(other)를 리턴합니다. Optional 클래스는 null이 호출됨에도 불구하고 value를 얻을 수 있도록 또다른 메서드인 orElseGet()을 제공합니다. 

 

3. ToElseGet(Supplier<? extends T> other)

//orElseGet
String name = Optional.ofNullable(nullName).orElsegET(() -> "john");

만약 값이 존재한다면 value를 리턴하고, 값이 존재하지 않는다면 other을 발생하고 이 결과를 리턴합니다.

orElseGet() 메서드는 orElse() 메서드와 비슷합니다. 그러나, Optional 값이 존재하지 않는다면, 리턴 값을 가지는 대신에, orElseGet() 메서드는 supplier 함수 인터페이스를 가집니다, 이것이 실행되고 결과값을 리턴합니다.

 

Optional을 사용하는 최고의 방법

 다른 프로그래밍 언어의 특징과 마찬가지로, 이 기능은 정확하게 의도한 대로사용될 수도 있으며, 잘못 사용 될 수 있습니다. Optional class를 제대로 사용하는 최선의 방법을 위해서 다음을 잘 이해해야 합니다.

 

1. 해결하려는 문제가 무엇인가

Optional은 자바 시스템에서 NullPointerException의 횟수를 줄이기 위한 시도입니다, 가끔 리턴값이 잘못되었다는 가능성을 설명하는 더욱 명백한 API를 만드는 것이 가능합니다.

 

만약 Optional이 처음부터 있던 기능이었다면, 대부분의 라이브러리와 어플리케이션은 잘못된 리턴값을 더 명백하게 처리 했을 것입니다. 즉, NullPointerException와 일반적으로 발생하는 전체 버그를 더 많이 줄였을 것입니다.

 

2. 해결하지 않으려는 문제가 무엇인가

Optional은 모든 종류의 NullPointerException을 피하기 위해서 만들어졌다는 것을 의미하지 않습니다. 예를 들어, 메서드의 파라미터와 생성자는 여전히 테스트를 해야 합니다.

 

null을 사용할때와 마찬가지로, Optional은 값이 없다는 것의 의미를 전달하는데 도움이 되지 않습니다. 비슷한 방식으로 null은 값이 없는 등의 다양한 상황들을 의미할 수 있기 때문에, Optional도 다양한 의미를 가질 수 있습니다. ( 이 말은 우리가 리턴값이 null인지 아닌지 가시적인 코드에서 확인할 수 없듯이, Optional 리턴값이 null인지 아닌지 확인할 수 없다는 것입니다. 대신, Optional은 null에 대한 여지를 남길 수 있도록 프로그래밍이 가능합니다.)

 

메서드의 호출자는 Optional을 적절하게 사용하길 원한다면 "Optional 이 없다는 의미"를 이해하기 위해서 javadoc을 계속 확인해야 합니다.

 

또한, 검사했던 예외가 빈 블럭에서 잡힐 수 있는 것과 비슷한 방식으로, 사용자가 get()을 호출하고, 계속 코드를 사용하는 것을 예방하는 어떠한 장치도 없습니다.

 

3. 언제 사용해야 할 것인가

Optional의 주된 사용 목적은 리턴 타입입니다. 이 타입의 객체를 얻은 이후에, 값이 존재한다면 따로 빼낼 수도 있으며, 존재하지 앟는다면 대안의 행위를 제공할 수 있습니다.

 

Optional 클래스의 굉장히 유용한 하나의 예는 유용한 API를 설계하기 위해 stream 혹은 Optional 값을 반환하는 다른 메서드와 연동하는 것입니다.

User user = users.stream().findFirst().orElse(new User("default", "1234"));

4. 언제 사용하지 않아야 할 것인가

a) serializeable하지 않기 때문에 클래스의 필드로서 사용하지 말 것

만약에 Optional 값을 포함하는 객체를 serialize한다면, Jackson 라이브러리는 Optional을 기본 객체로 다룰 수 있도록 지원합니다. 이것이 의미하는 것은 Jackson은 비어있는 객체를 null로 다루도록 하며, 값을 가지고 있는 객체는 그 값을 포함하고 있는 필드로서 다루도록 합니다. 이것은 기능적으로 jackson-modules-java8 프로젝트에서 확인할 수 있습니다.

( 이 말은 결국 우리가 Optional을 사용하지만, null을 가지게 되어 많은 에러를 낼 수 있음을 의미합니다.)

 

b) 생성자의 매개변수로 사용하지 말고, 불필요하게 복잡한 코드를 야기하는 메서드로 사용하지 말 것

User user = new User("john@gmail.com", "1234", Optional.empty());

 

정리

Optional은 값에 기반한 클래스입니다; Optional의 객체에 대해서 "==" 검사, hash code나 synchronization과 같은 동일성 연산들은 예측할 수 없는 결과를 가지기 때문에 피해야 합니다.

반응형