Dev_log/Java

[Java]프로젝트 시작을 앞두고 Optional 공부하기

Dev우키 2023. 7. 16. 13:10
반응형

Optional

Optional이란?

Java 8 버전에서 추가된 타입으로 값의 유무를 표현한다. null 값을 대체할 수 있다.

  • 프로젝트를 진행할 때 가장 번거로운 NullPointException을 대체할 수 있는 아주 좋은 방안이다.
  • 즉, null이 올 수 있는 값을 감싸는 wrapper 클래스로 참조해도 NullPointException이 발생하지 않도록 도와준다.

Optional 클래스 내부를 살펴 볼까? 하고 들어가 보니 value, EMPTY와 같은 필드가 존재해 null이더라도 NPE가 발생하지 않는다..!

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

또 살펴보니 EMPTY라는 변수가 static으로 선언되어 있다. 그렇기 때문에 Optional로 빈 객체를 여러 번 생생해야할 경우에도 1개의 EMPTY만 공유함으로써 메모리도 절약할 수 있다.

또한 람다식을 지원하므로 코드의 길이를 간략화 할 수 있다.

Optional 객체의 생성

  • Optional.of() : null가능성이 없는 값을 전달할 때 사용한다.
T t1 = new T(); 
Optional<T> optExample1 = Optional.of(t1); 

T t2; 
Optional<T> optExample2 = Optional.of(t2); // NullPointerException 발생
  • Optional.ofNullable() : null가능성이 있는 값을 전달할 때 사용한다.
T t; 
Optional<T> optExample = Optional.ofNullable(t);
  • Optional.empty() : 값이 없는(null) Optional 객체를 생성하고 싶을 때 사용한다.
Optional<T> emptyOpt1 = Optional.empty(); 

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

T t; 
Optional<T> emptyOpt2 = Optional.ofNullable(t);

Optional 객체의 값 획득

  • get() : 가장 단순하게 Optional 객체의 값을 획득할 때 사용한다. 그러나 빈 Optional 객체에 접근하면 Exception이 발생한다.
Optional<String> neOpt = Optional.of("not empty"); 
String neStr = neOpt.get(); 
Optional<String> eOpt = Optional.empty(); 
String eStr = eOpt.get(); // NoSuchElementException 발생
  • isPresent(), isEmpty() : 값의 유무를 확인할 때 사용한다.
    • isEmpty()의 경우 Java11부터 추가된 함수이다.
Optional<String> neOpt = Optional.of("not Empty"); 
neOpt.isPresent(); // true 
neOpt.isEmpty(); // false 
Optional<String> eOpt = Optional.empty(); 
eOpt.isPresent(); // false 
eOpt.isEmpty(); // true`
  • ifPresent(), ifPresentOrElse() : Optional객체가 값이 있다면, 다음 행동을 지시하기 위해 사용한다.기존의 if문을 사용한다면?
String value = getString(); // 값을 받아오는 임의의 함수 
    if (value != null) doNext(value); 
    else doEmpty();

Optional<String> opt = Optional.ofNullable("not empty"); 
opt.ifPresent(value -> doNext(value)); 
opt.ifPresentOrElse( value -> doNext(value), () -> doEmpty() );
  • orElse(), orElseGet(), or() : Optional객체에 값이 없을 때 다른 값을 사용하기 위해 사용한다.or()의 경우는 특이하게 Optional 객체를 반환한다.
Optional<String> optValue3 = opt.or(() -> Optional.of("defalut"));
String optValue1 = opt.orElse("default"); 
String optValue2 = opt.orElseGet(() -> getDefaultValue()); 
// String optValue2 = opt.orElseGet(() -> "defalut"); 값 반환도 가능`
  • orElseThrow() : Optional객체에 값이 없다면 Exception, 값이 있으면 해당 값을 반환한다.
Optional<String> eOpt = Optional.empty(); 
String optValue1 = eOpt.orElseThrow(() -> new MyException()); 
Optional<String> neOpt = Optional.of("not Empty"); 
String optValue2 = eOpt.orElseThrow(() -> new MyException()); // "not Empty"`

Optional객체의 중간연산

  • map : 함수를 실행해서 값을 변환한 Optional 객체를 반환한다.
Optional<Member> memberOpt = getMember(); // 임의의 Member 객체 획득 함수
Optional<String> memberBirth = memberOpt.map(mem -> mem.getBirth());
Optional<Integer> memberAge = memberBirth.map(birth -> birth.getAge());

Optional<Integer> memberAge = memberOpt.map(mem -> mem.getBirth())
                                        .map(birth -> birth.getAge());
  • flatMap : map()과 거의 동일하지만, 인자로 전달받은 함수의 반환값이 Optional일 때 사용한다.만약
Optional<Member> memberOpt = getMember(); // 임의의 Member 객체 획득 함수 Optional<Optional<String>> memberBirth = memberOpt.map(mem -> Optional.of(mem.getBirth()));

map()을 사용했을 경우엔 아래와 같이 두 번 감싸진 Optional 객체를 반환한다.

Optional<Member> memberOpt = getMember(); // 임의의 Member 객체 획득 함수
Optional<String> memberBirth = memberOpt.map(mem -> Optional.of(mem.getBirth()));

map과 flatMap 모두 빈 Optional 객체를 반환한다.

  • filter : 조건이 참이면 해당 값을 반환하고 거짓이면 빈 Optional 객체를 반환한다.
Optional<Integer> intOpt = getValue(); // 임의의 정수 획득 함수
Optional<Integer> filteredIntOpt = intOpt.filter(value -> value > 100)
                                          .ifPresent(value -> System.out.println(value));

Optional객체의 조합

  • 다음 예시와 같이 두 객체 모두 값이 있을때만 사용하고 싶다면 ?
// 기존 if 구문 사용
Member member = getMember(); // 임의의 Member 객체 획득 함수
if (member == null) return null;
String grade = getGrade(member);
if (grade == null) return null;
T result = doSomething(member, grade);   
return result;
// Optional map, flatMap 사용
Optional<Member> memberOpt = getMember(); // 임의의 Member 객체 획득 함수
Optional<T> result = memberOpt.flatMap(member -> {
    Optional<String> grade = getGrade(member);
    return grade.map(grade -> doSomething(member, grade));
});

memberOpt가 값이 있을때만 flatMap에 진입하고, 진입해도 grade가 있을때만 doSomething()이 실행된다.

  • 분기에 따라 특정 값만 사용하거나 둘 다 사용하고 싶다면?
// 기존 if 구문 사용
String value1 = getStr1();
String value2 = getStr2();

if (value1 == null && value2 == null) return null;

if (value1 == null) return value2;
if (value2 == null) return value1;

return value1.length > value2.length ? value1 : value2;

// Optional map, flatMap 사용
Optional<String> opt1 = getValue1(); // 임의의 Optional 객체 획득 함수
Optional<String> opt2 = getValue2(); // 임의의 Optional 객체 획득 함수

Optional<String> result = opt1.flatMap(o1 -> { // opt1이 값이 있다면 진입
    return opt2.map(o2 -> {
          return o1.length > o2.length ? o1 : o2;
      }).orElse(o1); // opt2가 null이면 opt 1 사용
    }).or(() -> o2);   // flatMap 결과가 없다면 opt2 사용

return result.orElse(null); // 둘 모두 없을 때 null 반환

사용 예시

  1. 유저 정보에서 우편번호를 조회하는 방법
//Optional을 만나기 전 java 코드 
public String findPostCode(){ 
    UserDTO userDTO = getUser(); 
        if (userDTO != null) { 
            Address address = user.getAddress();
            if (address != null) {     
                String zipCode = address.getZipCode(); 
                if (zipCode != null) { 
                    return zipCode; 
                } 
             } 
         } 
     return "우편번호 없음";
}
// 위의 코드를 Optional로 펼쳐놓으면 아래와 같다.  
public String findPostCode() {  
 // 위의 코드를 Optional로 펼쳐놓으면 아래와 같다.
    Optional<UserVO> userVO = Optional.ofNullable(getUser());
    Optional<Address> address = userVO.map(UserVO::getAddress);
    Optional<String> postCode = address.map(Address::getPostCode);
    String result = postCode.orElse("우편번호 없음");

    // 그리고 위의 코드를 다음과 같이 축약해서 쓸 수 있다.
    String result = user.map(UserVO::getAddress)
        .map(Address::getPostCode)
        .orElse("우편번호 없음");

정리

Optional은 NPE에 대한 개발자로써 부담을 줄이기 위한 Wrapper Class이다.

근데 Wrapping을 풀고, null일 떄 대체하는 함수 호출 등 오버헤드가 있어서 무작정 사용하면 성능 저하를 유발할 수 있다.

결국 메소드의 반환 값이 절대 null이 아니면 Optional 사용을 지양하자.

- null에 의해 오류가 발생할 가능성이 높을 때 반환 값으로만 쓰자

더불어 무작정 모든 if구문을 Optional로 바꾸는 것은 옳지 않다. 상황에 맞춰서 사용해야 하며
Optional을 사용하더라도 isPresent()의 사용보다는 map, flatMap, filter, orElse, ifPresent등의 사용을 지향하여 Optional 도입 의도에 맞게 사용하도록 하자!

참고자료


[Youtube] 자바 Optional 기초 - 최범균
망나니 개발자님 Optional

반응형