출시 3일만에 앱스토어 2위를 달성한 사이드 프로젝트

2023/08/17에 작성됨 · 읽는데 약 17분 소요

※ 이 글은 발표자료와 프롬프트를 옮겨 작성한 글로, 인프콘 2023 후기와 느낀점 등을 담은 글은 추후 작성할 예정입니다!

인프랩에서 현준님이 디스콰이엇에 작성한 글을 좋게 봐주시고 인프콘 2023에서 발표자로 초청해 주셨습니다.

덕분에 인프콘 2023에서 좋은 기회로 발표를 할 수 있게 되었습니다.

다시 한 번 이 자리를 빌려 발표자로 초청해 주시고 인프콘 2023을 운영/준비해 주신 인프랩 분들, 인프콘 당일 저희 발표 세션을 들어주신 분들 등 모든 분들께 감사의 말씀을 전합니다.

감사한 마음과 그 날의 추억을 기록으로 남기고자 발표 때 사용한 자료와 프롬프트를 공유하고 약간의 코멘트를 달아보고자 합니다.


현준님께서 진행하신 앞부분(비개발 이야기)은 제외하고 제가 발표한 뒷부분(개발 이야기)만 작성했습니다 :)

앞부분(비개발 이야기)은 나중에 인프런이나 인프런 유튜브 채널에 올라오는 영상을 참고해 주세요!


그리고 저희의 발표는 "저희가 이렇게 해서 1,500만원 벌고 앱스토어 2위도 해봤으니 무조건 이렇게 하세요!"가 아닌, "저희는 이렇게 해서 나름 괜찮은 수준의 사이드 프로젝트를 진행했으니 참고해 보세요!"로, 저희의 경험을 공유하는 발표였습니다. 이 점을 꼭 참고해 주세요!




희한한 개발 스택

먼저 저희의 개발 스택을 간략하게 소개해 드릴게요.

저희는 디자인의 경우 Figma로 레이아웃, 일러스트로 각종 아이콘들을 만들어서 기본적인 디자인을 진행했고, 스플라인으로 3D 작업을 진행했어요.

스플라인이라는 것을 처음 들어보신 분들이 계실 것 같은데, 스플라인은 3D 아이콘 작업을 위해 특화된 도구입니다.

디자이너 팀원에게 들은건데, 아직 국내에 많이 알려지지 않았고, 복잡한 3D 모델링을 다루기엔 어려운 도구라고 합니다.


다음으로 서버의 스택을 알아볼게요.

서버는 Flask를 사용했습니다. “아무리 사이드 프로젝트라고 해도 엄연히 ‘서비스’인데, Flask를 사용해도 되나?“라는 의문을 가지시는 분들이 계실 텐데요.

맞습니다. Flask는 요즘 사실 잘 쓰이고 있지 않죠. 정교한 서비스를 위해선 개발자가 설정해줘야 할 것도 너무 많고, 속도도 느린 등 문제가 많기 때문이에요.

그리고 만약 Python을 고집해야 한다면 FastAPI를 사용하는 것이 더 나은 선택지라고 저도 생각합니다.

그런데도 저희가 Flask를 선택한 이유는 일단 저희 팀에 백엔드를 전문적으로 담당하고 있는 개발자 팀원이 없었기 때문이에요.

더군다나 저희는 빠른 시장 진출을 위해 MVP 개발을 진행하고 있었기에 중간에 새로운 팀원을 모셔서 처음부터 새로 시작하는 것이 시간적으로 너무 타격이 컸었어요.

그래서 저의 얕은 지식을 가지고서 제가 백엔드를 개발하고 있었습니다.

저는 백엔드라곤 지금까지 Flask만 몇 번 건들여본 것이 전부였기에 자연스럽게 Flask를 선택하게 되었습니다.

물론 지금은 백엔드 개발자 팀원을 모셔서 Go로 마이그레이션한 상황입니다.

그리고 NoSQL인 MongoDB를 사용했어요. 이것도 상당히 희한하다고 할 수 있는데, 이것과 관련된 이야기는 조금 길어서 이따 뒤에서 더 자세히 다루도록 하겠습니다.

그리고 더 희한하게 클라우드 서버인 AWS 등을 사용하지 않고 홈서버를 사용했는데요, 이는 두가지 이유가 있었습니다.

“수익도 확실치 않은 사이드 프로젝트에서 서비스 어느 곳에서도 과금을 하기 싫다”는 이유와 “빠른 시장 진출을 위해 부족한 시간에 제게 익숙한 홈서버를 이용하자“는게 이유였습니다.


이제 저의 주 분야인 iOS와 Android입니다.

위에서 빠른 시장 진출을 위해 MVP 개발까지 하면서 시간이 촉박했다고 했는데, 왜 좋은 Flutter를 냅두고 iOS와 Android를 따로 개발했냐고 생각하시는 분들이 많이 계실 것 같은데요, 이는 제 욕심 때문이었습니다.

개발 부분 발표의 제목을 보신 분들은 눈치채신 분들도 계신 것 같은데, 저는 사용자 경험을 매우 중요하게 생각하고 있습니다.

그리고 제가 iOS 네이티브 개발자여서 그런진 모르겠는데, Flutter는 아직 iOS에서 완벽하고 안정적이며 네이티브, 즉 Swift만큼의 부드러움을 만족하지 못하고 있다고 생각해요.

이런 저의 생각의 연장선으로 Flutter로 iOS까지 개발하지 않았습니다.

그래서 iOS는 Swift와 애플에서 몇년 전부터 적극적으로 밀어주고 있는 SwiftUI로, Android는 Flutter로 개발을 진행했습니다.


개발 프로세스

일반적인 개발 사이클에서는 기획 후 디자인, 디자인 후 개발, 개발 후 테스트, 즉 QA를 진행하고 이후 문제가 없다면 출시하는 것이 일반적인 프로세스입니다.


하지만 저희는 빠른 시장 진출과 MVP 개발을 위해 기획 후 디자인이 나오면 바로 개발을 시작하고, 모든 기능의 개발된 후 테스트(QA)를 하고 출시하는 방법을 사용했습니다.

그리고 기능 개발을 큰 틀이 아닌 작은 단위로 세분화해 여러 번의 사이클을 겪어 최소한의 MVP가 개발되도록 했습니다.

물론 큰 틀의 앱의 전반적인 기능 기획은 완료된 상태에서 기능을 작은 단위로 나누어 개발했어요.

이러한 개발 프로세스 덕분에 모든 서비스의 기획부터 개발, 앱 심사 후 출시까지 약 3개월만에 빠르게 진행할 수 있었습니다.


문제? 해결! : 믿었던 JSON이..

다음으로는 저희가 난관에 부딛혔다가 해결했던 경험을 공유하고자 합니다.

먼저 JSON에 대해 있던 내용을 소개해 드릴게요.

초반에도 말씀드렸다시피 저는 백엔드 개발자가 아니고, 백엔드의 개발 경력도 매우 적습니다.

그런 저는 매우 소규모 서비스의 백엔드만 개발해 보았기에, 제가 평소에 백엔드를 구성하고 개발할 때 데이터를 JSON으로 저장하고 있었어요.

총 사용자가 1,000명도 되지 않는 소규모의 서비스를 개발하고 운영하고 있다 보니, 데이터를 JSON으로 저장하는 것이 뭐가 문제인지, 심지어는 JSON으로 이트터를 저장하면 안된다는 것 조차 모르고 있었습니다.

그래서 당연히 Skrr도 데이터를 JSON으로 데이터를 저장했죠.

개발 단계와 QA 단계에서는 멀쩡한 줄 알았어요.

그런데 문제는 베타테스트 단계에서 발생했어요.

Skrr의 앱 심사가 통과된 후 서비스를 오픈하기 전에 저희 디미고 학생을 대상으로 베타테스트를 진행했는데, 이 때 문제가 발생했어요.


베타테스트 당시 예상보다 많은 동시 요청으로, 데이터 읽기와 저장을 위해 JSON 파일의 읽기/쓰기 과정에서 먼저 온 A 사용자의 데이터를 저장하고 있는 과정에서 나중에 온 B 사용자의 데이터를 읽으면서 데이터의 뒷부분이 날라가서 덮어쓰기되는 문제가 발생했어요.


코드를 보면서 더 자세하게 설명드려 볼게요.

이건 아주아주 간단하게 요약한 JSON으로, 데이터를 저장할 때 필요한 JSON의 모습입니다.

중앙에 크게 보이는 코드들과 같이 거의 정적, 또는 많지 않게 업데이트되는 정보들과 JSON의 길이에 영향을 주지 않는 고정된 정보들도 있지만,


지금 중앙에 보이시는 코드들과 같이 매우 자주 업데이트되는 앱의 주 기능에 사용되는 데이터를 저장하는 부분도 있습니다.

심지어 위의 코드는 친구를 추가할 때마다 무한 생성되고 JSON이 업데이트되고, 아래 코드는 투표를 받을 때마다 무한 생성되고 업데이트됩니다.

JSON이 매우 자주 업데이트되는 것도 큰 문제이지만, 업데이트 주기보다 JSON의 길이가 길어지는 것이 더 큰 문제입니다.


저희는 이 문제를 위 3개가 문제라고 생각했습니다.

먼저 백엔드 코드에서 JSON “파일”을 읽고 쓰는 과정이 매우 많다는 것, 두번째로는 JSON 파일에 동시성 제어나 쓰기 작업 동기 처리가 되어있지 않았다는 것, 마지막으로는 가장 근본적인 문제인 JSON으로 저장하다 보니 파일이 매우 길어져 읽기/쓰기 속도가 느려진다는 것이었어요.


그래서 아까 A 사용자와 B 사용자로 예를 들며 설명드렸던 것과 같이더 먼저 들어온 요청에 대해 쓰기 처리를 진행하던 도중 다른 요청에서 읽기 작업을 시작해 중간이 끊긴 상태로 저장이 되어버리고, 이 이후로 JSON 파일이 손상되었으니 당연히 모든 요청에 에러가 생기는 상황이 생겨버린 것이죠.


그래서 저희는 JSON으로 구성된 기존의 백엔드 구성과 JSON 방식의 데이터 저장 방식을 변경해야겠다는 생각을 했고, 저희는 저희에게 맞는 저장 방식을 찾기 위한 여정을 떠났습니다.


몽고디비? Firebase 데이터베이스? Mysql? 아니면 기존 JSON을 유지하고 큐와 동시성 제어를 만들어서 데이터가 겹치는 문제를 해결할까?

어떤 방식으로 데이터를 저장할지 고민하던 중, 저희의 서비스 시작일이 다가오는 것과 기존에 만들어둔 백엔드 코드를 다 엎을 순 없다는 판단으로 JSON 구조를 거의 그대로 가져갈 수 있는 방법을 고민했습니다.


결론적으로 저희는 “과금 없이” 이용할 수 있고, “기존의 JSON 구조를 최대한 유지”하며, 관계형으로 연결되지 않아 SQL 구문을 사용하지 않아도 되는 “NoSQL DB”.

세 조건을 모두 만족하는 동시에, 제가 이전에 친숙하게 사용해 보았던 몽고디비를 선택했습니다.

JSON을 쓰듯이 사용할 수 있는 몽고디비를 사용하므로써 기존의 문제를 해결하고, 더 빠르고 안정적인 서비스를 유저에게 제공할 수 있도록 했습니다.

이게 아까 초반에 말씀드린 mongoDB를 사용하게 된 이유입니다.


문제? 해결! : 더 Speedy하게

다음으로는 두 번째 문제 해결 경험입니다.

앞에서 말씀드렸다시피 저는 사용자 경험을 매우 중요시하는데, 그 사용자 경험을 위해 서비스 속도를 향상시킨 경험을 소개해 드릴게요.

저희가 서비스를 정식으로 시작하고 현준님의 현란한 마케팅 실력과 많은 분들의 사랑으로 저희 Skrr이 앱스토어 2위에까지 오르며 많은 인기를 얻고 있었을 때, 이 유저들을 잃을 뻔 한 문제가 발생했어요.


저희의 API 모니터링 시스템에서 속도가 느려졌다는 경고가 발생했어요.

심지어는 서버의 업타임을 확인하는 시스템에서 TimeOut이 될 정도로 딜레이돼서 “서버가 죽었다”고 알림을 보낼 정도였어요.

투표받은 데이터를 로드하는 API는 거의 10초가 걸리고, 로그인 API는 86초 이상, 유저데이터를 로드 API는 56초 이상, 심지어 유저가 투표를 했을 때 투표를 처리하는 API는 무려 100초 이상이 걸렸어요.

UX, 즉 사용자 경험은 최악이 되었고, 앱 리뷰와 고객센터에는 "왜이렇게 앱이 느리냐”, “앱 최적화가 개판이다” 등의 안좋은 리뷰가 가득했어요.


저희는 이 문제에 대해 빠르게 인지하고 있었고, 문제 해결을 위해 최선을 다했습니다.

서버 처리 구조, 즉 백엔드의 구조를 개선하고, API 요청을 분산 처리하고, Return JSON의 구조를 개선하여 속도를 향상시키고자 했어요.


먼저 서버 처리 구조의 개선과 API 요청의 분산 처리를 위해 저희가 한 작업들을 알아보겠습니다.


초반에 말씀드렸다시피 저희는 홈서버 1대를 이용하여 서비스를 운영하고 있었습니다.

서비스가 이정도로 많은 사랑을 받을줄 모르고 홈서버 1대로 모든 요청을 처리하고 있었죠.

그런데 너무나도 서비스가 밀리는 문제가 발생했고, 저희는 저희 나름대로의 원인을 분석하기 시작했습니다.


먼저 서버 성능에 대해 의심했어요. 그런데 서버 성능이 이렇게 처리가 밀릴 만큼 낮은 것은 아니라는 결론이 나왔어요. Skrr의 서비스 처리가 밀릴 동안 이 서버에서 운영 중이던 다른 서비스의 처리는 너무나 멀쩡했기 때문이죠.

다음으로는 네트워크 성능에 대해서도 의심했어요. 그런데 이 역시 서버 성능과 마찬가지로 동일 네트워크의 다른 서비스는 너무나 멀쩡했기에 네트워크도 문제가 아니라는 결론을 냈습니다.

설정을 무언가 잘못 해둔 것이 아닐까? 하는 의심도 해보았어요. 이건 주관적인 결론이었지만, 설정의 문제도 아니라고 생각했습니다.

코드의 비효율성도 의심했지만, 이것도 주관적인 관점에서 문제가 없다고 생각했어요.

저희는 왜 처리가 밀리는지 미궁으로 빠졌고, 저희 학교의 다른 개발자 친구들에게 조언을 구하며 나름의 원인을 찾게 되었습니다.


문제가 되는 부분은 세가지로 추려졌어요.

서비스의 로그를 남기는 부분과 코드 내에서 비효율적인 부분, 그리고 서버 내 프로세스 1개에서 모든 요청을 수용하고 처리한다는 부분이었어요.

각각의 문제에 대해 더 자세한 원인을 알아보고 저희가 해결한 방법, 그리고 해결한 후의 성과에 대해서 자세하게 알아보겠습니다.


먼저 서비스 로그를 남기는 부분입니다.

저희는 기존에 모든 요청과 모든 부분에서 로그를 남기고 있었습니다. 파일에 별도로 남기고 있진 않았고 기본 콘솔에 남고 있었지만, 테스트를 해보니 기본 콘솔에 남기는 것 만으로도 정말 많은 속도 감소가 있더라구요.

그래서 저희는 따로 로그를 남기는 코드를 짜고 싶진 않으니, 기본적으로 콘솔에 남는 로그를 끄고 일부 중요한 로그만 콘솔에 남기기로 했습니다.

측정한 속도가 없어 정확하진 않으나, 이를 통해서 2배 이상의 엄청난 성능 향상이 있었습니다.


다음으로는 코드에서 비효율적인 부분입니다.

처음 저희가 자체적으로 원인을 분석했을 때에는 비효율적인 코드가 없다고 판단했는데, 이후 저희 학교의 다른 개발자 친구들이 코드에서 비효율적인 부분이 있다고 찾아주었어요.

하지만 이 부분은 너무 세세한 부분이고, 코드에서 작은 부분의 수정이 있었을 뿐, 큰 수정이 없었기에 별도로 자세하게 소개하진 않겠습니다.

이 수정을 통해 성능 향상은 물론 있었겠지만, 코드에서 워낙 작은 부수적인 기능의 수정이어서 그런지 성능 향상이 드라마틱하게 느껴지진 않더라구요.


마지막으로 홈서버 1대에서 프로세스 1개로 모든 요청을 수용하고 처리하는 부분입니다.

저희는 홈서버 1대에서 1개의 프로세스를 이용해서 모든 요청을 수용하고 처리하고 있었습니다. 더군다나 비동기 처리도 되어있지 않아 모든 요청이 순차적으로 처리되고 있었기에 처리 속도가 너무나도 느렸죠.

이것이 API 응답 속도에 가장 큰 영향을 미치는 요소였어요.

그래서 저희는 프록시 프로세스를 두고 밀림 정도에 따라 유동적으로 서비스 프로세스를 실행시켜 프록시 프로세스에서 각 프로세스로 분산 처리하려고 했으나..

프록시 방법을 모색하고 개발하던 중 API의 요청이 적어져 쓸모없게 되었고, 그래서 실 서비스에는 반영되지 않았습니다. 되게 열심히 프록시를 개발했는데, 상당히 아쉬웠습니다.


이제 다음으로는 Return JSON 구조의 개선를 위해 저희가 한 작업들을 알아보겠습니다.


기존의 Return JSON 구조에 대해 먼저 간략하게 알려드릴게요.

예를 들어 유저데이터를 얻어오는 API를 요청한 경우, 기존에는 모든 유저데이터를 Return했어요.

그런데 아까 보셨다시피 친구의 경우에도, 받은 투표의 경우에도 유저데이터 JSON을 무한히 생성하여 길게 만드는 요인이었죠. 그래서 JSON이 너무나 길어지는 문제가 발생했습니다.

당연히 Return되는 JSON에도 모든 유저데이터를 Return하고 있다 보니, Return되는 JSON 역시 너무나 길어지고 있었습니다.


그래서 저희가 한 번 얼마나 느려지고 있는지 속도를 측정해보기 위해 지금까지 사용 중인 유저를 대상으로 한 유저 당 유저데이터를 JSON에서 몇 줄 정도 소비하고 있는지 확인해 봤어요.

적은 경우인 투표 10개와 친구 10명일 때에는 400줄로 이상적인 수치였지만, 일반적인 경우인 투표 300개와 친구 50명까지만 가더라도 5,000줄로 소비하고 있는 줄이 많았습니다.

더 나아가서 인싸라고 할 수 있는 많은 경우인 투표 2,000개와 친구 300명일 때에는 30,000줄 이상으로, 너무 과하게 많은 줄을 소비하고 있었죠.

물론 이 유저데이터 전체를 Return하고 있었기에, Return되는 JSON의 줄 양도 동일했습니다.


이후 저희는 이렇게 모든 유저데이터를 Return하면 절대로 속도를 향상시킬 수 없다고 판단했고, 아까 보신 ‘친구와 받은 투표 등’ 무한히 생성되어 많은 양을 차지하지만 이 EndPoint의 요청에서 필요로 하지 않는 정보들은 제외하고 Return하기로 했습니다.

이에 따라 아까 400줄이었던 적은 경우는 280줄로, 5,000줄이었던 일반적인 경우는 1,400줄로, 30,000줄이 넘던 많은 경우는 6,000줄로 줄여 엄청나게 많은 시간을 단축시켰습니다.

백엔드를 개발하면서 이같은 처리는 당연한 것이라고 생각하실 수 있고, 지금의 저도 당연한 것이라고 생각합니다.

하지만 백엔드를 개발해 본 적이 없던 예전의 저는 이와 같은 처리를 생각하지 못했었습니다.


많은 경우를 기준으로 기존에는 단순히 유저데이터를 받아오는 API에서도 1,800ms가 넘게 걸리던 것을 이 작업 이후 기존 대비 53% 감소한 800ms로 속도를 향상시킬 수 있었습니다.

이렇게 저희는 속도를 개선하므로써 사용자 경험을 많이 높일 수 있었으며, 실제로도 보여드린 작업들 이후 속도가 느리다는 등의 앱 리뷰와 고객센터 문의가 0에 수렴하게 감소했습니다.


아직은 작지만, 언젠가 커질 우리들

저희의 경험 공유가 여러분께 많은 도움이 되셨길 바라며, 이상으로 저희의 경험 공유는 마무리하겠습니다.

아직은 능력도 부족하고 경력도 짧은 저희지만, 언젠가 시니어 개발자로 성장할 그 날을 기대하며, 모든 분들의 성장을 응원하겠습니다!


Hits


본 Note를 공유하시려면?

여기를 눌러 링크 복사