비동기 작업에서 Executor를 사용하는 방법은 CompletableFuture의 비동기 작업을 특정 Executor를 통해 실행하도록 설정하는 것입니다. 기본적으로 CompletableFuture.supplyAsync와 CompletableFuture.runAsync는 공용 ForkJoinPool의 공용 스레드 풀을 사용하지만, 특정 Executor를 지정하여 사용자 정의 스레드 풀을 사용할 수도 있습니다.
Executor를 사용하는 방법
1. Executor 생성
먼저, 사용할 Executor를 생성해야 합니다. 예를 들어, ExecutorService는 자주 사용되는 Executor 구현 중 하나입니다.
CompletableFuture의 비동기 작업을 시작할 때, Executor를 두 번째 인수로 전달합니다.
supplyAsync 사용 예
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class CompletableFutureWithExecutorExample {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(10);
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
// 비동기 작업 수행
return "Hello, World!";
}, executor);
future.thenAccept(result -> System.out.println("Result: " + result));
// Executor 서비스 종료
executor.shutdown();
}
}
3. 여러 비동기 작업을 결합하여 Executor 사용
thenApplyAsync, thenAcceptAsync, thenRunAsync 등의 메서드도 Executor를 인수로 받아서 지정할 수 있습니다.
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class CompletableFutureWithMultipleAsyncExample {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(10);
CompletableFuture.supplyAsync(() -> {
// 첫 번째 비동기 작업
return "Hello";
}, executor).thenApplyAsync(result -> {
// 두 번째 비동기 작업
return result + ", World!";
}, executor).thenAcceptAsync(result -> {
// 세 번째 비동기 작업
System.out.println("Result: " + result);
}, executor);
// Executor 서비스 종료
executor.shutdown();
}
}
요약
1.Executor 생성:ExecutorService와 같은 Executor 구현체를 생성합니다.
3.Executor 서비스 종료: 모든 비동기 작업이 완료되면 executor.shutdown()을 호출하여 Executor를 종료합니다.
executor.shutdown();
CompletableFuture와Executor
CompletableFuture와 Executor를 같이 사용하는 것은 여러 가지 이점을 제공합니다.
이 조합을 사용하면 비동기 작업의 효율성과 유연성을 극대화할 수 있습니다.
주요 이점
1. 병렬 처리의 효율성 증가
•설명: 여러 비동기 작업을 병렬로 처리함으로써 전체 작업의 수행 시간을 단축할 수 있습니다. Executor를 사용하면 특정 스레드 풀을 통해 작업을 분산시켜 병렬 처리의 효율성을 극대화할 수 있습니다.
2. 작업 스케줄링 제어
•설명:Executor를 사용하면 비동기 작업의 실행 정책을 세밀하게 제어할 수 있습니다. 예를 들어, 고정된 수의 스레드 풀, 캐시된 스레드 풀, 단일 스레드 풀 등을 사용하여 작업을 스케줄링할 수 있습니다.
3. 리소스 관리
•설명:Executor를 사용하면 스레드 생성 및 관리를 중앙집중식으로 제어할 수 있어 시스템 리소스를 효율적으로 관리할 수 있습니다. 이를 통해 과도한 스레드 생성으로 인한 성능 저하를 방지할 수 있습니다.
4. 작업의 독립성 보장
•설명: 각 비동기 작업을 별도의 스레드에서 실행하므로 작업 간의 간섭을 최소화할 수 있습니다. 이를 통해 독립적인 작업이 서로 영향을 주지 않고 안전하게 실행될 수 있습니다.
CompletableFuture<Void> future1 = CompletableFuture.runAsync(() -> {
// 첫 번째 작업
}, executor);
CompletableFuture<Void> future2 = CompletableFuture.runAsync(() -> {
// 두 번째 작업
}, executor);
5. 복잡한 비동기 작업 처리
•설명: 여러 비동기 작업을 조합하여 복잡한 비동기 워크플로우를 쉽게 구현할 수 있습니다. CompletableFuture의 다양한 메서드를 사용하여 작업 간의 의존성을 설정하고, Executor를 통해 이러한 작업을 효율적으로 처리할 수 있습니다.
6. 예외 처리 및 복구
•설명: 비동기 작업에서 발생하는 예외를 처리하고, 필요시 복구 작업을 수행할 수 있습니다. CompletableFuture의 exceptionally, handle 메서드와 함께 사용하여 예외 처리를 더 유연하게 할 수 있습니다.
CompletableFuture는 자바 8에서 도입된 java.util.concurrent 패키지의 클래스입니다. 비동기 프로그래밍을 쉽게 구현할 수 있도록 다양한 메서드와 기능을 제공합니다. CompletableFuture는 비동기 작업을 수행하고, 그 결과를 비동기적으로 처리할 수 있게 해줍니다.
1. CompletableFuture의 기본 개념
•비동기 프로그래밍: 메인 스레드와는 별도로 작업을 수행하여 응답성을 높입니다.
•비동기 작업의 관리: 작업의 완료 여부를 확인하고, 작업이 완료되면 후속 작업을 수행합니다.
•콜백 등록: 작업이 완료되면 실행할 콜백 함수를 등록할 수 있습니다.
2. CompletableFuture의 생성
CompletableFuture 객체는 여러 가지 방법으로 생성할 수 있습니다.
•직접 생성:
CompletableFuture<String> future = new CompletableFuture<>();
List<String> result = stream.collect(Collectors.toList());
- 반복 (Iteration):
stream.forEach(System.out::println);
람다 표현식의 필요성
•간결한 코드: 람다 표현식은 익명 함수를 표현하는 간결한 방법으로, 코드의 길이를 줄이고 가독성을 높입니다.
•함수형 프로그래밍: 함수형 인터페이스와 함께 사용되어 함수형 프로그래밍 패턴을 지원합니다.
람다 표현식의 기본 사용법
•기본 문법:(매개변수) -> { 함수 내용 }
(int a, int b) -> a + b;
•단순 예제:
•Runnable 인터페이스 구현:
Runnable r = () -> System.out.println("Hello, World!");
new Thread(r).start();
•함수형 인터페이스와 함께 사용:
•Predicate 인터페이스:
Predicate<String> isEmpty = s -> s.isEmpty();
boolean result = isEmpty.test("");
실습 예제 1: 리스트 필터링과 매핑
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class StreamExample {
public static void main(String[] args) {
List<String> items = Arrays.asList("Apple", "Banana", "Orange", "Apricot");
// 필터링: "A"로 시작하는 항목 필터링
List<String> filteredItems = items.stream()
.filter(item -> item.startsWith("A"))
.collect(Collectors.toList());
System.out.println("Filtered Items: " + filteredItems);
// 매핑: 모든 항목을 대문자로 변환
List<String> mappedItems = items.stream()
.map(String::toUpperCase)
.collect(Collectors.toList());
System.out.println("Mapped Items: " + mappedItems);
}
}
실습 예제 2: 숫자 리스트 처리
import java.util.Arrays;
import java.util.List;
public class LambdaExample {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
// 스트림을 사용하여 합계 계산
int sum = numbers.stream()
.reduce(0, (a, b) -> a + b);
System.out.println("Sum: " + sum);
// 람다 표현식을 사용하여 각 숫자를 출력
numbers.forEach(n -> System.out.println("Number: " + n));
}
}
•클래스 기반:Integer 클래스는 java.lang 패키지에 속하며, 객체로 취급됩니다.
•기본값:null (선언만 하고 초기화하지 않은 경우)
•기능: 추가 메서드와 기능 제공 (예: parseInt, valueOf, toString 등)
•오토박싱 및 언박싱: 자바 컴파일러가 기본 타입과 래퍼 클래스 간의 자동 변환을 수행합니다.
Integer x = 10; // 오토박싱
int y = x; // 언박싱
Integer z = Integer.valueOf(20);
int sum = x + z;
3. 차이점 요약
4. 언제 사용해야 하나?
•int 사용 시점:
- 성능이 중요한 경우
- 단순히 숫자 값을 저장하고 연산할 때
•Integer 사용 시점:
- 객체로 처리해야 할 때 (예: 컬렉션 프레임워크와 함께 사용)
- null 값을 허용해야 할 때
- 메서드와 추가 기능이 필요할 때
5. 기본 데이터 타입과 래퍼 클래스의 다른 예
자바에는 다양한 기본 데이터 타입과 그에 대응하는 래퍼 클래스가 있습니다. 각 래퍼 클래스는 기본 데이터 타입을 객체로 다룰 수 있도록 하며, 추가적인 메서드와 기능을 제공합니다.
기본 데이터 타입과 그에 대응하는 래퍼 클래스
요약
기본 데이터 타입은 메모리 효율적이고 성능이 좋지만, 객체로 다룰 수 없으며 null 값을 가질 수 없습니다. 반면 래퍼 클래스는 추가적인 메서드와 기능을 제공하며 컬렉션과 같은 객체 기반 데이터 구조에서 사용할 수 있습니다. 따라서 상황에 따라 적절한 타입을 선택하는 것이 중요합니다.
그렇다면 왜 이를 위한 별도의 클래스가 필요한 걸까요? 맞습니다. 하지만 아래 코드를 살펴보세요.
String country = student.getStudentDetails().getAddress().getCountry().toLowerCase();
switch(country) {
case "India":
// 뭔가를 수행합니다
case "USA":
// 뭔가를 수행합니다
// 다른 나라들
}
위 예제는 student, studentDetails, address 또는 country 객체 중 하나라도 null이라면 NullPointerException의 가능성이 여러 곳에서 발생할 수 있습니다.
이제 여러분은 이전처럼 null 체크를 추가하고 싶을지도 모릅니다. 그러나 그렇게 한다면 위 코드는 다음과 같이 변합니다.
보시는 대로 이 코드는 읽기 어렵고 유지 및 수정하기도 어렵습니다. 이러한 문제를 해결하기 위해 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<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를 반환합니다.
isEmpty() 이 메소드는 isPresent()와 반대로 작동하며 Optional 객체에 값이 없으면 true를 반환하고 그렇지 않으면 false를 반환합니다.
// create empty Optional value
Optional<String> optional = Optional.empty();
System.out.print("Optional contains a value? " + optional.isEmpty());
결과
Optional contains a value? true
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 메서드를 연결하는 것이 가능합니다.
두 번째 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
이 예에서 getBrandOptional() 메소드는 브랜드 값을 래핑하는 Optional<String>을 반환하므로 flatMap()은 브랜드 값을 직접 반환합니다.
How Optional avoids NullPointerException 이 기사의 시작 부분에서 NullPointerException을 나타내는 다음과 같은 문제가 논의되었습니다.
String country = student.getStudentDetails().getAddress().getCountry().toLowerCase();
이를 방지하려면 여러 개의 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은 반환되지 않습니다.
public class GreeterMain extends AbstractBehavior<GreeterMain.SayHello> {
public static class SayHello {
public final String name;
public SayHello(String name) {
this.name = name;
}
}
private final ActorRef<Greeter.Greet> greeter;
public static Behavior<SayHello> create() {
return Behaviors.setup(GreeterMain::new);
}
private GreeterMain(ActorContext<SayHello> context) {
super(context);
//#create-actors
greeter = context.spawn(Greeter.create(), "greeter");
//#create-actors
}
@Override
public Receive<SayHello> createReceive() {
return newReceiveBuilder().onMessage(SayHello.class, this::onSayHello).build();
}
// 3개의 GreetoerBot 을 생성 후 Greeter 에게 메시지와 누구에게 메시지를 보낼지 대상을 전달한다.
private Behavior<SayHello> onSayHello(SayHello command) {
//#create-actors
ActorRef<Greeter.Greeted> replyTo =
getContext().spawn(GreeterBot.create(3), command.name);
greeter.tell(new Greeter.Greet(command.name, replyTo));
//#create-actors
return this;
}
}
AbstractBehavior 클래스를 상속받은 GreeterMain 에게 Charles 라는 메시지를 보내면
AbstractBehavior 클래스의 createReceive 메소드에서 receive 받기로 한 메소드가 실행이 된다.
createReceive 에는 메시지 받는 타입과 메시지 받은 이후의 처리를 담당하는 handler 를 설정할 수 있다.
예제에서는 handler 가 onSayHello 메소드 이다.
onSayHello 의 메소드는 응답받을 3개의 GreeterBot 을 만든다.
그 후
ActorRef<Greeter.Greet> greeter;
ActorRef 변수 greeter 에게 메시지와 메시지를 보낼 대상일 GreeterBot(ActorRef) 객체를 보낸다.
Greeter.class
public class Greeter extends AbstractBehavior<Greeter.Greet> {
public static final class Greet {
public final String whom;
public final ActorRef<Greeted> replyTo;
public Greet(String whom, ActorRef<Greeted> replyTo) {
this.whom = whom;
this.replyTo = replyTo;
}
}
public static final class Greeted {
public final String whom;
public final ActorRef<Greet> from;
public Greeted(String whom, ActorRef<Greet> from) {
this.whom = whom;
this.from = from;
}
public static Behavior<Greet> create() {
return Behaviors.setup(Greeter::new);
}
private Greeter(ActorContext<Greet> context) {
super(context);
}
@Override
public Receive<Greet> createReceive() {
return newReceiveBuilder().onMessage(Greet.class, this::onGreet).build();
}
// GreeterMain actorRef 가 tell 할 경우 실행된다.
private Behavior<Greet> onGreet(Greet command) {
getContext().getLog().info("Hello {}!", command.whom);
//#greeter-send-message
command.replyTo.tell(new Greeted(command.whom, getContext().getSelf()));
//#greeter-send-message
return this;
}
}
여기서 또 AbstractBehavior 클래스를 상속받은 Greeter 가
createReceive 에서 연결된 Greet 타입으로 onGreet 메소드를 실행한다.
이번 onGreet 메소드에는 특이하게 GreeterMain 의 onSayHello 에서 보낸 GreeterBot(ActorRef) 가 있어서
GreeterBot 이 tell 하도록 한다.
GreeterBot.class
public class GreeterBot extends AbstractBehavior<Greeter.Greeted> {
public static Behavior<Greeter.Greeted> create(int max) {
return Behaviors.setup(context -> new GreeterBot(context, max));
}
private final int max;
private int greetingCounter;
private GreeterBot(ActorContext<Greeter.Greeted> context, int max) {
super(context);
this.max = max;
}
@Override
public Receive<Greeter.Greeted> createReceive() {
return newReceiveBuilder().onMessage(Greeter.Greeted.class, this::onGreeted).build();
}
// Greeter actorRef 가 onGreet 할때 tell 시킨다.
private Behavior<Greeter.Greeted> onGreeted(Greeter.Greeted message) {
greetingCounter++;
getContext().getLog().info("Greeting {} for {}", greetingCounter, message.whom);
if (greetingCounter == max) {
return Behaviors.stopped();
} else {
message.from.tell(new Greeter.Greet(message.whom, getContext().getSelf()));
return this;
}
}
}
코드 요약
GreeterMain 에게 tell 을 한 후 Main 에 생성 되어 있는 ActorRef(greeter) 에게 tell 을 하고, 응답을 ActoerRef(greeterBot) 이 응답한다.