[Java] Optional
Optional 이란
null
가능성이 있는 값을 감싸는 컨테이너 객체이다.null
체크를 명시적으로 하지 않고도 null
값으로 인해 발생할 수 있는 NullPointerException
을 예방할 수 있도록 도와주며, Optional
을 사용하면 코드의 가독성이 향상되고, null
관련 버그를 줄이고 핸들링 할 수 있도록 도와준다.
Optional을 사용하는 이유
먼저 Optional을 사용해야만 하는 이유에 대해서 살펴보자.
Optional이란 null
가능성이 있는 값을 감싸는 컨테이너 객체이다.
그렇다면 그냥 if 문으로 null 가능성이 있는 객체에 대해서 예외 처리를 해주면 되는데 왜 굳이 Optional을 사용해야 하는가...
OnlineClass springBoot = new OnlineClass(1, "spring boot", true);
Duration studyDuration = springBoot.getProgress()..getStudyDuration();
System.out.println(studyDuration);
이러한 예제 코드가 있다고 가정해보자.현재 OnlineClass 라는 클래스가 존재하고 내부에 Progress가 필드에 선언되어있다. 그리고 Progress 클래스에는 Duration 클래스가 Progress의 필드에 선언되어있는 상태이다.
해당 상태로 코드를 실행시키면 NullPointerException 예외가 터진다.(Progress 객체가 생성되지 않았으니까)
그렇다면 Null일 수도 있는 getProgress() 메서드에 if문으로 예외 핸들링 처리를 해줌으로써 NPE 에러는 막을 수가 있다.
하지만 이렇게 null일 수도 있는 객체마다 예외 코드를 작성해주는 방식은 좋은 방식이 아니다.
그 말 뜻 자체가 예외처리를 강제적으로 하고 있다는 뜻이기 때문에, 코드의 가독성도 떨어질 뿐더러 코드를 작성하는 개발자가 깜빡하고 null 체크를 안해줄 수도 있는 경우도 많기 때문이다.
그렇다면 if 문으로 항상 예외처리를 해주는 것 대신에 Optional 컨테이너에 객체를 한번 감싸서 보내는 방식이 있는데 Optional을 사용해서 리턴할 경우 더 명시적이며 코드의 가독성 또한 높아진다.
Optional이란 위에서 설명했듯이 null 가능성이 있는 값을 감싸는 컨테이너라는 것.그럼 해당 코드를 본 개발자는 "해당 코드가 null을 리턴할 수도 있겠다." 라고 이해할 수 있다.
복잡한 if문으로 예외를 핸들링 하는 것보다는 코드 한 줄로 더 명확하게 처리하는게 코드의 가독성을 높인다.
if문으로 처리한 것
public Progress getProgress() {
if (progress == null) {
throw new NullPointerException();
}
return progress;
}
------------------------------------------------------
optional로 처리한 것
public Optional<Progress> getProgress() {
return Optional.ofNullable(progress);
}
해당코드에서 null이 리턴 될 경우 Optional.empty가 반환된다.
첫번째 코드는 if문으로 예외 처리를 해준 코드이며 두번째 코드는 Optional로 객체를 한번 감싸서 리턴하는 코드이다.
Optional을 사용하면 Optional이 지원하는 많은 기능을 누릴 수 있으며 함수명(ofNullable)도 직관적이므로 코드의 의도가 쉽게 전달된다.
정리(Optional을 사용해야 하는 이유)
- 예외를 던진다.(위의 if문 코드.) -> 이는 예외 발생시 스택 트레이스를 찍게 되는데 비용이 비싸다는 측면때문이다.
- null을 리턴한다.(비용 문제가 없지만 해당 코드를 사용하는 클라이언트 코드가 주의해야 한다.)
허나 Optional을 사용할 경우 클라이언트 코드에게 명시적으로 빈 값일 수도 있다는 것을 알려주고, 빈 값인 경우에 대한 처리를 강제하기 때문에 해당 코드를 이용한 개발자는 null에 대한 처리를 까먹지 않는다는 것으로 인한 안정성 및 가독성이 높아진다는 것.
Optional 바르게 사용하는 방법
위의 예시에서는 return시 코드의 예제이다.
근데 이러한 return시에만 Optional을 권장하는 거 같다.
메소드의 매개변수 타입, 맵의 키 타입, 인스턴스 필드 타입으로 쓰지 말자
이유에 대해서는 코드로 살펴 보겠다.
- 메서드의 매개변수 타입으로 사용하게 될 경우해당 방식으로 사용하게 될 경우, 이를 사용하는 클라이언트 코드에서는
public void setProgress(Optional<Progress> progress) { progress.ifPresent( o -> progress.get()); }
springBoot.setProgress(null);
이렇게 null을 넣어줄 수 있다는 것이다.이는 개발자가 해당 코드를 의도했든 의도하지 않았든 간에, Optional을 사용하는 의미가 없어진다. 그리고 이렇게 사용할 경우 어떠한 컴파일 오류도 없기 때문에(이렇게 사용해도 문법상 오류가 전혀 없다는 것.)
- 맵의 키타입으로 Optional을 사용할 경우
맵의 키 값으로 Optional을 사용하게 될 경우, 해당 키 값이 참조하고 있는 value가 있는데, 키 값이 null일 수도 있다?? 라는 것은 Map이라는 인터페이스가 가지고 있는 가장 큰 특징을 깨트리는 것이라고 볼 수 있다. - 인스턴스 필드 타입으로 사용할 경우해당 방식으로 Optional을 사용하는 것의 의미는 필드에 포함하는, Progress 클래스가 있을 수도 있고, 없을 수도있다는 말인데.. 이는 도메인 설계상의 문제라고한다.
만약 해당 코드를 보게된다면, 반드시 고쳐야될 문제점이며, 해경 방법으로는 클래스를 쪼개는 방법이 있다.
Optional을 리턴하는 메서드에서 null을 리턴하지말자.
-> 이는 Optional을 사용하는 이유, 이점이 없다.
primitive 타입용 Optional이 따로 존재한다.
만약 Optional 컨테이너에 감싸야 하는 값이 프리미티브 타입이라면
Optional이 제공하는 타입을 사용하자.일반 Optional로 감싸게 되면 성능이 되게 안좋다.이유로는 프리미티브 타입을 사용하면 박싱 및 언박싱 작업이 이루어진다.
그런데 거기에다가 Optional로 감쌀 경우 Optional을 한번 더 푼 뒤에 박싱 및 언박싱 작업이 이루어진다.
이를 방지하기 위해 옵셔널에서 제공하는 프리미티브 전용 함수를 사용하자.
Collection, Map, Stream,Array, Optional은 Optional로 감싸지 말것.
이유로는 해당 컬렉션 타입의 인터페이스들은 컨테이너 성격을 띄고 있기 때문에 그 자체로도 비어있는지 안비어있는지를 판단 할 수 있는 함수를 제공하기 때문에 Optional로 감싸지 말것.
Optional 사용하기
optional의 get을 직접 사용하기 보다는 꺼내서 뭘 해야되는지에 대해서 생각해서 함수를 작성하면 된다.
-
Optinal 만들기 (리턴 시)
- Optional.of
- Optional.ofNullable
- Optional.empty
-
Optional에 값이 있는지 없는지 확인하기
- isPresent -> 값이 있을 경우 true
- isEmpty(더 명시적여서 선호) -> 값이 없을 경우 true
-
Optional에 값이 있는 경우 그 값을 가지고 ~~ 하라
- ifPresent(Consumer) -> 값이 없으면 아무 것도 하지 않음
-
Optional 값이 있으면 그 값을 가져오고 없는 경우에 ~~ 를 리턴하라
- orElse(T) -> 값이 있으면 해당 값을 가지고 오고, 없는 경우에 뭔가 의미있는 숫자를 넣어준다 0 or 1
-
Optional에 값이 있으면 가져오고 없는 경우에 ~~ 를 하라
- orElseGet(Supplier) - 값이 있으면 리턴 X 없으면 리턴 - 권장s pring이란 값이 있으면 createNewClass를 실행하지 않고, 없으면 해당 메서드를 실행한다.
-
Optional에 값이 있으면 가져오고 없는 경우에 에러를 던져라
{
Optional<OnlineClass> spring = springClasses.stream()
.filter(c -> c.getTitle().startsWith("jpa"))
.findFirst();
OnlineClass onlineClass = spring.orElseThrow();
System.out.println("orElseGet : " + onlineClass.getTitle());
}
- orElseThrow(원하는 에러 작성 가능 및 메서드 래퍼런스 방식으로도 가능)
값이 있으면 에러를 던지지 않고 없으면 NoSuchElementException를 실행 혹은해당 방식으로도 사용이 가능하다.
OnlineClass onlineClass = spring.orElseThrow( () -> {
return new IllegalArgumentException("해당 이름의 클래스가 존재 하지 않음.");
});
OnlineClass onlineClass = spring.orElseThrow(IllegalAccessError::new);
출력
Exception in thread "main" java.lang.IllegalArgumentException: 해당 이름의 클래스가 존재 하지 않음.
-
Optional에 들어있는 값 걸러내기
- Optional filter(Predicate) : 파라미터로 넘어오는 값이 있다는 가정하에 동작한다.
- 결과가 참이면 옵셔널에 감싸져서 값이 나오고, false일 경우 비어있는 옵셔널이 나오게 된다.
Optional<OnlineClass> onlineClass1 = spring.filter(oc -> oc.getId() > 10); System.out.println(onlineClass1.isEmpty()); // true
-
Optional에 들어있는 값 변환하기
- Optional Map(Function)
-
Optional<OnlineClass> spring = springClasses.stream() .filter(c -> c.getTitle().startsWith("spring")) .findFirst(); Optional<Integer> optionalInteger = spring.map(OnlineClass::getId); System.out.println(optionalInteger.isPresent());
- Optional flatMap(function) : Optional 안에 들어있는 인스턴스가 Optional인 경우에 사용하면 편리하다.
Progress는 현재 옵셔널을 리턴한다. Optional<Progress> progress = spring.flatMap(OnlineClass::getProgress);
#### 옵셔널은 for-each 가 안된다.
```java
Optional<String> s = spring.map(OnlineClass::getTitle);
for (OnlineClass springClass : s) {
System.out.println(springClass.getTitle());
}
Optional
은 컬렉션 또는 배열이 아니며, java.lang.Iterable
인터페이스를 구현하지 않다. 따라서 for-each
루프를 사용하여 직접 반복할 수 없다.
findAny, findFirst의 차이
아이디가 유니크하다는 조건이 있다면, findFirst() 또는 findAny() 중 어떤 것을 사용하더라도 결과는 동일하게 출력된다. 그러나 한가지 유의할 점은, findFirst()는 스트림의 순서를 보장하지만 findAny()는 그렇지 않다는 점.이 말은 즉, 병렬 스트림에서 findAny()는 어느 요소가 반환될 지를 보장하지 않는다.
자바 문서에 따르면, findAny()는 Stream의 병렬 처리에 유리하다고 설명되어 있다.왜냐하면, findAny()는 병렬 구조에서 이용 가능한 모든 요소를 살펴보는 과정에서 최적의 성능을 보여주기 때문이다.
한편 findFirst()는 스트림의 순서를 보장하며 처리 과정이 순차적다. 그렇기 때문에, 병렬화된 스트림에서는 성능상의 이점을 보지 못합니다. 단순히 하나의 요소를 찾고 싶다면 findAny()가 더 효율적일 수 있으며, 여러 요소 중 첫 번째 요소를 명확하게 알고 싶다면 findFirst()를 사용하면 된다.