[Java8] Optional 이란?
by 무작정 개발이번에는 Java 8에 추가된 새로운 인터페이스 Optional에 정리할 것이다.
1. Optional 이란?
오직 값이 1개가 들어있을 수도 없을 수도 있는 컨테이너
(1) Optional 등장 배경
먼저 Java 8 이전에 null 값을 체크했던 방식을 코드로 알아볼 것입니다.
public class Progress {
private Duration studyDuration;
private boolean finished;
public Duration getStudyDuration() {
return studyDuration;
}
public void setStudyDuration(Duration studyDuration) {
this.studyDuration = studyDuration;
}
}
public class OnlineClass {
private Integer id;
private String title;
private boolean closed;
private Progress progress;
public OnlineClass(Integer id, String title, boolean closed) {
this.id = id;
this.title = title;
this.closed = closed;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public boolean isClosed() {
return closed;
}
public void setClosed(boolean closed) {
this.closed = closed;
}
public Progress getProgress() {
return progress;
}
public void setProgress(Progress progress) {
this.progress = progress;
}
@Override
public String toString() {
return "OnlineClass{" +
"id=" + id +
", title='" + title + '\'' +
", closed=" + closed +
'}';
}
}
위와 같이 수업의 진행 상황정보 객체(Progress 클래스)와 수업 객체(OnlineClass 클래스)가 있다고 할 때
public static void main(String[] args) {
List<OnlineClass> springClasses = new ArrayList<>();
springClasses.add(new OnlineClass(1, "spring boot", true));
springClasses.add(new OnlineClass(2, "spring data jpa", true));
springClasses.add(new OnlineClass(3, "spring mvc", false));
springClasses.add(new OnlineClass(4, "spring core", false));
springClasses.add(new OnlineClass(5, "rest api development", false));
OnlineClass spring_boot = new OnlineClass(1, "spring boot", true);
Duration studyDuration = spring_boot.getProgress().getStudyDuration();
System.out.println("studyDuration = " + studyDuration);
}
이렇게 특정 수업의 진행 상황정보를 조회하면 Progress 클래스에는 값이 설정되지 않았기 때문에 NullPointException이 발생합니다.
Exception in thread "main" java.lang.NullPointerException
그래서 Java 8 이전에는 NullPointException을 막기 위해 아래와 같이 로직을 구현했습니다.
public static void main(String[] args) {
OnlineClass spring_boot = new OnlineClass(1, "spring boot", true);
Progress progress = spring_boot.getProgress();
if (progress != null) {
System.out.println(progress.getStudyDuration());
}
}
-> getProgress()로 해당 값을 가져와서 if문을 통해 null인지 아닌지 확인하여 null이 아니면 출력하도록 하였습니다.
이러한 방식은 Error를 발생할 여지가 많습니다.
- 코드를 작성하는 건 사람이기에 null 체크와 같은 유효성 검사를 놓칠 수 있고, 종종 NullPointException을 볼 수 있습니다.
- 메서드가 null을 반환하는 것 자체가 문제입니다. getProgress() 메서드는 Progress를 반환할 뿐 null을 반환하는 것은 메서드의 목적에 어긋납니다.
즉, Java 8 이전에는 NullPointException을 막을 방법이 딱히 없기에 if문으로 유효성 검사를 할 수밖에 없었습니다.
public Progress getProgress() {
if (this.progress == null) {
throw new IllegalStateException();
}
return progress;
}
아니면 상단 코드처럼 반환하는 측에서 해당 값이 null인 경우 의도적으로 Exception을 발생하는 방법도 있지만,
이 역시 Error 발생 시 스택트레이스(StackTrace)를 찍게 되고, 이는 성능 하락의 원인이 됩니다.
(2) Optional 적용 - Java 8 이후 등장
Java 8 이후 이런 null에 대한 처리를 위해 Optional이 등장했습니다.
이는 간혹 비어있는 객체를 반환할 수 있는 상황에서 Optional 객체로 반환 값을 래핑 해준다면 반환되는 값이 null 일지라도
Optional 객체로 래핑 되어 반환되기에 null (NullPointException)으로부터 안전해집니다.
public Optional<Progress> getProgress() {
return Optional.ofNullable(progress);
}
Optional은 클라이언트(Client)에 코드에게 명시적으로 빈 값일 수도 있다는 것을 알려주고 빈 값에 대한 처리를 강제합니다.
또한 Optional.ofNullable은 Optional로 감싸는 값이 null 일 수도 있고, 아닐 수도 있을 때 사용합니다.
(3) 주의사항
문법적으로 Optional은 어디에 들어가도 Error가 발생하거나 문제가 되진 않습니다.
하지만 API 공식 문서에서는 Optional을 리턴값으로만 쓰기를 권장하고 있습니다.
- Optional 타입을 메서드 매개변수 타입, Map의 key 타입, 인스턴스 필드 타입으로 쓰지 말아야 한다.
why?
1. 메서드 매개변수 타입
public void setProgress(Optional<Progress> progress) {
if (progress != null) {
progress.ifPresent(p-> this.progress = p);
}
}
-> Optional이 null을 고려한 타입이지만 매개변수가 null로 들어오는 경우를 고려해야 하기 때문에 결국 progress가 null인지 검사를 해야 합니다.
그리고 progress라는 Optional 타입 안에 값이 제대로 들어있는지 확인하는 ifPresent 메서드도 호출한 다음에서야 값을 넣을 수 있습니다.
이렇게 되면 Optional이 없을 때보다 더 번거로워집니다.
2. Map의 key 타입
- Map의 가장 큰 특징 중 하나는 Map의 key는 not null이라는 점인데 key가 Optional 타입으로 null일수도 있다고 해버리면 Map의 가장 큰 특징을 어긋나는 것입니다.
3. 인스턴스 필드 타입
- 예를 들어 필드가 progress라고 하면 이 해당 필드가 있을 수도 있고 없을 숫도 있다는 의미가 되는데
public class OnlineClass {
private Integer id;
private String title;
private boolean closed;
private Optional<Progress> progress;
...
}
-> 위의 코드를 토대로 설명하자면 이는 도메인클래스(Domain Class) 설계에 문제인데 권장되지 않고 분리하는 것이 좋습니다.
4. primitive type 용 Optional은 따로 있다.
Optional.of(10);
-> int, long, boolean 등 프리미티브 타입을 Optional에 넣게 되면 박싱/언박식이 이뤄지게 되는데 이렇게 불필요한 박싱/언박싱이 반복될수록 성능이 떨어지게 됩니다. 그렇게 때문에 OptionalInt, OptionalLong 등 primitive type용 Optional이 있으니 해당 Optional을 쓰는 것이 좋습니다.
Primitive Type(프리미티브 타입)이란?
Java에는 논리형, 문자형, 정수형, 실수형 타입이 있으며 primitive type은 값을 할당할 때 변수의 주소 값에 값이 그대로 저장되는 데이터 타입을 의미합니다.
- primitive type은 실제 데이터 값을 저장하는 타입
- primitive type은 기본값이 있기 때문에 null이 존재하지 않음
- primitive type은 값이 할당되면서 JVM의 Runtime Data Area 영역 중 Stack 영역에 값이 저장됨
- 값의 범위를 벗어나면 컴파일 에러 발생
5. 리턴 타입이 Optional인 메서드에서 null을 리턴하지 말자.
- Optional 반환 타입인 메서드를 호출할 때는 반환 값이 null이 아니고 null일지라도 Optional 객체로 래핑되어 올 것을 고려하기 때문에 반환값이 null이라면 메서드 호출하는 위치에서 Error가 발생할 수 있기에 반환값이 필요 없는 경우 Optional.empty()를 반환하는 것이 좋다.
6. Collection, Map, Stream Array, Optional은 Optional로 감싸지 말 것
- 해당 컨테이너들은 모두 그 자체로 빈 값(null)에 대한 처리가 가능한 객체들인데 Optional로 감싸게 되면 이중 래핑이 되기에 번거롭고 효율 또한 떨어지게 됩니다.
2. Optional API
이제 Optional을 다루는 Optional API에 대해 알아볼 것입니다.
(1) Optional 만들기
- Optional.of()
- Optional.ofNullable()
- Optional.empty()
public static void main(String[] args) {
Optional<Integer> integer = Optional.of(10);
Optional<Integer> integerOptional =
Optional.ofNullable((Math.random() * 10) > 5 ? null : 1);
Optional<Object> empty = Optional.empty();
}
1. Optional.of(10)
-> 값이 10인 Optional 객체를 만들고, 안의 객체로 들어갈 값은 null을 고려하지 않습니다.
만약 null 값이 들어간다면 NullPointException이 발생합니다.
2. Optional.ofNullable((Math.random() * 10) > 5 ? null : 1);
-> 0 ~ 9까지의 실수가 5보다 클 경우 null이 아닐 경우 1을 인자 값으로 Optional을 만듭니다.
이처럼 값이 null일수도 있을 경우 of는 Error를 발생하기 때문에 ofNullable()를 사용합니다.
3. Optional.empty()
-> 내부 값이 비어있는 Optional을 반환합니다. 주로 반환 값이 null일 때 사용합니다.
(2) Optional에 값이 있는지 없는지 확인
- isPresent()
- isEmpty() -> [ Java 11부터 제공 ]
public static void main(String[] args) {
Optional<Integer> integer = Optional.of(10);
Optional<Object> empty = Optional.empty();
System.out.println(integer.isPresent());
System.out.println(empty.isPresent());
// isEmpty() -> Java11부터 제공
System.out.println(integer.isEmpty());
System.out.println(empty.isEmpty());
}
1. integer.isPresent()
-> Optional 내부에 값이 있는지 여부를 논리 값으로 반환하는 메서드입니다.
integer는 내부에 10이라는 값을 가지고 있기 때문에 true를 반환합니다.
2. empty.isPresent()
-> empty의 값은 비어있기 때문에 false를 반환합니다.
3. integer.isEmpty()
-> Optional 객체 integer의 값이 비어있는지(null) 확인합니다. -> 비어있을 경우 true 반환
(3) Optional에 있는 값 가져오기
public static void main(String[] args) {
Optional<Integer> integer = Optional.ofNullable(10);
Optional<Object> empty = Optional.empty();
System.out.println(integer.get());
integer.ifPresent(System.out::println);
integer.orElse(testInteger());
integer.orElseGet(App::testInteger);
integer.orElseThrow(NoSuchElementException::new);
}
// ==================================================
public static Integer testInteger(){
System.out.println("create integer test");
return (int)(Math.random() * 10);
}
- get()
- 내부 값이 null인 것을 고려하는 경우
- ifPresent(Consumer)
- Optional에 값이 있는 경우 그 값을 Consumer Functional Interface에 전달 후 로직을 수행
- orElse(T)
- 값이 있는 경우 가져오고 없는 경우 인자 값으로 선언한 내용을 반환
- orElseGet(Supplier)
- 값이 있으면 가져오고 없는 경우에 Supplier Functional Interface 로직을 수행
- orElseThrow()
- 값이 있으면 가져오고 없으면 Error를 던진다.
- ifPresent(Consumer)
- 내부 값을 걸러내서 가져오기
- Optional filter(Predicate)
- Predicate Functional Interface를 수행하여 조건에 부합하는 값을 가져온다.
- Optional filter(Predicate)
- 내부 값 변환하기
- Optional map(Function)
- Function Functional Interface를 수행하여 내부 값을 순회하며 변경한 후 반환
- Optional flatMap(Function)
- Optional 안에 들어있는 인스턴스가 Optional인 경우 내부 원소 값을 꺼낼 때 사용
- Optional map(Function)
Reference
'Language > Java' 카테고리의 다른 글
[Java8] 자바 CompletableFuture 프로그래밍 (0) | 2022.11.12 |
---|---|
[Java8] Date와 Time (1) | 2022.11.11 |
[Java8] Stream API에 대해 (0) | 2022.11.10 |
[Java8] 인터페이스의 변화 - default method, static method (1) | 2022.11.10 |
[Java8] 함수형 인터페이스, 람다 표현식 (1) | 2022.11.08 |
블로그의 정보
무작정 개발
무작정 개발