프로젝트 아보카도에는 로그인 과정에서 이메일 인증을 필요로 하기 때문에, 이메일로 인증 코드를 발급한다.
이때, 이메일 송신 과정을 비동기
로 처리하곤 한다. (→ 우아한 테크코스 블로그 참고)
Java 5에서 Future 인터페이스가 비동기 계산을 다루도록 추가되었지만 여러 단점 및 한계들이 존재했다.
따라서 Java 8에서는 Future의 단점 및 한계를 개선하도록 CompletableFuture 인터페이스가 도입되었다!
직접 코드에서 도입 전후를 비교하여 살펴보자.
단순히 emailService.sendCertificatoinCode()
를 호출한다.
@RestController
@RequiredArgsConstructor
public class MailController {
private final EmailService emailService;
@PostMapping("/mail/certification-code")
public ResponseEntity<String> sendCertificationCode(@RequestBody final MailObject mailObject) {
final String code = emailService.sendCertificationCode(mailObject.email());
return ResponseEntity.ok(code);
}
}
CompletableFuture.supplyAsync()
팩토리 메서드를 이용해 동작시키고, get()
으로 결과를 가져온다.
@RestController
@RequiredArgsConstructor
public class MailController {
private final EmailService emailService;
@PostMapping("/mail/certification-code")
public ResponseEntity<String> sendCertificationCode(@RequestBody final MailObject mailObject) throws ExecutionException, InterruptedException {
final String code = CompletableFuture.supplyAsync(
() -> emailService.sendCertificationCode(mailObject.email())).get();
return ResponseEntity.ok(code);
}
}
요청 환경
Number of Threads (사용자 수) : 100
Ramp-up period (요청 시간) : 10
Loop Count (반복 횟수) : 1
기존 코드 : Error 24.00%, Throughput 2.2/sec
개선 코드 : Error 00.00%, Throughput 3.6/sec
기존 코드 테스트의 경우 Error 비율이 24%가 나왔다.
→ 발신자를 gmail로 이용 중인데, google에서 email API에 대해 제한사항을 둔 것이 이유인 것으로 판단하고 있다!
기존 코드는 get()
메서드를 이용했는데, 해당 메서드는 동기적인 호출이다.
@RestController
@RequiredArgsConstructor
public class MailController {
private final EmailService emailService;
@PostMapping("/mail/certification-code")
public ResponseEntity<String> sendCertificationCode(@RequestBody final MailObject mailObject) throws ExecutionException, InterruptedException {
final String code = CompletableFuture.supplyAsync(
() -> emailService.sendCertificationCode(mailObject.email())).get();
return ResponseEntity.ok(code);
}
}
get()
메서드 사용시 주의 사항
get()
메서드는 작업의 완료를 기다리면서 현재 스레드를 블록시킨다.get()
메서드는 작업의 완료를 무기한으로 대기하므로, 작업이 지연되거나 무한히 실행되는 경우에는 문제가 될 수 있다.CompletableFuture
의 다른 메서드를 사용하여 작업을 처리하는 방법을 고려해야 한다.CompletableFuture는 동기적인 호출(get()
) 외에 논블로킹 호출(join()
)도 제공한다.
get()
:CompletableFuture
에서 동기적인 호출
- 동기적인 호출은 메서드를 호출한 후 그 호출이 완료될 때까지 대기하는 방식을 의미!
- 동기 호출은 호출한 코드의 흐름을 차단하고 다음 코드를 실행하지 않는다!
join()
:CompletableFuture
에서 블록킹하지 않는 호출
- 블록킹하지 않는 호출은 호출한 후에 결과를 기다리지 않고 즉시 다음 코드를 실행하는 방식을 의미!
- 호출한 코드는 결과가 준비되지 않았을 때 다른 작업을 수행할 수 있고, 비동기적으로 작업을 처리하고 병렬성을 활용하는 데 유용하다!
요청 환경
Number of Threads (사용자 수) : 100
Ramp-up period (요청 시간) : 1
Loop Count (반복 횟수) : 1
get()
메서드를 이용 시 :Average: 34548 , Error: 0.00%, 처리량 : 1.8/sec
join()
메서드를 이용 시 :Average: 17831 , Error: 0.00%, 처리량 : 2.9/sec
CompletableFuture를 사용하여 비동기 API의 성능을 향상시키려면 여러 방법이 있지만 그 중 한가지는 직접 Executor를 지정하는 것이다!
스레드 풀의 설정을 조정할 때는 프로젝트의 요구사항과 환경을 고려해야 한다.
# 기본 Executor 스레드풀 관련 설정
spring:
task:
execution:
pool:
core-size: 10
max-size: 100
queue-capacity: 10
@Bean
public ThreadPoolTaskExecutor mailExecutor() {
final ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(20);
executor.setQueueCapacity(10);
executor.setThreadNamePrefix("MailExecutor-");
executor.initialize();
return executor;
}
요청 환경
Number of Threads (사용자 수) : 100
Ramp-up period (요청 시간) : 1
Loop Count (반복 횟수) : 1
요청 수를 10개와 같이 적게 했을 때
Error: 0.00%, Throughput: 1.1/sec
Error: 0.00%, Throughput: 1.0/sec
과 같이 별 차이가 없지만,위 테스트 처럼 100개
의 요청을 보냈을 경우
Error: 0.00%, Throughput: 1.7/sec
아직 완성된 게시글이 아닙니다. 추가 개선기가 있을 예정입니다. 잘못된 정보가 있다면 지적 부탁드립니다.
제목을 바꾸세요. "그래서, CompletableFuture이 뭔데?"