오늘은 V3의 위치 조회 기능을 마무리했다!
어제의 피드백 이후로 우리 서비스의 조회는 목록 조회와 지도 조회로 나뉘게 되었는데,
모든 조회는 검색 기능과 함께하지만, 이를 목록으로도 확인할 수 있고, 지도의 핀으로도 확인할 수 있게 된다.
목록 조회 시에는 기준이 되는 지역을 정하면, 해당 지역의 중심 좌표로부터 원형으로 반경 5km 이내의 모임을 필터링하게 된다.
처음 사용자가 진입할 때는 사용자가 저장해둔 지역으로 필터링되지만, 이후에는 사용자가 이 지역을 커스텀해서 필터링할 수 있게 된다.
지도로 조회 시에는 처음에는 사용자의 GPS 기준으로 지도가 불러와지고, 이후에는 사용자가 지도를 이동하며 모임을 조회할 수 있게 된다.
어제까지 진행했던 내용은 모임 목록 조회에 대한 Redis 적용이었는데, 오늘은 이 부분에 대해 여러 테스트를 진행해보았다.
첫 번째로는 DB 조회 시, 두 방법에 있어 옵티마이저가 어떤 방법으로 조회하게 되는지를 확인하였다.
EXPLAIN ANALYZE를 통해 DB의 실행 계획을 볼 수 있었는데, DB 조회 시에는 생성해둔 spatial index를 사용하고, 레디스 필터링 후에는 ID 값을 기준으로 조회하기 때문에 id index를 사용한다.
그래서 사실상 DB 자체의 처리에서는 큰 차이가 나지 않았다.
두 번째로는 포스트맨을 통해 단일 요청에 대한 전체 응답 처리 시간을 비교해보았다.
| 검색 조건 | DB 조회 소요 시간 | Redis 조회 소요 시간 |
|---|---|---|
| 추가 조건 X | 303ms | 608ms |
| 추가 조건 X(두번째 호출) | 175ms | 76ms |
| 검색어 O | 152ms | 117ms |
| 가까운순 정렬 | 401ms | 305ms |
추가 조건이 없을 때 중심 위치를 다르게 하여 2번 요청을 보냈고, 이후에는 검색어를 포함하는 경우와 가까운 순으로 정렬하는 경우를 비교해보았다.
이때 눈에 띄는 것은 Redis 사용 시, 첫 번째 요청의 처리 시간이 굉장히 오래 걸린다는 것이다.
이 이유를 찾아보니, 첫 요청 시에는 레디스와 연결을 해야하는 등의 작업이 필요하기 때문이었다.
하지만, 실제 서비스에서 이러한 문제를 겪게 될 사용자가 많지는 않을 것이기 때문에, 두 번째 요청부터 유의미한 결과로 보았다.
추가 조건이 없을 때는 두 경우의 결과 차이가 크지만, 검색어가 있다던가 정렬이 추가되면 사실 두 요청에 대한 단일 응답 시간에는 큰 차이가 보이지 않는다.
세 번째로는 부하 테스트를 진행해보았다.
GPT의 도움을 받아.. k6를 통해 진행할 부하 테스트의 시나리오를 작성해보았는데,
1. Hot 요청(40%) : 같은 위치에서 같은 요청 반복 (ex: 새로고침)
2. Warm 요청(40%) : 중심지 조금씩 이동 (ex: 지도 이동)
3. Cold 요청(20%) : 새로운 중심지로 조회 요청 (ex: 완전히 새로운 동네로 필터링)
이렇게 3가지 요청으로 30초 간 20명, 1분 간 50명, 1분 간 100명의 사용자가 0.1초 간격을 두고 반복적으로 조회를 요청하도록 시나리오를 구성하였다.
사실 이전까지의 테스트에서는 괜히 레디스까지 사용하는 것인가 싶었는데,
| 지표 | DB 조회 | Redis 조회 |
|---|---|---|
| 평균(avg) | 1.26s | 21ms |
| 중간값(med) | 1.1s | 13.6ms |
| p90 | 2.31s | 42.6ms |
| p95 | 2.46s | 63.8ms |
| 최대(max) | 7.08s | 597ms |
| http_reqs | 34.262973/s | 380.228533/s |
부하 테스트의 결과에서 레디스와 DB의 확연한 차이가 드러났다.
레디스가 각각의 요청을 처리하는 시간이 굉장히 빨랐기 때문에, 같은 시간 동안 10배가 넘는 요청들을 처리할 수 있었다.
레디스를 사용할 때 비용이 증가하게 된다는 한계가 있지만, 이 정도의 성능 개선이라면 레디스를 사용하는 것이 훨씬 이익이 될 것이라 판단한다.
이제 이후에 추가로 인덱스를 생성하는 등 검색 성능을 더 높여볼 예정이다.
이렇게 레디스의 성능을 알아냈으니 이제 지도 조회 API에도 레디스를 적용해야 했는데..
우선 레디스의 GEOSPATIAL에서는 GEOSEARCH key FROMMEMBER member BYBOX width height m|km|ft|mi와 같이 특정 위치를 중심으로 박스 형태로 범위를 만들어 조회하는 명령어를 지원한다.
우리가 프론트엔드로부터 받을 수 있는 내용은 중심 좌표와 좌상단, 우하단의 좌표 뿐이었다.
하지만, 레디스에서는 중심 좌표로부터의 거리를 중심으로 조회할 수 있었기 때문에, 우선 지도 ViewPort 간 넓이와 높이를 계산해야했다.
이전에 만들어둔 GeometryUtil에 두 좌표간의 거리를 계산하는 메서드가 있었기 때문에, 이 내용을 가져와서 사실 간단하게 계산할 수 있었다.
그리고 위 레디스 명령어를 스프링에서 사용할 수 있는 형식으로 바꾸어 진행하였다.
사실 레디스를 사용하지 않는다면 이런 계산 단계를 거치지 않아도 됐다.
MySQL에서는 사각형을 기준으로 그 안에 포인트가 위치하는지 아닌지 판단할 수 있는 함수가 있기 때문이다.
그래서 구현을 진행하면서 계산 단계가 추가되었는데도 과연 레디스가 더 효율적일 것인지 의아했었다.
| 지표 | DB 조회 | Redis 조회 |
|---|---|---|
| 평균(avg) | 607ms | 25ms |
| 중간값(med) | 531ms | 12.6ms |
| p90 | 1.27s | 42.7ms |
| p95 | 1.43s | 77ms |
| 최대(max) | 3.15s | 1.15s |
| http_reqs | 65.777278/s | 357.787201/s |
그런데, 위와 같은 시나리오로 부하 테스트를 진행해을 때 결과는 확연하게 달랐다.
레디스가 훨씬 더 많은 양을 빠르게 처리할 수 있었다.
우리 팀이 작성한 코드는 깃허브를 통해 업로드해두었다.
GitHub 보러가기
사실 위치 조회 기능을 하루면 다 끝낼 수 있을 줄 알았는데.. 생각보다 까다롭더라.
처음 보는 함수로 사용해봐야 했고, 특히 레디스의 BYBOX로 검색하는 내용에 대해 인터넷 자료들이 별로 없었다.
그래서 결국에는 인터넷에서 자료를 찾기보다 스프링에서 메서드들을 타고 타고 들어가서 내가 원하는 기능을 찾을 때까지 뒤져보았다.
그래도 기능 구현도 해보고, 전후 차이를 비교해보는 과정들이 재미있었다.
이제 하나 끝났으니까.. SSE 알림 시스템을 빨리 시작해봐야겠다!