goodbye

Optional -3 본문

Java

Optional -3

goodbye 2023. 1. 20. 01:45

1. 개요

아래에서 인용한 글에 따르면 Optional은 null을 반환하면 오류가 발생활 가능성이 매우 높은 경우에 결과 없음(반환 값이 없음)을 나타나는 명확한 방법을 제공하기 위한 의도로 설계되었지만 실제로는 이러한 의도와 다르게 사용되는것에 대해서 우려하고 있으며, 그로 인해 많은 부작용이 발생 할 수 있기 때문에 아래의 내용을 참고하셔서 Optional 을 올바르게 사용하길 바랍니다

Java Language Architect Brian Goetz스택오버플로우 에서 Optional 을 만든 의도에 대해서 다음과 같이 설명하고 있습니다.

Of course, people will do what they want. But we did have a clear intention when adding this feature, and it was not to be a general purpose Maybe type, as much as many people would have liked us to do so. Our intention was to provide a limited mechanism for library method return types where there needed to be a clear way to represent "no result", and using null for such was overwhelmingly likely to cause errors.

For example, you probably should never use it for something that returns an array of results, or a list of results; instead return an empty array or list. You should almost never use it as a field of something or a method parameter.

I think routinely using it as a return value for getters would definitely be over-use.

There's nothing wrong with Optional that it should be avoided, it's just not what many people wish it were, and accordingly we were fairly concerned about the risk of zealous over-use.

물론 사람들은 원하는대로 할 것입니다. 그러나 우리는 이 기능을 추가할 때 분명한 의도를 가지고 있었고 , 많은 사람들이 우리가 원했을 만큼 범용적인 Maybe 유형 이 아니 었습니다. 우리의 의도는 "결과 없음"을 나타내는 명확한 방법이 필요한 라이브러리 메서드 반환 유형에 대한 제한된 메커니즘을 제공하는 것이었고 그러한 메커니즘을 사용 null하면 압도적으로 오류가 발생할 가능성이 높습니다.

예를 들어 결과 배열이나 결과 목록을 반환하는 용도로 사용해서는 안 됩니다. 대신 빈 배열이나 목록을 반환합니다. 무언가의 필드나 메소드 매개변수로 사용해서는 거의 안 됩니다.

게터에 대한 반환 값으로 일상적으로 사용하는 것은 확실히 과도하게 사용하는 것이라고 생각합니다.

옵셔널은 피해야 할 아무런 문제 가 없으며, 많은 사람들이 바라는 바가 아니기 때문에 과도한 남용의 위험에 대해 상당히 우려했습니다.

2. Optional 사용 주의사항

1. Optional 에 null 을 할당 금지

// avoid
Optional<User> emptyUser = null;

// prefer
Optional<User> emptyUser = Optional.empty();

null value 대신 Optional.empty() 을 통해 Optional 을 초기화하는것이 좋습니다.

Optional 은 단지 컨테이너/상자 일뿐이기때문에, null 로 초기화하는것은 의미가 없습니다

2. 값을 가져오는 단밀 목적으로 Optional 메서드 사용 피할것 (대신 null 사용)

// avoid 
return Optional.ofNullable(emailAddress).orElse(defaultEmailAddress);

// prefer
return email != null ? email : defaultEmailAddress;

단순히 값 또는 null 얻을 목적인 경우에는 Optional 보다는 null 비교를 하는것이 효율적입니다

3. 값이 없으면 orElseThrow() 로 noSuchElementException 발생 (Java10)

// avoid
Optional<String> email = ...;
if (email.isPresent()) {
    return email.get();
} else {
    throw new NoSuchElementException();        
}

// prefer
Optional<String> email = ... ;
return email.orElseThrow();

Optional.orElseThorw() 메서드를 사용하는것은 isPresent-get() 을 사용하는것의 훌륭한 대안이 될 수 있습니다. Optional value 가 없을때 java.util.NoSuchElementException 예외를 throw 하기만 하면 됩니다. 그리고 Java10 부터는 인자없이 orELseThorw() 메서드를 실행하면 됩니다. 다만, Java8~9 의 경우에는 명시적인 예외를 전달하도록 해야합니다

4. 값이 없으면 명시적 예외 발생 orElseThrow(Supplier<? extends X> exceptionSupplier)

// avoid
Optional<String> email = ... ;
if (email.isPresent()) {
    return email.get();
} else {
    throw new IllegalStateException(); 
}

// prefer
Optional<String> email = ... ; 
return email.orElseThrow(IllegalStateException::new);
}

Java8, 9 에서 값이 없을때 명시적인 예외를 전달 할 수 있습니다.

5. isPresent() - get() 보다 Optional.ifPresent() 을 사용

// avoid
Optional<String> email = ... ;
if (email.isPresent()) {
    return email.get();
}

// prefer
email.ifPresent(System.out::println);

Optional.ifPresent() 는 값을 소비(꺼내거나 계산) 해야할때 isPresent() - get() 의 좋은 대안에 해당합니다. 그리고 값이 없는 경우에는 아무 작업도 수행하지 않습니다

6. Optional 대신 비어있는 컬렉션 리턴할것

// avoid
List<User> emails = user.getEmails();
return Optional.ofNullable(emails);

/* Good Case */
List<User> emails = user.getEmails();
return emails != null ? emails : Collections.emptyList();

컬렉션은 null 보다 비어있는 컬렉션을 리턴하는것이 좋을때가 많고 효율적입니다

// avoid
public interface UserRepository<User, Long> extends JpaRepository {
    Optional<List<User>> findAllByEmailContaining(String email);
}

// prefer
public interface UserRepository<User, Long> extends JpaRepository {
    List<User> findAllByEmialContaining(String email);
}

Collection 을 반환하는 Spring Data JPA Repository 메서드도 null 을 반환하지 않고 비어있는 컬렉션을 반환해주기때문에 Optional 로 감싸서 반환할 필요가 없습니다

7. Optional 을 필드로 사용 금지

Optional 은 필드에 사용할 목적으로 만들어지지 않았기 때문에, Serializable 을 구현하지 않았습니다.

따라서 Optional 을 필드로 사용하는것은 바람직하지 않습니다. 다만 Class 필드에 Optional 을 사용해도 괜찮다는 의견도 있으니 참고 하셔도 좋습니다.

// avoid
public class User {

    private Long id;
    private String name;
    private Optional<String> email = Optional.empty();
}

// prefer
public class User {

    private Long id;
    private String name;
    private String email;
}

8. Optional 타입의 필드를 선언 금지

Optionalsetter 를 포함한 메서드 또는 생성자 인자 에서 사용하지 않는것이 좋습니다

OptionalSerializable 을 구현하고 있지 않고 있고 Java Bean 의 속성으로 사용하기 위한 것이 아니기 때문입니다.

// avoid
public class User {

    [access_modifier] [static] [final] Optional<String> email;
    [access_modifier] [static] [final] Optional<String> email = Optional.empty();
    ...
}

// prefer
public class User {

    [access_modifier] [static] [final] String email;
    [access_modifier] [static] [final] String email = "";
    ...
}

9. 생성자의 인자에 Optional 사용 금지

Optional 은 다른 수준의 추상화로 객체를 감싸기 때문에 생성자의 인자에 Optional 을 사용하는것 피하는것이 좋으며 이러한 방법은 Optional 의도에 반하는 사용법입니다.

이런 경우는 아래와 같이 단순히 다른 코드를 추가해서 해결하는게 좋습니다

// avoid
public class User {

    private final String name;             // cannot be null
    private final Optional<String> email;  // optional field, thus may be null

    public User(String name, Optional<String> email) {
        this.name = Objects.requireNonNull(name, () -> "Name cannot be null");
        this.email = email;
    }

    public Optional<String> getEmail() {
        return email;
    }
    ...
}
// prefer
public class User {

    private final String name;     // cannot be null
    private final String email; // optional field, thus may be null

    public Cart(String name, String email) {
        this.name = Objects.requireNonNull(name, () -> "Name cannot be null");
        this.email = email;
    }

    public Optional<String> getEmail() {
        return Optional.ofNullable(email);
    }
    ...
}

10. Setter 인자에 Optional 사용 금지

Optional Java Bean 의 속성 또는 영구 속성 유형으로 사용하기 위한 것이 아니고 위에서 언급한것처럼 Serializable 하지 않습니다. Setter 에서 Optional 을 사용하는 것도 또 다른 안티패턴에 해당합니다.

따라서 엔티티 속성을 Optional 로 맵핑하는것은 피해야하며 도메인 모델에서 Getter 에 Optional 을 사용하는 것을 권장합니다

// AVOID
@Entity
public class User implements Serializable {

    private static final long serialVersionUID = 1L;
    ...
    @Column(name="email_address")
    private Optional<String> emailAddress; // optional field, thus may be null

     public Optional<String> getEmailAddress() {
       return emailAddress;
     }

     public void setEmailAddress(Optional<String> emailAddress) {
       this.emailAddress = emailAddress;
     }
     ...
}
// PREFER
@Entity
public class User implements Serializable {

    private static final long serialVersionUID = 1L;
    ...
    @Column(name="email_address")
    private Optional<String> emailAddress; // optional field, thus may be null

     public Optional<String> getEmailAddress() {
       return Optional.ofNullable(emailAddress);
    }

    public void setPostcode(String emailAddress) {
       this.emailAddress = emailAddress;
    }
    ...
}

11. 메서드 인자에 Optional 사용 금지

Optional 필드나 Setter 또는 생성자의 인수로 Optional 을 사용하는것은 바람직하지 않습니다.

메서드 인자에서 Optional 을 사용하는것은 또 다른 문제를 만들수 있습니다.

메서드를 호출하는곳에서 Optional을 생성하도록 강제하는것 대신 인자를 확인하는 책임을 전가하게 되고 코드를 복잡하게 만들어 종속성을 유발할 수 있게 됩니다.

// AVOID
public void renderCustomer(Cart cart, Optional<Renderer> renderer,
                           Optional<String> name) {     
    if (cart == null) {
        throw new IllegalArgumentException("Cart cannot be null");
    }

    Renderer customerRenderer = renderer.orElseThrow(
        () -> new IllegalArgumentException("Renderer cannot be null")
    );    

    String customerName = name.orElseGet(() -> "anonymous"); 
    ...
}

// call the method - don't do this
renderCustomer(cart, Optional.<Renderer>of(CoolRenderer::new), Optional.empty());
// PREFER
public void renderCustomer(Cart cart, Renderer renderer, String name) {

    if (cart == null) {
        throw new IllegalArgumentException("Cart cannot be null");
    }

    if (renderer == null) {
        throw new IllegalArgumentException("Renderer cannot be null");
    }

    String customerName = Objects.requireNonNullElseGet(name, () -> "anonymous");
    ...
}

// call this method
renderCustomer(cart, new CoolRenderer(), null);

한편 NullPointException 을 사용하는것을 피하고 IllgalArgumentException 을 사용하거나 다른 예외를 사용하는것이 좋습니다

// PREFER
// write your own helper
public final class MyObjects {

    private MyObjects() {
        throw new AssertionError("Cannot create instances for you!");
    }

    public static <T, X extends Throwable> T requireNotNullOrElseThrow(T obj, 
        Supplier<? extends X> exceptionSupplier) throws X {       

        if (obj != null) {
            return obj;
        } else { 
            throw exceptionSupplier.get();
        }
    }
}

public void renderCustomer(Cart cart, Renderer renderer, String name) {

    MyObjects.requireNotNullOrElseThrow(cart, 
                () -> new IllegalArgumentException("Cart cannot be null"));
    MyObjects.requireNotNullOrElseThrow(renderer, 
                () -> new IllegalArgumentException("Renderer cannot be null"));    

    String customerName = Objects.requireNonNullElseGet(name, () -> "anonymous");
    ...
}

// call this method
renderCustomer(cart, new CoolRenderer(), null);

12. Collection 에 Optional 사용 금지

Collection 에 Optional 을 사용하는것은 바람직한 방법이 아닙니다.

null 일수도 있는 Collection 이 null 일 수 있는 객체를 포함하는 자료구조를 가질 수 있기때문에 Optional 안의 Optional 이 구조가 되는 문제를 야기 할 수 있습니다.

이러한 경우 Map 자료구조의 getOrDefault 메서드를 사용해서 기본값을 리턴하는것이 좋습니다

// AVOID
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) {
    System.out.println("This key cannot be found");
} else {
    String unwrappedItem = item.orElse("NOT FOUND");
    System.out.println("Key found, Item: " + unwrappedItem);
}
//PREFER
Map<String, String> items = new HashMap<>();
items.put("I1", "Shoes");
items.put("I2", null);
...
// get an item
String item = get(items, "I1");  // Shoes
String item = get(items, "I2");  // null
String item = get(items, "I3");  // NOT FOUND

private static String get(Map<String, String> map, String key) {
  return map.getOrDefault(key, "NOT FOUND");
}

13. optional.of() 와 optional.ofNullable() 혼동하지 말것

optional.ofNullable() 대신 optional.of() 를 사용하거나 그 반대의 경우 이슈가 발생 할 수 있습니다.

optional.of(null)NullPointException 을 야기 할 수 있지만 optional.ofNullable()optional.empty 를 리턴합니다.

// AVOID
public Optional<String> fetchEmal(long id) {

    String email = ... ; // this may result in null
    ...
    return Optional.of(itemName); // this throws NPE if "itemName" is null :(
}

// PREFER
public Optional<String> fetchEmail(long id) {

    String email = ... ; // this may result in null
    ...
    return Optional.ofNullable(email); // no risk for NPE    
}

14. map() 또는 faltMap() 을 통한 값 변경

Optional.map()Optional.flatMap() 은 값을 변경하는데 매우 편리합니다.

// AVOID
Optional<String> lowername ...; // may be empty

// transform name to upper case
Optional<String> uppername;
if (lowername.isPresent()) {
    uppername = Optional.of(lowername.get().toUpperCase());
} else {
    uppername = Optional.empty();
}

// PREFER
Optional<String> lowername ...; // may be empty

// transform name to upper case
Optional<String> uppername = lowername.map(String::toUpperCase);
// AVOID
List<Product> products = ... ;

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

String name;
if (product.isPresent()) {
    name = product.get().getName().toUpperCase();
} else {
    name = "NOT FOUND";
}

// getName() return a non-null String
public String getName() {
    return name;
}

// PREFER
List<Product> products = ... ;

String name = products.stream()
    .filter(p -> p.getPrice() < 50)
    .findFirst()
    .map(Product::getName)
    .map(String::toUpperCase)
    .orElse("NOT FOUND");

// getName() return a String
public String getName() {
    return name;
}
// AVOID
List<Product> products = ... ;

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

String name = null;
if (product.isPresent()) {
    name = product.get().getName().orElse("NOT FOUND").toUpperCase();
}

// getName() return an Optional
public Optional<String> getName() {
    return Optional.ofNullable(name);
}

// PREFER
List<Product> products = ... ;

String name = products.stream()
    .filter(p -> p.getPrice() < 50)
    .findFirst()
    .flatMap(Product::getName)
    .map(String::toUpperCase)
    .orElse("NOT FOUND");

// getName() return an Optional
public Optional<String> getName() {
    return Optional.ofNullable(name);
}

15. filter() 를 먼저 사용하여 값 유무를 체크

filter() 메서드를 사용하면 Wrapped 된 값을 꺼내지 않고 값을 꺼낼지 말지를 미리 판단할수 있기때문에 좀 더 효율적인 코드를 작성 할 수 있습니다

// AVOID
public boolean validatePasswordLength(User userId) {

    Optional<String> password = ...; // User password

    if (password.isPresent()) {
        return password.get().length() > 5;
    }

    return false;
}

// PREFER
public boolean validatePasswordLength(User userId) {

    Optional<String> password = ...; // User password

    return password.filter((p) -> p.length() > 5).isPresent();
}

16. optional.stream() 사용

자바 9부터 Optional 인스턴스를 stream 을 적용하여 Optional.stream() 메서드를 사용할 수 있습니다.

이 메서드는 하나의 Stream 을 생성하거나 Optional 의 값이 없는 경우 빈 stream 을 생성해주기때문에 활용할 수 있는 상황에서 고려하는것이 좋습니다

// AVOID
public List<Product> getProductList(List<String> productId) {

    return productId.stream()
        .map(this::fetchProductById)
        .filter(Optional::isPresent)
        .map(Optional::get)
        .collect(toList());
}

public Optional<Product> fetchProductById(String id) {
    return Optional.ofNullable(...);
}
// PREFER
public List<Product> getProductList(List<String> productId) {
    return productId.stream()
        .map(this::fetchProductById)
        .flatMap(Optional::stream)
        .collect(toList());
}

public Optional<Product> fetchProductById(String id) {
    return Optional.ofNullable(...);
}

그리고 filter()map() flatMap()Optional.stream() 으로 바꿀수 있고 Optional 을 List 로 변경할수도 있습니다

public static <T> List<T> convertOptionalToList(Optional<T> optional) {
    return optional.stream().collect(toList());
}

17. Optional 이 비어있으면 boolean 을 리턴할것

자바 11부터는 Optional.isEmpty() 메서드를 통해 Optional 비어있는 경우 true 를 리턴할수 있습니다

// AVOID (Java 11+)
public Optional<String> fetchCartItems(long id) {

    Cart cart = ... ; // this may be null
    ...    
    return Optional.ofNullable(cart);
}

public boolean cartIsEmpty(long id) {

    Optional<String> cart = fetchCartItems(id);

    return !cart.isPresent();
}

// PREFER (Java 11+)
public Optional<String> fetchCartItems(long id) {

    Cart cart = ... ; // this may be null
    ...    
    return Optional.ofNullable(cart);
}

public boolean cartIsEmpty(long id) {

    Optional<String> cart = fetchCartItems(id);

    return cart.isEmpty();
}

5. Test Code


Optional 은 객체 탐색을 하더라도 NullPointException이 발생하지 않습니다.

비어있을 경우 isEmpty가 출력됩니다. 테스트 코드는 아래와 같습니다

@Test
public void Optional_not_empty() {
    final Optional<Insurance> name = Optional.of(new Insurance("name", 10));
    final Optional<Car> car = Optional.of(new Car("car", name));
    final Optional<Person> person = Optional.of(new Person("name", "address", car));

    final String result = person.flatMap(Person::getCar)
            .flatMap(Car::getInsurance)
            .map(Insurance::getName)
            .orElse("isEmpty");

    System.out.println(result); // Insurance name  출력
}

@Test
public void Optional_empty() {
    final Optional<Insurance> name = Optional.of(new Insurance("name", 10));
    final Optional<Car> car = Optional.empty();
    final Optional<Person> person = Optional.of(new Person("name", "address", car));

    final String result = person.flatMap(Person::getCar)
            .flatMap(Car::getInsurance)
            .map(Insurance::getName)
            .orElse("isEmpty"); // Insurance name  출력

    System.out.println(result);
}

Reference

💡

Uploaded by N2T

'Java' 카테고리의 다른 글

Optional -2  (0) 2023.01.18
Optional -1  (1) 2022.12.26
Java Stream & Function API  (0) 2022.12.21
Concurrency solution -1  (0) 2022.10.22
추상클래스와 인터페이스  (0) 2021.12.12
Comments