인구 밀도 데이터 조회 시간 단축시키기 - boostcampwm-2022/web33-Mildo GitHub Wiki
- Mildo에서 제공하는 서울시 인구 밀도 정보는
서울시 실시간 도시데이터 api
에서 인구 밀도 정보만 추출·가공하여 사용하고 있다.
- 서울시 주요 50장소의 인구 밀도 정보를 한번의 api 요청으로 전부 가져올 수 없고, 각 장소에 대해 개별적으로 api 요청을 보내 정보를 가져왔다.
// 서울 실시간 도시데이터 호출 API, 핫스팟 장소명은 필수 요청인자이다.
http://openapi.seoul.go.kr:8088/<인증키>/xml/citydata/1/5/<핫스팟 장소명>
- 50장소에 대해 각각의 api를 호출하는 경우
Promise.all
를 통해 비동기적으로 값을 가져왔으나, 서울 openapi 서버에서 서버 오류가 발생하여 50장소 중 일부만 가져오는 것을 볼 수 있다.
// 기존 코드
const allAreaNames = await areaService.getAllAreaCoordinate(); // 모든 장소
const cityData = await Promise.all(allAreaNames.map(async areaName =>
getAxiosSeoulArea(areaName, `SEOUL_CITY_API_ACCESS_KEY`);
));
-
Promise.all
이 아닌 단순히 for문을 이용해서 동기적으로 로직을 변경하였더니 50장소를 서울 open api 서버 오류없이 가져오는 것을 볼 수 있다.
// 변경된 코드
const allAreaNames = await areaService.getAllAreaCoordinate(); // 모든 장소
const cityData = [];
for (const areaName of Object.keys(allAreaNames)) { // 각각의 장소에 대해 api 호출
const cityDataXml = await getAxiosSeoulArea(areaName, `SEOUL_CITY_API_ACCESS_KEY`);
cityData.push(cityDataXml);
}
-
그러나 api 1회 요청에 2~3초 정도가 소요되었고, 모든 주요 장소 정보를 가져오는데 최대 3.5분 소요되었다.
- 네트워크 측정
-
서울시 실시간 도시데이터 API에서 광화문·덕수궁의 도시 데이터를 호출하였는데 2.68초가 소요되었다.
-
50곳의 장소를 가져오는 데에 약 3.5분이 소요되었다.
-
- 네트워크 측정
-
이용자가 서비스 이용 시 인구 밀도 정보를 확인하는 데에 너무 오래 걸리고, 개인이 사용할 수 있는 API 호출 횟수가 하루에 최대 1000번이었기 때문에 이용자가 서비스에 접속할 때마다 api를 호출할 수 없었다.
-
개인이 사용할 수 있는 api 호출 횟수
- 개발을 시작할 때는 1000번 제한이 있었으나, 현재는 제한이 없다고 한다.
-
-
또한
서울시 실시간 도시데이터
서비스에서 완전한 실시간이 아닌, 30분 간격으로 업데이트를 하고 있었다. -
그래서 데이터를 30분마다 주기적으로 받아와 데이터베이스에 저장하고, 사용자가 서비스에 접속하면 서울 open api 서버에 데이터를 요청하는 것이 아닌 데이터베이스에서 최신 데이터를 불러오기로 했다.
- 잡스케줄러인 node-cron을 사용하여 주기적으로 50곳에 대한 데이터를 MongoDB에 저장했다.
// ??시 0분, ??시 30분 마다 node-cron 동작 -> 0,30 * * * *
cron.schedule(`0,30 * * * *`, cronController.cronSeoulData);
-
client, api를 제공하는 서버와 분리하여 cron-server를 구축하였고, api 서버에 문제가 생겨도 주기적인 데이터 저장 만큼은 보장할 수 있게 했다.
-
기존의 api 서버와 cron 서버를 분리하여 운영하는 모습이다.
-
cron 서버의 로그를 확인하니 주요 장소 50곳의 데이터를 받아오는데 주로 70~80초 정도가 소요되는 것을 볼 수 있다.
-
이제 api 서버에서는 MongoDB에서 데이터를 가져오면 된다.
-
처음에는 MongoDB에 저장된 데이터의 시간을 내림차순으로 정렬하여 위에서부터 50장소 가져오도록 했다.
const recentData = await Population.find() .sort({ created: -1 }) .limit(50);
-
하지만 단순하게 최근 50장소를 가져오게 되면 중간에 누락된 장소가 있을 수 있기 때문에 각 장소에 대한 데이터를 추출한 후, 그중 가장 최신 데이터를 가져오도록 했다.
const allAreas = await Area.find(); const recentData = await Promise.all( allAreas.map(async area => { const name = area.areaName; return await Population.findOne({ areaName: name }) .sort({ created: -1 }); }) );
-
현재 주요 50장소의 인구 밀도 가져오는데 걸리는 시간 : 약 650ms 소요
-
네트워크 측정
-
- 서비스를 계속 유지하게 된다면 점점 인구 밀도 데이터가 쌓이게 되어 정렬하는 시간이 더 오래 걸릴 것이라고 예상했다.
- 최근 24시간 데이터는 서버 메모리에 따로 저장해두면 MongoDB에 접근할 필요없이 빠르게 가져올 수 있을 것이라고 생각했다.
- 하지만 현재 cron 서버와 api 서버가 분리되어 있어, cron 서버의 메모리에 최근 인구 밀도를 저장하면 api 서버는 이 데이터에 접근할 수 없다.
- 50개의 데이터를 불러오는 방식 회의
-
데이터베이스를 시간 순서로 정렬 후 가져오기
- 데이터가 점점 많아질 경우 정렬 속도가 느려질 것 같음
-
최근 시간을 서버에 변수로 저장하기
- crontab 서버에서 시간을 변수로 가지고 있는데, 이렇게 되면 api 서버에선 시간이 저장된 변수를 접근할 수 없음
-
API에서 데이터를 불러올 때마다 area 스키마에
updatedAt
같은 필드를 생성해서 50개의 데이터를 모두 업데이트-
한 번 불러올 때마다 50개의 데이터를 모두 업데이트 해야 됨 → 비효율적
-
24시간 이내의 데이터를 불러올 때는 Embeded 형식으로 넣으면 됨
ex) { 오전1시: 2022-11-22T00:20:00, 오전2시: … }
-
-
시간 컬렉션을 만들어서 그 컬렉션에 계속 최근 시간을 넣기
-
redis 사용 (챌린지)
- 최근 시간을 가지고 있는 것보다 최근 장소 50곳 또는 24시간 이내 장소 밀도를 모두 업데이트하여 저장
- 24시간 이내의 정보를 빠르게 제공해주는 것이 우리 서비스에 있어서 중요한 부분이기 때문에 가장 적합하다고 보임
- 이걸 하려면 크론탭 서버를 빨리 구축해야 함
- 러닝커브가 발생할 것 같음
- RabbitMQ라는 메시지 큐 기반 데이터베이스도 있는데 러닝커브가 적다고 함
-
⇒ 데이터 캐싱을 위해 Redis를 사용하기로 결정
- cron 서버는 서울 주요 50장소의 인구 밀도를 가져올 때 MongoDB에 저장함과 동시에 Redis에도 저장한다.
- Redis에는 만료기간을 24시간으로 설정하여 저장한다.
// Redis 구조
'recent' : '20221211 17:55'
'20221211 17:55' : { <인구밀도정보> }
'20221211 17:25' : { <인구밀도정보> }
...
- api 서버는 Redis에 접근하여 인구 밀도 정보를 가져온다. 이젠 MongoDB의 쿼리를 이용하지 않고도 데이터를 가져올 수 있다.
- 현재 주요 50장소의 인구 밀도 가져오는데 걸리는 시간 : 약 430ms 소요
-
네트워크 측정
-