Java 8 Optional
optional 은 java.util 패키지의 Java 8에 추가된 새로운 클래스입니다. 이 도움말에서는 Java Optional 클래스와 해당 개체에 대해 자세히 설명합니다.
String str = null;
if(str.equals("codippa")) {
// do something
}
null 참조에서 메서드를 호출하려고 하기 때문에 java.lang.NullPointerException을 발생시킵니다. 이 문제를 해결하기 위해 Optional 클래스가 추가되었습니다.
간단한 null 체크로 NullPointerException을 피할 수 있다고 생각할 수 있습니다. 아래 코드와 같이 말이죠.
String str = null;
if(str != null && str.equals("codippa")) {
//
}
그렇다면 왜 이를 위한 별도의 클래스가 필요한 걸까요?
맞습니다. 하지만 아래 코드를 살펴보세요.
String country = student.getStudentDetails().getAddress().getCountry().toLowerCase();
switch(country) {
case "India":
// 뭔가를 수행합니다
case "USA":
// 뭔가를 수행합니다
// 다른 나라들
}
위 예제는 student, studentDetails, address 또는 country 객체 중 하나라도 null이라면 NullPointerException의 가능성이 여러 곳에서 발생할 수 있습니다.
이제 여러분은 이전처럼 null 체크를 추가하고 싶을지도 모릅니다. 그러나 그렇게 한다면 위 코드는 다음과 같이 변합니다.
String country = null;
if(student != null) {
StudentDetails studentDetails = student.getStudentDetails();
if(studentDetails() != null) {
Address address = studentDetails().getAddress();
if(address() != null) {
Country studentCountry = address.getCountry();
if(studentCountry != null) {
country = studentCountry.toLowerCase();
}
}
}
}
보시는 대로 이 코드는 읽기 어렵고 유지 및 수정하기도 어렵습니다. 이러한 문제를 해결하기 위해 Optional이 만들어졌으며, Optional에 대해 자세히 학습한 후에 다시 살펴보겠습니다.
Optional이란?
Optional은 객체 위에 래퍼 또는 컨테이너입니다. 값(또는 객체)을 포함할 수도 있고 포함하지 않을 수도 있습니다.
Optional은 어떤 객체든 포함할 수 있으므로 Optional<String>, Optional<Cat> 등의 형식으로 선언되며 아래와 같이 생겼습니다.
Optional을 사용하면 값이 존재할 때만 해당 값을 가져오거나, 보유하는 객체가 없거나 null인 경우 기본값을 반환할 수 있습니다.
이를 통해 NullPointerPointerException 문제를 해결하며 코드를 기존의 null 체크보다 더 읽기 쉽게 만듭니다. Optional을 깊게 학습한 후에 위의 예제를 Optional 객체를 사용하여 어떻게 수정할 수 있는지 살펴보겠습니다.
Optional 객체 생성
Optional 객체는 생성자를 통해 생성할 수 없습니다. 이는 생성자가 private이기 때문입니다.
Optional 객체는 정적 메서드를 사용하여만 생성할 수 있으며 아래에 설명된대로 생성됩니다.
1. of() 메서드 사용
Optional은 Optional.of() 메서드를 호출하여 해당 Optional이 보유할 객체를 제공하여 생성할 수 있습니다.
예제,
Student student = new Student();
Optional<Student> optional = Optional.of(student);
만약 of에 제공된 객체가 null이면 NullPointerException이 발생합니다. 객체가 null일 가능성이 있는 경우에는 다음에 설명하는 메서드를 사용하세요.
2. ofNullable() 메서드 사용
이것은 Optional 객체를 생성하는 두 번째 방법입니다.
ofNullable()도 정적 메서드이며 인수로 객체를 받아 이 객체를 포함하는 Optional을 생성합니다.
만약 객체가 null이라면 빈 Optional 객체를 생성하고 반환합니다. 예제,
Student student = new Student();
Optional<Student> optional = Optional.ofNullable(student);
3. 빈 optional 객체 생성
아래와 같이 정적 빈 메소드를 사용하여 빈 값을 가진 Optional 객체를 생성하는 것도 가능합니다.
공백으로 반환된 Optional 은 모든 참조 유형에 할당 될 수 있습니다.
즉 Optional<String>, Optional<Shape>, Optional<Object>
Value of Optional
앞에서 설명한 것처럼 Optional은 객체를 둘러싼 래퍼이지만 기본 객체에 어떻게 액세스합니까? Optional은 래핑하는 객체를 반환하는 get 메소드를 제공합니다. 이 객체의 유형은 Optional 유형과 동일합니다.
String color = "red";
Optional<String> optional = Optional.of(color);
String back = optional.get();
System.out.print("Color is : " + back);
결과
Color is : red
Check Optional value
Optional 객체에 값이 포함되어 있지 않은 경우, 즉 그것이 empty() 또는 ofNullable() 메서드를 사용하여 생성된 빈 Optional이고 get()을 사용하여 해당 값에 액세스하려고 하면 오류가 발생합니다.
Exception in thread “main” java.util.NoSuchElementException: No value present
이 오류를 방지하려면 isPresent() 또는 isEmpty() 메서드를 사용하여 Optional에 액세스하기 전에 Optional에 값이 포함되어 있는지 확인하는 것이 좋습니다.
isPresent()
이 메서드는 Optional에 값이 포함되어 있으면 true를 반환하고 그렇지 않으면 false를 반환합니다.
Optional<String> optional = Optional.empty();
System.out.print("Optional contains a value? " + optional.isPresent());
결과
Optional contains a value? false
isEmpty()
이 메소드는 isPresent()와 반대로 작동하며 Optional 객체에 값이 없으면 true를 반환하고 그렇지 않으면 false를 반환합니다.
// create empty Optional value
Optional<String> optional = Optional.empty();
System.out.print("Optional contains a value? " + optional.isEmpty());
결과
Action on value present
마지막 섹션에서는 Optional에 값이 포함되어 있는지 확인하는 방법을 살펴보았습니다.
Optional에 값이 포함된 경우 일부 작업을 수행하려면 다음 줄을 생각해 보세요.
Optional<String> o = Optional.empty();
// check if optional contains a value
if(o.isPresent()) {
System.out.print(o.get());
}
그러나 Optional은 if 조건이 필요하지 않은 단축 방법을 제공합니다.
Optional의 isPresent() 메소드를 사용합니다.
이 메소드는 기능적 인터페이스인 java.util.function.Consumer 유형의 인수를 허용하므로 해당 구현을 Lambda 표현식으로 직접 제공할 수 있습니다.
Optional<String> o = Optional.of("Learning optional");
// print value of optional if non-empty
o.ifPresent((e) -> System.out.println(o.get()));
결과
Learning optional
Default optional value
위의 예는 Optional에 값이 있는 경우 취해야 할 조치와 값이 없는 경우 취해야 할 조치를 보여줍니다.
Optional은 비어 있는 경우 일부 작업을 수행하는 방법을 제공합니다.
이러한 방법은
1. orElse()
이 메서드는 값을 인수로 받아들이고 Optional이 비어 있으면 이 값을 반환합니다.
이 인수의 유형은 Optional 유형과 동일해야 합니다.
// simple pizza
Pizza plainPizza = new Pizza();
plainPizza.setDescription("Pizza without any toppings");
// create empty Optional
Optional<Pizza> empty = Optional.empty();
// get pizza from optional
Pizza p = empty.orElse(plainPizza);
System.out.print(p.getDescription())
이 예제에서는 빈 Optional을 생성한 다음 기본 객체를 인수로 사용하여 orElse() 메서드를 호출합니다. 이 코드의 출력은 다음과 같습니다
Pizza without any toppings
이는 orElse()가 반환한 객체가 해당 인수와 동일함을 보여줍니다.
2. orElseGet()
이 메서드는 orElse()와 유사하며 Optional이 비어 있으면 값을 반환합니다.
이는 java.util.function.Supplier 유형의 인수를 허용하므로 공급자가 반환한 값을 반환합니다.
orElseGet()의 예가 앞서 제공됩니다.
// create empty optional
Optional<String> empty = Optional.empty();
String s = empty.orElseGet(() -> "Optional is empty");
System.out.println(s);
결과
Optional is empty
공급자가 기능적 인터페이스이기 때문에 orElseGet() 메서드에 대한 인수는 Lambda 표현식입니다.
위 구문이 혼란스러워 보인다면 orElseGet()에 대한 호출을 별도의 줄로 나누는 아래 코드를 살펴보세요.
// define a supplier
Supplier<String> supplier = () -> "Optional is empty";
String s = empty.orElseGet(supplier);
Difference between orElse() & orElseGet()
1. 두 메소드 사이의 눈에 띄는 차이점은 orElse()는 Optional 객체와 동일한 유형의 인수를 허용하는 반면 orElseGet() 메소드는 java.util.function.Supplier 유형의 인수를 허용한다는 것입니다.
2. 또 다른 차이점은 orElse()가 단순히 제공된 인수를 반환하는 반면 orElseGet()은 결과를 검색하기 위해 공급자 메서드를 호출한다는 것입니다.
3. 하지만 그 이면에는 또 다른 차이점이 있는데, 이 차이점은 아래 예에서 확인할 수 있습니다.
private void getData() {
System.out.println("Fetching data");
}
// create non-empty optional
Optional<String> o = Optional.of("Learning optional");
System.out.println("------------ orElse ------------");
// using orElse
String s = o.orElse(getData());
System.out.println(s);
System.out.println("\n------------orElseGet-------------");
// using orElseGet
s = o.orElseGet(() -> getData());
System.out.println(s);
orElse() 및 orElseGet()에 값을 직접 제공하는 대신 값을 반환하는 메서드를 호출합니다.
이 코드를 실행하면 다음과 같은 출력이 생성됩니다.
———— orElse ————
Fetching data
Learning optional
————orElseGet————-
Learning optional
이 출력에서 Optional이 비어 있지 않더라도
orElse()는 여전히 메서드를 실행하는 반면 orElseGet()은 Optional이 비어 있는 경우에만 메서드를 실행합니다.
이는 데이터를 가져오는 데 관련된 네트워크 또는 데이터베이스 호출이 있는 경우 상당히 중요합니다.
3. or()
이 메소드는 Java 9에 추가되었습니다. Optional에 값이 포함되어 있으면 동일한 Optional을 반환하고 그렇지 않으면 다른 기본 Optional 개체를 반환합니다.
or()는 java.util.function.Supplier 유형의 인수를 기대합니다. 반환된 기본 Optional은 이 인수 공급자 함수에 의해 제공됩니다.
String s = "optional value";
// create an optional
Optional<String> o = Optional.ofNullable(s);
// get Optional with or
Optional<String> or = o.or(()-> Optional.of("Default"));
System.out.println("Optional value:: " + or.get());
// create an optional with null value
Optional<String> empty = Optional.ofNullable(null);
// get Optional with or
Optional<String> defaultOptional = empty.or(()-> Optional.of("Default"));
System.out.println("Optional value:: " + defaultOptional.get());
결과
Optional value:: Optional value
Optional value:: Default
Optional에 값이 포함되어 있으면 또는 동일한 Optional을 반환합니다.
그러나 비어 있으면 or의 인수 함수에서 제공하는 Optional이 반환됩니다.
Throwing exceptions
지금까지 Optional이 비어 있는 경우 기본값을 반환하는 방법을 살펴보았습니다. 그러나 때로는 비어 있는 Optional을 오류로 간주해야 하는 경우도 있습니다.
이러한 상황을 처리하기 위해 Optional이 비어 있을 때 오류가 발생할 수도 있습니다.
이에 대한 방법은 다음과 같습니다
1. orElseThrow(Supplier)
이 메소드는 공급자 인터페이스 유형의 인수를 승인하고 이 공급자 함수에 의해 반환되는 예외를 발생시킵니다.
orElseThrow()는 Optional이 비어 있는 경우에만 예외를 발생시킵니다.
// create empty optional
Optional<String> o = Optonal.ofNullable(null);
// throw exception when optional does not have any value
o.orElseThrow(() -> new IllegalArgumentException("Empty Optional"));
결과
Exception in thread “main” java.lang.IllegalArgumentException: Empty Optional
at com.codippa.demo.DemoApplication.lambda$0(DemoApplication.java:22)
at java.base/java.util.Optional.orElseThrow(Optional.java:408)
at com.codippa.demo.DemoApplication.main(DemoApplication.java:22)
2. orElseThrow()
이 메소드는 위에서 설명한 orElseThrow()와 유사하지만 어떤 인수도 허용하지 않으며 Optional이 비어 있는 경우 기본적으로 "값이 없습니다"라는 오류 메시지와 함께 java.util.NoSuchElementException을 발생시킵니다.
// create empty optional
Optional<String> o = Optonal.ofNullable(null);
// throw exception when optional does not have any value
o.orElseThrow();
결과
Exception in thread “main” java.util.NoSuchElementException: No value present
at java.base/java.util.Optional.orElseThrow(Optional.java:382)
at com.codippa.demo.DemoApplication.main(DemoApplication.java:22)
이 메소드는 Java 10에 추가되었습니다.
Filtering data
값이 특정 기준을 충족하는 경우에만 결과를 반환하려는 조건이 있을 수 있습니다.
Optional은 일치할 조건을 나타내는 Predicate를 허용하고 값이 조건자(또는 조건)와 일치하면 Optional 개체를 반환하고 그렇지 않으면 빈 Optional을 반환하는 필터 메서드를 제공합니다.
Example,
Laptop apple = new Laptop("Apple");
Optional<Laptop> laptop = Optional.of(apple);
// check if
Optional<Laptop> o = laptop.filter((l) -> "Apple".equals(l.getBrand()));
System.out.println(o.orElseThrow());
위의 예에서는 노트북 브랜드를 확인하고 브랜드가 Apple이 아닌 경우 빈 Optional을 반환하도록 필터링합니다.
제공된 Predicate가 false를 반환하는 경우, 즉 조건이 일치하지 않는 경우 filter() 메서드는 빈 Optional을 반환합니다.
orElseThrow는 반환된 Optional이 비어 있으면 예외를 발생시킵니다.
Transforming Optional
map 및 flatMap 메소드를 사용하여 일부 유형의 Optional을 다른 유형의 Optional로 변환하는 것이 가능합니다.
map()
map()은 인수를 승인하고 값을 반환하는 단일 메소드가 있는 기능적 인터페이스인 java.util.function.Function 유형의 인수를 승인합니다.
map() 인수 함수는 매퍼 함수라고도 합니다.
아래 map() 메소드 예는 Optional<Laptop>을 Optional<String>으로 변환합니다.
Laptop apple = new Laptop("Apple");
// create Optional object
Optional<Laptop> laptop = Optional.of(apple);
// map laptop Optional to a string Optional
Optional<String> strOptional = laptop.map(l -> l.getBrand());
// get Optional value
String brand = strOptional.get();
System.out.println(brand); //prints Apple
다음은 위 예의 지도 방법과 관련된 몇 가지 중요한 사항입니다.
map(l -> l.getBrand()) 문에서 map에는 단일 인수가 포함된 Lambda 표현식이 제공되며 이는 값을 반환합니다.
map()은 map()에 인수로 제공된 Lambda 표현식의 반환 유형으로 수정된 유형의 Optional을 반환합니다.
따라서 위의 예에서 map()은 String 유형의 Optional을 반환합니다.
map()은 Optional을 반환하므로 map() 뒤에 다른 Optional 메서드를 연결하는 것이 가능합니다.
String brand = laptop.map(l -> l.getBrand()).map(s -> s.toUpperCase()).get();
System.out.println(brand); //prints APPLE
두 번째 map() 메소드에 대한 값 유형은 첫 번째 map() 메소드에서 반환된 Optional 유형인 String입니다.
인수 함수가 null을 반환하면 map()은 빈 Optional을 반환합니다.
flatMap()
Optional 객체를 변환하는 또 다른 방법은 flatMap() 메서드를 사용하는 것입니다.
map() 메소드와 flatMap() 메소드에는 두 가지 차이점이 있습니다.
1. flatMap()의 인수는 반환 유형이 Optional 객체인 함수를 기대하는 반면, map() 메소드의 인수는 위 예에서 반환 유형이 문자열과 같은 값인 함수를 기대합니다.
2. map()은 일부 유형의 값을 래핑하는 Optional을 반환하지만 flatMap()은 값을 직접 반환하며 Optional을 반환하지 않습니다.
3. flatMap()은 Optional 인수가 비어 있으면 Optional을 반환하고 Optional 인수가 비어 있지 않으면 값을 반환하며, map()은 두 경우 모두 Optional을 반환합니다.
flatMap()의 예는 다음과 같습니다.
Optional<Laptop> laptop = Optional.of(apple);
String result = laptop.flatMap(l -> l.getBrandOptional()).orElseThrow();
System.out.println(result); // prints Apple
How Optional avoids NullPointerException
이 기사의 시작 부분에서 NullPointerException을 나타내는 다음과 같은 문제가 논의되었습니다.
이를 방지하려면 여러 개의 Null 검사가 필요했거나 솔루션이 Optional을 사용하고 있었습니다.
이제 Optional에 대해 배운 후에는 Optional이 NullPointerException을 어떻게 방지하는지 생각해야 합니다.
이 예제에 사용된 모든 클래스를 아래와 같이 수정합니다.
class Student{
StudentDetails studentDetails;
public Optional<StudentDetails> getStudentDetails() {
return Optional.ofNullable(studentDetails);
}
}
class StudentDetails {
Address address;
public Optional<Address> getAddress(){
return Optional.ofNullable(address);
}
}
class Address {
private Country country;
public Optional<Country> getCountry(){
return Optional.ofNullable(country);
}
}
class Country {
private String country;
public Optional<String> getCountry(){
return Optional.ofNullable(country);
}
}
보시다시피, getter 메소드는 이제 객체 대신 필요한 객체를 직접 래핑하는 Optional을 반환합니다.
국가를 얻으려면 명령문이 다음과 같이 작성됩니다.
Student st = null;
String country = Optional.ofNullable(st).
flatMap(s -> s.getStudentDetails()).
flatMap(sd -> sd.getAddress()).
flatMap(a -> a.getCountry()).
flatMap(c -> c.getCountry()).
orElse("Country not found");
flatMap()에 대한 각 호출은 Optional 객체를 반환하고 마지막으로 getCountry()에서 반환된 Optional이 비어 있으면 오류 메시지가 반환되지만 NullPointerException은 반환되지 않습니다.
'Java > Java 8' 카테고리의 다른 글
자바 CompletableFuture 를 사용하여 비동기 프로그래밍 구현 (0) | 2024.06.07 |
---|---|
자바 스트림의 중간 연산과 최종 연산 종류 (0) | 2024.06.07 |
스트림 API와 람다 표현식 소개 (0) | 2024.06.07 |
[자바] Java 8 스트림 특징 (0) | 2020.12.18 |
[JAVA8] LocalDate, LocalDateTime (0) | 2020.12.18 |