Optional

박영준·2023년 1월 26일
0

Java

목록 보기
43/111

1. 정의

public final class Optional<T> {

  // null 이 아닌 경우, 값이 존재 O
  // null 인 경우, 값이 존재 X
  private final T value;
  ...
}

Optional<T>

  • null 이 올 수 있는 값을 감싸는 Wrapper 클래스

    • value 에 값을 저장하기 때문에, 참조할 때 값이 null 이더라도 바로 NPE(NullPointerException)가 발생 X

    • 코드가 Null-Safe 해진다. (= 예상치 못한 null 에 대응 가능해진다.(null 그 자체를 의미하는 것이 아님!))

    • 객체가 null 인지 아닌지 판별하기 위해 사용

  • 클래스이기 때문에, 각종 메소드를 제공해줌

2. 주의점

1) Optional 은 비싸다

잘못된 사용

// 단순히 값을 얻으려고 Optional을 사용한다.
public String findUserName(long id) {
    String name = ... ;
    
    return Optional.ofNullable(name).orElse("Default");
}
  • 메소드의 반환 값이 null 일 경우,
    오버헤드(대체하는 함수를 호출하는 등...)로 인해 시스템 성능의 저하 위험이 있으므로, Optional을 사용하지 않는 것이 좋다.

  • 다음과 같은 경우에 반환값으로만 사용하자.

    • 메소드의 결과가 null 이 될 수 있으며,
    • null 에 의해 오류가 발생할 가능성이 매우 높을 때

오버헤드

  • 프로그램의 실행흐름 도중에 동떨어진 위치의 코드를 실행시켜야 할 때, 추가적으로 시간, 메모리, 자원이 사용되는 현상
  • 특히, 외부 함수를 사용할 때 나타난다

2) 직렬화를 지원하지 않는다

public class User implements Serializable {

    private Optional<String> name;
}
  • 기본적으로 Optional 은 직렬화(Serialize)를 지원하지 않으므로, 캐시나 메세지큐 등...과 연동 시 문제가 발생할 수 있다.
// 잘못된 코드
public class User {

    private final String name;
    private final Optional<String> postcode;

    public Customer(String name, Optional<String> postcode) {
        this.name = Objects.requireNonNull(name, () -> "Cannot be null");
        this.postcode = postcode;
    }

	// name을 얻기 위해 Optional.ofNullable()로 반환
    	// Getter에 Optional을 얹어 반환하는 것은 남용이다.
    public Optional<String> getName() {
        return Optional.ofNullable(name);
    }
    
    public Optional<String> getPostcode() {
        return postcode;
    }
}
  • Optional 을 필드 값으로 사용하지 말자.

  • Optional 을 생성자, 수정자, 메소드 파라미터 등으로 넘기지 말자.

    • Optional 을 파라미터로 넘길 경우,
      넘겨온 파라미터를 위해 자체 null체크도 추가로 해주어야 하고, 코드도 복잡해지는 등... 복잡해진다.

3) 코드의 가독성 저하

optionalUser의 값이 비어있으면 NoSuchElementException가 발생할 수 있으므로, 값의 유무를 검사하고 꺼낸다.

public void temp(Optional<User> optionalUser) {
    User user = optionalUser.orElseThrow(IllegalStateException::new);
    
    // 이후의 후처리 작업 진행...
}

그러나, optionalUser 객체 자체가 null 일 경우에는 불필요하게 코드 글자수를 늘려야만 한다.

public void temp(Optional<User> optionalUser) {
    if (optionalUser != null && optionalUser.isPresent()) {
        // 이후의 후처리 작업 진행...
    }
    
    throw new IllegalStateException();
}

4) 시간적, 공간적 비용(or 오버헤드) 증가

시간적 비용

Optional 안에 있는 객체를 얻기 위해서는 Optional 객체를 통해 접근해야 하므로, 접근 비용이 증가

공간적 비용

Optional 은 객체를 감싸는 컨테이너이므로, Optional 객체 자체를 저장하기 위한 메모리가 추가로 필요

3. 메서드

get()
비어있는 optional 객체 반환

empty()
optional 객체를 null 로 초기화

isPresent()
optional 객체에 저장된 값이 null 인지 확인

orElse()
저장된 값이 존재하면, 해당 값을 반환
값이 존재하지 않으면, '파라미터의 값을 반환'

orElseGet()
저장된 값이 존재하면, 해당 값을 반환
값이 존재하지 않으면, '파라미터의 람다식 결과값을 반환'

orElseThrow()
저장된 값이 존재하면, 해당 값을 반환
값이 존재하지 않으면, '파라미터의 예외를 발생'시킴

4. 사용법

1) 값이 있는지 판단하자 Optional.empty()

(1) 잘못된 사용

Optional<User> optionalUser = ... ;

// optional이 갖는 value가 없으면 NoSuchElementException 발생
User user = optionalUser.get();
  • Optional로 받은 변수를 값이 있는지 판단하지 않고 접근하려고 한다면, NPE 대신 NoSuchElementException 가 발생
public Optional<Cart> fetchCart() {

    Optional<Cart> emptyCart = null;
    ...
}

값이 없는 경우라면 Optional.empty() 로 초기화하자.

  • Optional 은 컨테이너/박싱 클래스일 뿐이다.
    • Optional 변수에 null 을 할당하는 것은 Optional 변수 자체가 null인지 또 검사해야 하는 문제를 야기한다.

(2) 올바른 사용

방법 1

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

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

Optional.empty() 를 사용하자.

  • 값이 없는 경우(= 값이 Null 인 경우), Optional.empty() 로 생성 가능하다.
    • 단, Optional 은 Wrapper 클래스이기 때문에, 값이 없을 수도 있음

방법 2

public final class Optional<T> {

	// Optional 클래스는 내부에서 static 변수로 EMPTY 객체를 미리 생성해서 가지고 있다.
    private static final Optional<?> EMPTY = new Optional<>();
    private final T value;
    
    private Optional() {
        this.value = null;
    }
    ...
}
  • 빈 객체를 여러 번 생성해줘야 하는 경우에도, 1개의 EMPTY 객체를 공유함으로써 메모리를 절약할 수 있다.

2) 값이 반드시 존재한다면 Optional.of()

// Optional의 value는 절대 null이 아니다.
Optional<String> optional = Optional.of("MyName");
  • 값이 있는 경우(= 어떤 데이터가 절대 null 이 아니라면), Optional.of() 로 생성 가능하다.

  • Optional.of() 로 Null을 저장하려고 하면, NPE(NullPointerException) 가 발생

3) 값이 있는지 잘 모르겠다면 Optional.ofNullbale() + orElse/orElseGet

// Optional의 value는 값이 있을 수도 있고 null 일 수도 있다.
Optional<String> optional = Optional.ofNullable(getName());
String name = optional.orElse("anonymous"); // 값이 없다면 "anonymous" 를 리턴
  • 값이 없을 수도 있을 수도 있는 경우(= 어떤 데이터가 null 이 올 수도 있고 아닐 수도 있는 경우),
    Optional.ofNullbale 로 생성 후 + orElse 또는 orElseGet 메소드로 값이 없는 경우라도 안전하게 값을 가져올 수 있음

(1) orElse

  • 파라미터로 값을 받는다.

  • 값이 미리 존재하는 경우에 사용

  • null을 반환해야 하는 경우라면, orElse(null) 을 활용

  • 비용 : orElse > orElseGet

    • orElse 의 사용을 최대한 피하자.

사용법 1

public final class Optional<T> {

    ... // 생략

	// orElse
    public T orElse(T other) {
        return value != null ? value : other;
    }
}    

사용법 2

// 값이 비어있을 때, orElse 를 호출
public void findUserEmailOrElse() {
    String userEmail = "Empty";
    String result = Optional.ofNullable(userEmail)	// 1-1) Optional.ofNullable로 "EMPTY"를 갖는 Optional 객체 생성
    	.orElse(getUserEmail());	// 1-2) getUserEmail()가 실행되어, 반환값을 orElse 파라미터로 전달
        
    System.out.println(result);
}

/* 출력 결과
Empty
*/
  • "EMPTY"가 Null이 아니므로 "EMPTY"를 그대로 가짐

사용법 3

public void findByUserEmail(String userEmail) {
    return userRepository.findByUserEmail(userEmail)
            .orElse(createUserWithEmail(userEmail));
}
  • userEmail을 Unique한 값으로 갖는 시스템에서 orElse 를 사용할 경우 → 문제 발생
    • orElse에 의해 userEmail이 이미 존재해도 유저 생성 함수가 호출되어 에러 발생
    • Optional의 단말 연산으로 orElse를 사용하므로, 조회 결과와 무관하게 createUserWithEmail 함수가 반드시 실행된다.
      그러나, Database에서는 userEmail이 Unique로 설정되어 있기 때문에 오류가 발생

(2) orElseGet

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

  • 값이 미리 존재하지 않는 경우에 사용(거의 대부분의 경우)

  • 값이 없어서 throw해야하는 경우라면, orElseThrow를 사용

사용법 1

public final class Optional<T> {

    ... // 생략
    
	// orElseGet
    public T orElseGet(Supplier<? extends T> other) {
        return value != null ? value : other.get();
    }
}

사용법 2

// 값이 비어있을 때, orElseGet 을 호출
public void findUserEmailOrElseGet() {
    String userEmail = "Empty";
    String result = Optional.ofNullable(userEmail)	// 2-1) Optional.ofNullable로 "EMPTY"를 갖는 Optional 객체 생성
    	.orElseGet(this::getUserEmail);		// 2-2) getUserEmail() 함수 자체를 orElseGet 파라미터로 전달
        
    System.out.println(result);
}

/* 출력 결과
Empty  
*/
  • "EMPTY"가 Null이 아니므로 "EMPTY"를 그대로 가지며 getUserEmail()이 호출되지 X

사용법 3

// 값이 비어있을 때, orElseGet 을 호출 + Optional의 값으로 null이 있다면
public void findUserEmailOrElseGet() {
    String result = Optional.ofNullable(null)	// 3-1) Optional.ofNullable로 null를 갖는 Optional 객체 생성
    	.orElseGet(this::getUserEmail);		// 3-2) getUserEmail() 함수 자체를 orElseGet 파라미터로 전달
        
    System.out.println(result);
}

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

/* 출력 결과
Empty
*/
  • orElseGet 인 경우 + Optional의 값으로 null이 있다면 orElseGet이 호출됨
    • 값이 Null이므로 other.get()이 호출되어 getUserEmail()가 호출됨

사용법 4

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);
}
  • userEmail을 Unique한 값으로 갖는 시스템에서 orElseGet 을 사용할 경우
    • orElseGet에 의해 파라미터로 createUserWithEmail 함수 자체가 넘어가므로, Null이 아니면 유저 생성 함수가 호출되지 않음
      (조회 결과가 없을 경우에만 사용자를 생성하는 로직이 호출)

4) 상황에 따른 사용법

(1) Optional<T> 와 Lambda

List<String> nameList = Optional.ofNullable(getNames())
    .orElseGet(() -> new ArrayList<>());

(2) Collection

잘못된 코드

public Optional<List<User>> getUserList() {
    List<User> userList = ...; 		// null이 올 수 있음

    return Optional.ofNullable(items);
}

올바른 코드

public List<User> getUserList() {
    List<User> userList = ...; 		// null이 올 수 있음

    return items == null 
      ? Collections.emptyList() 
      : userList;
}
  • Collection의 경우 굳이 Optional로 감쌀 필요가 없이, 빈 Collection 을 사용하는 것이 깔끔하고 처리가 가볍다.

(3) HashMap

잘못된 코드

public Map<String, Optional<String>> getUserNameMap() {
    Map<String, Optional<String>> items = new HashMap<>();
    items.put("I1", Optional.ofNullable(...));
    items.put("I2", Optional.ofNullable(...));
    
    Optional<String> item = items.get("I1");
    
    if (item == null) {
        return "Default Name"
    } else {
        return item.orElse("Default Name");
    }
}

올바른 코드

public Map<String, String> getUserNameMap() {
    Map<String, String> items = new HashMap<>();
    items.put("I1", ...);
    items.put("I2", ...);
    
    return items.getOrDefault("I1", "Default Name");
}
  • map에 getOrDefault 메소드가 있는 점을 활용한다.

참고: [Java] Optional이란? Optional 개념 및 사용법 - (1/2)
참고: [Java] 언제 Optional을 사용해야 하는가? 올바른 Optional 사용법 가이드 - (2/2)
참고: Flutter의 null safety 이해하기
참고: [Java] Optional, Optional의 메서드, Optional 사용시 주의사항

profile
개발자로 거듭나기!

0개의 댓글