arrow_upward
본문 바로가기
Java

[Java] Optional 개념 및 사용법

by dawncode 2024. 9. 11.

자바 8버전에서 추가된 Optional 클래스의 개념과 사용방법을 정리해본다.

 

null

자바에서 null 값은 참조형 변수가 아무 객체를 가리키고 있지 않음을 나타내는 값이다.

null 값은 참조형 변수의 초기값으로 아직 참조할 객체가 없음을 명시할 때, 필요 없는 객체를 가비지 컬렉터가 처리할 수 있도록 참조를 끊기 위해 사용된다.

 

NullPointerException

NullPointerException은 null 값을 참조하려고 할 때 발생하는 예외다. 객체나 변수가 초기화되지 않았거나, 할당되지 않은 객체를 사용하려고 시도할 때 발생한다.



Optional

NullPointerException은 자바 개발자들이 자주 겪게되는 문제로, 코드의 안정성을 떨어뜨리고 디버깅을 힘들게 한다.
참조형 객체의 값을 할당할 때 null일 수 있기 때문에 올바른 값인지 아니면 잘못된 값인지 판단할 수 없다.

Optional 클래스는 저바 8버전에서 추가된 기능으로 NullPointerException 문제를 해결하고 코드의 가독성을 높이기 위해서 도입됐다.
Optional 클래스는 참조형 객체를 감싼다. 값이 존재하면 객체를 반환하고, 값이 없으면 Optional.empty()를 반환한다. Optional.empty()는 Optional의 특별한 싱글톤 인스턴스를 반환하는 정적 팩토리 메서드다.

객체에 null 대신 Optional<T>을 사용하면, String 객체라면 Optional<String>이 되며, 이는 값이 없을 수 있다는 것을 명시적으로 보여준다.




Optional 객체 생성

Optional을 사용하기 위해서 Optional 객체를 생성하는 다양한 방법을 확인해보자.

빈 Optional 객체를 생성하려면 Optional.empty()를 사용해서 얻을 수 있다.

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



null이 아닌 값으로 Optional 객체를 만들 때는 Optional.of()로 값을 포함하는 Optional을 만들 수 있다.
이 때 Optional.of()의 인수값이 null이라면 NullPointerException이 발생하므로 주의해야 한다.

Optional<String> optionalString = Optional.of(str);



null값으로 Optional 객체를 만들 때는 정적 팩토리 메서드인 Optional.ofNullable()로 값을 저장하는 Optional 객체를 만들 수 있다.
Optional.ofNullable()을 사용하면 인수가 null이면 Optional.empty가 반환된다.

Optional<String> optionalString = Optional.ofNullable(str);




Optional 객체의 값 읽기

이전에 Optional 객체를 생성하는 방법들을 확인했다면 이번에는 Optional 객체의 값을 읽는 방법들을 살펴본다.

get()

get()은 값을 읽는 가장 간단한 메서드지만, 안전하지 않은 메서드다. Optional.get()은 래핑된 값이 있으면 해당 값을 반환하고 값이 없으면 NoSuchElementException을 반환한다.
Optional에 값이 반드시 존재한다고 가정할 수 있는 상황이 아니면 get()은 사용하지 않는 것이 좋다.

Optional<String> optionalString = Optional.ofNullable(str);
String value = optionalString.get(); // 값이 존재하지 않으면 NoSuchElementException 발생



orElse()

orElse(T other)는 Optional이 값을 포함하지 않을 때 기본 값을 제공할 수 있다.

Optional<String> optionalString = Optional.ofNullable(str);
String value = optionalString.orElse("default");



orElseGet()

orElseGet(Supplier<? extends T> other)은 orElse()에 대응하는 lazy 버전의 메서드다. Optional의 값이 없을 때만 Supplier가 실행된다.
디폴트 메서드를 만드는 데 시간이 걸리거나(효율성 때문) Optional이 비어있을 때만 기본값을 생성하고 싶다면(기본값이 반드시 필요할 때) 사용한다.

Optional<String> optionalString = Optional.ofNullable(str);
String value = optionalString.orElseGet(Sample::getDefault);



orElse()orElseGet()의 차이점은 다음과 같다.

  • orElse()는 기본 값을 미리 계산하기 때문에 Optional 객체가 값이 비어있든 아니든 상관없이 메서드의 값이 호출된다.
  • orElseGet()은 Supplier를 통해 지연 평가(lazy evaluation)를 하기 때문에 Optional 객체가 비어있다면 Supplier를 호출해서 값을 반환하고, Optional 객체의 값이 존재한다면 그 값을 반환한다.

 

orElseThrow()

orElseThrow(Supplier<? extends X> exceptionSupplier)는 Optional 객체가 비어있을 때 예외를 발생시킨다.
Supplier를 통해 원하는 예외를 지정해서 예외를 선택할 수 있고, orElseThrow()처럼 예외를 지정하지 않으면 NoSuchElementException이 발생한다.

Optional<String> optionalString = Optional.ofNullable(str);
String value = optionalString.orElseThrow(() -> new IllegalArgumentException("값이 존재하지 않습니다."));

 

ifPresent()

ifPresent(Consumer<? super T> consumer)를 사용하면 값이 존재할 때 인수로 넘겨준 동작을 실행할 수 있다. 값이 없으면 동작하지 않는다.

Optional<String> optionalString = Optional.ofNullable(str);
optionalString.ifPresent(System.out::println);

 

ifPresentOrElse(Consumer<? super T> action, Runnable emptyAction)은 Optional 객체가 비었을 때 실행할 수 있는 Runnable을 인수로 받는 다는점만 다르다.

isPresent()

isPresent()로 Optional 객체가 값을 포함하는지 여부를 판단할 수 있다. 값이 존재하면 true, 존재하지 않으면 false를 반환한다.

if (MemberRepository.findById(request.getId()).isPresent()) {
    throw new IllegalArgumentException("중복된 회원입니다.");
}




그 외의 Optional이 제공하는 메서드

설명된 메서드 외에 Optional이 제공하는 메서드를 확인해본다.

filter()

filter(Predicate<? super T> predicate)는 Predicate를 인수로 받아서 Option 객체가 값을 가지면서 Predicate와 일치하면 filter()는 아무연산을 하지 않고, 일치하지 않으면 Optional.empty를 반환한다.
연산하는 Optional 객체가 비어있다면 filter 연산은 아무 동작도 하지 않고, 값이 존재할 때만 그 값에 Predicate를 적용한다.

Optional<String> optionalString = Optional.ofNullable(str);
optionalString.filter((value) -> value.equals("default")).ifPresent(System.out::println);



map()

map(Function<? super T, ? extends U> mapper)은 Optional 객체의 값을 매핑함수를 적용한 Optional 객체를 반환한다. 이 때 빈 Optional 객체는 매핑함수가 적용되지 않는다.

Optional<String> optionalString = Optional.ofNullable(str);
Optional<String> optionalEmptyString = Optional.empty();
String upperValue = optionalString.map(String::toUpperCase).get();
String empty = optionalEmptyString.map(String::toUpperCase).orElseGet(Test::getDefault);




기본형 Optional을 사용하지 않는 이유

스트림에서 기본형 특화 스트림인 IntStream, LongStream 등을 사용하면 오토박싱되는 비용을 줄여 성능을 향상 시킬 수 있다.
하지만 Optional의 최대 요소는 한 개이므로 OptionalInt, OptionalLong과 같은 기본형 특화 Optional 객체로는 성능을 개선할 수 없다. 이러한 이유로 기본형 Optional 객체는 지양하는 것이 좋다.



Optional을 사용한 예제

Optional 객체는 NullPointerException을 방지하기 위해서 null을 감싸는 객체이므로, 반환값이 null 이 올 수 있을 때 사용하면 된다.

자바의 Map 객체의 get()으로 Map 객체의 요소를 Key로 조회할 때 값이 엾으면 null을 반환한다. 이 때 Optional로 감싸서 처리할 수 있다.

Map<Integer, String> map = new HashMap<>();
Optional<String> result = Optional.ofNullable(map.get(1));



자바에서는 클래스에서 다른 클래스를 의존할 때 참조형은 null일 수 있으므로 객체의 참조값을 조회할 때도 Optional로 감싸서 처리할 수 있다.

public class Bank {
    private String name;
    private Money money;

    // getter and setter ...
}

public class Money { 
    //... 
}

public class Main {
    public static void main(String[] args) {
        Bank bank = new Bank();
        Optional<Money> money = Optional.ofNullable(bank.getMoney());
    }
}



Spring Data Jpa에서는 엔티티를 조회하는 findById()의 반환 값을 Optional 객체로 감싸서 반환한다.

Optional<Member> findMember = memberRepository.findById(1L);
findMember.orElseThrow(() -> new IllegalArgumentException("회원이 존재하지 않습니다."))