2021. 4. 28. 01:03ㆍ개발 관련/java
Optional :
Java8부터 새롭게 추가된 null 처리를 쉽게 하기 위한 함수
"존재할 수도 있지만 안 할 수도 있는 객체"
"null일 수도 있는 객체"를 감싸는 래퍼 클래스
java.util.Optional<T>
목차
1. 기존 null 처리 관련 문제점
2. Optional 사용 시 장점( + ) 및 단점( - )
3. Optional 사용법
+ 선언 및 초기화 ( 시작 )
+ stream처럼 사용하는 방법 ( map, filter, stream, or ), ( 중간 )
+ orElseGet ( 종단 )
4. 사용 방법
+ return null일 때
+ 예외 처리 try/catch문에 Optional 적용
기존 ( Java 8 이전 ) null 처리 관련 문제점
- 런타임 중 NPE ( Null Pointer Exception ) 예외 발생 위험성
- NPE 방어를 위해 null check logic이 코드 가독성과 유지 보수성을 저하시킴 ( 비즈니스 로직이 보이지 않음 )
1번 예시 : NPE 예외 발생
/* address로 place찾기 */
getPlace(getAddress(str));
// 만약 str이 null이라면 ?
// 만약 getAddress(str)이 null이라면?
// 만약 getPlace(...)이 null이라면?
2번 예시 : null check로 인해 가독성 저하
/* address로 place찾기 */
if ( str != null ) {
if ( getAddress(str) != null ) {
if ( getPlace(getAddress(str) != null ) {
place = getPlace(getAddress(str));
}
}
}
place = "default";
Optional 을 사용하면
- + : NPE 유발하는 null을 직접 다루지 않아도 됨
- + : null check logic을 Optional에 전가 ( 코드 가독성 증가 )
- + : 명시적으로 변수가 null일 가능성을 표현 ( 방어 로직 감소 )
- - : Optional은 비싸다 -> 값을 얻을 목적일 뿐이라면 null 비교가 낫다.
- - : Collections은 Optional 대신 비어있는 컬렉션을 반환하는 게 낫다. (JPA Repository 메서드도 마찬가지이다.)
Optional 사용법
Optional 객체 생성 ( 시작 연산자 )
선언 및 초기화 - Optional.of(~) 주의!
// 선언
Optional<Place> place; // Place 타입의 객체를 감쌀 수 있는 Optional 타입의 변수
// 선언 및 초기화 ( 빈 Optional 객체로 초기화 )
Optional<Place> place = Optional.empty();
// myplace가 null일 때 NPE exception을 throw, 반드시 값이 있어야 하는 객체일 때 사용
// null이 아닌 객체를 담고 있는 Optional 객체 생성
Optional<Place> place = Optional.of(myplace);
// null일 수도 있는 객체를 담고 있는 Optional 객체 생성
// ( Optional.empty() + Optional.ofNullable(value) )
// null일 경우 : Optional.empty()와 같이 빈 Optional 객체 생성
Optional<Place> place = Optional.ofNullable(myplace);
Optional 중간 처리 ( stream처럼 사용 )
Optional을 최대 1개의 원소를 가진 Stream처럼 사용하자!
Stream이 가지고 있는 map(), flatMap(), filter()을 Optional도 가지고 있다.
+ map()
// Location으로 address를 얻어서 Place 정보를 얻자!
public String getPlace(Location loc) {
return Optional.ofNullable(loc) // null인 경우 대비
.map(Location::getAddress)
.map(Address::getPlace)
.orElse("none"); // default
// Optional<Location> -> Optional<Address> -> Optional<String>
+ filter() : if 문처럼 쓰면 된다.
// Location으로 address를 얻어서 Place 정보를 얻자!
public String getPlace(Location loc) {
return Optional.ofNullable(loc) // null인 경우 대비
.filter(l -> l.getlati() < 361 ) // if 문과 같다
.map(Location::getAddress)
.map(Address::getPlace)
// Optional<Location> -> Optional<Address> -> Optional<String>
+ stream() : 리스트 사용 시 기존 stream 처리와 같게 가능
List<String> result = List.of(1, 2, 3, 4)
.stream()
.map(val -> val % 2 == 0 ? Optional.of(val) : Optional.empty())
.flatMap(Optional::stream)
.map(String::valueOf)
.collect(Collectors.toList());
System.out.println(result); // print '[2, 4]'
+ or() : orElseGet()과 유사하지만 우선 순위를 결정할 수 있음. 해당 or()연산자가 비어있는 Optional이 된다면 다음 or()로 진행하게 된다.
String result = Optional.ofNullable("test")
.filter(value -> "filter".equals(value))
.or(Optional::empty)
.or(() -> Optional.of("second"))
.orElse("final");
System.out.println(result); // print 'second'
Optional 종단 처리 ( orElseGet ... )
import java.util.Optional;
...
// 선언 및 초기화 ( 빈 Optional 객체로 초기화 )
Optional<Place> place = Optional.empty();
// Optional이 담고 있는 객체가 존재할 경우 해당 값 반환
// null일 때 ( 비어있을 때 ) 다르게 작동
// null일 때 : NoSuchElementException 발생
place.get(..);
/* bad -> 값이 존재할 때 불필요한 객체 생성을 피하자 */
// place에 값이 있든 없든 new Location()은 무조건 실행된다.
// 이미 생성된 적 있는 값을 매개변수로 사용하는 것은 good
// null일 때 : 넘어온 인자 반환 ( 매개변수 반환 )
place.orElse(new Location()); // place.orElse(T other);
/* good */
place.orElse(null);
/* good */
// place에 값이 없을 때만 Location::new 실행된다.
// null일 때 : 넘어온 함수형 인자를 통해 생성된 객체 반환
place.orElseGet(Location::new); //place.orElseGet(Supplier<? extends T> other);
// null일 때 : 넘어온 함수형 인자를 통해 생성된 예외를 throw
place.orElseThrow(() -> new NoSuchElementException()); // place.orElseThrow(Supplier<? extends X> exceptonSuppler);
※ ifPresent(Consumer<? super T> consumer)는 Optional 객체가 감싸는 값이 존재할 때만 실행될 로직을 함수형 인자로 넘김. 비동기 메소드의 콜백 함수처럼 작동
ifPresent() != isPresent()
Optional<String> maybeCity = getAsOptional(cities, 3); // Optional
maybeCity.ifPresent(city -> {
System.out.println("length: " + city.length());
});
Optional의 잘못된 사용
/* Optional의 잘못된 사용 */
// isPresent() : 객체 존재 여부를 bool타입으로 반환하는 메소드
String text = getText();
Optional<String> maybeText = Optional.ofNullable(text);
int length;
if (maybeText.isPresent())
length = maybeText.get().length();
else
length = 0;
/* 원래 코드 */
String text = getText();
int length;
if (text != null)
length = maybeText.get().length();
else
length = 0;
Optional 적용 후 null 체크를 할 필요가 없으니 하지마라! ( 이미 Optional에 null 체크를 위임했다.)
int length = Optional.ofNullable(getText()).map(String::length).orElse(0);
// Optional.ofNullable(getText())가 null이 아니라면 text 반환
// ~.map(String::length).orElse(0) : 위의 text를 length로 바꿈, null이라면 0을 저장
- return null일 때
/* return null일 때 */
// String city = cities.get(4); // returns null
Optional<String> maybeCity = Optional.ofNullable(cities.get(4)); // Optional
// int length = city == null ? 0 : city.length(); // null check
int length = maybeCity.map(String::length).orElse(0); // null-safe
- exception 발생 시 : 예외 처리 메소드 생성
이전 코드 - 예외 발생
/* 이전 코드 - 예외 발생 */
String city = null;
try {
city = cities.get(3); // throws exception
} catch (ArrayIndexOutOfBoundsException e) {
// ignore
}
int length = city == null ? 0 : city.length(); // null check
System.out.println(length);
Optional 적용
/* Optional 적용 */
// 아래 예외처리 메소드를 생성 ( 예외 처리부를 감싸서 정적 유틸리티 메소드로 분리 )
public static <T> Optional<T> getAsOptional(List<T> list, int index) {
try {
return Optional.of(list.get(index));
} catch (ArrayIndexOutOfBoundsException e) {
return Optional.empty();
}
}
Optional<String> maybeCity = getAsOptional(cities, 3); // Optional
int length = maybeCity.map(String::length).orElse(0); // null-safe
System.out.println("length: " + length);
감사합니다!!
참고 :
null 처리 관련 Optional : www.daleseo.com/java8-optional-before/
Optional 사용 시, 안티패턴과 올바른 사용법 자바 8 기준 : homoefficio.github.io/2019/10/03/Java-Optional-%EB%B0%94%EB%A5%B4%EA%B2%8C-%EC%93%B0%EA%B8%B0/
Optional을 올바르게 사용하기 위한 정보 정리, 번역해주심!(목차 有) : www.latera.kr/blog/2019-07-02-effective-optional/#13-%EB%B3%80%ED%99%98%EC%97%90-map-%EA%B3%BC-flatmap-%EC%82%AC%EC%9A%A9%EC%9D%84-%EA%B3%A0%EB%A0%A4%ED%95%A0-%EA%B2%83
시작, 중간, 종단처리, Java8, 9, 10로 분류 : jdm.kr/blog/234