Java/Java8
[Java8] Optional
메성
2021. 9. 28. 21:37
반응형
더 자바, Java 8 - 인프런 | 강의
자바 8에 추가된 기능들은 자바가 제공하는 API는 물론이고 스프링 같은 제 3의 라이브러리 및 프레임워크에서도 널리 사용되고 있습니다. 이 시대의 자바 개발자라면 반드시 알아야 합니다. 이
www.inflearn.com
Optional은 언제 사용되는 것인가?
- 아래 소스를 확인해보자.
public class App {
public static void main(String[] args) {
OnlineClass spring_boot = new OnlineClass(1, "spring boot", true);
Duration studyDuration = spring_boot.getProgress().getStudyDuration();
//출력을 하면 결과는 어떻게 될까?
System.out.println(studyDuration);
}
}
public class OnlineClass {
private Integer id;
private String title;
private boolean closed;
public 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 Progress getProgress() {
return progress;
}
public void setProgress(Progress progress) {
this.progress = progress;
}
}
public class Progress {
private Duration studyDuration;
private boolean finished;
public Duration getStudyDuration() {
return studyDuration;
}
public void setStudyDuration(Duration studyDuration) {
this.studyDuration = studyDuration;
}
}
- main을 보면 OnlineClass를 생성 후 OnlineClass에 있는 Progress 인스턴스가 의존하고 있는 Duration을 호출하려는 로직을 가지고 있다.
- 하지만 결과는 'NullPointerException'이 나타난다.
- 이유는 당연하다. 현재 OnlineClass의 Progress는 Null이기 때문이다.
- 그렇다면 이런 이슈를 방지 하기 위해 우리는 기존에 어떻게 체크를 해왔는가?
Optional 전 해결방안(조건문 활용)
-
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()); } }
- 보시는 바와 같이 OnlineClass에서 Progress를 가져온 후 조건문을 통해 null 체크하는 방식을 사용해왔다.
- 하지만 이런 방식은 개발자 이슈가 나타날 수 있는 경우이다. 즉, 사람이기에 null 체크하는 부분을 잊을 수 있다는 것이다.
Optional 전 해결방안(throw Exception)
-
public class OnlineClass { ... public Progress getProgress() { if (this.progress == null) { throw new IllegalStateException(); } return progress; } }
- getProgress()할 때 미리 null 체크를 하여 Exception을 던지는 방법이다.
- 하지만 위 같은 방법을 보면 로직 처리 하는 부분에 Exception을 던지도록 하였는데, 이 방법은 그리 좋은 방법이 아니다.
- 그 이유는, Exception을 발생하도록 하게 되면 Java에서는 Stack Trace를 찍게 된다. 즉, 해당 Exception이 발생하기 까지의 어떤 콜 스택을 거쳐서 Exception이 발생했는지에 대해 찍게 된다. 결과적으로 리소스를 사용하게 된다는 것이다.
- 그러므로 정말 필요한 경우에믄 Exception을 사용해야된다.
Optional 사용 해결방안
-
public class OnlineClass { ... public Optional<Progress> getProgress() { //progress가 null일 경우 optional.empty()를 반환 return Optional.ofNullable(progress); } } public static void main(String[] args) { ... OnlineClass spring_boot = new OnlineClass(1, "spring boot", true); spring_boot.getProgress() .ifPresent(duration -> { System.out.println(duration.getStudyDuration()); }); }
Optional 사용 시 주의사항
- 리턴값으로만 쓰기를 권장한다.
- 메소드 매개변수 타입으로 쓰지 말아라(매개변수 타입으로 Null이 들어올 수가 있다.)
-
public void setProgress(Optional<Progress> progress) { progress.ifPresent(p -> this.progress = p); } //progress 매개변수가 Null로 들어올 경우 위 로직은 다시 NullPoineException이 발생한다. //만약 정상적으로 실행하고 싶다면 아래와 같이 한번 더 Null 체크를 해줘야 한다. if(progress != null) { progress.ifPresent(p -> this.progress = p); }
- 맵의 키 타입, 인스턴스 필드 타입으로 쓰지 말아라.
- Optional을 리턴하는 메소드에서 null을 리턴하지 말아라.
- 굳이 빈값을 리턴하고 싶으면 null을 리턴하지 말고 Optinal.empty()를 리턴하도록 해라
- Primitive 용 Optional은 따로 존재한다.
- OptionalDouble, OptionalInt, ...
- 만약 Optional.of(10)을 하게되면 Optional 안에서 박싱, 언박싱을 진행하므로 성능상 이슈가 있을 수 있다.
Optional 주요 사용 메소드
-
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)); }
- Optional of()와 ofNullable()
- of(); -> of 메소드 안에 파라미터가 Null이 아님을 확신할 때 사용. 만약 파라미터가 Null이면 NullPointerException 발생
- ofNullable(); -> ofNullable 메소드 안에 파라미터가 Null일수도 있을 경우 사용.
- Optional 값 여부 확인
- isPresent(); -> Optoinal의 값이 있는지 판단(true/false 반환)(Null이면 NPE)
- isEmpty(); (Java 11 부터 제공) -> true/false 반환(Null이면 NPE)
-
public static void main(String[] args) { ... //isPresent Optional<OnlineClass> spring = springClasses.stream() .filter(onlineClass -> onlineClass.getTitle().startsWith("spring")) .findFirst(); boolean present = spring.isPresent(); System.out.println(present); //true //isEmpty Optional<OnlineClass> jpa = springClasses.stream() .filter(onlineClass -> onlineClass.getTitle().startsWith("jpa")) .findFirst(); boolean present2 = jpa.isEmpty(); System.out.println(present2); //true }
-
- Optional 값 호출
- get(); -> Optional 값 가져옴(Null이면 NPE).
-
public static void main(String[] args) { ... Optional<OnlineClass> optional = springClasses.stream() .filter(onlineClass -> onlineClass.getTitle().startsWith("spring")) .findFirst(); //get을 통해 Optional의 값을 호출할 수 있음 OnlineClass onlineClass = optional.get(); System.out.println(onlineClass.getTitle()); //spring boot }
- 위 소스 처럼 get()을 통해 Optional의 값을 호출할 수 있으나, 만약 Optional이 없는 경우에는 어떻게 될까?
-
public static void main(String[] args) { ... Optional<OnlineClass> optional = springClasses.stream() .filter(onlineClass -> onlineClass.getTitle().startsWith("jpa")) .findFirst(); //get을 통해 Optional의 값을 호출할 수 있음 OnlineClass onlineClass = optional.get(); System.out.println(onlineClass.getTitle()); //NoSuchElementException }
- 현재 'jpa'로 시작되는 타이틀은 존재하지 않는다. 이런 경우에는 NoSuchElementException으로 RuntimeException이 발생하게 된다. 결과적으로 get()이란 메소드를 사용하는 것은 크게 권장하지 않는다.
-
-
- get(); -> Optional 값 가져옴(Null이면 NPE).
- Optional 값 있는 경우 해당 값 사용
- ifPresent(); -> 값이 있을 경우에만 해당 메소드 로직 수행(Null이면 NPE)
- 위에서 사용한 get() 메소드 대신 ifPresent()를 사용해보자.
-
public static void main(String[] args) { ... Optional<OnlineClass> optional = springClasses.stream() .filter(onlineClass -> onlineClass.getTitle().startsWith("spring")) .findFirst(); optional.ifPresent(onlineClass -> System.out.println(onlineClass.getTitle())); //결과 : spring boot Optional<OnlineClass> optional = springClasses.stream() .filter(onlineClass -> onlineClass.getTitle().startsWith("jpa")) .findFirst(); optional.ifPresent(onlineClass -> System.out.println(onlineClass.getTitle())); //결과 : 빈값 }
- 이 처럼 Optional의 값을 사용하고 싶을 때는 ifPresent를 사용하는 것이 효율적이다.
- ifPresent(); -> 값이 있을 경우에만 해당 메소드 로직 수행(Null이면 NPE)
- Optional 값 있는 경우 가져오고, 없는 경우 정의한 값 리턴
- orElse(); -> (상수로 이미 만들어져있는 것을 사용할 때 사용하는 것을 권장)(Null이면 NPE)
-
public static void main(String [] args) { Optional<OnlineClass> optional = springClasses.stream() .filter(onlineClass -> onlineClass.getTitle().startsWith("jpa")) .findFirst(); //orElse : 값이 있는 경우 해당 값을 가져오고, 없는 경우 orElse 매개변수에 있는 값을 리턴한다. OnlineClass onlineClass = optional.orElse(createNewClass()); System.out.println(onlineClass.getTitle()) } private static OnlineClass createNewClass() { System.out.println("creating new class"); return new OnlineClass(6, "New Class", false); } //결과 //creating new class //New Class
- 위 처럼 값이 없는 경우에는 새롭게 생성한 클래스를 리턴하는 모습을 볼 수 있다.
- 값이 있는 경우에도 확인해보자.
-
public static void main(String [] args) { Optional<OnlineClass> optional = springClasses.stream() .filter(onlineClass -> onlineClass.getTitle().startsWith("spring")) .findFirst(); //orElse : 값이 있는 경우 해당 값을 가져오고, 없는 경우 orElse 매개변수에 있는 값을 리턴한다. OnlineClass onlineClass = optional.orElse(createNewClass()); System.out.println(onlineClass.getTitle()) } private static OnlineClass createNewClass() { System.out.println("creating new class"); return new OnlineClass(6, "New Class", false); } //결과 //creating new class //spring boot
- 여기서 주의해야할 점은, 분명 값이 있는데도 불구하고 새롭게 생성하는 클래스의 로직을 타는 것을 볼 수 있다.
- 즉, 값이 있든 없든 orElse()에 속해있는 로직은 무조건 탄다.
- 'create new class' 출력
-
-
- orElse(); -> (상수로 이미 만들어져있는 것을 사용할 때 사용하는 것을 권장)(Null이면 NPE)
- Optional 값 있는 경우 가져오고, 없는 경우 로직 처리
- orElseGet(); -> (동적으로 작업을 해서 만들어야할 경우 사용하는 것을 권장)(Null이면 NPE)
- orElse()를 보면 뭔가 찝찝하다.. 값이 있다면 새롭게 생성하는 클래스를 호출하기 싫은데.. 이럴 때 orElseGet()을 사용하면 된다.
public static void main(String [] args) { Optional<OnlineClass> optional = springClasses.stream() .filter(onlineClass -> onlineClass.getTitle().startsWith("spring")) .findFirst(); //orElseGet : 값이 있는 경우 해당 값을 가져오고, 없는 경우에만 orElseGet 매개변수에 있는 값을 리턴한다. OnlineClass onlineClass = optional.orElseGet(createNewClass()); System.out.println(onlineClass.getTitle()) } private static OnlineClass createNewClass() { System.out.println("creating new class"); return new OnlineClass(6, "New Class", false); } //결과 //spring boot
- orElseGet(); -> (동적으로 작업을 해서 만들어야할 경우 사용하는 것을 권장)(Null이면 NPE)
- Optional 값 있는 경우 가져오고 없는 경우 예외 처리
- orElseThrow();
- 또한, 클래스를 새롭게 생성하지 않고 Exception을 날려줄 수도 있다.(Null이면 NPE)
public static void main(String [] args) { Optional<OnlineClass> optional = springClasses.stream() .filter(onlineClass -> onlineClass.getTitle().startsWith("jpa")) .findFirst(); //람다식 OnlineClass onlineClass = optional.orElseThrow(() -> { return new IllegalArgumentException(); }); //메소드 레퍼런스 OnlineClass onlineClass = optional.orElseThrow(IllegalArgumentException::new) System.out.println(onlineClass.getTitle()) } private static OnlineClass createNewClass() { System.out.println("creating new class"); return new OnlineClass(6, "New Class", false); }
- orElseThrow();
- 결과적으로 isPresent() 와 get()은 사용하지 않는 것이 좋다.
- isPresent(); 와 get(); 대신 ifPresent, orElseGet, orElseThrow등을 사용하라
- 이유는 get(); isPresent()를 통해서 로직을 구성할 시 코드가 길어지는 것을 볼 수 있다.
-
public static void main(String [] args) { Optional<OnlineClass> optional = springClasses.stream() .filter(onlineClass -> onlineClass.getTitle().startsWith("spring")) .findFirst(); //isPresent()와 get() 사용 if(optional.isPresent()) { OnlineClass onlineClass = optional.get(); } else { OnlineClass onlineClass = createNewClass(); } //orElseGet() 사용 OnlineClass onlineClass = optional.orElseGet(createNewClass());
- Java9에서는 ifPresentOrElse();를 통해 ifPresent와 orElseGet을 동시에 사용할 수 있다.
- isPresent(); 와 get(); 대신 ifPresent, orElseGet, orElseThrow등을 사용하라
- Optional에 있는 값 필터링
- filter();
- Optional의 filter() 메소드를 통해 Optional의 조건 값을 줄 수 있다.
public static void main(String [] args) { ... springClasses.add(new OnlineClass(5, "rest api development", false)); Optional<OnlineClass> optional = springClasses.stream() .filter(onlineClass -> onlineClass.getTitle().startsWith("rest")) .findFirst(); //filter를 통해 optional 안에 있는 값의 조건을 줄 수 있다. Optional<OnlineClass> optional1 = optional.filter(onlineClass -> onlineClass.isClosed()); System.out.println(optional1.isEmpty()); } //결과 : true
- filter();
- Optional에 있는 값 변환
- map();
- Optional의 mpa() 메소드를 통해 Optional의 리턴값을 변환할 수 있다.
public static void main(String [] args) { ... springClasses.add(new OnlineClass(5, "rest api development", false)); Optional<OnlineClass> optional = springClasses.stream() .filter(onlineClass -> onlineClass.getTitle().startsWith("rest")) .findFirst(); //map을 통해 getId()를 호출할 시 Optional<Integer> 형태로 리턴이 된다. Optional<Integer> integer = optional.map(onlineClass -> onlineClass.getId()); System.out.println(integer.isPresent()); } //결과 : true
- flatMap();
- map으로 추출 시 Optional<>이 아닌 Opitonal을 한번 더 감싼 Optional<Optional<>>이라면 flatMap을 통해 한번에 Optional<>로 변경할 수 있다.
public class OnlineClass { public Optional<Progress> getProgress() { return Optional.ofNullable(progress); } } public static void main(String [] args) { ... springClasses.add(new OnlineClass(5, "rest api development", false)); Optional<OnlineClass> optional = springClasses.stream() .filter(onlineClass -> onlineClass.getTitle().startsWith("rest")) .findFirst(); //map으로 표현하면 아래와 같다. Optional<Optional<Progress>> progress1 = optional.map(onlineClass -> onlineClass.getProgress()); Optional<Progress> progress2 = progress1.orElse(Optional.empty()); //flatMap을 사용할 시 쌍으로 되어있는 Optional을 풀 수 있다. Optional<Progress> progress = optional.flatMap(onlineClass -> onlineClass.getProgress()); }
- Stream의 flatMap()을 사용하는 경우는 input은 하나인데 output이 여러개일 경우 사용하는 것으로서 Optional과는 다르다.
- Ex. input은 하나인데, list1과 list2가 output 되는 경우, flatMap을 사용하여 list1과 list2를 하나의 list로 변경
- map();
반응형