NullPointerException
Member member = this.MemberService.findOne(id);
member.setUsername(userName);
위의 소스코드는 Member라는 객체가 있음을 보장하고 Member 메서드 setUsername을 수행합니다. 만약 MemberService의 findOne 메서드의 수행 결과가 null이라면 NullPointerException이 발생합니다.
이 예외를 막기 위해 Try-Catch문과 if-else문을 사용할 수 있으나 소스코드가 복잡해지고 메서드의 목적에서 벗어나게 됩니다. 또 null이 반환되지 않으리라 생각해 유효성 검사를 건너뛸 수 있는 문제도 가집니다.
Java 8에서는 이러한 상황을 예방할 수 있도록 Optional 클래스를 제공합니다. Optional 클래스는 null이 될 수도 있는 객체를 감싸는 Wrapper 클래스로, 이를 활용해 null을 직접 다루지 않아도 되며 유효성 검사 위임과 해당 변수가 null일 수 있음을 표현할 수 있습니다.
Optional 객체 생성
Optional 객체 인스턴스를 생성하려면 Optional의 메서드 empty(), of(), ofNullable()을 사용할 수 있습니다.
empty()
public void whenCreatesEmptyOptional_thenCorrect() {
Optional<String> empty = Optional.empty();
System.out.println(empty.isPresent()); // false
}
empty 메서드로 빈 Optional 객체를 생성할 수 있습니다. isPresent 메서드로 Optional 객체 인스턴스에 값이 있으면 true, null이면 false를 반환합니다.
of(T value)
public void givenNonNull_whenCreatesNonNullable_thenCorrect() {
String name = "baeldung";
Optional<String> opt = Optional.of(name);
System.out.println(opt.isPresent()); // true
}
of 메서드에 전달되는 인자는 null일 수 없습니다. 만약 null이 전달된다면 NullPointerException이 발생합니다.
ofNullable(T value)
public void givenNonNull_whenCreatesNullable_thenCorrect() {
String name = "baeldung";
Optional<String> opt = Optional.ofNullable(name);
System.out.println(opt.isPresent()); // true
}
public void givenNull_whenCreatesNullable_thenCorrect() {
String name = null;
Optional<String> opt = Optional.ofNullable(name);
System.out.println(opt.isPresent()); // false
}
null 값이 전달되리라 예상되면 ofNullable 메서드를 사용할 수 있습니다. 값이 있으면 해당 객체를, null이면 빈 Optional 객체 인스턴스를 반환합니다.
존재 확인
Optional 객체 인스턴스에 값이 존재하는지 확인하려면 Optional의 메서드 isPresent(), isEmpty()를 사용할 수 있습니다.
isPresent()
public void givenOptional_whenIsPresentWorks_thenCorrect() {
Optional<String> opt = Optional.of("Baeldung");
System.out.println(opt.isPresent()); // true
opt = Optional.ofNullable(null);
System.out.println(opt.isPresent()); // false
}
Optional 객체 인스턴스에 값이 있다면 true, 없다면(null이면) false를 반환합니다.
isEmpty()
public void givenAnEmptyOptional_thenIsEmptyBehavesAsExpected() {
Optional<String> opt = Optional.of("Baeldung");
System.out.println(opt.isEmpty()); // true
opt = Optional.ofNullable(null);
System.out.println(opt.isEmpty()); // false
}
Java 11부터 사용할 수 있는 메서드로, Optional 객체 인스턴스에 값이 있다면 false, 없다면(null이면) true를 반환합니다.
orElse(T other)
public void whenOrElseWorks_thenCorrect() {
String nullName = null;
String name = Optional.ofNullable(nullName).orElse("john");
System.out.println("john".equals(name)); // true
}
Optional 객체 인스턴스에 값이 있으면 그 값을 반환하고, 없다면(null이면) orElse 메서드의 인자로 전달된 값을 반환합니다.
orElseGet(Supplier other)
public void whenOrElseGetWorks_thenCorrect() {
String nullName = null;
String name = Optional.ofNullable(nullName).orElseGet(() -> "john");
System.out.println("john".equals(name)); // true
}
orElse 메서드와 비슷합니다. Optional 객체 인스턴스에 값이 있으면 그 값을 반환하고, 없다면(null이면) 인자로 전달된 매개변수 없는 람다식의 반환 값을 반환합니다.
orElse와 orElseGet
Optional 객체 인스턴스에 값이 없으면 정확히 같은 방식으로 이 메서드들은 동작합니다. 그러나 값이 있을 때 차이가 있습니다.
- orElseGet 메서드는 값이 있을 때 인자로 전달된 함수를 실행하지 않습니다.
- orElse 메서드는 값이 있든 없든 관계없이 인자로 전달된 함수를 실행합니다.
이는 Supplier 인터페이스의 지연 연산과 관련이 있습니다. 값이 있을 때 orElse 메서드의 매개변수는 T 타입을 요구하기에 값이 있든 없든 함수가 실행됩니다. orElseGet 메서드는 Supplier를 매개변수로 요구하기에 함수가 실행되지 않습니다. 내부의 삼항연산자에 의해 값이 있다면 값을 반환하고 값이 null이라면 get 메서드를 통해 인자로 넣은 매개변수가 없는 함수의 반환 값을 반환합니다.
Optional에 정의된 orElse
public T orElse(T other) {
return this.value != null ? this.value : other;
}
orElse 메서드는 T 타입을 매개변수로 받아 T 타입을 반환합니다. 삼항연산자에 의해 Optional 객체 인스턴스의 값이 있으면 해당 값을 반환하고 없다면 매개변수로 받은 T 타입을 반환합니다.
Optional에 정의된 orElseGet
public T orElseGet(Supplier<? extends T> supplier) {
return this.value != null ? this.value : supplier.get();
}
orElseGet 메서드는 Supplier를 매개변수로 받아 T 타입을 반환합니다. 삼항연산자에 의해 Optional 객체 인스턴스의 값이 있으면 해당 값을 반환하고 없다면 매개변수로 받은 Supplier를 실행 후 반환받은 T 타입을 반환합니다.
이처럼 함수를 인자로 전달할 때 orElse를 사용하면 중복 객체를 생성할 수 있으므로 orElseGet을 사용하는 것이 좋습니다.
orElseThrow(Supplier exceptionSupplier)
public void whenOrElseThrowWorks_thenCorrect() {
String nullName = null;
String name = Optional.ofNullable(nullName).orElseThrow(
IllegalArgumentException::new); // 예외 발생
}
Optional 객체 인스턴스에 값이 있으면 그 값을 반환하고, 없다면(null이면) orElseThrow 메서드의 인자로 전달된 예외를 발생시킵니다.
Java 8에서는 람다식이 하나의 메서드를 호출할 때 혹은 단순히 객체를 생성하고 반환할 때 불필요한 매개변수를 제거해 편리하게 사용할 수 있도록 이중 콜론 연산자를 지원합니다. 가령 IllegalArgumentException::new
는 () -> { new IllegalArgumentException(); }
과 동일합니다.
get()
public void givenOptional_whenGetsValue_thenCorrect() {
Optional<String> opt = Optional.of("baeldung");
String name = opt.get();
System.out.println("baeldung".equals(name)); // true
}
Optional 객체 인스턴스의 값 또는 null을 반환합니다. 빈 Optional 객체에 get 메서드를 사용한다면 NoSuchElementException이 발생하므로 값이 있는지 여부를 확인해야 합니다. 그러나 이러한 방식은 기존의 방식과 차이가 없어 Optional을 사용하는 의미가 없습니다. 따라서 이전에 서술한 메서드 orElse, orElseGet, orElseThrow를 이용하는 것이 좋습니다.
filter(Predicate predicate)
public void whenOptionalFilterWorks_thenCorrect() {
Integer year = 2016;
Optional<Integer> yearOptional = Optional.of(year);
boolean is2016 = yearOptional.filter(y -> y == 2016).isPresent();
System.out.println(is2016); // 값이 반환돼 true
boolean is2017 = yearOptional.filter(y -> y == 2017).isPresent();
System.out.println(is2017); // null이 반환돼 false
}
객체 인스턴스가 특정 조건에 해당한다면 Optional 객체 인스턴스의 값을 반환하고 그렇지 않으면 null을 반환합니다.
map(Function mapper)
public void givenOptional_whenMapWorks_thenCorrect() {
List<String> companyNames = Arrays.asList(
"paypal", "oracle", "", "microsoft", "", "apple");
Optional<List<String>> listOptional = Optional.of(companyNames);
int size = listOptional
.map(List::size)
.orElse(0);
System.out.println(6 == size); // true
}
public void givenOptional_whenMapWorksWithFilter_thenCorrect() {
String password = " password ";
Optional<String> passOpt = Optional.of(password);
boolean correctPassword = passOpt.filter(
pass -> pass.equals("password")).isPresent();
System.out.println(correctPassword); // false
correctPassword = passOpt
.map(String::trim)
.filter(pass -> pass.equals("password"))
.isPresent();
System.out.println(correctPassword); // true
}
Optional 객체 인스턴스의 값을 변환할 수 있습니다. 변환된 값은 T 타입을 감싼 Optional 객체 인스턴스로 반환됩니다.
flatMap(Function mapper)
public class Person {
private String name;
private int age;
private String password;
public Optional<String> getName() {
return Optional.ofNullable(name);
}
public Optional<Integer> getAge() {
return Optional.ofNullable(age);
}
public Optional<String> getPassword() {
return Optional.ofNullable(password);
}
// normal constructors and setters
}
public void givenOptional_whenFlatMapWorks_thenCorrect2() {
Person person = new Person("john", 26);
Optional<Person> personOptional = Optional.of(person);
Optional<Optional<String>> nameOptionalWrapper
= personOptional.map(Person::getName);
Optional<String> nameOptional
= nameOptionalWrapper.orElseThrow(IllegalArgumentException::new);
String name1 = nameOptional.orElse("");
System.out.println("john".equals(name1)); // true
// map 대신 flatMap을 이용하면...
String name = personOptional
.flatMap(Person::getName)
.orElse("");
System.out.println("john".equals(name)); // true
}
Optional 객체 인스턴스의 값을 변환할 수 있습니다. 변환된 값은 T 타입으로 반환됩니다.
equals(Object obj)
boolean comparePersonById(long id1, long id2) {
Optional<Person> maybePersonA = findById(id1);
Optional<Person> maybePersonB = findById(id2);
if (!maybePersonA.isPresent() && !maybePersonB.isPresent()) { return false; }
return findById(id1).equals(findById(id2));
}
Optional 객체 인스턴스의 값이 서로 같은지 확인할 수 있습니다.
사용 시 주의사항
- Optional 변수에 null을 할당하지 않습니다.
- get 메서드 호출 전 Optional 객체 인스턴스가 값을 갖고 있음을 보장해야 합니다.
- 값이 있는 경우 이를 활용하고 없으면 아무 동작을 하지 않는다면 ifPresent 메서드를 사용합니다.
- ifPresent - get으로 된 값 존재 확인 후 사용하는 패턴은 orElse, orElseGet, orElseThrow로 대체합니다.
- Optional은 반환을 목적으로 설계되었으므로 메서드의 매개변수로 사용하거나 클래스의 필드(멤버 함수)로 선언하지 않아야 합니다.
- List, Set, Map과 같은 Collection은 Optional로 감싸지 말고 빈 Collection을 사용하는 것이 깔끔합니다.
- 값을 얻는 목적으로 Optional을 사용하지 않습니다. 그런 목적이라면 단순한 조건문으로 값을 반환하는 것이 좋습니다. 왜냐하면 Optional을 사용하면 메모리 사용량이 증가하기 때문입니다.
사용 예
Node node = nodeRepository.findById(nodeId).orElseThrow(
()-> new NotFoundException("Node is not found (by nodeId)", nodeId)
);
- Spring Data JPA 패키지에 있는 JpaRepository의 findById 메서드는 Optional 객체 인스턴스를 반환합니다. 객체 인스턴스에 값이 있다면 정류장 객체가 반환돼 저장되고, 값이 없다면 NotFoundException 예외를 던져 어떤 부분이 잘못 되었는지 쉽게 확인하도록 합니다. Optional 클래스를 사용하지 않았더라면 NullPointerException이 왜 발생했는지 알기 어려웠을 것입니다.
return Optional.ofNullable(cacheKeyAnnotationOfFieldExtractor(v, keyClass))
.orElseGet(() -> Optional.ofNullable(cacheKeyAnnotationOfMethodExtractor(v, keyClass))
.orElseThrow(() -> new IllegalStateException(String.format("@CacheKey 애노테이션이 어느 필드/메소드 에도 존재하지 않음 - %s", v.getClass().getName()))));
- Optional 클래스의 ofNullable 메서드로 cacheKeyAnnotationOfFieldExtractor 메서드의 반환 값이 있다면 그대로 반환하고 null이면 Supplier를 실행합니다. Optional 클래스의 ofNullable 메서드로 cacheKeyAnnotationOfMethodExtractor 메서드의 반환 값이 있다면 그대로 반환하고 null이면 예외를 던집니다. Optional 클래스를 사용하지 않았더라면 중첩 if - else문을 사용하여 코드가 복잡해졌을 것입니다.
참고자료
- Optional (Java Platform SE 8) : Oracle
- Guide To Java 8 Optional : Baeldung
- [Java] Optional이란? Optional 개념 및 사용법 - (1/2) : MangKyu's Diary
- [Java] 언제 Optional을 사용해야 하는가? 올바른 Optional 사용법 가이드 - (2/2) : MangKyu's Diary
- Optional 제대로 활용하기 : Increment
- [자바의 정석] Optional<T>와 OptionalInt : June
- [JAVA] Java 함수 orElse vs orElseGet : SJ BackEnd Log
- [Java] Lazy Evaluation (지연 연산) : 조민서
- 이중 콜론 연산자 (Double Colon Operator) : mallin
- ☕ 함수형 인터페이스 표준 API 총정리 : Inpa Dev 👨💻