포포핀
815
2021-05-18 10:01:01 작성 2021-05-20 15:52:20 수정됨
2
911

Java Optional Best Practices


  1. Optional 변수에 null을 할당하지 마라

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // AVOID
    public Optional<Cart> fetchCart() {
    Optional<Cart> emptyCart = null;
    ...
    }

    // PREPER
    public Optional<Cart> fetchCart() {
    Optional<Cart> emptyCart = Optional.empty();
    ...
    }

    빈값으로 초기화하려면 Optional.empty()를 사용하자.


  2. Optional.get()을 사용하기 전에는 반드시 Optional에 값이 있는지 확인해라

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // AVOID
    Optional<Cart> cart = CartRepository.findOne();
    Cart myCart = cart.get();

    // PREPER
    Optional<Cart> cart = CartRepository.findOne();
    if(cart.isPresent()) {
    Cart myCart = cart.get();
    }else {
    //값이 없을 때
    }

    cart값이 없으면 cart.get()은 에러를 발생시킨다. 그래서 항상 get() 호출 이전에 isPresent()로 체크를 해줘야한다. Optional이 생긴이유가 null check에서 벗어나기 위함인데 이렇게 사용한다면 결국 다를게 없다. 아래에 나오는 orElse()나 orElseGet()을 사용하자.

  3. isPresent()-get() 보다는 orElse()

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    public static final String USER_STATUS = "UNKNOWN";

    // AVOID
    public String findUserStatus(long id) {
    Optional<String> status = ... ;

    if (status.isPresent()) {
    return status.get();
    } else {
    return USER_STATUS;
    }
    }

    // PREPER
    public String findUserStatus(long id) {
    Optional<String> status = ... ;
    return status.orElse(USER_STATUS);
    }

  4. orElseGet()

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    public String computeStatus() {
    // status를 계산하여 리턴하는 로직
    }

    // AVOID
    public String findUserStatus(long id) {
    Optional<String> status = ... ;

    if (status.isPresent()) {
    return status.get();
    } else {
    return computeStatus();
    }
    }

    // AVOID
    public String findUserStatus(long id) {
    Optional<String> status = ... ;
    status.orElse(computeStatus());
    }

    // PREPER
    public String findUserStatus(long id) {
    Optional<String> status = ... ;
    return status.orElseGet(this::computeStatus);
    }

    첫번째 AVOID는 isPresent()-get()을 피하라는 것이고, 두번째 AVOID는 얼핏보면 문제 없을 것 같지만 orElse()는 status에 값이 있어도 computeStatus()메소드를 호출하기 때문에 orElseGet()을 쓰는 것이 좋다.

  5. orElseThrow()

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // AVOID
    public String findUserStatus(long id) {
    Optional<String> status = ... ;

    if (status.isPresent()) {
    return status.get();
    } else {
    throw new NoSuchElementException();
    }
    }

    // PREFER
    public String findUserStatus(long id) {
    Optional<String> status = ... ;
    return status.orElseThrow();
    }

    orElseThrow()는 Java 10부터 사용가능하다.
    이하 버전은 orElseThrow(Supplier<? extends X> exceptionSupplier)를 사용해야한다.


  6. Optional.ifPresent()

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // AVOID
    Optional<String> status = ... ;
    if (status.isPresent()) {
    System.out.println("Status: " + status.get());
    }

    // PREFER
    Optional<String> status ... ;
    status.ifPresent(System.out::println);

  7. lamda 사용으로 Optional 얻기

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    List<Product> products = ... ;

    // AVOID
    Optional<Product> product = products.stream()
    .filter(p -> p.getPrice() < price)
    .findFirst();

    if (product.isPresent()) {
    return product.get().getName();
    } else {
    return "NOT FOUND";
    }

    // PREFER
    return products.stream()
    .filter(p -> p.getPrice() < price)
    .findFirst()
    .map(Product::getName)
    .orElse("NOT FOUND");

    Stream 의 findFirst(), findAny(), reduce() 같은 메소드는 Optional을 리턴한다. 적절히 사용하면
    코드의 분리없이 메소드 체이닝을할 수 있다.

  8. Optional의 과도한 사용
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // AVOID
    public String fetchStatus() {
    String status = ... ;
    return Optional.ofNullable(status).orElse("PENDING");
    }

    // PREFER
    public String fetchStatus() {
    String status = ... ;
    return status == null ? "PENDING" : status;
    }

    단순 값을 가져오는 메서드에 Optional을 사용하는 것은 Optional의 목적에 맞지않는 과도한 사용이다.

  9. 필드 선언에 사용하지마라

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // AVOID
    public class Customer {
    Optioanl<String> zip;
    Optioanl<String> zip = Optional.empty();
    }

    // PREFER
    public class Customer {
    String zip;
    String zip = "";
    }

  10. 생성자, 메서드, Setter 인자로 사용하지마라


  11. 빈 Collection이나 Array를 리턴할 때 Optional을 쓰지마라

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // AVOID
    public Optional<List<String>> fetchCartItems() {
    List<String> items = cart.getItems();
    return Optional.ofNullable(items);
    }

    // PREFER
    public List<String> fetchCartItems() {
    List<String> items = cart.getItems();
    return items == null ? Collections.emptyList() : items;
    }

  12. 기본자료형에 Optional 제네릭을 사용하지마라

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // AVOID
    Optional<Integer> price = Optional.of(50);
    Optional<Long> price = Optional.of(50L);
    Optional<Double> price = Optional.of(50.43d);

    // PREFER
    OptionalInt price = OptionalInt.of(50);
    OptionalLong price = OptionalLong.of(50L);
    OptionalDouble price = OptionalDouble.of(50.43d);

  13. 동등성(Equality) 비교를 위해 unWrap할 필요가없다.

    1
    2
    3
    4
    5
    6
    7
    8
    Optional<String> actual = Optional.of("shoes");
    Optional<String> expected = Optional.of("shoes");

    // AVOID
    assertEquals(expected.get(), actual.get());

    // PREFER
    assertEquals(expected, actual);

    Optional의 equals 메소드는 내부 값을 비교하도록 구현되어있기 때문에 바로 비교하면된다.


14. Optional 값을 변경하거나 필터링할 때는 스트림 API를 이용하자

  • map()
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    Optional<String> lowerName = Optional.of("optional");
    Optional<String> upperName;

    // AVOID
    if(lowerName.isPresent()) {
    upperName = Optional.of(lowerName.get().toUpperCase());
    }else {
    upperName = Optional.empty();
    }

    // PREFER
    upperName = lowerName.map(String::toUpperCase);

  • filter, flatMap()
    1
    2
    3
    4
    5
    String uppercase = items.stream()
    .filter(i -> i.getPrice() > 50)
    .findFirst()
    .flatMap(i -> Optional.of(i.getName()))
    .map(String::toUpperCase).orElse("NOT FOUND");
    15.indentity 기반 연산을 하지마라
  1. 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    Product product = new Product();
    Optional<Product> op1 = Optional.of(product);
    Optional<Product> op2 = Optional.of(product);

    // AVOID
    if (op1 == op2) {
    ...
    }

    // PREFER
    if (op1.equals(op2)) {
    ...
    }

    // NEVER DO
    synchronized(op1) {
    ...
    }

    Optional은 Value-based class 이므로 equality (==), identity hash-based, synchronization 연산을 하면 예상과 다르게 동작될 수 있다. equals()를 사용하자.


 참조 : https://dzone.com/articles/using-optional-correctly-is-not-optional
5
  • 댓글 2

  • fender
    22k
    2021-05-18 11:34:09 작성 2021-05-18 11:34:33 수정됨

    기본적으로 매우 유용한 조언인 것 같습니다. 자바에 옵셔널이 비교적 최근에 도입된 내용이라 아직 사용을 안하는 분들도 많을 것 같은데, 일단 익숙해지고 개념이 없는 다른 언어를 쓰면 필요성을 절감하게 되는 그런 기능인 것 같습니다.

    다만 개인적으로 9, 10에 대해서는 조금 다르게 생각합니다. 자바빈즈를 전제로 한다면 맞는 말이겠지만 옵셔널을 반드시 잠재적으로 직렬화가 필요한 유형에 사용한다고 가정할 이유가 없는 만큼 굳이 규칙으로 생각할 필요는 없지 않나 싶기도 합니다.

    저는 기본적으로 옵셔널의 존재 의미가 널 값이나 널 체크 자체가 과거의 유물이라는 생각에 바탕을 둔다면, 이는 꼭 API 수준의 반환값 시그네쳐 뿐 아니라 내부 구현에 사용하는 코드나 오버로딩 처리가 어려운 인자값에도 적용을 피할 이유가 없다고 봅니다.

  • 개발자2
    191
    2021-05-19 01:25:22

    오타 AVIOD 가 눈에 밟히네요 ㅠ

  • 로그인을 하시면 댓글을 등록할 수 있습니다.