기누 2022. 12. 16. 17:17

REST API 코드리뷰에서 받은 피드백 중, get() 사용 시 NoSuchElementException이 발생할 수 있어 orElseThrow를 사용해보라는 피드백을 받았다. 코드를 수정하면서 Optional의 주의사항을 간단히 찾아보았는데, 블로그에 정리를 하면 좋을 것 같다고 생각이 되어 글을 작성하게 되었다!

 

Optional 이란?

먼저 공식문서에서 정의하는 Optional<Type>은 위와 같다. 해석하자면, Optional은 non-null값을 가지고 있을 수도 있고 가지고 있지 않을 수도 있는 컨테이너 객체이다. 만약 값이 존재한다면 isPresent() 메소드 사용 시, true를 리턴하게 된다. 반면 값이 존재하지 않는다면, 객체는 비어있는 것(empty)으로 여겨지고, isPresent() 메소드 사용 시 false를 리턴하게 된다. 

또한 Optional에는 값의 유무에 따라 달라지는 메소드들이 존재한다. 예를 들면, 값이 존재하지 않을 시 디폴트값을 리턴하는 orElse(), 값이 존재할 시에만 수행하는 ifPresent()가 있다. 

 

내가 이해하기로 Optional은 null이 올 수 있는 값을 감싸는 wrapper 클래스이며, 값이 존재하는지에 대한 여부를 명시적으로 확인하기 위해 사용할 수 있다. Optional 클래스는 아래와 같이 value에 값을 저장(non-null이면 해당 값 저장, null이면 null을 저장)하기 때문에, 값이 null일 때 참조하더라도 NullPointerException이 발생하지 않도록 도와준다. 이러한 측면에서 Optional은 value-based 클래스라고 할 수 있다. 

 

Optional 활용

Optional 객체 생성 

값이 null인 경우

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

System.out.println(optional1); 			// Optional.empty
System.out.println(optional1.isPresent()); 	// false

먼저 값이 null일 경우, Optional.empty()를 통해서 빈 값의 optional1을 생성했다.

optional1을 출력하게 되면 Optional.empty가 출력되게 된다. 아래 사진에서 볼 수 있는 것 처럼 Optional 클래스는 내부에서 static 변수로 EMPTY 객체를 미리 생성해서 가지고 있다. 이러한 이유로 빈 객체를 여러번 생성하는 경우에도 1개의 EMPTY 객체를 공유함으로 메모리 절약을 할 수 있다. 

 

 

값이 null이 아닌 경우 

Optional<String> optional2 = Optional.of("Name");

System.out.println(optional2); 			// Optional[Name]
System.out.println(optional2.isPresent()); 	// true

값이 null이 아닐 경우, Optional.of(값)를 통해서 값을 가지고 있는 optional2를 생성했다. 

optional2을 출력하게 되면 Optional[Name]이  출력되게 된다. 

 

 

optional2을 출력하게 되면 Optional[Name]이  출력되게 된다. 그렇다면 Optional.of()로 null 값을 저장하면 어떻게 될까? 

 

Optional<Integer> optional2 = Optional.of(null);

위 공식문서에 적혀있는 것 처럼 of를 통해서는 non-null value만 전달할 수 있다. 즉, 값이 null이라면 NullPointerException이 발생하게 된다. 

 

값이 null일 수도 있고, 아닐 수도 있는 경우 

Optional<String> optional3 = Optional.ofNullable(null);
Optional<String> optional4 = Optional.ofNullable("Kiyoung");

System.out.println(optional3); 			// Optional.empty
System.out.println(optional4);			// Optional[Kiyoung]
System.out.println(optional3.isPresent()); 	// false
System.out.println(optional34isPresent()); 	// true

값이 있을 수도 있고, 없을 수도 있는 경우에는 Optional.ofNullable()을 통해 생성할 수 있다.

동일하게 ofNullable()을 사용해서, null 값인 optional3과 non-null인 optional4를 생성했다. isPresent() 메소드를 사용해서 optional3과 optional4를 출력해보면, 정말로 값이 있을 수도 없고, 없을 수도 있는 것을 볼 수 있다.  

 

Optional 값에 접근

get()

System.out.println(optional4.get());	// Kiyoung

get()은 값을 가져오고, 비어있는 Optional 객체에 대해서는 NoSuchElementException 예외를 던진다. 

위 코드를 실행했을 때 optional4는 값을 가지고 있기 때문에 Kiyoung이라는 값이 잘 출력이 되지만, 

 

System.out.println(optional3.get());	// 오류!

optional3은 값을 가지고 있지 않기 때문에 예외가 발생한다. 

 

 

orElse()

String name3 = optional3.orElse("anonymous"); // 값이 없다면 "anonymous" 를 리턴
String name4 = optional4.orElse("anonymous"); // 값이 없다면 "anonymous" 를 리턴

System.out.println(name3); // anonymous
System.out.println(name4); // Kiyoung

orElse() 메소드는 값이 존재하면 그 값을 그대로 리턴하고, 값이 존재하지 않는다면 orElse로 넘어온 인자(T other)를 대신 리턴한다.

위 예시에서 optional3은 null로 값이 존재하지 않았다! 이 name3, name4라는 String에 orElse("anonymous")를 사용해서 만약 값이 존재하지 않을 경우 "anonymous" 라는 값 할당하고, 값이 존재할 경우 존재하는 값을 할당하였다.

그리고 optional3과 optional4를 출력해보면 name3에는 null 값 대신 anonymous 라는 값이 잘 할당된 것을 볼 수 있다.  

 

즉, orElse() 메소드는 값이 없을 때 할당할 수 있는 '기본 값'을 설정할 수 있기 때문에, name3은 null이 아닌 객체가 초기화되는 것을 보장할 수 있다. 

 

 

orElseGet()

String str3 = optional3.orElseGet(() -> "no name");
String str4 = optional4.orElseGet(() -> "no name");
System.out.println(str3);		// no name
System.out.println(str4);		// Kiyoung

Optional이 null인 경우 어떤 함수를 실행하고, 그 실행 결과를 대입하고 싶을 때는 orElseGet() 메소드를 사용하면 된다. 

 

orElse()와 orElseGet()의 차이점? 

먼저 공식 문서를 살펴보자 

 

orElse()
orElseGet()

값이 존재할 때 그 값을 리턴하는 것은 동일하지만,

값이 없을 때 orElse()는 T other을, orElseGet()은 supplying function의 값을 리턴한다. 

Supplier
리턴할 값을 만드는 supplying function으로, 인자가 없고 리턴 값이 있는 함수형 인터페이스이다. 람다 식으로 구현할 수 있다. 

 

정리하자면

orElse()

  • 파라미터로 값을 받고, 그 값을 리턴
  • 값이 미리 존재하는 경우에 사용

orElseGet()

  •  파라미터로 함수형 인터페이스(함수)를 받고, 함수의 결과 값을 리턴
  • 값이 미리 존재하지 않는 대부분의 경우에 사용

그냥 그렇구나~ 하고 넘어가기에는 뭔가 찝찝하다. 아래 예시 코드를 통해 둘의 차이를 조금 더 느껴보자.

 

public void findUserNameOrElse() {
	String userName = "EMPTY";
        String userName2 = null;
        String result = Optional.ofNullable(userName)
                .orElse(getUserName());	
        System.out.println(result);
        
        System.out.println("=================");

        String result2 = Optional.ofNullable(userName2)
                .orElse(getUserName());	
        System.out.println(result2);
}

public void findUserNameOrElseGet() {
   	String userName = "EMPTY";
        String userName2 = null;
        String result = Optional.ofNullable(userName)
                .orElseGet(Test::getUserName);	
        System.out.println(result);
        
        System.out.println("=================");

        String result2 = Optional.ofNullable(userName2)
                .orElseGet(Test::getUserName);	
        System.out.println(result2);
}

private String getUserName() {
    System.out.println("getUserName() Called");
    return "Kiyoung";
}

 

findUserNameOrElse()와 findUserNameOrElseGet()의 출력 결과는 어떻게 다를까? 

아래 답을 확인하기 전에 어떻게 출력이 될 지 생각해보면 좋을 것 같다 :) 

 

// findUserNameOrElse() 호출
getUserName() Called
EMPTY
=================
getUserName() Called
Kiyoung
  1. Optional.ofNullable로 "EMPTY" (userName)를 가지는 Optional 객체 생성
  2. getUserName() 메소드가 실행되어 반환 값을 orElse 파라미터로 전달
  3.  orElse()의 호출
  4. "EMPTY"는 null이 아니므로 "EMPTY"를 그대로 반환
  5. Optional.ofNullable로 null (userName2)값인 Optional 객체 생성
  6. getUserName() 메소드가 실행되어 반환 값을 orElse 파라미터로 전달
  7. orElse()의 호출
  8. null이므로 "Kiyoung"을 반환 

orElse()는 값을 파라미터로 받는다. 따라서 orElse()에 파라미터로 "값"을 넘겨주기 위해 getUserName()이 호출되게 된다.

즉, orElse()에 전달되는 파라미터 값이 null인지 null이 아닌지 확인해야하기 때문에 무조건 getUserName()이 호출되는 것 같다.

 

하지만 함수형 인터페이스를 파라미터로 받는 orElseGet()에서는 동작이 달라진다.

 

// findUserNameOrElseGet() 호출
EMPTY
=================
getUserName() Called
Kiyoung
  1. Optional.ofNullable로 "EMPTY" (userName)를 가지는 Optional 객체 생성
  2. getUserName() 함수 자체를 orElseGet 파라미터로 전달 
  3. orElseGet()의 호출
  4. "EMPTY"는 null이 아니므로 "EMPTY"를 그대로 반환 (getUserName()이 호출되지 않음!
  5. Optional.ofNullable로 null (userName2)값인 Optional 객체 생성
  6. getUserName() 함수 자체를 orElseGet 파라미터로 전달 
  7. orElseGet()의 호출
  8. null이므로 getUserName()이 호출되며, "Kiyoung"을 반환 

orElseGet()에서는 Optional의 값이 null인 경우에만 orElseGet()의 파라미터로 넘어온 getUserName() 함수가 실행된다. 

orElse()의 경우, 함수의 "값"을 파라미터로 받은 것이지만, orElseGet()은 "함수"를 파라미터로 받는 것이다!

 

즉, 값이 필요한 orElse()는 null의 유무와 상관 없이 값이 필요하므로 getUserName() 메소드가 먼저 호출 되고, 그 이후에 orElse()가 호출되어 null인지 아닌지에 대해 검사한다. 반면, orElseGet()은 메소드 호출 전, 값이 아니라 함수가 필요한 것이기 때문에 getUserName()이 호출될 필요가 없다 (값이 필요한 것이 아니니까). 따라서 orElseGet()이 호출되어 null인지 아닌지 검사하고, null일 경우에만 getUserName()이 호출되게 되는 것이다. 

 

 

orElseThrow()

try {
    String orElseThrow3 = optional3.orElseThrow(NullPointerException::new);
    System.out.println(orElseThrow3);
} catch (NullPointerException e) {
    System.out.println("NullPointerException");
} // NullPointerException

try {
    String orElseThrow4 = optional4.orElseThrow(NullPointerException::new);
    System.out.println(orElseThrow4);
} catch (NullPointerException e) {
    System.out.println("NullPointerException");
} // Kiyoung

Optional이 null을 리턴하는 경우, 예외를 발생시키고 싶다면 orElseThrow()를 사용하여 처리할 수 있다.

아래 예제에서 optional3.orElseThrow() 호출 시, optional3는 null 값을 가지고 있기 때문에, 인자로 전달된 NullPointerExcepion을 발생시키게 된다. 반면 optional4의 경우, null이 아닌 객체를 가지고 있기 때문에 정상적으로 객체가 리턴된다. 

 

 

그렇다면 왜 전부 Optional 클래스를 사용하지 않을까?

Optional은 Wrapper 클래스이기 때문에 두 개의 참조를 가지므로 생성비용이 비싸다. 또한 null일 경우에는 대체하는 함수를 호출하는 등의 오버헤드가 있으므로 무분별하게 사용하면 시스템 성능이 저하된다.

Java를 설계한 Brian Goetz는 Optional에 대해 다음과 같이 정의한다. 

Optional is intended 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. - Brian Goetz(Java Architect)

 

Optional은 애초에 필드로 사용하기 위해 고안된 것이 아니라 값을 반환하는 용도로, 매우 제한적으로 설계되었다는 것이다. 

따라서 메소드의 반환 값이 null일 경우가 없다면 Optional을 사용하지 않는 것이 좋다

👉 다시 말해 Optional은 결과가 null이 될 수 있으며, null에 의해 오류가 발생할 가능성이 매우 높을 때만 반환값으로 사용되어야 한다.  

 


오늘은 이렇게 Optional과 대표적인 메소드 몇 개에 대해 알아보았다.

Optional에 공부하고 나니 어떤 상황에서 Optional을 사용할 수 있는지에 대해서도 궁금해졌다! 다음에 정리해서 또 포스팅해야겠다 :) 

 

[참고]

https://docs.oracle.com/javase/8/docs/api/java/util/Optional.html

https://mangkyu.tistory.com/70