분당 1,500건의 요청을 받는 서비스 리팩토링
배경
앞선 마이그레이션으로 시간을 번 직후부터 다음 작업을 준비했습니다. 먼저 앱이 ID 를 정수로 다루던 것을 String 으로 바꿨고, 그 배포가 사용자의 99% 에 닿을 때까지 기다렸습니다. 마침 운영팀에서 랜덤 상자의 확률 조정 기능을 파악하기 어려우니 리뉴얼하자는 요청이 왔고, 대표님께서도 승인해주셔서 드디어 리팩토링에 착수할 수 있었습니다. 상자 서비스의 /luckybox 엔드포인트는 그때 분당 1,500건 안팎의 요청을 받고 있었습니다.
상자를 어디에 저장할 것인가
먼저 상자를 어디에 저장할지 고민했습니다. 예전 설계는 상자를 전부 MySQL 에 저장해, 상자를 만들 때마다 INSERT 가, 상자를 열 때마다 UPDATE 가 발생했습니다. 이를 DynamoDB 로 옮기고, 파티션 키로 읽고 쓰는 속도가 빠르다는 장점을 최대한 활용하기로 했습니다.
상자가 생기면 배열에 값을 더하고, 상자를 열면 그 항목만 고치면 됐습니다. 겉으로 보기에는 MySQL 과 다르지 않지만 파티션 키 하나로 조회하고 수정하기 때문에 훨씬 빨랐습니다. 또 사용자가 그날 처음 접속하면 쌓여 있던 데이터를 지우게 해, 이전처럼 값이 끝없이 늘어나는 일도 막았습니다.
새로 생긴 정책으로, 한 사용자는 최대 20개의 상자만 열 수 있다는 제약도 있었습니다. 이 또한 상자 배열의 length 만 세면 깔끔하게 해결할 수 있었습니다.
랭킹과 설정은 어디에 둘 것인가
랭킹 시스템도 새로 만들었습니다. Redis 의 ZSET 은 중복 없는 키를 점수 순으로 정렬해두고, 순위를 읽고 갱신하는 작업에 O(log n) 의 시간복잡도를 가집니다. 그리고 고액 당첨자를 일정 기간 다시 당첨되지 못하게 막아야 한다는 요구사항도 있었는데, 당첨된 사용자의 ID 를 TTL 을 걸어 Redis 에 저장해두면 데이터베이스에서 당첨 기록을 조회할 필요 없이 간단하게 해결할 수 있었습니다.
당첨 확률 같은 설정 값은 MySQL 에 남겼습니다. 관리자 콘솔과의 연동을 고려해야 했기 때문입니다. 상자를 열 때마다 MySQL 을 한 번 읽어야 한다는 단점이 있지만, 설정을 실시간으로 반영할 수 있고 조회만 하므로 큰 부담은 아니라고 판단했습니다.
결과
완성된 시스템은 조금씩 트래픽을 받았습니다. 아침에는 10% 정도만 받으면서 지켜보다가, 문제가 없어 오후에 100% 로 올렸습니다. MySQL 의 부하가 내려갔고, 무엇보다 랭킹에 활기가 돌았습니다. 6시간에 한 번 갱신되던 값이 실시간으로 동작했기 때문입니다. 운영팀에서도 당첨 확률을 설정하기가 편해졌다며 만족스러워했습니다.
문제를 발견해 팀장님께 보고한 날부터 마지막 리팩토링까지 꼬박 1년이 걸렸습니다. 제가 쓴 코드가 조금씩 트래픽을 받아 100% 까지 배포되는 모습을 지켜본 경험은 아직도 잊히지 않는 개발의 즐거움입니다.