비지니스 로직을 하다 보면 외부 API 호출을 할 때가 많은데, 속도를 위해 비동기 처리를 하곤 합니다.
API로 구현된 비동기 처리는 예제가 많았지만, 서비스로 구현된 비동기 처리는 예제를 찾기 힘들었습니다.
아래는 메인 로직 후 이메일을 보내는 비동기 후처리 로직을 분리하고 테스트한 예제입니다.
결론
- Reactor 객체를 메서드의 반환 값으로 받습니다.
- 테스트할 때는 block() 을 걸어 동기 처리를 합니다.
비동기 이메일 전송 메소드
Before
- request 메서드에서 request 객체를 만들고 비동기 발송까지 모두 하고 있습니다.
- 지금은 후처리 로직이 로깅뿐이지만, 로직이 들어가는 경우 메서드가 복잡해집니다.
- 비지니스 로직의 메서드인 sendEmail에서는 발송 결과를 알지 못합니다.
// 외부 API 서비스의 메소드
public Void request(SendEmailRequestDTO sendEmailRequestDTO) {
webClient.post()
.uri(uri)
.contentType(MediaType.APPLICATION_JSON)
.headers(httpHeaders -> {
...
})
.body(BodyInserters.fromValue(sendEmailRequestDTO))
.retrieve()
.bodyToMono(String.class)
.onErrorMap(throwable -> {
log.error("email send failed. message={}. cause={}", throwable.getMessage(), throwable.getCause());
return throwable;
})
.subscribe(response -> log.info("email send. response={}", response));
return null;
}
// 메인 비지니스 로직의 메소드
private void sendEmail(...) {
SendEmailRequestDTO sendEmailRequestDTO = new SendTemplateEmailRequestDTO(...);
sendEmailService.request(sendEmailRequestDTO);
}
After
- requestSpec 분리, 비동기 객체 분리
- sendEmail 메서드에서 비동기 객체를 구독함으로써 메인 비즈니스 로직에서 결과를 알 수 있고, 후처리 로직 작성을 분리할 수 있습니다.
public Mono<String> request(SendEmailRequestDTO sendEmailRequestDTO) {
return createRequestSpec(sendEmailRequestDTO)
.retrieve()
.bodyToMono(String.class);
}
private RequestHeadersSpec<?> createRequestSpec(SendEmailRequestDTO body) {
return webClient.post()
.uri(SEND_MAIL.getEndPoint())
.contentType(MediaType.APPLICATION_JSON)
.headers(httpHeaders -> {
...
})
.body(BodyInserters.fromValue(body));
}
// 메인 비지니스 로직의 메소드에서 구독을 함으로서 결과를 알 수 있고, 후처리 로직 구현이 쉬워집니다.
private void sendEmail(...) {
sendEmailService.request(sendEmailRequestDTO)
.doOnError(throwable -> log.error("email send failed. message={}. cause={}", throwable.getMessage(), throwable.getCause()))
.subscribe(response -> log.info("email send. response={}", response));
}
테스트
Before
- 아래와 같이 구현하면 비동기 응답이 오기 전에 스레드가 종료되어 버립니다.
- doOnError, subscribe 처리가 되지 않습니다.
- Thread.sleep()을 통해 스레드 종료를 방지합니다.
- 하지만 응답이 일찍 온다고 해도 sleep() 만큼 기다려야 되고, 그 이상의 응답 시간이 걸리면 스레드가 종료되어 테스트가 정상 처리됩니다.
- 테스트의 신뢰도가 낮습니다.
@Test
public void normal_FRANCHISE_BRAND(...) throws InterruptedException {
// given
SendEmailRequestDTO sendEmailRequestDTO = new SendTemplateEmailRequestDTO(...);
// when
Mono<String> mono = sendEmailService.request(sendEmailRequestDTO);
// then
mono.doOnError(throwable -> {
assertThat(throwable).isNull();
log.error("email send failed. message={}. cause={}", throwable.getMessage(), throwable.getCause());
})
.subscribe(response -> {
assertThat(response.contains("requestId")).isTrue();
log.info("email send. response={}", response);
});
Thread.sleep(1100);
}
After
- mono.block() 사용
@Test
public void normal_FRANCHISE_BRAND(...) {
// given
SendEmailRequestDTO sendEmailRequestDTO = SendTemplateEmailRequestDTO.of(...);
// when
Mono<String> mono = sendEmailService.request(sendEmailRequestDTO);
String response = mono.block();
// then
assertThat(response).isNotNull();
assertThat(response.contains("requestId")).isTrue();
log.info("email send. response={}", response);
}
회고
고민한 것 치고는 해결법이 너무 쉬웠던……
비동기 처리를 더 공부해야겠습니다.
'Study' 카테고리의 다른 글
토끼책을 읽고 - 메시지를 믿을 수 있을까? (2) (0) | 2021.03.08 |
---|---|
토끼책을 읽고 - 메시지를 믿을 수 있을까? (1) (0) | 2021.03.08 |
Querydsl 로 가는 길 (0) | 2020.12.02 |
ParameterizedTest 를 잘해보자 (0) | 2020.08.21 |
Stream 의 병렬 처리란? (0) | 2020.08.21 |