티스토리 뷰

728x90
반응형

💡 Null 탄생의 비하인드 스토리?

  • 1965년 토니 호어라는 영국 컴퓨터 과학자가 힙에 할당되는 레코드를 사용하며, 형식을 갖는 최초의 프로그래밍 언어 중 하나인 알골을 설계하면서 처음 Null 참조가 등장하였다고 합니다. 토니 호어는 "구현하기 쉬웠기 때문에 Null을 도입했다" 라고 말했으며 컴파일러의 자동확인 기능으로 모든 참조를 안전하게 사용할 수 있을 것을 목표로 정했다고 합니다.
    그 당시 Null 참조 및 예외로 같이 없는 상황을 가장 단순히 구현할 수 있다고 판단했고 그렇게 탄생을 하게 되었습니다. 후에 Null을 만든 결정을 십억 달러짜리의 실수라고 표현을 했다고 합니다.

 

값이 없는 상황을 어떻게 처리할까?


  • 우선 사람은 차를 가질 수 있고 차는 브랜드를 가질 수 있습니다.
@Getter
public class Person {

    private String name;
    private Car car;
}

@Getter
public class Car {

    private String name;
    private Brand brand;
}

@Getter
public class Brand {

    private String name;
}

 

🧨 문제가 발생할 수 있는 코드

  • 다음 같은 경우 최종적으로 사람이 소유하고 있는 자동차 브랜드의 이름을 가져올려고 NPE의 발생 가능성이 있습니다. getCar() 메서드 호출 시 Null 이거나 getBrand() 메서드 호출 시 Null일 가능성이 있습니다.
Person person = new Person();
person.getCar().getBrand().getName(); // NPE 발생 가능성!

 

NPE 발생 방지 1) 깊은 의심

  • 해당 코드에서는 각 메서드마다 Null 인지 의심하므로 변수에 접근할 때마다 중첩된 if가 추가되면서 들여쓰기 수준이 증가하게 됩니다. 따라서 이와 같은 반복 패턴 코드를 깊은 의심이라 부릅니다.
Person person = new Person();
if (person != null) {
    Car car = person.getCar();
    if (car != null) {
        Brand brand = car.getBrand();
        if (brand != null) {
            brand.getName();
        }
    }
}

 

NPE 발생 방지 2) 너무 많은 출구

  • 아래 코드는 중첩 if 블럭을 없앴습니다. 변수가 Null 이라면 즉시 "Unknown"이라는 값을 반환하지만 그렇게 좋은 코드는 아닙니다. 
Person person = new Person();

String result = null;
if (person == null) {
    result = "Unknown";
}
Car car = person.getCar();
if (car == null) {
    result = "Unknown";
}
Brand brand = car.getBrand();
if (brand == null) {
    result = "Unknown";
}
result = brand.getName();

 

💡 Null로 인해 발생할 수 있는 문제들

  • 에러의 근원입니다. 
  • 코드를 어지럽힙니다. -> 중첩 if로 인해 가독성을 떨어트릴 수 있습니다.
  • 아무 의미가 없습니다. -> Null은 아무 의미도 표현하지 않습니다.
  • 자바 철학에 위배됩니다. -> 자바는 개발자로부터 모든 포인터를 숨겼습니다. 하지만 예외가 있는데 그것은 바로 Null 포인터입니다.
  • 형식 시스템에 구멍을 만듭니다.

 

Optional 클래스 소개


  • 자바 8부터는 java.util.Optional<T> 클래스를 제공하고 있습니다.
  • Optional<T>는 값이 있으면 값을 감싸고, 값이 없다면 Optional.empty 메서드로 Optional을 반환하는데 Optional.empty는 Optional의 특별한 싱글턴 인스턴스를 반환하는 정적 팩토리 메서드 입니다.
public final class Optional<T> {

    private static final Optional<?> EMPTY = new Optional<>();
    
    public static<T> Optional<T> empty() {
        @SuppressWarnings("unchecked")
        Optional<T> t = (Optional<T>) EMPTY;
        return t;
    }
}

 

Optional 적용 패턴


  • Optional.of(), Optional.ofNullable(), Optional.of() 로 Optional 객체를 만들 수 있습니다.

💡 map() 메서드를 사용하여 Optional의 값을 추출하고 변환하기

  • Person 클래스의 Car를 Optional로 포장하고, Car 클래스의 Brand를 Optional로 포장했습니다. 이때 getBrandName() 메서드는 제대로 컴파일이 되지 않습니다.
@Getter
public class Person {

    private String name;
    private Optional<Car> car; // Optional
}

@Getter
public class Car {

    private String name;
    private Optional<Brand> brand; // Optional
}

@Getter
public class Brand {

    private String name;
}

// reason: no instance(s) of type variable(s) exist so that Optional<Car> conforms to Car
public static Optional<String> getBrandName(Person person) {
    Optional<Person> personOptional = Optional.of(person);
    Optional<String> brandName = personOptional.map(Person::getCar)
                                               .map(Car::getBrand)
                                               .map(Brand::getName);
    return brandName;
}

 

🤔 map 메서드를 이용하면 컴파일 에러가 발생하는 이유

  • map 메서드를 사용하면 처음 map 메서드에서 Optional<Optional<Car>> 처럼 래핑이 되고 두번째 map에서는 Optional<Optional<Brand>>처럼 래핑이 됩니다. 그렇기 때문에 flatMap 메서드를 사용하여 평준화를 시켜줘야 합니다.
public <U> Optional<U> map(Function<? super T, ? extends U> mapper) {
    Objects.requireNonNull(mapper);
    if (!isPresent()) {
        return empty();
    } else {
        return Optional.ofNullable(mapper.apply(value));
    }
}

 

💡 map() 대신 flatMap() 사용

  • flatMap 연산으로 Optional을 평준화할 수 있습니다. 평준화란 이론적으로 두 Optional을 합치는 기능을 수행하면서 둘 중 하나라도 Null이면 빈 Optional을 생성하는 연산입니다.
  • flatMap을 빈 Optional에 호출하면 아무일도 일어나지 않고 그대로 반환됩니다. 반면 Optional이 Person을 감싸고 있다면 
    flatMap에 전달된 Function에 Person이 적용됩니다. 마지막 map은 Optional을 반환하는게 아닌 String을 반환하기 때문에 
    flatMap을 사용할 필요가 없습니다.
// Optional 클래스의 flatMap 메서드
public <U> Optional<U> flatMap(Function<? super T, ? extends Optional<? extends U>> mapper) {
    Objects.requireNonNull(mapper);
    if (!isPresent()) {
        return empty();
    } else {
        @SuppressWarnings("unchecked")
        Optional<U> r = (Optional<U>) mapper.apply(value);
        return Objects.requireNonNull(r);
    }
}

public static Optional<String> getBrandName(Person person) {
    Optional<Person> personOptional = Optional.of(person);
    Optional<String> brandName = personOptional.flatMap(Person::getCar)
                                               .flatMap(Car::getBrand)
                                               .map(Brand::getName);

    return brandName;
}

 

💡 Optional 스트림 조작

  • 자바 9에서는 Optional을 포함하는 스트림을 쉽게 처리할 수 있도록 Optional에 stream() 메서드를 추가하였습니다.
public static Set<String> getBrandName(List<Person> person) {
  return person.stream()
          .map(Person::getCar)                     // return: Stream<Optional<Car>>
          .map(car -> car.flatMap(Car::getBrand))  // return: Stream<Optional<Brand>>
          .map(brand -> brand.map(Brand::getName)) // return: Stream<Optional<String>>
          .flatMap(Optional::stream)// Stream<Optional<String>> 을 Stream<String> 으로 평준화
          .collect(Collectors.toSet());
}

 

  • Optional과 filter사용
public static Set<String> getBrandName(List<Person> person) {
  return person.stream()
          .map(Person::getCar)
          .map(car -> car.flatMap(Car::getBrand))
          .map(brand -> brand.map(Brand::getName))
          .filter(Optional::isPresent) // name이 null이 아닌것만 필터링
          .map(Optional::get)          // 값이 있는것만 필터링했기 때문에 비로 get해도 무방
          .collect(Collectors.toSet());
}

 

💡 Optional의 orElse와 orElseGet의 차이

  • Optional 클래스의 orElse 메서드와 orElseGet 메서드입니다.
public T orElse(T other) {
    return value != null ? value : other;
}

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

 

공통점

  • 둘다 value가 null이 아닐경우 value를 반환하고 있습니다.

차이점

  • orElse 메서드는 값이 null인 경우 T 타입의 other를 반환합니다.
  • orElse 메서드는 해당 값이 null 이든 아니든 관계없이 항상 실행됩니다.
  • orElseGet 메서드는 값이 null인 경우 Supplier 인터페이스의 get 메서드를 통해 값을 반환합니다.(Lazy)
  • orElseGet 메서드는 값이 항상 null인 경우만 실행됩니다.

예제 코드 

  • 아래 코드를 보면 text는 Null이 아니지만 orElse 메서드에서 getText() 메서드를 호출했을 경우 실행되는 것을 알 수 있습니다.
  • 반면 text가 Null인 경우 orElse, orElseGet 메서드 모두 getText() 메서드가 호출되는 것을 알 수 있습니다.
  • 중요한 사실은 orElse, orElseGet 메서드 모두 value가 Null이든 아니든 둘다 호출 됩니다. orElse의 경우 value가 Null인 경우 바로 T 타입의 값을 반환하지만 orElseGet 메서드는 Supplier 제네릭으로 래핑되어 있기 때문에 Null이 아닐 때 인스턴스가 생성되고 값을 반환하기 때문입니다.
// text가 Null이 아닌 경우
public static void main(String[] args) {
    String text = "Not Null";

    String result1 = Optional.ofNullable(text).orElse(getText());
    System.out.println(result1);
    // Output -> 과연 호출될까요?
    // Output -> Not Null

    String result2 = Optional.ofNullable(text).orElseGet(() -> getText());
    System.out.println(result2);
    // Output -> Not Null
}

// text가 Null인 경우
public static void main(String[] args) {
    String text = null;

    String result1 = Optional.ofNullable(text).orElse(getText());
    System.out.println(result1);
    // Output -> 과연 호출될까요?
    // Output -> Text is Null

    String result2 = Optional.ofNullable(text).orElseGet(() -> getText());
    System.out.println(result2);
    // Output -> 과연 호출될까요?
    // Output -> Text is Null
}

private static String getText() {
    System.out.println("과연 호출될까요?");
    return "Text is Null";
}

 

기본형 Optional은 사용하지 말자


  • 스트림에서는 기본형으로 특화된 DoubleStream, IntStream, LongStream을 제공하고 있습니다. Optional에서도 기본형에 특화된 OptionalDouble, OptionalInt 등을 제공하고 있는데 Optional에서는 최대 요소 수는 한 개 이므로 기본 특화형 Optional을 사용한다고 해서 성능을 개선할 수 없습니다.
  • 기본형 특화 Optional은 Optional 클래스에서 유용한 map, flatMap, filter 등을 지원하지 않으므로 기본형 특화 Optional을 사용하는 것을 권장하지 않습니다.
  • 기본형 특화 Optional로 생성한 결과는 다른 일반 Optional과 혼용할 수 없습니다.

 

 

 

 

 

728x90
반응형