본문 바로가기
Programming/Languages (Java, etc)

[Java] Java 8 사용하기 : Optional로 Null 지혜롭게 다루기

by kghworks 2023. 9. 3.

목차

  • Null을 다루는 방식 (Java 8 이전)
  • Java 8에 추가된 Optional
  • 유용한 case와 기타 API

 Java 8에 추가된 주요 기능 중 개인적으로 손에 꼽는 것은 Streams와 Optional이다. Optional은 java.util 패키지에 Java 8부터 추가된 클래스로 클래스명 그대로 Optional 한 값을 다룰 때 유용하게 사용할 수 있는 클래스이다. 포스팅을 간단히 요약하면 Optional을 사용해 아래와 같이 코드를 개선해 볼 수 있게 된다.

// Java 8 이전
if(idol != null) {
    Leader leader = idol.getLeader();
    if(leader != null) {
        Car car = member.getCar();
        if(car != null) {
            return car.getName();
        }
    }
   return "Unknown";
}

// Java 8 ~
Optional.ofNullable(idol)
        .map(Idol::getLeader)
        .map(Leader::getCar)
        .map(Car::getName)
        .orElse("Unknown");

Null을 다루는 방식 (Java 8 이전)

 

 Java는 참조형 값에 Null이 가능하다. 그러나 Java 개발자들은 NullPointerException (이제부터 NPE) 때문에 이골이 나있기 때문에 우린 항상 방어적으로 코딩을 해왔다.

return idol
    .getLeader()
    .getCar()   
    .getName();

 

 위 코드는 Idol 타입의 객체를 객체 그래프 탐색해 가며 Idol의 Leader가 가진 Car의 이름을 반환하는 코드인데, 3가지 참조형 객체 (Idol, Leader, Car) 중 하나라도 Null이었다가는 NPE가 날아오기 때문에 아래처럼 방어적으로 코드를 짜볼 수 있다. 

 

if(idol != null) {
    Leader leader = idol.getLeader();
    if(leader != null) {
        Car car = member.getCar();
        if(car != null) {
            return car.getName();
        }
    }
   return "Unknown";
}

 

 완벽하게 NPE를 방어했으나 중첩이 많아 가독성이 떨어진다.

 

if(idol == null) {
    return "Unknown";
}

Leader leader = idol.getMember();

if(leader == null) {
    return "Unknown";
}

Car car = leader.getCar();

if(car == null) {
    return "Unknown";
}

return car.getName();

 

이제 NPE를 완벽히 방어했다! 근데 코드가 너무 장황하고 길다. 기존에 Java의 Null을 다루던 문제점을 말해보자

 

Nullable 대응 코드의 문제점

 Java에서 Null을 다루는 방식이 굉장히 까다롭고, 위험하다는 것은 Java 개발자라면 누구나 아는 사실이다. Tony Hoare는 the absence of value (값이 없음) 을 표현하기 위해 null을 도입했으나 이는 큰 실수였다고 후에 시인한 바 있다고 한다.

 

 실제로 개발, 운영 중 만나는 예외 중 가장 잦은 건 NullPointerException이다. 그만큼 Null을 참조하다 예외를 만나게 되는 것인데, 개발 중에 아무리 방어적으로 짠다고 해도 한계가 있다. 

 

 

return idol		// idol이 null이면?
    .getLeader()	// leader가 null이면?
    .getCar()   	// car가 null이면?
    .getName();		// name이 null이면?

 

 일단 제일 처음에 봤던 코드를 다시 보면, 위에서 참조하는 4가지 객체는 죄다 nullable이다. 그 중 name을 제외하면 null을 참조하려는 순간 호출에서 바로 NPE가 발생한다. 그래서 방어적인 코드를 짰지만 그 코드 자체에도 문제가 있다. 

 

  • 코드양 증가 : NPE를 피하기 위해 Null-check 코드가 많아짐
  • null의 의미 : Null의 의미? 값이 없음을 Null이 표현했지만, 그 자체로 위험성이 있음 (NPE)
  • pointer 노출 : java는 대외적으로 pointer를 숨겼지만, NPE가 발생한 순간 이미 pointer가 깨진 것을 시인한 셈
  • null의 자유분방함 : null은 어느 참조형 타입이건 가능

Java 8에 추가된 Optional

 

 

 Java 8부터  java.util.Optional이 추가되었다. Optional은 nullable한 객체를 감싸서 (wrapping) 내용물이 null일 경우 null이 아닌 Optional.empty() (=내용물이 비어있는 Optional)을 반환한다. 이게 대체 무슨 차이를 주는지는 일단 참고 한번더 보자. 

 

  먼저 예제에서 봤던 Car, Leader 필드가 모두 nullable임을 인지했고, 혹은 보수적으로 NPE를 방지해 보겠다고 하면 Optional API를 사용해서 아래와 같이 코드를 작성할 수 있다.

 

Optional.ofNullable(idol)
        .map(Idol::getLeader)
        .map(Leader::getCar)
        .map(Car::getName)
        .orElse("Unknown");

 

 대체 뭐가 다르냐면, 저기서 어느 하나라도 Null이면 "Unkown" 문자열을 리턴하게 된다. NPE는 없다! ofNullable()은 Optional 타입이 아닌 Idol 타입을 Optional 타입으로 wrapping 한 것이다. 그 외에도 Optional이 가지는 장점은 아래와 같다.

 

  • wrapping 한 value가 있으면 value 반환, 없으면 Optional.empty() 반환 (Null 반환 X)
  • 가독성 : Optional은 명시적으로 Nullable을 나타내고, null을 다루는 API를 통해 꺼내야 함 (NPE 방지)

 

Optional 적용해보기

// Optional 만들기 (empty)
Optional<Leader> member = Optional.empty();

// Optional wrapping, NPE 가능성 없음
Optional<Leader> member = Optional.ofNullable(karina); // karina가 null이면 Optional.empty() 반환

// Optional wrapping, NPE 가능성
Optional<Leader> member = Optional.of(karina); // karina가 null, NullPointerException 발생

String carName = Optional.ofNullable(aespa)
        .map(Idol::getLeader) // return Optional<Leader>
        .map(Leader::getCar)// return Optional<Car>
        .map(Car::getName) // return Optional<String>
        .orElse("unkown, something is null");

 

map()은 Strems API의 map()과 비슷하게 동작한다. Optional안의 요소를 꺼내서 또 다른 Optional에 wrapping 해서 반환한다. 

 

map() vs faltMap()

 

만일 map()의 인자로 넘긴 java.util.Function 람다식에서 또 다른 Optionl을 wrapping해서 반환한다면 어떨까. 아래 예제를 보자. 

public Optional<Car> getCar() {
	return Optional.ofNullable(car);
}

...

// compile error!!
String carName = Optional.ofNullable(aespa)
    .map(Idol::getLeader)
    .map(Leader::getCar) // return Optional<Optional<Car>>
    .map(Car::getName) // compile error : method getName in class ... cannot be applied to given types
    .orElse("no car");
    
// flatMap 사용
String carName = Optional.ofNullable(aespa)
        .map(Idol::getLeader) // return Optional<Leader>
        .flatMap(Leader::getCar)// return Optional<Car>
        .map(Car::getName) // return Optional<String>
        .orElse("unkown, something is null");

 

 왜냐하면 이미 getCar()의 리턴 타입이 Optional <Car>인데, 한번 더 Optional로 wrapping 했기 때문이다. 이 때도 역시 Streams API에서 faltMap()을 사용하는 것처럼 flatMap()이 제공된다.

 

map() (위), faltMap() (아래)

stream() : Optional을 Stream으로 변환 (Java 9)

public Optional<Leader> getLeader() {
	return Optional.ofNullable(leader);
}

public Optional<Car> getCar() {
	return Optional.ofNullable(car);
}

...

public Set<String> getLeadersCarName(List<Idol> idolList) {

    return idolList.stream()
        .map(Idol::getLeader) // Stream<Optional<Leader>>
        .map(optLeader -> 
                optLeader.flatMap(Leader::getCar)) // Stream<Optional<Car>>
        .map(optCar -> 
                optCar.map(Car::getName)) // Stream<Optional<String>>
        .flatMap(Optional::stream) // Stream<Optional<String>> -> Stream<Stream<String>> -> Stream<String>
        .collect(Collectors.toSet()); // Stream<String> -> Set<String>
}

 

2개의 Optional로 하나의 Optional 생성

public Optional<Car> getBiggerCar(Leader l1, Leader l2) {
    // ... business logic
    return beggerOptional;
}

Optional<Leader> optKarina = Optional.ofNullable(karina);
Optional<Leader> optJenny = Optional.ofNullable(jenny);

optKarina.flatMap(k ->
                optJenny.flatMap(j ->
                        getBiggerCar(k, j)))
        .map(Car::getName) // return Optional<String>
        .ifPresent(System.out::println);

 


유용한 케이스와 기타 API

 

1. 타입이 Optional이 아닐 때, Optional로 만들기

// return nullable value
Object karina = map.get("specific key"); 

// Optional
Optional<Object> optKarina = Optional.ofNullable(map.get("specific key");

 

2.  예외 다루기

public static Optional<Integer> strToInt(String str) {
    try {
        return Optional.of(Integer.parseInt(str)); // return Optional<Integer>
    } catch (NumberFormatException e) {
        return Optional.empty(); // return Optional.empty()
    }
}

 

Optional API

method description 사용 tip
get() Optional의 value를 반환
간단하지만 가장 덜 안전
Optional이 empty일 경우 NoSuchElementException 발생
value가 있을 것이라고 확신할 때
orElse(T other) Optional이 empty일 경우 other를 반환 Optional이 empty일 경우 default value가 필요할 때
orElseGet(Supplier<? extends T> other) Optional이 empty일 경우 Supplier의 value를 반환
orElse()보다 lazy
Supplier는 Optional이 empty일 경우에만 호출
default value 생성이 오래걸릴 때
optional이 empty 일경우 Supplier 동작이 필요할 때
or(Supplier<? extends Optional<? extends T>> supplier) Optional이 empty일 경우 Supplier의 Optional을 반환
value가 있다면, unwrapping 안하고 Optional을 반환
Optional이 empty일 경우 Supplier의 Optional이 필요할 때
orElseThrow(Supplier<? extends X> exceptionSupplier) Optional이 empty일 경우 Supplier의 Exception을 발생시킴 Optional이 empty일 경우 Exception을 발생시키고 싶을 때
ifPresent(Consumer<? super T> consumer) Optional이 empty가 아닐 경우 Consumer를 실행
empty일 경우 아무 동작 없음
Optional이 empty가 아닐 경우에만 Consumer를 실행하고 싶을 때
ifPresentOrElse(Consumer<? super T> action, Runnable emptyAction) Optional이 empty가 아닐 경우 Consumer를 실행
empty일 경우 Runnable을 실행
Optional이 empty가 아닐 경우 Consumer를 실행하고, empty일 경우 Runnable을 실행하고 싶을 때

댓글