[자바의 정석] Optional<T>와 OptionalInt

아래 내용들은 자바의 정석에 나오는 내용을 발췌한 것이다.

Optinoal<T>와 OptionalInt

java.util.Optional은 JDK1.8부터 추가되었다.
Optional<T\>은 지네릭 클래스로 'T타입의 객체'를 감싸는 래퍼 클래스이다.
그래서 Optional타입의 객체는 모든 타입의 참조변수를 담을 수 있다.

public final class Optional<T> {
    private final T value; // T타입의 참조변수
    	...
}

최종 연산의 결과를 그냥 반환하는게 아니라 Optional 객체에 담아서 반환하는 것이다. 이처럼 객체에 담아서 반환을 하면, 반환된 결과가 null인지 매번 if문으로 체크하는 대신 Optional에 정의된 메서드를 통해서 간단히 처리할 수 있다.
이제 널 체크를 위한 if문 없이도 NullPointerException이 발생하지 않는 코드 작성이 가능하다.

Optional 객체 생성하기

Optional 객체를 생성할 때는 of() 또는 ofNullable()을 사용한다.

String str = "abc";
Optional<String> optVal = Optional.of(str);

만약 참조변수의 값이 null일 가능성이 있으면, of() 대신 ofNullable()을 사용해야한다. of()는 매개변수의 값이 null이면 NullPointerException이 발생하기 때문이다.

Optional<String> optVal = Optional.of(null); //NPE 발생
Optional<String> optVal = Optional.ofNullable(null); // OK

Optional<T>타입의 참조변수를 기본값으로 초기화할 때는 empty()를 사용한다. null로 초기화하는 것도 가능하지만, empty()로 초기화하는 것이 바람직하다.

Optional<String> optVal = null ;
Optional<String> optVal = Optional.<String>empty(); // 빈 객체로 초기화

empty()는 지네릭 메서드라서 앞에 <T>를 붙였다. 추정 가능하므로 생략할 수 있다.

Optional 객체의 값 가져오기

Optional객체에 저장된 값을 가져올 때는 get()을 사용한다. 값이 null일 때는 NoSuchElementException이 발생하며, 이를 대비해서 orElse()로 대체할 값을 지정할 수 있다.

Optional<String> optVal = Optional.of("abc");
String str1 = optVal.get(); //optVal에 저장된 값을 반환. null이면 예외 발생
String str2 = optVal.orElse(""); //optVal에 저장된 값이 null일 때는 ""를 반환

orElse()의 변형으로는 null을 대체할 값을 반환하는 람다식을 지정할 수 있는 orElseGet() 과 null일 때 지정된 예외를 발생시키는 orElseThrow()가 있다.

String str3 = optVal2.orElseGet(String::new); // () -> new String()와 동일
String str4 = optVal2.orElseThrow(NullPointerException::new); //널이면 예외발생

Stream처럼 Optional객체에도 filter(), map(), flatMap()을 사용할 수 있다. map()의 연산결과가 Optional<Optional<T>>일 때, flatMap()을 사용하면 Optional<T>를 결과로 얻는다. 만일 Optional 객체의 값이 null이면, 이 메서드들은 아무 일도 하지 않는다.

int result = Optional.of("")
			.filter(x->x.length() > 0)
            .map(Integer::parseInt).orElse(-1); // result = -1

우리가 이미 알고 있는 것처럼 parseInt() 는 예외가 발생하기 쉬운 메서드이다. 만일 예외처리된 메서드를 만든다면 다음과 같을 것이다.

static int optStrToInt(Optional<String> optStr, int defaultValue) {
    try {
        return optStr.map(Integer::parseInt).get();
    } catch (Exception e) {
        return defaultValue;
    }
}

isPresent()Optional 객체의 값이 null이면 false를, 아니면 true를 반환한다. isPresent(Consumer<T> block)은 값이 있으면 주어진 람다식을 실행하고, 없으면 아무일도 하지 않는다.

if(str != null) {
    System.out.println(str);
}

위와 같은 조건문을 isPresent()를 이용해서 다음과 같이 쓸 수 있다.

if(Optional.ofNullable(str).isPresent()) {
    System.out.println(str);
}

이 코드를 ifPresent()를 이용해서 바꾸면 더 간단히 할 수 있다. 아래의 문장은 참조변수 str이 null이 아닐 때만 출력하고, null이면 아무 일도 일어나지 않는다.

Optional.ofNullable(str).ifPresent(System.out::println);

OptionalInt, OptionalLong, OptionalDouble

IntStream과 같은 기본형 스트림에는 Optional도 기본형을 값으로 하는 OptionalInt, OptionalLong, OptionalDouble을 반환한다.

OptionalInt는 다음과 같이 정의도어있다.

public final class OptionalInt {
    ...
    private final boolean isPresent; //값이 저장되어 있으면 true
    private final int value; //int 타입의 변수

기본형 int의 기본값은 0이므로 아무런 값도 갖지 않는 OptionalInt에 저장되는 값은 0일 것이다. 그러면 아래의 두 OptionalInt객체는 같은 것일까?

OptionalInt opt = OptionalInt.of(0); //OptionalInt에 0을 저장
OptionalInt opt2 = OptionalInt.empty(); //OptionalInt에 null을 저장

다행히 저장된 값이 없는 것과 0이 저장된 것은 isPresent라는 인스턴스 변수로 구분이 가능하다. isPresent()는 이 인스턴스 변수의 값을 반환한다.

System.out.println(opt.isPresent()); // true
System.out.println(opt2.isPresent()); // false

System.out.println(opt.getAsInt()); // 0
System.out.println(opt2.getAsInt()); // NoSuchElementException 발생

System.out.println(opt.equals(opt2); // false

그러나 Optional객체의 경우 null을 저장하면 비어있는 것과 동일하게 취급한다.

Optional<String> opt = Optional.ofNullable(null);
Optional<String> opt2 = Optional.empty();

System.out.println(opt.equals(opt2)); //true

기타 블로그 정리

만약 좋다면 모든 곳에 왜 다 안쓸까?

public class FooBar {
    private Optional<String> alpha;
    private Optional<String> beta;
    private Optional<String> gamma;
    private Optional<String> delta;
    private Optional<String> epsilon;
    private Optional<String> zeta;
    private Optional<String> eta;
    private Optional<String> theta;
    private Optional<String> iota;
    ...
}

딱봐도 이상하다.

Optional is primarily intended for use as a method return type where there is a clear need to represent "no result," and where using null is likely to cause errors. A variable whose type is Optional should never itself be null; it should always point to an Optional instance.

Optional은 return 타입에만 쓰는 것이 적절하다.

Brian Goetz는 스택오버플로우에서 Optional을 만든 의도에 대해 다음과 같이 말했다.

… 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” …

출처: Java Optional 바르게 쓰기
정확한 사용 예제까지 담고 있다.

출처: https://johngrib.github.io/wiki/java-optional/

바르게 쓰기

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. orElse(new ...) 대신 orElseGet(() -> new ...)

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

// 안 좋음
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()를 사용해도 무방

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

Optional은 비싸다. 따라서 단순히 값 또는 null을 얻을 목적이라면 Optional 대신 null 비교를 쓰자.

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

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

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

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

// 안 좋음
List<Member> members = team.getMembers();
return Optional.ofNullable(members);

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

컬렉션을 반환하는 Spring Data 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 불필요
}

Optional을 필드로 사용 금지

Optional은 필드에 사용할 목적으로 만들어지지 않았으며, Serializable을 구현하지 않았다. 따라서 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을 생성자나 메서드 인자로 사용 금지

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

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

of(X)은 X가 null이 아님이 확실할 때만 사용해야 하며, X가 null이면 NullPointerException 이 발생한다.
ofNullable(X)은 X가 null일 수도 있을 때만 사용해야 하며, X가 null이 아님이 확실하면 of(X)를 사용해야 한다.

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

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



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

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

9. Optional<T> 대신 OptionalInt, OptionalLong, OptionalDouble

출처: http://homoefficio.github.io/2019/10/03/Java-Optional-%EB%B0%94%EB%A5%B4%EA%B2%8C-%EC%93%B0%EA%B8%B0/

실제 내 코드에서 예시


    @GetMapping("/{id}")
    @ResponseStatus(HttpStatus.OK)
    public Task getTaskById(@PathVariable Long id) {
        Optional<Task> task = taskRepository.getTaskById(id);
        if (task.isEmpty()) {
            throw new DataNotFoundException();
        }
        return task.get();
    }

위의 코드도 나빠 보이지는 않지만 더 깔끔하게 바꿀 수 있다.

    @GetMapping("/{id}")
    @ResponseStatus(HttpStatus.OK)
    public Task getTaskById(@PathVariable Long id) {
        return taskRepository.getTaskById(id).orElseThrow(DataNotFoundException::new);
    }

이렇게 되면 훨씬 깔끔하다! 그리고 확실히 리턴값은 Task라는 보장이 생긴다.

참고로 :: 은 이중 콜론 연산자로, 함수를 호출하는 것이다.

좋은 웹페이지 즐겨찾기