부스트캠프 웹・모바일8기(boostcamp) 그룹 프로젝트 3주차 회고

최근혁(GeunH)·2023년 12월 5일

그룹 프로젝트

목록 보기
3/6
post-thumbnail

MVP 구현

저번 주에 서버를 구성하고, 이번 3주차에 할 일은 api 기능 구현이었다.
여러 api 엔드포인트 url에 맞는 응답 구조를 만드는 것이 이번 주차에 힘써야 할 일이었다.

MVP 버전과 FINAL 버전을 분류하고, MVP 버전에 맞는 기능들을 먼저 개발하기로 했다.
저번 주에, 같은 분야 백엔드 캠퍼와 할 일을 나눠 놓았기에 기능 개발의 분업이 이루어졌다.

시작부터 문제

우리의 프로젝트는 '음식점 공유 플랫폼 앱' 으로 당연히 기존 음식점 데이터를 받아와 DB에 저장해야 했다. 클라이언트의 음식점 관련 데이터 호출마다 지도 api를 호출하는 방법은 상태를 저장할 수 없고 서버에 오버헤드가 너무 클 것이기에 DB를 사용해야만 했다.

기존 생각했었던 음식점 정보 api는 지도 검색 api ( ex : 네이버 맵, 카카오 맵 )를 이용한 리스트를 받아오는 것이었다.

그러나, 위 지도 검색 api의 경우에서 각 검색 api 호출 시에 반환되는 결과는 연관된 데이터 모두가 아닌 45개정도의 제한이 있었고 호출 횟수 자체에도 일일 허용량이 있었다.

당장 서울시 음식점 데이터를 추정한다면 10만개 이상이 될 것이라 판단했기에 서버에 음식점 데이터를 업데이트 하기 위한 데일리 api 호출에서 45개( 더 적을수도 있음 )씩 여러번 호출한다면 일일 허용량을 넘길 수도 있었고, 각 검색마다 제한되는 응답개수를 해결하기위해서 검색마다 키워드를 다르게 하는 것은 무리가 있다고 판단했다.


해결 방안

때문에 다른 api를 이용하기로 했는데 마침 발견한 것이 서울시 공공 api 였다.
회원가입을 하여 받을 수 있는 개인 key를 이용해 api 호출을 할 수 있었고, 호출당 1000개의 데이터씩 end-page가 나올때까지 페이지네이션이 가능했다.

총 50만개의 데이터가 있었고, 이중 "영업중"인 약 15만개의 데이터를 DB에 저장하는 방식을 사용했다.

이어서 또 문제

음식점 정보를 받아올 api 문제는 해결하였으나 다음은 DB에 저장하는 속도가 문제였다.
Nest.js가 TypeScript이지만 기본은 Node.js 였기에 싱글스레드였다. 때문에 async await으로 해당 작업을 처리한다면 api를 이용해 데이터를 받아와 이를 DB에 저장하는 13분간 기다려야만 했기에 이러한 비효율적인 상황을 해결해야 했다.

해결법 1.

처음 생각한 해결법은 worker 스레드 사용해보기였다.
기존 Challenge를 수행하며 사용해봤던 worker 스레드를 통해 병렬로 처리한다면 작업이 훨씬 빨라지겠다고 생각했다. 그래서 스레드를 4개로 하여 각 스레드가 api 호출로 데이터를 받아와 DB에 저장하는 방법을 사용했다. 이러한 방법은 api를 호출하여 받아오는 음식점 데이터가 DB에 저장되는 순서가 상관이 전혀 없었기에 사용할 수 있었다.

총 13분이 넘던 작업이 3분 50초 정도로 줄어들었으나, 각 스레드가 DB에 저장하는 순간에 DeadLock현상이 발생했다. 이유를 생각하니 당연한 것이었다. 4개나 되는 스레드가 api를 받아와 DB에 저장하는 순간에 스레드 간 순서없이, 이미 작업하는 pk를 다음 스레드가 중복해서 저장하려 하니 문제가 발생했다.

또한, CPU 연산 작업이 없는 단순 api로 데이터를 받아와 DB에 저장하는 역할을 굳이 worker thread를 사용할 필요가 없었다.

해결법 2.

위와 같은 상황을 해결하기 위해서, 생각한 방안은 promise all 비동기 처리 방식이었다.
promise객체를 담을 빈 list를 만들고, promise를 계속 생성하였다.
각 promise는 page인자를 받아 이 page를 토대로 api를 호출하여 음식점 리스트에서 page에 맞게 담당하는 데이터를 가져왔다.

그러다 promise가 15개가 되면 ( if문 조건문 사용 ) 1000개씩 x 15인 15000개의 데이터를 한꺼번에 가지고 DB 저장 작업을 진행했다. 각 promise가 받아온 1000개의 데이터를 잠시 모아뒀다 15000개의 데이터를 한번에 DB에 저장하니 DeadLock 문제는 발생하지 않았다.

저장 후, promise list에 push되었던 promise들은 모두 해제시켜주었고, 이 작업을 endpage까지 반복하였다.

이 방식으로 동기 싱글 스레드 방식에서 13분 50초였던 작업을 1분 20초까지 단축할 수 있었다.

DB 트랜잭션 고립레벨?

위와 같이 1분 50초라는 서버의 DB 데이터 업데이트라는 시간은 비교적 짧은 시간이지만, 해당 작업이 이루어지는 동안 클라이언트의 서버 api 호출이 이루어진다면 문제가 생길 수도 있다고 판단했다.

예를 들어, 어제까지 있었던 음식점이 오늘 업데이트를 하며 사라지는 상황( ex : 폐업 ) 에서 DB의 트랜잭션 기본 고립레벨인 Read Commited 상황에서는 데이터가 그대로 읽히기에 사용자에게 불편을 줄 수 있지 않을까 라고 판단했다.

그렇다고 트랜잭션 레벨을 Serializable로 변경해버리면 1분 50초 이후에 작업이 이루어져야하니 이 또한 문제가 있다고 판단했다.

결론은, 서버 주기 업데이트 ( Cron, SetInterval ) 시간을 사용자의 요청이 비교적 뜸한 새벽 시간에 진행하기로 하였다.

API 구현

DB 엔티티를 Nest.js에서 TypeORM을 이용해 생성하고 관리하며 api 응답을 구조화 하는 것은 비교적 어렵지 않았다.

문제가 될 수 있다면, 당장은 기능 구현에 집중했기에 비교적 서버 api 응답 함수가 비효율적이더라도 단위 테스트에 통과할 수 있다면 구현하였다.

이후 MVP 버전 기능 구현 후에, FINAL 버전에서 부하 테스트를 거치며 이러한 점들을 개선하기로 계획하였다.

느낀 점

이번 한 주는 MVP 버전을 위한 본격적인 서버 API 구현에 시간을 쏟았던 한 주 였다.
음식점 API를 사용하며 여러 트러블이 있었지만 결국 잘 해결해 낸 것 같다.

다음 주 부터는 Object Storage, DB의 음식점 위치 데이터 반환 등 여러 부분을 고려해야 할 것 같다.

profile
목표 : 스스로 성장하는 개발자

0개의 댓글