본문 바로가기

공부 정리/Java

Optional을 제대로 사용하는 26가지 방법 ( 2 )

728x90
반응형

Item 14 : Optional을 생성자 매개변수에서 사용하지 말 것

// 잘못된 방법
public class Customer {

    private final String name;               // null일 수 없다
    private final Optional<String> postcode; // optional이므로 null일 수 있다

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

    public Optional<String> getPostcode() {
        return postcode;
    }
    ...
}

// 옳은 방법
public class Customer {

    private final String name;     // null일 수 없다
    private final String postcode; // null이 수 없다

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

    public Optional<String> getPostcode() {
        return Optional.ofNullable(postcode);
    }
    ...
}

Optionalsetter를 포함하여 메서드의 필드로 사용하면 안됩니다. 

이것은 Optional에 대한 또다른 의도입니다. Optional은 객체를 다른 추상화 수준으로 래핑합니다. 그 경우에 단지 과도한 상용구 코드를 거하는 꼴입니다. 위의 예제에서 getter는 Optional을 반환합니다. 위의 예제를 모든 getter를 Optional로 만들어야 한다는 것으로 생각해서는 안됩니다. 대부분의 경우, getter는 collection이나 array를 반환합니다. Optional 대신에 비어있는 collection/array를 반환하는 방식이 사용됩니다. 위의 예제는 단지 예시일 뿐입니다.

 

Item 15 : Optional을 Setter 매개변수에 사용하지 말 것

// 잘못된 방법
@Entity
public class Customer implements Serializable {

    private static final long serialVersionUID = 1L;
    ...
    @Column(name="customer_zip")
    private Optional<String> postcode; // optional이므로 null일 수 있음

     public Optional<String> getPostcode() {
       return postcode;
     }

     public void setPostcode(Optional<String> postcode) {
       this.postcode = postcode;
     }
     ...
}

// 옳은 방법
@Entity
public class Customer implements Serializable {

    private static final long serialVersionUID = 1L;
    ...
    @Column(name="customer_zip")
    private String postcode;

    public Optional<String> getPostcode() {
      return Optional.ofNullable(postcode);
    }

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

Optional은 Java Bean의 속성으로 사용되거나 영구적인 속성타입으로 사용하도록 만들어지지 않았습니다. OptionalSerializable이 아닙니다.

Optional을 setter에서 사용하는 것은 또다른 안티-패턴입니다. 대개, Optional을 영구 속성 타입으로 사용하는 경우를 보이기도 하지만, Domain Model entites에서만 Optional을 사용하는 것이 가능합니다.

 

Item 16 : Optional을 메서드 매개변수로 사용하지 말 것

// 잘못된 방법
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"); 
    ...
}

// 이렇게 사용 하지 말 것
renderCustomer(cart, Optional.<Renderer>of(CoolRenderer::new), Optional.empty());


// 옳은 방법1 - null
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");
    ...
}

// 이렇게 사용할 것
renderCustomer(cart, new CoolRenderer(), null);


// 옳은 방법2 - NullPointerException()
public void renderCustomer(Cart cart, Renderer renderer, String name) {

    Objects.requireNonNull(cart, "Cart cannot be null");        
    Objects.requireNonNull(renderer, "Renderer cannot be null");        

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

// 이렇게 사용할 것
renderCustomer(cart, new CoolRenderer(), null);


// 옳은 방법3 - IllegalArgumentException() , 다른 예외
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");
    ...
}

// 이렇게 사용 할 것
renderCustomer(cart, new CoolRenderer(), null); 

Optional은 필드와 setter, 생성자의 매개변수로 사용해서는 안됩니다. Optional을 만들도록 호출을 강제하면 안됩니다.

메서드 매개변수에 Optional을 사용하는 것은 또다른 실수입니다. 이것은 불필요한 복잡성만 증가시킬 뿐입니다. Optional을 생성하기 위해 모든 호출을 강제하는 것보다는 매개변수를 검증할 수 있는 책임감을 높여야합니다. Optional은 단지 또다른 객체(컨테이너) 라는 사실을 인지해야합니다. -  베어 참조 메모리의 4배를 소비합니다! 

 

Item 17 : Optional을 비어있는 Collection과 Array의 반환형으로 사용하지 말 것

// 잘못된 방법
public Optional<List<String>> fetchCartItems(long id) {

    Cart cart = ... ;    
    List<String> items = cart.getItems(); // null을 리턴하는 코드

    return Optional.ofNullable(items);
}

// 옳은 방법
public List<String> fetchCartItems(long id) {

    Cart cart = ... ;    
    List<String> items = cart.getItems(); // null을 리턴하는 코드

    return items == null ? Collections.emptyList() : items;
}

Collections.emptyList(), emptyMap(), emptySet() 이라는 메서드가 있으므로, Optional로 리턴을 하지 않아도 됩니다. 크린코드를 유지하고 경량화시키기 위해서 Optional 혹은 collection/array을 null로 반환하는 것을 피하십시오. 비어있는 array, collection이면 충분합니다.

 

Item 18 : Optional을 Collections 안에서 사용하지 말 것

// 잘못된 방법
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);
}


//옳은 방법
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");
}


//최악의 경우
Map<Optional<String>, String> items = new HashMap<>();
Map<Optional<String>, Optional<String>> items = new HashMap<>();

collections 에서 Optional을 사용하는 것은 개선이 필요합니다. 이 접근 방법을 유지시키기 위해 가장 많이 사용되는 매개변수는 Map일 것입니다. 결론적으로 말해, Optional은 공짜 비용의 기능이 아닙니다. 단지 메모리와 필요사항들을 소비하는 또다른 객체일 뿐이라는 것을 명심해야 합니다. 

 

Item 19 : Optional.of()와 Optional.ofNullable()을 혼동하지 말 것

 

* Optional.ofNullable()을 사용하는 경우

// 잘못된 방법
public Optional<String> fetchItemName(long id) {

    String itemName = ... ; // null을 리턴하는 코드
    ...
    return Optional.of(itemName); // NPE 예외 발생
}

// 옳은 방법
public Optional<String> fetchItemName(long id) {

    String itemName = ... ; // null을 리턴하는 코드
    ...
    return Optional.ofNullable(itemName); // 아무 문제 없음!
}

 

*Optional.of()를 사용하는 경우

// 잘못된 방법
return Optional.ofNullable("PENDING"); // ofNullable은 아무 작동 안함

// PREFER
return Optional.of("PENDING"); // NPE 예외 걱정 없음!

Optional.of(null)NullPointerException을 발생시킬 것이지만, Optional.ofNullable(null)Optional.emtpy()를 리턴합니다. 이 두가지의 차이점에 명심합니다.

 

Item 20 : Optional<T>을 사용하지 말고 Generic이 아닌 OptionalInt, OptionalLong, OptionalDouble을 선택할 것

 

// 잘못된 방법
// 박싱된 원시타입이 필요할 때만 사용할 것
Optional<Integer> price = Optional.of(50);
Optional<Long> price = Optional.of(50L);
Optional<Double> price = Optional.of(50.43d);

// 올바른 방법
OptionalInt price = OptionalInt.of(50);           // unwrap via getAsInt()
OptionalLong price = OptionalLong.of(50L);        // unwrap via getAsLong()
OptionalDouble price = OptionalDouble.of(50.43d); // unwrap via getAsDouble()

특정 요구에 의해 박싱되어 있는 원시값들이 필요한 것이 아니라면, Optional<T>를 피하고 OptionalInt, OptionalLong, OptionalDouble을 사용해야 합니다.

박싱과 언박싱은 비싼 연산이므로 성능저하를 초래할 수 있습니다. 이러한 위험을 없애기 위해서 우리는 위의 코드를 사용할 수 있습니다. 이것들은 int, long, double 원시타입에 대한 래퍼들입니다.

 

Item 21 : 동등성(Equality)를 검증하기 위해서 Optional을 언래핑하지 말 것

 

// 잘못된 방법
Optional<String> actualItem = Optional.of("Shoes");
Optional<String> expectedItem = Optional.of("Shoes");        

assertEquals(expectedItem.get(), actualItem.get());

// 옳은 방법
Optional<String> actualItem = Optional.of("Shoes");
Optional<String> expectedItem = Optional.of("Shoes");        

assertEquals(expectedItem, actualItem);

assertEquals() 안에 2개의 Optional 값을 가지는 것은 언래핑 값을 필요로 하지 않습니다. 이것이 가능한 이유는 Optional#equals()가 언래핑한 값을 비교하지, Optional 객체를 비교하는 것이 아니기 때문입니다.

 

* Optional.equals() 소스

@Override
public boolean equals(Object obj) {
    if (this == obj) {
        return true;
    }

    if (!(obj instanceof Optional)) {
        return false;
    }

    Optional<?> other = (Optional<?>) obj;
    return Objects.equals(value, other.value);
}

 

Item 22 : Map()과 flatMap()을 사용해서 값을 변형시킬 것

 

*map() 사용 

 

예시 1

// 잘못된 방법
Optional<String> lowername ...; // empty를 리턴

// 대문자로 변경
Optional<String> uppername;
if (lowername.isPresent()) {
    uppername = Optional.of(lowername.get().toUpperCase());
} else {
    uppername = Optional.empty();
}


// 올바른 방법
Optional<String> lowername ...; // empty를 리턴

// 대문자로 변경
Optional<String> uppername = lowername.map(String::toUpperCase);

예시 2

//잘못된 방법
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;
}


// 올바른 방법
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;
}

 

*flatMap() 사용

// 잘못된 방법
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);
}


// 올바른 방법
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);
}

Optional.ap()과 Optional.flatMap()은 Optional 값을 변형하는데 굉장히 편안한 방법입니다. map() 메서드는 함수 매개변수를 값에 적용하고, Optional 안에 래핑된 결과를 리턴합니다. 그와 반대로, flatMap() 메서드는 Optional 값에 적용된 함수 매개변수를 획득ㄷ하고, 결과를 바로 리턴합니다.

 

 

Item 23 : filter()를 사용해서 미리 정의 된 규칙을 기반으로 래핑된 값을 쓰지 말 것

// 잘못된 방법
public boolean validatePasswordLength(User userId) {

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

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

    return false;
}


// 옳은 방법
public boolean validatePasswordLength(User userId) {

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

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

 조건문을 매개변수로 사용하고 Optional 객체를 획득해야 합니다. 만약 조건문이 충족되면, 초기의 Optional을 사용해야 하며, 조건이 충족되지 않으면 빈 Optional을 바환합니다.

 

래핑된 값을 처리하기 위해 filter()를 사용하는 것은 굉장리 편리합니다. 왜냐하면 그것은 명시적으로 래핑되지 않은 값들도 처리할 수 있기 때문입니다.

 

Item 24 : Stream을 사용해서 Optional을 연결해야 할까? ( 정답은, 그렇다!)

// 잘못된 방법
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(...);
}


// 옳은 방법
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(...);
}

Optional.stream() 메서드를 통해서 가능합니다. 자바9를 사용하면 우리는 Optional.stream() 메서드를 이용해서 OptionalStream으로 다룰 수 있습니다. 이것은 OptionalStream으로 연속적으로 사용할 때 유용합니다. 이 메서드는 하나 요소의 Stream을 만들거나 Optional이 존재하지 않는다면 비어있는 Stream을 만듭니다. 더욱이, 우리는 Stream API에서 사용가능한 모든 메서드들을 이용할 수 있습니다.

 

실용적으로 Optional.stream()은 우리가 filter()map()이나 flatMap()으로 대체할 수 있도록 합니다.

 

다음은 OptionalList로 만드는 방법입니다.

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

 

Item 25 : Optional로 동일성(Identity) 연산을 하지 말 것

// 잘못된 방법
Product product = new Product();
Optional<Product> op1 = Optional.of(product);
Optional<Product> op2 = Optional.of(product);

// op1 == op2 => true를 기대하였으나, false
if (op1 == op2) { ...


// 옳은 방법
Product product = new Product();
Optional<Product> op1 = Optional.of(product);
Optional<Product> op2 = Optional.of(product);

// op1.equals(op2) => true를 기대하고, true
if (op1.equals(op2)) { ...

Optional 클래스는 값을 기반으로하는 클래스입니다.

 

Item26 : Optional이 비어있으면, boolean을 리턴할 것. 자바 11은 Optional.Empty()를 지원

// 잘못된 방법 (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();
}

// 옳은 방법 (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();
}

자바 11에서는, Optional이 비어있다면 isEmpty() 메서드를 통해서 true를 리턴할 수 있습니다.

 

 

*결론

Optional을 사용하는 것은 첫눈에 볼때만큼 쉬운 것은 아닙니다. 주로, Optional은 리턴타입으로 사용되도록 만들어졌으며, 유연한 API를 만들기 위해서 스트림과 같이 사용하면 좋습니다. 그러나, 오히려 예상하지 않은 행동들을 유발하거나 코드의 품질을 떨어뜨리는 많은 함정들을 나도 모르게 저지르도록 유혹에 빠지곤 합니다. 이 26가지 규칙을 기억한다면, 이런 함정들에 빠지지 않을 것입니다. 

 

728x90
반응형