Java, Optional

작성일

Optional이란

Optional을 사용하면 null이 올 수 있는 값을 감싸서 참조하더라도 NPE가 발생하지 않도록 도와준다. Optional은 Java8 부터 지원한다.

참고 NPE(NullPointerException) 개발을 할 때 가장 많이 발생하는 예외 중 하나가 바로 NPE이다. null 여부를 검사하는 코드는 복잡하고 번거롭다. 그래서 null 대신 초기값을 사용하길 권장하기도 한다.

Optional에서 제공하는 메서드

Optional 객체 생성

// of(): 값이 널인 경우 NPE 오류 발생. 값이 반드시 있어야 한다.
Optional.of(value);

// ofNullable(): 값이 널인 경우 비어있는 Optional 반환. 값이 널일 수도 있는 경우에 사용한다.
Optional.ofNullable(null);

// empty(): 비어있는 Optional 객체 생성
Optional.empty();

Optional 중간 처리

// filter(): 값이 참이면 해당 필터를 통과시키고 거짓이면 통과시키지 않는다.
Optional.of("1").filter((val) -> "1".eqauls(val)).orElse("NO DATA"); // 1
Optional.of("0").filter((val) -> "1".eqauls(val)).orElse("NO DATA"); // NO DATA

// map()
// Convert String to Integer
Integer test = Optional.of("1").map(Integer::valueOf).orElseThrow(NoSuchElementException::new);

Optional 종단 처리

// ifPresent()
Optional.of("test").ifPresent((value) -> {...}); // something to do
Optional.ofNullable(null).ifPresent((value) -> {...}); // nothing to do

// isPresent(): 객체가 존재하는지 여부 판별
Optional.ofNullable("1").isPresent(); // true
Optional.ofNullable("1").filter((val) -> "0".eqauls(val)).isPresent(); // false

// get(): 객체를 꺼낸다. 비어있는 객체이면 오류가 발생한다.
Optional.of("test").get(); // 'test'
Optional.ofNullable(null).get(); // Exception!!!

// orElse(): 객체가 비어있다면 기본값을 제공한다.
Optional.ofNullable(null).orElse("default"); // 'default'

// orElseGet(): 객체가 비어있다면 기본값을 제공한다.
Optional.ofNullable("input").filter("test"::equals).orElseGet(() -> "default"); // 'default'

// orElseThrow(): 객체가 비어있거나 값이 맞는 부분이 없으면 Exception을 던진다.
Optional.ofNullable("input").filter("test"::equals).orElseThrow(NoSuchElementException::new);

만약, 빈 Optional 객체에 get() 메서드를 호출하면 오류가 발생하므로 Optional 객체를 가져오기 전에 값이 있는지 확인 해야 한다.

이때, 값이 있는지 확인하는 방법은 두가지가 있다.

  1. isPresent()-get()
  2. orElse(), orElseGet(), orElseThrow() (권장)

두가지 방법 중 권장하는 방법은 orElse(), orElseGet(), orElseThrow()를 사용하는 것이다.

Optional 사용 시 주의할 점

Optional 사용 시 잘못 사용하는 안티 패턴올바르게 사용하는 방법에 대해 알아보자.

1. orElse()/orElseGet()/orElseThrow()

isPresent()-get() 대신 orElse(), orElseGet(), orElseThrow()를 사용하면 코드를 줄일 수 있다.

// 안 좋음
Optional<Member> member = ...;
if (member.isPresent()) {
    return member.get();
} else {
    return null;
}

// 좋음
Optional<Member> member = ...;
return member.orElse(null);

// 안 좋음
Optional<Member> member = ...;
if (member.isPresent()) {
    return member.get();
} else {
    throw new NoSuchElementException();
}

// 좋음
Optional<Member> member = ...;
return member.orElseThrow(() -> new NoSuchElementException());

2. orElseGet(() → new …)

orElse(…)에서 …은 Optional에 값이 있든 없든 무조건 실행된다. 따라서, …가 새로운 객체를 생성하거나 새로운 연산을 수행하는 경우 orElse() 대신 orElseGet()을 써야 한다.

Optional에 값이 없으면 orElse()의 인자로서 실행된 값이 반환되므로 실행한 의미가 있지만, Optional에 값이 있으면 orElse()의 인자로서 실행된 값이 무시되고 버려진다.

// 안 좋음
Optional<Member> member = ...;
return member.orElse(new Member());  // member에 값이 있든 없든 new Member()는 무조건 실행됨

// 좋음
Optional<Member> member = ...;
return member.orElseGet(Member::new);  // member에 값이 없을 때만 new Member()가 실행됨

// 좋음
Member EMPTY_MEMBER = new Member();
...
Optional<Member> member = ...;
return member.orElse(EMPTY_MEMBER);  // 이미 생성됐거나 계산된 값은 orElse()를 사용해도 무방

3. 단지 값을 얻을 목적이라면 Optional 대신 null 비교

// 안 좋음
return Optional.ofNullable(status).orElse(READY);

// 좋음
return status != null ? status : READY;

4. Optional 대신 비어있는 컬렉션 반환

Optional은 비용이 비싸다. 그리고 컬렉션은 null이 아니라 비어있는 컬렉션을 반환하는 것이 좋을 때가 많다. 따라서, 컬렉션은 Optional로 감싸서 반환하지 말고 비어있는 컬렉션을 반환하자.

List<Member> members = team.getMembers();
return Optional.ofNullbale(members);

List<Member> members = team.getMembers();
return members != null ? members : Collections.emptyList();

마찬가지 이유로 Spring Data JPA Respository 메서드 선언 시 컬렉션을 Optional로 감싸서 반환하는 것은 좋지 않다. 컬렉션을 반환하는 JPA Repository 메서드는 null을 반환하지 않고 비어있는 컬렉션을 반환해주므로 Optional을 사용할 필요가 없다.

// 안 좋음
public interface MemberRepository<Member, Long> extends JpaRepository {
    Optional<List<Member>> findAllByNameContaining(String part);
}

// 좋음
public interface MemberRepository<Member, Long> extends JpaRepository {
    List<Member> findAllByNameContaining(String part);  // null이 반환되지 않으므로 Optional 불필요
}

5. Optional을 필드로 사용 금지

// 안 좋음
public class Member {
    private Long id;
    private String name;
    private Optional<String> email = Optional.empty();
}

// 좋음
public class Member {
    private Long id;
    private String name;
    private String email;
}

6. Optional을 생성자나 메서드 인자로 사용 금지

// 안 좋음
public class HRManager {
    public void increaseSalary(Optional<Member> member) {
        member.ifPresent(member -> member.increaseSalary(10));
    }
}
hrManager.increaseSalary(Optional.ofNullable(member));

// 좋음
public class HRManager {
    public void increaseSalary(Member member) {
        if (member != null) {
            member.increaseSalary(10);
        }
    }
}
hrManager.increaseSalary(member);

7. Optional을 컬렉션의 원소로 사용 금지

// 안 좋음
Map<String, Optional<String>> sports = new HashMap<>();
sports.put("100", Optional.of("BasketBall"));
sports.put("101", Optional.ofNullable(someOtherSports));
String basketBall = sports.get("100").orElse("BasketBall");
String unknown = sports.get("101").orElse("");

// 좋음
Map<String, String> sports = new HashMap<>();
sports.put("100", "BasketBall");
sports.put("101", null);
String basketBall = sports.getOrDefault("100", "BasketBall");
String unknown = sports.computeIfAbsent("101", k -> "");

8. of(), ofNullable() 혼동 주의

of(X)는 X가 null이 아님이 확실할 때만 사용해야 한다.

// 안 좋음
return Optional.of(member.getEmail());  // member의 email이 null이면 NPE 발생

// 좋음
return Optional.ofNullable(member.getEmail());

// 안 좋음
return Optional.ofNullable("READY");

// 좋음
return Optional.of("READY");

9. Optional 대신 OptionalInt, OptionalLong, OptionalDouble

// 안 좋음
Optional<Integer> count = Optional.of(38);  // boxing 발생
for (int i = 0 ; i < count.get() ; i++) { ... }  // unboxing 발생

// 좋음
OptionalInt count = OptionalInt.of(38);  // boxing 발생 안 함
for (int i = 0 ; i < count.getAsInt() ; i++) { ... }  // unboxing 발생 안 함

카테고리: