본문 바로가기
Old Posts/Java

[Java] Optional 사용법 및 예제

by A6K 2021. 8. 19.

자바 8부터 Optional이라는 클래스가 지원되기 시작했다. Optional 클래스란 어떤 목적으로 사용되는지 알아보자.

Java NPE 예방

자바 프로그램 코드를 작성하다보면 null 값에 대해 고려해야하는 경우가 많다. null 값을 제대로 처리하지 않으면 NPE(NullPointerException)을 만나게된다. 안정적인 실행을 위해 NPE가 발생하지 않도록 중간중간 null 체크를 해줘야하는데, 이게 코드를 여간 더럽히는게 아니다.

예를 들어보자.

List<String> items = getItems();
System.out.println(items.size());

getItems() 메소드를 통해 문자열 리스트 객체를 얻어온 다음 엘리먼트 개수를 출력하는 간단한 코드다. getItems() 메소드가 null 값을 리턴하면 'items.size()' 코드에서 NPE가 발생하게 된다.

따라서 이 코드는 다음처럼 수정해야한다.

List<String> items = getItems();
if (items != null) {
    System.out.println(items.size());
}

조건절이 하나 더들어가 코드가 약간 더 지저분해졌다.

참조 체인을 사용하는 경우엔 더 지저분해진다. House 객체를 받아서 집 주인 이름과 집 주소를 출력하는 소스코드를 생각해보자.

House house = getRandomHouse();
if (house != null && house.getOwner() != null && house.getOwner().getName() != null) {
    System.out.println("House owner : " + house.getOwner().getName());
}

if (house.getAddress() != null) {
    System.out.println("House address : " + house.getAddress());
}

null 값을 가질 수 있는 가능성을 고려해야하기 때문에 조건절의 조건이 더 길어졌다.


Java Optional<T>

자바 8부터 이런 null 값에 대한 처리를 좀 더 깔끔하게 할 수 있도록 Optional 클래스가 추가되었다.

public final class Optional<T> {
    ...
    /**
     * If non-null, the value; if null, indicates no value is present
     */
     private final T value;
    ...
}

Optional<T> 클래스는 null 값일 수도 있는 어떤 변수를 감싸주는 '래퍼(Wrapper)' 클래스다. Optional 클래스는 제너릭(Generic)으로 값의 타입을 지정해줘야한다. Optional 클래스는 여러가지 메소드를 통해 value 값에 접근하기 때문에 바로 NPE가 발생하지 않으며, null 일 수도 있는 값을 다루기 위한 다양한 메소드들을 제공한다.

위에서 봤던 코드를 Optional<String> 클래스를 이용해 다음과 같이 재작성 할 수 있다.

House house = getRandomHouse();
Optional.of(house)
        .map(House::getOwner)
        .map(House::getName)
        .ifPresent(n -> System.out.println("House owner : " + n));

Optional.of(house)
        .map(House::getAddress)
        .ifPresent(a -> System.out.println("House address : " + a));

얼핏봤을 때, 자바 스트림(Java Stream)과 비슷하게 보인다. (관련글 : [Java] 자바 스트림(Stream) 사용법 및 예제)


Java Optional 사용법 - 생성

Optional 객체를 생성하기 위해서는 다음 메소스들을 사용해야한다.

Optional<String> optional = Optional.of(value);

이 경우 value 변수의 값이 null인 경우 NPE 예외가 발생한다. 반드시 값이 있어야 하는 경우에 of() 메소드를 사용한다.

Optional<String> optional = Optional.ofNullable(value);

이 경우 value 변수의 값이 null일 수 있다. value 변수가 null인 경우 Optional.empty()가 리턴된다.

Optional<String> optional = Optional.empty();

빈 Optional 객체를 생성한다. 비어있는 Optional 객체라하면, Optional 객체 자체는 있지만 내부에서 가리키는 참조가 없는 경우를 빈 객체라고 한다. Optional.empty() 객체는 미리 생성되어 있는 싱글턴 인스턴스다.


Java Optional 사용법 - 중간처리

Optional 객체를 가져와서 어떤 처리를 하고 다시 Optional 객체를 반환하는 메소드들이 있다. 중간처리 메소드들을 연이어 이어붙여 원하는 로직을 반복해서 사용할 수 있다.

filter()

filter 메소드의 인자로 받은 람다식이 참이면, Optional 객체를 그대로 통과시키고 거짓이면 Optional.empty()를 반환해서 추가로 처리가 안되도록 한다.

// ABCD
Optional.of("ABCD").filter(v -> v.startsWith("AB")).orElse("Not AB");

// Not AB
Optional.of("XYZ").filter(v -> v.startsWith("AB")).orElse("Not AB");

"ABCD"로 시작한 Optional 객체의 filter() 조건은 참이므로 "ABCD"가 리턴된다. "XYZ"로 시작한 Optional 객체의 경우 filter()에서 빈 Optional 객체라 리턴되므로 orElse()에 적어놓은 "Not AB"가 리턴된다.

Java Stream에서의 filter와 유사하다.

map()

Optional 객체의 값에 어떤 수정을 가해서 다른 값으로 변경하는 메서드다.

// xyz -> 소문자로 변환
Optional.of("XYZ").map(String::toLowerCase).orElse("Not AB");

Java Optional 사용법 - 값을 리턴하는 메소드

중간처리 메소드들은 Optional 객체를 리턴해서 메소드 체인으로 사용할 수 있는 반면, 값을 리턴해서 메소드 체인을 끝내는 메소드들도 있다.

isPresent()

isPresent() 메소드는 Optional 객체의 값이 null인지 여부, 즉 값이 존재하는지 여부만 판단해준다.

Optional.of("TEST").isPresent(); // true
Optional.of("TEST").filter(v -> "Not Equals".equals(v)).isPresent(); // false

ifPresent()

ifPresent() 메소드는 람다식을 인자로 받아, 값이 존재할 때 그 값에 람다식을 적용해준다. 만약 Optional 객체에 값이 없다면 람다식이 실행되지 않는다.

// TEST 출력
Optional.of("TEST").ifPresent(System.out::println);

// 아무것도 출력되지 않음
Optional.ofNullable(null).ifPresent(System.out::println);

get()

Optional 객체가 가지고 있는 value 값을 꺼내온다. 만약 Optional 객체에 값이 없다면, NoSuchElementException이 발생한다.

Optional.of("TEST").get(); // "TEST" 리턴
Optional.ofNullable(null).get(); // NoSuchElementException

orElse()

중간처리 메소드들을 거치면서 혹은 원래 Optional 객체가 비어있었다면, orElse() 메소드에 지정된 값이 기본값으로 리턴된다.

// "Not AB" 리턴
Optional.of("XYZ").filter(v -> v.startsWith("AB")).orElse("Not AB");

orElseGet()

중간처리 메소드들을 거치면서 혹은 원래 Optional 객체가 비어있었다면, orElseGet() 메소드의 인자로 입력되어 있는 Supplier 함수를 적용하여 객체를 얻어온다.

// "Not AB" 리턴
Optional.of("XYZ").filter(v -> v.startsWith("AB")).orElseGet(() -> "Not AB");

orElse() 메소드는 메소드의 인자를 항상 평가한다. 즉, orElse에 객체를 생성해서 Optional 객체가 비어있는 경우 리턴하도록 할 수도 있는데, orElse() 메소드의 인자 평가가 항상 발생한다. 따라서 객체를 생성하는 비용이 크다면 orElse() 메소드를 사용하면 안된다.

대신 orElseGet() 메소드는 Optional 객체가 비어있는 경우에만 Supplier 함수를 실행한다. 따라서 객체를 생성하는 비용이 크다면 orElseGet() 메소드를 사용해야한다.

orElseThrow()

중간처리 메소드들을 거치면서 혹은 원래 Optional 객체가 비어있었다면, Supplier 함수를 실행해 예외를 발생시킨다.

Optional.of("XYZ").filter(v -> v.startsWith("AB")).orElseThrow(NoSuchElementException::new);

Java9에서 추가된 메소드들

or()

중간처리 메소드로 orElse(), orElseGet()과 비슷하다. 하지만 or() 메소드는 Optional 객체를 리턴한다. 메소드 체인 중간에서 Optional.empty()가 되었을 때, Optional.empty() 대신 다른 Optional 객체를 만들어서 뒤쪽으로 넘겨주고 싶을 때 사용한다.

Optional.ofNullable(value)
        .map(value.getValue())
        .or(() -> Optional.ofNullable(user.getFavorite()))
        .orElse("No favorite");

value가 null이거나 value의 getValue() 메소드가 null을 리턴했을 때, Optional.ofNullable(user.getFavorite())를 실행해서 만들어진 Optional 객체를 넘겨준다. 만약 user.getFavorite() 메소드가 null을 리턴한다면 "No favorite" 문자열을 리턴할 것이고 아니라면 getFavorite() 메소드로 얻어진 값이 리턴된다.

ifPresentOrElse()

최종적으로 값을 반환하는 메소드다. ifPresent() 메소드와 유사하지만 인자를 하나 더 받는다. 첫 번째 인자로 받은 람다식은 Optional 객체에 값이 존재하는 경우 실행된다. 두 번째 인자로 받은 람다식은 Optional 객체가 비어있을 때 실행된다.

stream()

stream() 메소드는 중간처리 연산자다. 자바 8에서 Optional 객체가 바로 stream 객체로 전환되지 않아 불편했떤 부분을 해소하기 위한 메소드다.

List<String> result = List.of(Optional.of("ABCD"), Optional.of("ABCC"), Optional.of("XYCD"))
    .flatMap(Optional::stream)
      .filter(v -> v.startsWith("AB"))
        .collect(Collectors.toList());

Java10에서 추가된 메소드

Java 10에서도 메소드가 하나 추가되었다.

orElseThrow()

자바8에서 사용했던 메소드와 동일하다. 하지만 인자를 받지 않는다

Optional.ofNullable(value).orElseThrow(NoSuchElementException::new);
Optional.ofNullable(value).orElseThrow();

위 두 메소드는 동일하다.


기본형에 대한 Optional

int, long, double 형 자료에 대한 래퍼 클래스로 OptionalInt, OptionalLong, OptionalDouble 클래스가 제공된다. Optional, Optional, Optional처럼 사용할 수는 있다. 하지만 이경우 Auto Boxing, Unboxing이 발생한다. 굳이 그럴 필요가 없는 경우에는 기본형 값을 위한 Optional 클래스인 OptionalInt, OptionalLong, OptionalDouble를 사용하도록 하자.

null 값에 대한 처리를 Optional을 이용하면 코드를 조금 더 간결하게 유지할 수 있어 가독성을 향상시킬 수 있다. 다만 Optional을 너무 남용하지는 말자.

댓글