지금부터 쓸 글은
제가 WebClient 를 비동기적으로 사용하려고 아둥바둥 거린 점을 기록함을 명시합니다. 솔직히 아직까지도 잘 모르겠습니다.
프로그램 정보를 프론트에 한 api 에 반환하고 있습니다.
그 정보는 open api 를 통해 프로그램 상세 정보, 사람 상세 정보, Provider 상세 정보 을 요청한 이후 각각의 정보에서 가공 처리를 거친 후에(changeActorAndDirectorToKorea
,및 밑에 주석처리한 부분
) 사용자에게 반환합니다.
//API 요청 첫번째: 프로그램 상세 정보
OAProgramDetailsDto OAProgramDetailsDto = getProgramDetails(program.getTmDbProgramId(),
program.getType());
ProgramDetailResponse programDetailResponse = createProgramDetailResponse(
OAProgramDetailsDto);
//API 요청 두번째: 사람 상세 정보
OAProgramCreditsDto oaProgramCreditsDto = getCreditsDto(program.getTmDbProgramId(),
program.getType());
changeActorAndDirectorToKorea(oaProgramCreditsDto);
//API 요청 세번째: Provider 상세 정보
OAProgramProviderDto oaProgramProviderDto = getProviderDto(program.getTmDbProgramId(),
program.getType());
Optional<OACountryDetailsDto> kr = Optional.ofNullable(
oaProgramProviderDto.getResults().get("KR"));
//사용자에게 보낼 DTO 를 만들고 OTT 의 이름을 한글로 변환시키는 작업을 수행합니다.
Optional<ProgramProviderListResponseDto> programProviderListResponseDto = kr.map(
oaCountryDetailsDto -> changeOTTtoKoreanAndMakeProviderResponseDto(
oaCountryDetailsDto));
// 사용자에게 보내줄 DTO
ProgramResponseDto programResponseDto = new ProgramResponseDto(programDetailResponse,
oaProgramCreditsDto, programProviderListResponseDto.orElse(null),
program.getAverageRating());
return programResponseDto;
또한 각각의 open api 를 통해 정보를 얻어오는 메서드는
<프로그램의 상세 정보를 얻어오는 부분>
private OAProgramDetailsDto getProgramDetails(Long tmDbId, ProgramType programType) {
if (programType == ProgramType.Movie) {
OAMovieDetailsDto oaMovieDetailsDto = webClient.get()
.uri("/movie/" + tmDbId + "?language=ko")
.retrieve()
.bodyToMono(OAMovieDetailsDto.class)
.block();
return oaMovieDetailsDto;
} else {
OATvDetailsDto oaTvDetailsDto = webClient.get()
.uri("/tv/" + tmDbId + "?language=ko")
.retrieve()
.bodyToMono(OATvDetailsDto.class)
.block();
return oaTvDetailsDto;
}
}
<프로그램의 인물 정보를 얻어오는 부분>
private OAProgramCreditsDto getCreditsDto(Long tmDbProgramId, ProgramType programType) {
RequestHeadersUriSpec<?> requestHeadersUriSpec = webClient.get();
if (programType == ProgramType.Movie) {
requestHeadersUriSpec.uri("/movie/" + tmDbProgramId + "/credits");
} else {
requestHeadersUriSpec.uri("/tv/" + tmDbProgramId + "/credits");
}
return requestHeadersUriSpec
.retrieve()
.bodyToMono(OAProgramCreditsDto.class)
.block();
}
<프로그램의 OTT 제공 정보를 얻어오는 부분>
private OAProgramProviderDto getProviderDto(Long tmDbProgramId, ProgramType programType) {
RequestHeadersUriSpec<?> requestHeadersUriSpec = webClient.get();
if (programType == ProgramType.Movie) {
requestHeadersUriSpec.uri("/movie/" + tmDbProgramId + "/watch/providers");
} else {
requestHeadersUriSpec.uri("/tv/" + tmDbProgramId + "/watch/providers");
}
return requestHeadersUriSpec
.retrieve()
.bodyToMono(OAProgramProviderDto.class)
.block();
}
기존에 RestTemplate
을 동기적으로 사용했던 것과 마찬가지로
이런 식으로 .block()
을 적어서 동기적으로 호출하도록 하였습니다.
postman
을 통해서 program 정보를 호출하였을 때 너무 느렸습니다.
어벤져스 엔드 게임 기준(programID:84) 기준
이런 식으로 두번째 호출 이후 부터는 평균적으로 40ms 후반에서 최대 60 중후반까지 나오는게 보통이였습니다.
여기까지는 아 오케 불편한게 안느껴졌습니다.
하지만 처음호출하는 것은 얘기가 달랐습니다.
<처음 호출 시>
<두번째 호출 시>
이에 대한 원인을 따져보기 위해서 각각의 open api 호출 시작과 전에 시간을 찍어보았습니다.
이렇게 말입니다.
그래서 그 결과는
open api 를 호출하는데 드는 시간이 오래 걸리는 것이었고
이는 현재 사용하고 있는 tmdB
에 문제점이 있다고 생각했습니다.
확실하지 않으나 아마 tmdB내부에 자주 사용되거나 최근에 호출된 것은 캐시에 저장하거나 그런 성능상의 이점을 갖기 위해 된게 아닐까 추측합니다.
다른 영화도 처음 호출하는 것은
정말 오래걸렸습니다.
WebClient
의 차별화된 장점은 비동기 란 점을 알고 있었으나, 여러가지 한번에 호출하는 것 아냐?? 정도의 기초적인 지식만 가지고 있어서 이에 대한 정보를 먼저 모으고 정리하였습니다.
호출 함수가 자신이 할 일을 모두 마칠 때까지 제어권을 계속 가지고서 호출된 함수에게 바로 돌려주지 않으면 Block
Application 이 Kernel 로 작업 요청을 할 때 kernel 에서는 요청에 대한 로직 실행
Application 은 요청에 대한 응답을 받을 때까지 대기
Application은 Kernel 이 작업을 끝낼 때까지 백그라운드에서 작업이 끝났는지 지속적 확인
호출된 함수가 자신이 할 일을 채 마치지 않았더라도 바로 제어권을 건내주어(return) 호출한 함수가 다른 일을 진행할 수 있도록 해주면 Non-Block =⇒ WebClient 의 특
Application이 요청을 하고 바로 제어권을 받음.
따라서 다른 로직을 실행할 수 있다.
동기 비동기는 요청한 작업에 대한 완료 여부를 신경 써서 작업을 순차적으로 수행할지 아닌지에 대한 관점.
동기 작업은 순서대로 요청한 작업을 수행하지만, 비동기 작업은 순서가 지켜지지 않을 수 있다.
블로킹 논블로킹 : 현재 작업이 block 되느냐 아니냐에 따라 다른 작업을 수행할 수 있는지에 대한 관점
다른 요청의 작업을 처리하기 위해 현재 작업을 block(차단,대기) 하냐 안하냐 의 유무를 나타내는 프로세스의 실행 방식
동기/비동기: 전체적인 작업에 대한 순차적인 흐름 유무
(주로 call back 함수사용)
블로킹/논블로킹: 전체적인 작업의 흐름 자체를 막냐 안막냐
예시: 자바 스크립트의 setTimeout 함수는 타이머 작업 완료 여부를 신경 쓰지 않고 다음 콘솔 작업을 수행하기 때문에 비동기
또한 setTimeout 함수는 자신의 타이머 작업을 수행 위해 메인 함수를 블락하지 않았으니 논블라킹
https://gngsn.tistory.com/154 => 그림으로 이해 가능
현재 .block()
을 사용하여 동기식으로 차례대로 호출하고 있으니 비동기적으로 적용할 방법을 찾으려고 했습니다.
그러던 중 저와 비슷한 상황을 가진 블로그 글을 찾았습니다.
https://suyeonchoi.tistory.com/61
위 글을 요약하면
A.block() , B.block() , c.block() 대신 Mono.zip(A,B,C) 이런 식으로 바꾼다면
Mono.zip을 통해 최종적인 결과는 동기적으로 처리되지만, 외부 API를 호출하는 각각의 Mono는 스케줄러에 의해 비동기적으로 수행된다.
라는게 블로그의 내용요약입니다.
또한 스프링 공식 문서의
https://docs.spring.io/spring-framework/reference/web/webflux-webclient/client-synchronous.html
이 부분을 보면
여러 번 multiple calls 가 필요할 때 각각을 따로 blocking
하는 것보다 결합된 결과를 기다리는 것이 더 효율적이다 라는 문구를 볼 수 있습니다.
어쨌든 성능이 개선된다는 이야기가 됩니다!!
private OAProgramDetailsDto getProgramDetails(Long tmDbId, ProgramType programType) {
if (programType == ProgramType.Movie) {
OAMovieDetailsDto oaMovieDetailsDto = webClient.get()
.uri("/movie/" + tmDbId + "?language=ko")
.retrieve()
.bodyToMono(OAMovieDetailsDto.class)
.block();
return oaMovieDetailsDto;
} else {
OATvDetailsDto oaTvDetailsDto = webClient.get()
.uri("/tv/" + tmDbId + "?language=ko")
.retrieve()
.bodyToMono(OATvDetailsDto.class)
.block();
return oaTvDetailsDto;
}
}
기존에 이 코드에서 OAMovieDetailsDto
,OATvDetailsDto
는 OAProgramDetailsDto
를 상속받아서 사용하고 있었습니다.
이를 Mono 형식으로 반환하려 하였는데 ...
이런 식으로 오류가 발생하였습니다.
따라서
아래와 같이
private Mono<?> getProgramDetails(Long tmDbId, ProgramType programType) {
if (programType == ProgramType.Movie) {
Mono<OAMovieDetailsDto> oaMovieDetailsDto = webClient.get()
.uri("/movie/" + tmDbId + "?language=ko")
.retrieve()
.bodyToMono(OAMovieDetailsDto.class);
return oaMovieDetailsDto;
} else {
Mono<OATvDetailsDto> oaTvDetailsDto = webClient.get()
.uri("/tv/" + tmDbId + "?language=ko")
.retrieve()
.bodyToMono(OATvDetailsDto.class);
return oaTvDetailsDto;
}
}
로 수정하였습니다.
SSLException: No PSK available. Unable to resume.
계속 이 오류가 Tuple 을 사용했을 때마다 발생하였는데
이는 Java 11.0.2 버젼을 사용했을 때 이럴 수 있다고 한다.
현재 프로젝트에 배포되어 있는 최신 자바 버젼으로 바꾸었다.
https://bugs.openjdk.org/browse/JDK-8213202
@Override
public ProgramResponseDto showDetails(Long programId) {
long firstPart = System.currentTimeMillis();
Program program = programRepository.findById(programId)
.orElseThrow(() -> new NotFoundException(ErrorCode.PROGRAM_NOT_FOUND));
//API 요청 첫번째: 프로그램 상세 정보
Mono<?> programDetailsMono = getProgramDetails(
program.getTmDbProgramId(),
program.getType()).subscribeOn(Schedulers.boundedElastic());
/*ProgramDetailResponse programDetailResponse = createProgramDetailResponse(
(OAProgramDetailsDto) programDetailsMono.block());
*/
//API 요청 두번째: 사람 상세 정보
Mono<OAProgramCreditsDto> oaProgramCreditsDtoMono = getCreditsDto(
program.getTmDbProgramId(),
program.getType()).subscribeOn(Schedulers.boundedElastic());
//API 요청 세번째: Provider 상세 정보
Mono<OAProgramProviderDto> oaProgramProviderDtoMono = getProviderDto(
program.getTmDbProgramId(),
program.getType()).subscribeOn(Schedulers.boundedElastic());
System.out.println("Mono 부분 전 시간:" + (System.currentTimeMillis() - firstPart));
long openApiPart = System.currentTimeMillis();
Tuple3<?, OAProgramCreditsDto, OAProgramProviderDto> tuple3 = Mono.zip(programDetailsMono,
oaProgramCreditsDtoMono, oaProgramProviderDtoMono).block();
System.out.println("open api 호출 부분:" + (System.currentTimeMillis() - openApiPart));
long restPart = System.currentTimeMillis();
ProgramDetailResponse programDetailResponse = createProgramDetailResponse(
(OAProgramDetailsDto) tuple3.getT1());
changeActorAndDirectorToKorea(tuple3.getT2());
Optional<OACountryDetailsDto> kr = Optional.ofNullable(
tuple3.getT3().getResults().get("KR"));
//사용자에게 보낼 DTO 를 만들고 OTT 의 이름을 한글로 변환시키는 작업을 수행합니다.
Optional<ProgramProviderListResponseDto> programProviderListResponseDto = kr.map(
oaCountryDetailsDto -> changeOTTtoKoreanAndMakeProviderResponseDto(
oaCountryDetailsDto));
// 사용자에게 보내줄 DTO
ProgramResponseDto programResponseDto = new ProgramResponseDto(programDetailResponse,
tuple3.getT2(), programProviderListResponseDto.orElse(null),
program.getAverageRating());
System.out.println("나머지 부분: " + (System.currentTimeMillis() - restPart));
return programResponseDto;
}
이렇게 수정하였습니다 .
나머지 메서드는 반환 타입을 Mono<Dto>
로 바꾸고 .block()
을 제외하였습니다.
참고:
https://devfunny.tistory.com/915
https://projectreactor.io/docs/core/release/reference/#schedulers
이제는 새로운 프로그램을 검색해도 500ms
대로 속도가 줄어들었습니다.
어떤 새로운 영화 및 TV 의 상세 정보를 검색해도 0.7 초의 속도는 나오지 않습니다!
이제는 같은 프로그램(초기:programId 84) 를 두번 세번 계속 검색해보겠습니다.
기존 같은 데이터를 조회했을 때 50~60 이 이제는 30~40 으로 바뀐 것을 볼 수 있습니다.
여러 open api 의 데이터를 한번에 block()
처리 하였는데 이를 통해 처음에 호출된 open api 의 정보는 대략 600~700 ms 의 속도가 나오던게 4,500ms 단위로 줄었고 반복적으로 open api 를 호출하면 그 속도가 빨라지는데 그 속도는 50~60 이 30~40 정도로 빨라졌습니다.
완전히 비동기로 한번 작성해보고 싶었다.
subscribe
와 flatMap
등을 활용하고 싶었다.
하지만 제대로 된 이해 없이 마구잡이로 사용해봤자 의미가 없어 보인다.
쓰레드에 대한 개념과 연계해서
어떻게 해야 잘 사용할 수 있을지 배경지식이 더욱 필요해 보인다. 꼭 다시 하자!