최근에 새로 프로젝트를 하나 시작했는데요!
Riot API를 통해서 전적 검색 API를 개발하고 있던 중이였습니다.
바로 사용자가 플레이 한 기록을 조회하는 API, 전적 갱신입니다
리그오브레전드에는 한 판에 10명의 플레이어가 존재하고, 그 안에 굉장히 많은 정보가 담기게 되는데요,
그래서 이를 구현하기 위해서는 해야할 것이 몇 가지 있습니다.
- 이름으로 최근 전적 Id List 조회
- 전적 List의 상세 정보 조회
- 전적 별 플레이어 정보 조회
그러나 전적 조회를 할 때마다 외부 API에 의존하면서 다시 조회하는 것은
리소스 낭비라고 생각해서 첫 플레이어 조회 시에 이를 저장하는 것까지 하기로 하였습니다
( 그러지 말았어야 했다 )
개발은 순조롭게 진행되고 있었고, 이 전적 갱신이 아주 원활하게 이루어지고 있었습니다.
하지만 문제가 발생했습니다.
API가 너무 오래걸린다는 것이였습니다..
첫 번째 시도
당시 외부 API 호출을 위해 WebClient를 채택했습니다,
이유는 RestTemplate보다 성능이 좋다고 알고있어서 사용한 것이였는데
왜 이렇게 API가 느린지 도무지 알 수 없었습니다.
도무지 알 수 없었기에 구글에 검색해보았더니,
WebClient를 사용했을 때 느린 경우에 보통 .block()으로 외부 API를 호출하면
동기적으로 실행해서 성능이 떨어진다고 합니다..
혹시나 하는 마음에 코드를 보니 메소드 마지막에 .block()이 있던 것이였습니다!!!!!!!!!!!!
이 메소드는 WebClient를 동기적으로 실행한다는 의미이며, 요청이 여러 개일수록 성능이 매우 떨어지는 설정였습니다.. 그렇게 나는 이 악의 근원을 제거했습니다 😡
하지만 문제는 또 다른 문제를 낳았고..
두 번째 시도 WebFlux
Mono..?
이게 뭐지 싶었던 저는 검색을 했고 이게 WebFlux의 Reactor 타입인 것을 알았습니다..
그래도 문제를 해결할 수 있다는 희망을 안고 WebFlux를 공부하면서 프로젝트에 적용하기 시작하였다
(stay……. stay……………!!!!!!!!!)
간략하게 WebFlux의 개념에 대해 설명하자면
구독과 발행으로, 구독자가 데이터를 요청하고 발행자가 데이터를 제공하는 비동기 데이터 처리 패턴입니다
유튜버와 구독자의 관계를 생각해보시면 어떤 느낌인지 이해하실 수 있어요! 😀
여튼, 프로젝트에 빠르게 적용시켜보았습니다!
val monoSummoner: Mono<SummonerDto> = remoteSummoner.getPuuid(name)
monoSummoner.subscribe { summoner
summonerRepository.save(Summoner(summoner))
}
이런 식으로 전적 갱신 API에 WebFlux를 다 묻히고,
설레는 마음으로 테스트를 시도했습니다!! (두근두근)
하지만 여전히 API의 성능은 달라지지 않았습니다.
그 이유는 제 프로젝트 환경에 있었는데요…..
현재 환경은 Spring MVC + JPA를 이용해서 개발을 진행하고 있었고,
여기에 WebFlux를 붙인다고 해서 성능이 개선되지 않습니다.
왜냐하면,
WebFlux는 Blocking이 없어야 하기 때문입니다
이유는 WebFlux의 동작 방식에서 알 수 있는데,
Non-Blokcing API(WebFlux) 는 "기다리는 시간"이 존재하지 않습니다.
이는 WebFlux Thread-Model의 특성으로, 작업을 수행하는 시간 동안 또 다른 Thread에서 다른 작업을 실행시키고
이를 반복하면서 작업 처리량을 극대화합니다.
Blocking API(Spring MVC)는 작업을 실행시키고 결과를 받기 위해 "기다립니다".
이 때문에 다음 작업을 실행하려면 이 전 작업이 끝나기까지 기다려야 합니다.
그러나 전적 갱신의 로직상 특정 작업을 기다려야 하는 부분이 필요했지만,
위에서 설명드렸듯 WebFlux는 기다리는 작업이 있어선 안됐습니다.
(기다리는 작업이 있을 경우 작업 중인 다른 Thread들이 대기 상태에 걸려버리기 때문에)
따라서 WebFlux 사용은 전면 폐지…
한 줄기 희망 Coroutine..
하지만 마침 Kotlin을 사용하고 있었고, Kotlin을 주로 사용하는 안드로이드 개발자 친구에게 물어보니 비동기 처리는 Coroutine으로도 한다고 하여서 정말 마지막 실날같은 희망으로 Coroutine을 찾아보게 되었습니다.
정말 감격스럽게도 Coroutine은 성능 향상은 할 수 있고,
때에 따라 마음대로 순서를 지정할 수 있는 저에게 기적이였습니다.
Coroutine으로 해결할 수 있는 이유는, 제가 작성한 전적 갱신의 로직 때문입니다..(제가 부족한 탓..)
원래 로직은 다음과 같았습니다.
외부 API를 호출 → 요청 응답 이후 → 다음 외부 API 재호출
하지만 이 로직은 요청의 응답이 이루어져야 다음 외부 API를 재호출하므로 대기시간이 발생하여 API 지연시간이 발생하게 됩니다.
따라서 외부 API 요청마다 대기시간이 생기는 것이 아닌,
필요한 모든 외부 API를 동시에 요청하고, 모든 요청이 반환되었을 때 저장할 수 있도록
Coroutine의 runBlocking을 통해 이를 구현했습니다
코루틴으론 I/O 작업을 비동기적으로 실행하고 blocking을 통해 작업의 순서를 지킬 수 있는 async/await과 runBlocking의 조합으로 풀어낼 수 있었습니다.
마무리
설명을 위해 WebFlux가 Spring MVC에 비해 매우 빠르다고 글을 작성하였으나, 사실 Spring MVC도 충분히 빠른 성능을 지니고 있으며, WebFlux에 비해 코드의 흐름이 읽기 쉽다는 장점이 있습니다.
또한 WebFlux는 Flow상 Blocking이 존재해서는 안되므로 모든 프로젝트에 적용하기에 어렵다는 단점을 가지고 있습니다.
둘 다 좋은 기술이고, 각자 다른 쓰임이 있어 어느 것이 더 우월하다 라는 건 없습니다.
긴 글 읽어주셔서 감사하고,
틀린 점이나 부족한 부분이 있다면 댓글로 남겨주시면 감사하겠습니다!!! 😁
github source code : https://github.com/Easy-Easy-Go/Glol-Server