
최근에 서비스 하나를 새롭게 개발하고 있다.
그런데, Optional을 어떤 상황에 써야할지 모르겠고, 뒤죽박죽으로 사용하는 생각이 들었다.
그래서 찾아보니 Optional을 올바르게 사용하기 위한 방법이 정리되어 있는 글이 있어 정리하면서 토이 프로젝트에도 적용해보고자 한다.
Optional을 올바르게 사용하기에 앞서 java에 Optional을 추가하게 된 의도를 먼저 알아야 한다.
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
https://docs.oracle.com/javase/9/docs/api/java/util/Optional.html
자바 공식 문서에 따르면 다음과 같았다.
Optional은 주로 "결과 없음"을 표시해야 하는 명확한 필요성이 있고 null을 사용하면 오류가 발생할 가능성이 있는 경우 메서드 반환 유형으로 사용하기 위한 것입니다. 유형이 Optional인 변수는 그 자체가 null이 되어서는 안 됩니다. 항상 Optional 인스턴스를 가리켜야 합니다.
Optional 자체가 반환값이 null인 경우를 대비하기 위한 객체이므로 Optional에 null을 할당해서는 안된다.
/**
* 잘못된 방법
**/
public Optional<BusStopDto> findBusStopById(Long busStopId) {
if (busStop == null) {
return null;
}
}
/**
* 올바른 방법
**/
public Optional<BusStopDto> findBusStopById(Long busStopId) {
if (busStop == null) {
return Optional.empty();
}
}
null을 반환하는것 보다 Optional.empty()를 반환하는게 Optional 의도에 더 알맞는 방법이다.
다만, null을 리턴하더라도 결과는 같다.
즉, 아래의 코드는 모두 Optiona.empty()가 할당된다.
Optional<BusStopDto> emptyObj = Optional.empty();
if (emptyObj.isPresent()) {
System.out.println("emptyObj is present");
}
Optional<BusStopDto> nullObj = null;
if (nullObj.isPresent()) {
System.out.println("nullObj is present");
}
Optional.get() 메서드를 사용하면 Optional 내부 객체를 가져올 수 있는데, 빈 Optional 객체에 get() 메서드를 호출하면 NoSuchElementException이 발생하므로 주의해야한다.
/***
* 잘못된 방법
***/
Optional<BusStopDto> emptyObj = Optional.empty();
if (emptyObj.isPresent()) {
System.out.println("emptyObj is present");
BusStopDto busStopDto1 = emptyObj.get();
} else {
System.out.println("emptyObj NoSuchElementException!!!");
}
/***
* 올바른 방법
***/
Optional<BusStopDto> emptyObj = (Optional<BusStopDto>) Optional.empty()
.orElseThrow(
() -> new TrafficException(BusStopErrorCode.BUS_STOP_NOT_EXIST)
);
내 생각엔 isPresent 함수로 체크하는것도 나쁘지 않다고 생각했는데, 가독성 측면에서 좋지 않다고 한다.
가급적 orElse 방법을 사용하자.(더 좋은 방법이 있다고 한다.)
/***
* 잘못된 방법, new BusStop()은 값이 있든 없든 항상 실행된다.
***/
BusStop busStop = busStopRepository.findById("1")
.orElse(new BusStop());
/***
* 올바른 방법
***/
public BusStop EMPTY_BUS_STOP = new BusStop();
BusStop busStop = busStopRepository.findById("1")
.orElse(EMPTY_BUS_STOP);
이번에 알게 된 내용인데, orElse 메서드의 인자는 Optional 객체가 존재할 때에도 수행된다고 한다.
즉, orElse()의 수행 순서는 다음과 같다.
1) orElse() 수행
2) findById() 수행 이후 덮어쓰기
2번에서 값이 존재하면 덮어쓰기 때문에 1번 연산의 결과는 결과적으로 버려진다.
사실, 이름 때문에 findById의 값이 존재하지 않으면 orElse가 수행될거라고 생각했는데, 그게 아니었다
따라서, orElse는 항상 실행되기 때문에 이미 만들어진 값을 넣어두는게 좋다.
따라서 매번 새로운 객체를 만들어야한다면 4번을 사용해야한다고 한다.
/***
* 잘못된 방법, new BusStop()은 값이 있든 없든 항상 실행된다.
***/
BusStop busStop = busStopRepository.findById("1")
.orElse(new BusStop());
/***
* 올바른 방법
***/
BusStop busStop = busStopRepository.findById("1")
.orElseGet(BusStop::new);
orElseGet(Supplier) 메서드는 Supplier에 Optional 값이 없을 때만 수행된다고 한다. 따라서 항상 실행되는게 아니므로 새로운 값을 만들어도 큰 자원을 사용하지 않는다.
값이 존재하지 않는 경우 기본값 대신 예외를 던져야하는 경우도 있다.
BusStop busStop = busStopRepository.findById("1")
.orElseThrow( () -> new TrafficException(BusStopErrorCode.BUS_STOP_NOT_EXIST));
/***
* 잘못된 방식
***/
Optional<BusStop> find = busStopRepository.findById("ADB354000076");
if (find.isPresent()) {
System.out.println("잘못된 방식이다.");
System.out.println("값이 존재한다는 로그를 무조건 찍어야해..");
}
/***
* 올바른 방식
***/
Optional<BusStop> find = busStopRepository.findById("ADB354000076");
find.ifPresent( busStop -> {
System.out.println("올바른 방식이다");
System.out.println("값이 존재한다!!!!!");
});
Optional.ifPresent는 Optional 객체 안에 값이 있는 경우 실행할 람다를 인자로 받는다.
값이 있는 경우에는 실행되고 없는 경우 실행되지 않아야하는 경우 ifPresent()를 활용하자.
isPresent() 를 사용해서 값의 존재 유무를 체크하고 get() 을 호출하는 방식은 잘못된 방식이다. orElse(), orElseGet(), orElseThrow()로 대체하자.
java에서 Optional을 도입한 의도를 다시 보면 아래와 같이 되어 있다.
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.
위 설명에서 유심히 봐야할점은 method return type이다.
즉, Optional은 반환 타입으로 사용하기 위해 의도된 객체이다.
따라서 필드 타입 또는 메서드의 인자로 사용하는것은 의도에 반하는 행위이다.
/***
* 잘못된 방법
***/
@Data
@AllArgsConstructor
public static class BusStopResponse {
private Optional<String> busStopId;
private Optional<String> busStopName;
private Optional<String> cityCode;
private Optional<String> city;
private Optional<String> detailCity;
}
/***
* 올바른 방법
***/
@Data
@AllArgsConstructor
public static class BusStopResponse {
private String busStopId;
private String busStopName;
private String cityCode;
private String city;
private String detailCity;
}
Optional을 생성자나 메서드의 인자로 사용하면, 호출 할때 마다 Optional을 생성해서 인자로 전달해줘야 한다.
호출되는 쪽에서 null 체크를 하는게 더 낫다.
Optional을 비싼 객체이기 떄문에 과도하게 사용하면 안된다.
따라서 단순히 값을 얻거나 null을 얻고자 한다면 Optional 대신 null 체크를 하자.
10번과 동일하게 단순한 빈 컬렉션이나 배열을 반환하고자 한다면 빈 컬렉션이나 배열을 직접 반환하는게 더 명확하다.
유사하게 Spring Data JPA를 사용하는 경우 컬렉션을 Optional 감싸서 반환하는건 좋지 않다고 한다.
컬렉션을 반환하는 Spring Data JPA의 경우 null을 반환하지 않고 비어있는 컬렉션을 반환해주므로 Optional 감쌀 필요가 없다.
/***
* 잘못된 방법
***/
Optional<List<BusStop>> all = Optional.ofNullable(busStopRepository.findAll());
/***
* 올바른 방법
***/
List<BusStop> all = busStopRepository.findAll();
컬렉션에 Optional 원소로 사용하지 말고 원소를 꺼낼때나 사용할때 null 체크하는게 좋다. 특히 Map은 getOrDefault(), putIfAbsent(), computeIfAbsent(), computeIfPresent() 처럼 null 체크가 포함된 메서드를 제공하므로 Map의 원소로 Optional을 사용하지 말고 Map이 제공하는 메서드를 사용하자
Optional.of(X) 는 null이 아님이 확실한 경우 사용해야 한다.
X가 null이라면 NullPointerException이 발생한다.
Optional.ofNullable(X)는 X가 null일 가능성이 있을때 사용해야 한다. X가 null이 아님이 확실하다면 of(X)를 사용해야 한다.
Optional<Object> o = Optional.of(null);
->
java.lang.NullPointerException
at java.base/java.util.Objects.requireNonNull(Objects.java:208)
Optional<Object> o1 = Optional.ofNullable(null);
->
정상
원시타입(Primitive Type)을 Optional로 사용하면 Boxing과 UnBoxing을 거치면서 오버헤드가 발생한다.
반드시 Optional의 제네릭 타입을 맞춰야하는 경우가 아니라면 int, long, double 타입에는 OptionalXXX 타입 사용을 고려하는게 좋다. 이들은 내부 값을 래퍼 클래스가 아닌 원시 타입으로 갖고, 값의 존재 여부를 나타내는 isPresent 필드를 함께 갖는 구현체들이다.
/***
* 잘못된 방식
***/
Optional<Integer> cnt = Optional.of(10); // boxing 발생
for(int i = 0; i < cnt.get(); i++) {
...
} // unboxing 발생
/***
* 올바른 방식
***/
OptionalInt cnt = OptionalInt.of(10); // boxing 발생 안 함
for(int i = 0; i < cnt.getAsInt(); i++) {
...
} // unboxing 발생 안 함
Optional.equals 내부 구현을 보면 아래와 같다.
/**
* Indicates whether some other object is "equal to" this
* {@code OptionalInt}. The other object is considered equal if:
* <ul>
* <li>it is also an {@code OptionalInt} and;
* <li>both instances have no value present or;
* <li>the present values are "equal to" each other via {@code ==}.
* </ul>
*
* @param obj an object to be tested for equality
* @return {@code true} if the other object is "equal to" this object
* otherwise {@code false}
*/
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
return obj instanceof OptionalInt other
&& (isPresent && other.isPresent
? value == other.value
: isPresent == other.isPresent);
}
중요하게 봐야할 부분은 value == other.value이다.
내부에서 처리하고 있기 때문에 굳이 get()으로 꺼내서 비교하지 않아도 된다.
Optional.filter도 스트림처럼 값을 필터링하는 역할을 한다.
인자로 전달된 predicate이 참인 경우 기존의 내부 값을 유지한 Optional이 반환되고, 그렇지 않은 경우 비어 있는 Optional이 반환된다.
boolean present = Optional.of(new BusStopDto())
.filter(this::isFilter1)
.filter(this::isFilter2)
.filter(this::isFilter3)
.isPresent();
public boolean isFilter1(BusStopDto dto) {
return true;
}
public boolean isFilter2(BusStopDto dto) {
return true;
}
public boolean isFilter3(BusStopDto dto) {
return true;
}