Study

비동기 테스트 만들기

Hyunec 2021. 2. 25. 20:25

비지니스 로직을 하다 보면 외부 API 호출을 할 때가 많은데, 속도를 위해 비동기 처리를 하곤 합니다.
API로 구현된 비동기 처리는 예제가 많았지만, 서비스로 구현된 비동기 처리는 예제를 찾기 힘들었습니다.
아래는 메인 로직 후 이메일을 보내는 비동기 후처리 로직을 분리하고 테스트한 예제입니다.

결론

  1. Reactor 객체를 메서드의 반환 값으로 받습니다.
  2. 테스트할 때는 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);
}

회고

고민한 것 치고는 해결법이 너무 쉬웠던……
비동기 처리를 더 공부해야겠습니다.