본문 바로가기

Language/Java

[Java] Optional<T> 클래스

728x90
반응형

Optional 개념

Java8부터 지원하는 Optional<T> 클래스는 null이 올 수 있는 값을 감싸는 Wrapper 클래스로, NPE(Null Pointer Exception)이 발생하지 않도록 도와준다.

 

* NPE(Null Pointer Exception)이란?

개발할 때 가장 많이 발생하는 예외 중 하나이다. NPE를 피하려면 null 여부를 검사해야 하는데, null 검사를 해야하는 변수가 많은 경우 코드가 복잡해지고 번거롭다. 그래서 null 대신 초기값을 사용하기를 권장하기도 한다.

List<String> names = getNames();
names.sort();  // names가 null이라면 NPE 발생

List<String> names = getNames();
// NPE를 방지하기 위해 null 검사 해야함
if(names != null){
	names.sort();
}

 

Optional 클래스는 아래와 같은 value에 값을 저장하기 때문에 값이 null이더라도 바로 NPE가 발생하지 않으며, 클래스이기 때문에 각종 메소드를 제공해준다.

public final class Optional<T>{
	private final T value;
	...
}

Optional 생성하기

[ Optional.empty() - 값이 null인 경우 ]

Optional<String> optional = Optional.empty();
System.out.println(optional);   // optional.empty
System.out.println(optional.isPresent());  // false

 

Optional 클래스는 내부에서 static 변수로 EMPTY 객체를 미리 생성해서 가지고 있다. 이러한 이유로 빈 객체를 여러 번 생성해주어야 하는 경우에도 1개의 EMPTY 객체를 공유함으로써 메모리를 절약한다.

public final class Optional<T> {
    private static final Optional<?> EMPTY = new Optional<>();
    private final T value;
    
    private Optional() {
        this.value = null;
    }
    ...
}

 

[ Optional.of() - 값이 Null이 아닌 경우 ]

어떤 데이터가 절대 null이 아니라면 Optional.of()로 생성할 수 있다. 만약 Optional.of()로 Null을 저장하려고 하면 NullPointerException이 발생한다.

Optional<String> optional = Optional.of("MyName");

 

[ Optional.ofNullable() - 값이 Null일 수도, 아닐수도 있는 경우 ]

만약 어떤 데이터가 null이 올 수도 있고 아닐 수도 있는 경우에는 Optional.ofNullable로 생성할 수 있다. 그리고 이후에 orElse 또는 orElseGet 메소드를 이용해서 값이 없는 경우라도 안전하게 값을 가져올 수 있다.

Optional<String> optional = Optional.ofNullable(getName());
String name = optional.orElse("anonymous");  // 값이 없다면 "anonymous" 리턴

orElse와 orElseGet의 차이

  • orElse: 파라미터로 값을 받는다.
  • orElseGet: 파라미터로 함수형 인터페이스(함수)를 받는다.

실제 Optional 코드를 보면 orElse와 orElseGet이 각각 구현되어 있음을 확인할 수 있다.

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를 호출하도록 되어 있고, 두 번째 함수는 orElseGet을 호출하도록 되어 있다.

public void findUserEmailOrElse(){
	String userEmail = "Empty";
	String result = Optional.ofNullable(userEmail)
		.orElse(getUserEmail());

	System.out.println(result);
}

public void findUserEmailOrElseGet(){
	String userEmail = "Empty";
	String result = Optional.ofNullable(userEmail)
		.orElseGet(this::getUserEmail);

	System.out.println(result);
}

private String getUserEmail(){
	System.out.println("getUserEmail() Called");
	return "elinismus@tistory.com";
}

실행 결과

// 1. orElse인 경우
getUserEmail() Called
Empty

// 2. orElseGet인 경우
Empty

 

이러한 결과가 발생한 이유를 자세히 살펴보자. 먼저 OrElse인 경우에는 다음과 같은 순서로 처리된다.

  1. Optional.ofNullable로 "EMPTY"를 갖는 Optional 객체 생성
  2. getUserEmail()가 실행되어 반환값을 orElse 파라미터로 전달
  3. orElse가 호출됨. "EMPTY"가 Null이 아니므로 "EMPTY"를 그대로 가짐

위와 같이 동작하는 이유는 Optional.orElse()가 값을 파라미터로 받고, orElse 파라미터로 값을 넘겨주기 위해 getUserEmail()이 호출되었기 때문이다.

 

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

  1. Optional.ofNullable로 "EMPTY"를 갖는 Optional 객체 생성
  2. getUserEmail() 함수 자체를 orElseGet 파라미터로 전달
  3. orElseGet이 호출됨. "EMPTY"가 Null이 아니므로 "EMPTY"를 그대로 가지며 getUserEmail()이 호출되지 않음

orElseGet에서는 파라미터로 넘어간 값인 getUserEmail 함수가 Null이 아니므로 .get에 의해 함수가 호출되지 않는다. 만약 Optional의 값으로 null이 있다면, 다음과 같은 흐름에 대해 orElseGet의 파라미터로 넘어온 getUserEmail()이 실행될 것이다.

public void findUserEmailRoElseGet(){
	String result = Optional.ofNullable(null)
		.orElseGet(this::getUserEmail);

	System.out.println(result);
}

private String getUserEmail(){
	System.out.println("getUserEmail() Called");
	return "elinismus@tistory.com";
}
  1. Optional.ofNullable로 null를 갖는 Optional 객체 생성
  2. getUserEmail() 자체를 orElseGet 파라미터로 전달
  3. orElseGet이 호출됨. 값이 null이므로 other.get()이 호출되어 getUserEmail()이 호출됨

[orElse에 의해 발생가능한 장애 예시]

userEmail을 Unique한 값으로 갖는 시스템에서 아래와 같은 코드를 작성하였다고 하자.

public void findByUserEmail(String userEmail){
	return userRepository.findByUserEmail(userEmail)
		.orElse(createUserWithEmail(userEmail));
}

private String createUserWithEmail(String userEmail){
	User newUser = new User(userEmail);
	return userRepository.save(newUser);
}

 

위의 예제는 Optional의 단말 연산으로 orElse를 사용하고 있기 때문에, 조회 결과와 무관하게 createUserWithEmail 함수가 반드시 실행된다. 하지만 Database에서는 userEmail이 Unique로 설정되어 있기 때문에 오류가 발생할 것이다. 따라서 위와 같은 경우에는 다음과 같이 해당 코드를 orElseGet으로 수정해야 한다. 이렇게 코드를 수정하였다면 파라미터로 createUserWithEmail 함수 자체가 넘어가므로, 조회 결과가 없을 경우에만 사용자를 생성하는 로직이 호출될 것이다.

public void findByUserEmail(String userEmail){
	return userRepository.findByUserEmail(userEmail)
		.orElseGet(createUserWithEmail(userEmail));
}

private String createUserWithEmail(String userEmail){
	User newUser = new User(userEmail);
	return userRepository.save(newUser);
}

 

실제 서비스에서 위와 같은 오류를 범한다면 큰 시스템 장애로 돌아오게 된다. 설령 문제가 없다 하더라도 orElse는 값을 생성하여 orElseGet보다 비용이 크므로 최대한 사용을 피해야 한다.

728x90
반응형