[Java][Spring] Optional 정리 : null 체크 관리 메소드

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 처리 관련 문제점

  1.  런타임 중 NPE ( Null Pointer Exception ) 예외 발생 위험성
  2.  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 을 사용하면

  1. + : NPE 유발하는 null을 직접 다루지 않아도 됨
  2. + : null check logic을 Optional에 전가 ( 코드 가독성 증가 )
  3. + : 명시적으로 변수가 null일 가능성을 표현 ( 방어 로직 감소 )
  4. - : Optional은 비싸다 -> 값을 얻을 목적일 뿐이라면 null 비교가 낫다.
  5. - : 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