[Java] Optional 사용법
1. Optional을 사용하는 이유
개발을 할 때 많이 만나는 예외가 NullPointerException(NPE)이다. NPE를 피하기 위해 조건문을 사용해서 null 여부를 검사하곤 하지만, null 값을 검사하는 로직이 추가되면서 코드가 복잡해진다.
List<String> datas = getData();
datas.sort(); // datas가 null이면 NullPointerException이 발생함
List<String> datas = getData();
// NPE를 방지하기 위해 조건문으로 null 검사 진행해야 함
if (datas != null) {
datas.sort();
}
조건문을 사용하지 않으면서 NPE를 방지하기 위해 Java8부터 도입된 기능이 Optional<T> 클래스이다. 다시 말해 Optional<T> 클래스는 null이 올 수 있는 값을 감싸는 Wrapper 클래스로써 null값을 가지더라도 NPE를 발생하지 않도록 도와준다.
쉽게 이야기하면 Optional 클래스 내부 변수 value에 값을 저장하기 때문에 값이 null이라도 바로 NPE가 발생하지 않으며, 클래스인 덕분에 각종 메소드를 제공해준다.
public final class Optional<T> {
private final T value;
...
}
2. Optional 활용하기
(1) 값이 없는 경우
Optional은 Wrapper 클래스이다. 따라서 값이 없는 경우는 Optional.empty()로 생성할 수 있다.
Optional<String> optional = Optional.empty();
System.out.println(optional); // Optional.empty
System.out.println(optional.isPresent()); // false
(2) 무조건 값이 있는 경우
반대로 어떤 데이터가 절대 null이 아니라면 Optional.of()로 생성할 수 있다. 만약 Optional.of()로 null을 저장하려고 한다면 NullPointerException이 발생한다.
// Optional의 value는 절대 null이 아니다
Optional<String> optional = Optional.of("MyName");
(3) 값이 null일 수도, 아닐수도 있는 경우
만약 어떤 데이터가 null이 올 수도 있고, 아닐 수도 있는 경우에는 Optional.ofNullable로 생성할 수 있다. 그리고 이후에 orElse 또는 orElseGet 메소드를 이용해서 값이 없는 경우에도 안전하게 값을 가져올 수 있다.
// Optional의 value는 값이 있을 수도, null일 수도 있음
Optional<String> optional = Optional.ofNullable(getName());
String name = optional.orElse("값 없음!"); // 값이 없으면 "값 없음!"을 리턴
- orElse(), orElseGet()
두 메소드 모두 가져온 값이 null 일 때 해당 값을 반환하라는 메소드이다. 두 메소드 모두 null 일 때 대체값을 반환하기 위한 메소드이다.
하지만 차이는 orElse()는 T 타입의 값을 그대로 반환하는 한편,편, orElseGet()은 Supplier의 인터페이스를 통해 그 인터페이스의 결과를 반환한다.
- orElse() : null 여부와 상관없이 '값'을 반환한다.
- orElseGet : 해당 값이 null일 때만 불린다.
public final class Optional<T> {
... // 생략
public T orElse(T other) {
return value != null ? value : other;
}
public T orElseGet(Supplier<? extends T> other) {
return value != null ? value : other.get();
}
}
쉽게 말해서 orElse()는 null 여부와 상관없이 실행되서 값을 반환하기 때문에, 만약 어떤 Optional 객체가 중복을 허용하지 않는데 null이 아니라도 동작하는 로직에 의해서 오류를 반환할 수 있다. 해당 값이 null 인 경우에만 로직이 수행되도록 하려면 orElseGet() 메소드를 실행 시켜야 한다.
3. Optional 사용 방법
1) Optional 변수에 null을 할당하지 말자.
: Optional은 컨테이너/박싱 클래스일 뿐, Optional 변수에 null을 할당하는 것은 Optional 변수 자체가 null인지 다시 검사해야 하는 문제를 발생시킨다. 그래서 값이 없는 경우에는 Optional.empty()로 초기화하자.
// Bad
Optional<Data> optional = null;
// good
Optional<Data> optional = Optional.empty();
2) Optional.orElseX() 메소드를 사용해서 기본 값을 반환하자.
: Optional의 장점 중 하나는 함수형 인터페이스를 통해 가독성 좋은 코드를 작성할 수 있다. 그래서 isPresent()로 검사하고 get()으로 값을 꺼내는 것보다, orElseGet() 등을 활용하자.
// bad
Optional<String> optionalName = ...;
if (optionalName.isPresent()) {
return optionalName.get();
} else {
return findDefaultName();
}
// good
Optional<String> optionalName = ...;
return optionalName.orElseGet(this::findDefaultName());
3) 단순히 값을 얻으려는 목적으로만 Optional을 사용하지 말자.
: Optional은 null 또는 값을 감싸서 NPE로부터 부담을 줄이기 위해 등장했다. 따라서 값을 Wrapping하고 다시 풀고, null일 경우에는 대체하는 함수를 호출하는 등 연산이 들어가기 때문에 시스템 성능 저하가 일어난다. 따라서 메소드의 반환 값이 절대 null이 아니라면 Optional을 사용하지 않는 것이 좋다. 즉, Optional은 메소드의 결과가 null이 될 수 있으며, null에 의해 오류가 발생할 가능성이 매우 높을 때만 반환값으로 사용되어야 한다. Optional은 비용을 발생한다는 것을 명심하자.
4) Optional을 생성자, 수정자, 메소드 파라미터 등으로 넘기지 말자.
: Optional을 파라미터로 넘기는 것은 상당히 의미가 없다. 넘겨온 파라미터를 위해 자체 null 체크도 추가해야 하고, 코드도 복잡해지기 때문이다. Optional은 반환 타입을 위해 설계되었음을 잊지 말자!
5) 값이 없는 경우, Optional.orElseThrow() 를 통해서 명시적으로 예외를 던지자.
: 값이 없는 경우 기본 값을 반환하는 대신 예외를 던져야 하는 경우가 있다. 이 경우에는 Optional.orElseThrow()를 사용하면 된다.
// bad
if (findById(0).isPresent()) {
return findById(0).get();
} else {
return throw new NoSuchElementException("멤버가 없습니다");
}
// good
Member member = findById(0).orElseThrow(() -> new NoSuchElementException("멤버가 없습니다"));
6) 값이 있는 경우 이를 사용하고, 없는 경우 아무런 동작을 안한다면 ifPresent() 를 활용하자.
: ifPresent()는 Optional 객체 안에 값이 있는 경우 실행할 람다를 인자로 받는다. 따라서 값이 있는 경우에 실행되고, 값이 없는 경우에는 실행되지 않는 로직에 ifPresent()를 사용할 수 있다.
// bad
Optional<Member> optionalMember = findById(0);
if (optionalMember.isPrensent()) {
System.out.println("member : " + optionalMember.get());
}
// good
Optional<Member> optionalMember = findById(0);
optionalMember.ifPresent(System.out::println);
7) 원시 타입을 Optional로 사용할 땐, OptionalInt, OptionalLong, OptionDouble을 사용하자.
: 원시 타입을 Optional로 사용하면 Boxing과 UnBoxing을 거치며 비용이 발생된다. 따라서 int, long, double 타입에는 OptionalXXX 타입 사용을 고려하면 좋다. 이들은 내부 값을 래퍼 클래스가 아닌 원시 타입으로 갖는다.
// bad
Optional<Integer> count = Optional.of(10); // Boxing 발생
// good
OptionalInt count = OptionalInt.of(10); // Boxing 발생 안함
8) 제약사항이 있는 경우 filter을 사용하자.
: Optional.filter 도 스트림처럼 값을 필터링하는 역할을 한다. 인자로 전달된 predicate이 참인 경우 기존의 내부 값을 유지한 Optional이 반환되고, 그렇지 않은 경우 비어 있는 Optional을 반환한다.
4. Optional 메소드
1) 생성
- of(value) : value 값으로 Optional 객체를 생성. 값이 반드시 있어야 하고, 없으면 NPE이 발생
- ofNullable(value) : value 값으로 Optional 객체 생성. 값이 null인 경우 Optional.empty()가 리턴되어 NPE이 발생하지 않는다.
- empty() : 빈 Optional 객체를 생성한다. Optional 객체 자체는 있지만, 내부에서 가리키는 참조가 없는 경우이다. Optional.empty() 객체는 미리 생성되어 있는 싱글턴 인스턴스이다.
Optional<String> optionalOf = Optional.of(value);
Optional<String> optionalOfNullable = Optional.ofNullable(value);
Optional<String> optionalEmpty() = Optional.empty();
2) 중간 처리 (Optional 객체를 가져와서 처리를 하고 다시 Optional 객체를 반환)
- filter(람다식) : 메소드 인자로 받은 람다식이 '참'이면 Optional 객체를 그대로 통과시키고, 거짓이면 Optional.empty()를 반환해서 추가 처리가 안되게 한다.
Optional.of("ABCD").filter(s -> s.startWith("AB")).orElse("Not AB"); // ABCD
Optional.of("XYZ").filter(s -> s.startWith("AB")).orElse("Not AB"); // Not AB
- map(람다식) : Optional 객체의 값에 어떤 수정을 가해서 다른 값으로 변경한다.
Optional.of("ABC").map(String::toLowerCase).ofElse("Not ABC");
3) 값을 리턴하는 메소드
- isPresent() : Optional 객체의 값이 null 인지 여부를 판단해준다.
Optional.of("ABC").isPresent(); // true
Optional.of("ABC").filter(s -> "TEST".equals(s)).isPresent(); // false
- ifPresent(람다식) : 값이 존재할 때 해당 값을 인자값으로 받은 람다식을 적용한다. 만약 Optional 객체에 값이 없다면 람다식이 실행되지 않는다.
Optional.of("ABC").ifPresent(System.out::println); // ABC
Optional.ofNullable(null).ifPresent(system.out::println); // 아무것도 출력 안됨
- get() : Optional 객체가 가지고 있는 value값을 꺼내온다. 만약 Optional 객체에 값이 없다면 NPE가 발생한다.
- orElse() : Optional 객체가 비어있으면 orElse() 메소드에 저장된 값이 기본값으로 리턴한다.
- orElseGet() : Optional 객체가 비어있으면 orElseGet() 메소드의 인자로 입력된 Supplier 함수를 적용해 객체를 얻어온다.
Optional.of("ABC").get(); // "ABC" 리턴
Optional.nullable(null).get(); // NullPointerException 발생
Optional.of("ABC").filter(v -> v.startWith("AB")).orElse("Not AB"); // "ABC" 리턴
Optional.of("ABC").filter(v -> v.startWith("AB")).orElseGet(() -> "Not AB"); // "ABC" 리턴
- orElseThrow() : Optional 객체가 비어있다면 Supplier 함수를 실행해 예외를 발생한다.
Optional.of("ABC").filter(v -> v.startWith("XZ")).orElseThrow(NoSuchElementException::new);