온라인 트리를 만든 산타들의 이야기

온라인 트리를 만든 산타들의 이야기

의 기적,
117만 동시 접속을 견딘 비결은?

#개발자 #토이프로젝트 #백엔드

'내 트리를 꾸며줘' 홈페이지 (산타파이브)

작년 연말 SNS를 뜨겁게 달궜던
'내 트리를 꾸며줘'를 기억하시나요?

2021년 12월 20일에 오픈해서 2022년 1월 10일에 종료된 이 서비스는 계정 주인의 지인들이 트리에 메시지를 남기면, 크리스마스 자정에 한 번에 공개되는 익명 메시지 서비스입니다.
'내 트리를 꾸며줘'는 250만 개의 계정, 3700만 개의 선물 수, 117만명의 동시 접속자라는 엄청난 성장 및 성과와 함께 마무리되었죠.

2022년 2월, 인프런엔 이 프로젝트에 참여한 산타파이브의 개발자 이예찬님 강현우님이 찾아오셨어요.
유니크굿컴퍼니의 백엔드 개발자인 두 분이 '내 트리를 꾸며줘'라는 토이 프로젝트의 진행과 117만 동시 접속을 견뎌낸 노하우를 들려주셨답니다.

이번 <주간 인프런>에는 두 분의 이야기를 담아보려고 해요.
2시간 가까이 진행된 열정적인 배움과 나눔의 현장을 여러분께 공유할게요.

😮 스크롤 압박 주의! 😮

주간 인프런 #45 목차 🌿

본 강연 1 (이예찬 님)

  • 기술 선택 배경
    • Azure App Service
    • Cosmos DB
    • Azure Static Web Apps
    • CDN과 스토리지 서비스
  • 수많은 문제 발생
    • 클라이언트가 뜨지 않는다
    • API 호출이 안 된다
    • API 호출이 느리다
    • 데이터베이스가 작동하지 않는다
    • CDN 비용이 높다
  • 25일 어떡하지?
    • 사용자 인증

본 강연 2 (강현우 님)

  • 또 다른 문제
    • 로그인과 회원가입
    • 중복 트리
  • 한 가지 실험 - 로드 테스팅

인프런 개발 파트와의 Q&A (이예찬 · 강현우 님)

이예찬

반갑습니다. 산타파이브의 이예찬입니다. 유니크굿컴퍼니라는 회사에서 리얼 월드라는 앱을 서비스하고 있습니다. 저희가 크게 두 파트로 나눠서 발표를 진행할 건데요, 저는 처음에 프로젝트를 시작할 때부터 오픈하기 전, 오픈할 때까지 혹은 오픈하고 나서 트래픽이 몰렸을 때 어떻게 대응했는지 설명드릴 예정입니다.

강현우

안녕하세요. 저는 유니크굿컴퍼니에서 예찬님 바로 옆 자리에 앉아 백엔드 개발을 하고 있는 강현우라고 합니다. 전 23일부터 프로젝트에 참여를 했는데요, 참여하고 보니 여러 가지 이슈들이 있었어요. 데이터베이스 중복과 같은 문제를 어떻게 해결하고 마이그레이션을 했는지에 대해서 설명을 해보도록 하겠습니다.

본 강연 1 (이예찬 님) 🗂️

기술 선택 배경

이 서비스를 만들 때 저희가 어떤 기술을 왜 선택했는지 서비스를 만들기 시작한 제 입장에서 설명을 드릴게요.

일단 아이디어가 있었기 때문에 빨리 만들고 싶었어요. 제가 아이디어가 생기면 뭔가 해봐야 직성이 풀리는 스타일이거든요. 당시에는 백엔드가 저 혼자였기 때문에 하고 싶은 대로 백엔드를 구성했었죠.

그래서 저한테 익숙한 C#이나, C#하면 따라오는 ASP.NET Core, 그리고 거기에 ORM으로 Entity Framework, 이걸 호스팅하기 위한 Azure App Service를 썼어요. 그리고 데이터베이스로는 제대로 안 써본 기술을 써보자 해가지고 Azure Cosmos DB라는 AWS의 Dynamo DB랑 비슷한 데이터베이스를 사용하게 되었습니다. 프론트엔드 같은 경우는 Azure Static Web Apps라는 정적 사이트를 호스팅하는 서비스를 활용해서 프론트엔드 서버를 구성했습니다. 

Azure App Service

Azure App Service는 PaaS(Platform as a Serviece) 서비스인데 이걸 왜 선택했는지 말씀드릴게요. 일단 PaaS 서비스이기 때문에 설정 같은 게 별로 안 복잡했어요. 아파치(Apache)나 Nginx, 윈도의 경우 IIS 같은 것들도 별로 설정을 할 필요가 없었고요. 배포도 굉장히 편리했습니다. 그냥 코드만 깃 저장소에 푸시하면 빌드 후에 배포가 되는 그런 시스템이었어요. 트래픽 같은 경우도 원터치로 스케일업 하고 스케일아웃이라거나 오토 스케일링도 다 되는 방식이었어요. Application Insights라는 걸로 로그 분석도 굉장히 편리하게 돼요. 요청이 들어왔을 때 어느 시점부터 데이터베이스 호출이 들어가서 어느 시점에서 그 호출이 끝났고, 요청하는 데 걸린 총 시간은 얼마였는지 등을 상세하게 분석했습니다. 

Cosmos DB

데이터베이스로는 Cosmos DB를 사용했는데요. Cosmos DB는 DB as a Service라고 보시면 돼요. 이걸 사용했던 이유는 세팅이라든지 스케일링도 알아서 해주기 때문에 아무 고민이 필요 없다고 생각했어요. 그리고 저희가 만드는 서비스 자체가 관계가 복잡하지 않았어요. 사용자 계정, 그 계정에 딸린 선물 수, 선물 이렇게만 있었거든요. 또 정합성이 크게 중요하지도 않아서 NoSQL 데이터베이스를 사용했습니다. 그래서 코스모스 데이터베이스를 메인 DB로 사용해서 받은 선물 정보랑 사용자 정보를 저장했었어요. 

Asure Cosmos DB는 Microsoft의 독점적인 다중 모델 데이터베이스 서비스입니다. (홈페이지)

Azure Static Web Apps

그 다음에 Azure Static Web Apps라는 게 있어요. 이거는 프론트엔드 파일을 제공하기 위해 사용했어요. 깃허브 액션을 통해 프론트엔드 소스를 자동으로 빌드해서 자동 배포해 주고, 풀 리퀘스트가 생성되면 그 풀 리퀘스트에 맞는 스테이징 환경이 생성돼서 풀 리퀘스트 프리뷰가 가능해요. 그래서 웹사이트를 호스팅하기에 좋은 환경이었어요.

CDN과 스토리지 서비스

그 다음 사용했던 게 CDN이랑 스토리지 서비스였는데요. CDN은 빠른 속도로 이미지를 제공하는 서비스니까 당연히 써야겠다는 생각으로, 큰 고민 없이 도입했어요. 프론트에는 소스 파일이랑 리소스랑 분리하고 싶다는 생각이 들어서 이렇게 진행했어요. 실제로 이걸 사용하니까 이미지가 굉장히 빠르게 떴어요. 트리를 띄웠을 때 트리에 있는 오너먼트들이 바로 뜨는 장점이 있었죠. 이렇게 시작됐습니다.


수많은 문제 발생

근데 하루만에 20만 명이 가입을 하면서 트래픽이 굉장히 많이 몰렸거든요. 그래서 이런 구조만으로 해결할 수 없는 수많은 문제들이 발생했어요.

1) 클라이언트가 뜨지 않는다

클라이언트가 안 뜬다는 건 정적 웹사이트가 요청을 못 받아주는 상황이에요. '429 Too Many Request'가 뜨는 상황이었어요. 왜 그런가 했더니 Azure Static Web Apps에서 문제가 발생하고 있었죠. 

그래서 API 서버 호출 수를 봤더니 Static Web Apps가 살아있을 때는 호출 수가 증가했다가 앱스가 꺼졌을 때는 줄었다가 해요. 1분마다 트래픽이 초기화되는 상황이더라고요. 나중에 기술지원에 문의하니까 이걸 쓸 거면 엔터프라이즈급 엣지라는 걸 활용해서 트래픽을 처리해야 된다고 말씀하시더라고요. 근데 굳이 엔터프라이즈 급으로 해야 될까? 정적 웹사이트니까요. 정적 파일만 서빙하면 되는 부분이기 때문에 의아해서 제가 트위터에서 여쭤봤었어요.

되게 감사하신 분께서 클라우드플레어 페이지스(Cloudflare Pages)라는 서비스를 추천을 해주셨어요. 이거는 Azure Static Web Apps에서 제공하는 기능을 다 제공해줘요. 프리뷰 기능도 있고 자동으로 빌드해서 배포하는데, 공짜예요. 트래픽이 무제한이 됐어요. 아무튼 이걸로 이사를 가게 됐는데 이걸 쓰려면 DNS 변경이 필요해요. 처음에는 DNS 레코드 자체가 Azure Static Web Apps로 설정이 되어 있었는데, 일반적인 경우에는 레코드 변경만으로 처리가 가능하거든요. 근데 클라우드플레어 페이지스 같은 경우는 자체 DNS를 꼭 써야 되더라고요. 그래서 이 DNS 네임 서버 자체를 변경하느라 시간이 많이 걸리긴 했어요. 그런 소소한 문제도 있었는데, 결과적으로 클라우드 플레어 페이지스를 조합한 아키텍처가 구성되었습니다. 

한 트위터 이용자분이 예찬님께 추천해주신 클라우드플레어 페이지스(Cloudflare Pages).


2) API 호출이 안 된다

이건 클라이언트와 앱 서비스 사이에서 발생했던 문제예요. '503 Serviece Unavailable' 메시지가 떴어요. 제 생각에는, 앱 서비스도 스케일 아웃이 되잖아요. 스케일 아웃이 된다는 건 앞에 로드 밸런서가 붙어 있다는 거고 그 로드 밸런서가 트래픽을 분산해주잖아요. 그런데 분산된 트래픽을 받는 쪽에서 죽어 있어서 못 받았다는 생각이 들었어요. 인스턴스마다 갖고 있는 트래픽 양을 보면 어떤 건 살아 있고 어떤 건 죽어 있고. 과도한 요청으로 인해 프로세스가 죽었을 때 복구가 기대보다 느린 것 같다고 생각했습니다.

그래서 PaaS인 App Service 대신에 Function as a Service인 Azure Functions을 사용해 보자고 생각했어요. 왜냐하면 조금 더 관리가 잘 될 거라는 추측이 있었거든요. 그래서 Azure Functions이라는, 어떻게 보면 AWS 람다(Lambda)랑 비슷한 걸 사용하게 됐습니다. 이것도 마찬가지로 사용하는 데 별 고민이 필요 없었어요. 세팅도 알아서 되고 스케일링도 알아서 되기 때문에 저희는 코드만 배포하는 되는 상황이었습니다. 이걸로 변경하는 게 프로세스가 죽어서 트래픽 감당이 안 되는 것보단 낫겠다는 생각으로 넘어갔어요. 저희가 ASP.NET을 사용하는데 그 ASP.NET 코드를 거의 그대로 사용하면서 넘어갈 수 있었기 때문에 더 쉽게 넘어갈 수 있었습니다. 그래서 App Service가 있던 자리가 Functions으로 교체가 된 구조가 되었습니다.

3) API 호출이 느리다

저희가 데이터베이스를 어떻게 보면 두 개의 컬렉션을 사용하고 있었어요. 사용자 컬렉션이랑 그 사용자에게 달린 선물 목록 컬렉션이 따로 있었어요. 이건 관계형 데이터베이스가 아니기 때문에 조인을 해서 가져온다거나 그런 것들이 불가능했었고, 무조건 DB콜을 2번 이상 해야 되는 그런 상황이었어요. 레이턴시(latency) 같은 걸 조금이라도 줄이기 위해 캐시를 하나 도입을 하게 됐습니다. 레디스 캐시(Redis Cache)를 도입해서 조합된 데이터를 레디스 캐시에 저장했고, 그 이후부터는 캐시에서 바로 데이터를 가져와서 Response로 넘겨주는 형식을 취하게 되었습니다. 솔직히 말하면 이게 어느 정도나 효과가 있었는지는 잘 모르겠어요.

캐시를 도입하는 이유는 서비스 이용자가 증가하면 DB만으로는 부하를 견딜 수 없다고 생각하기 때문이죠. 레디스(Redis)는 키-값 기반의 인-메모리 데이터 저장소예요. (redis 홈페이지)


4) 데이터베이스가 작동하지 않는다

이건 약간 크리티컬한 문제였는데요. Cosmos DB가 세팅도 알아서 스케일링도 알아서 해준다는 게 알고 보니 스케일링을 어느 정도까지만 알아서 해주는 거더라고요.

이걸 설명드리기 위해서는 Request Unit(RU)이라는 단위에 대해서 설명을 드려야 될 것 같아요. 이게 쿼리 작업에 들어가는 비용을 나타내는 표준화된 단위라고 보시면 돼요. CPU나 메모리 사용량, 디스크 사용량 이런 것들에 대해 서버상 어느 정도나 소모하는지를 정규화해서 나타낸 걸 Request Unit이라고 표현하더라고요.

Cosmos DB에 크게 두 가지 용량 모드가 있어요. 프로비전된 용량 모드랑 서버리스 용량 모드인데요. 서버리스 방식 같은 경우는 문서를 쭉 읽었을 때는 자동 스케일링이 된대요. 오토 스케일링 프로비전드 같은 경우는 저희가 최대 RU를 설정하면 그 안에서 자유롭게 스케일링이 된다고 되어 있었어요. 그럼 서버리스(serverless)를 써도 되겠지 생각하고 서버리스를 사용했습니다. 근데 알고 보니까 서버리스 같은 경우는 최대 5000RU까지만 설정이 가능하다는 제약이 있었어요. 저희가 실제로 100만 RU까지 설정을 했었거든요. 근데 그거의 발끝에도 못 미치는 5000RU까지가 최대 한도였으니까 치명적이었죠.

그래서 서버리스에서 오토 스케일링 프로비전드 방식으로 전환을 해야 되는 상황이었습니다. 하지만 이거는 GUI나 콘솔을 통해서 쉽게 변환이 안 되더라고요. 이사(Migration)를 가야 되는 상황이었습니다. 그래서 ‘서버가 이사 중이에요’라는 메시지를 띄우고 이사를 갔어요.

'문제 발생 - 이사' 과정의 연속. 좋은 서비스를 위한 끈기.

이사 가는 도중에는 Azure에서 제공해 주는 데이터 팩토리라는 기능을 썼어요. 소스와 데스티네이션만 설정해 주면, Request Unit에 대한 조율 같은 걸 알아서 해주고 데이터를 다 옮겨주더라고요. 그래서 이걸 이용해서 데이터를 하나 옮기고, 한 2시간 정도 텀으로 데이터를 옮겨서 복구를 했습니다. 그렇게 프로비전 방식으로 전환했습니다.

5) CDN 비용이 높다

CDN 비용이 너무 비싸다는 문제도 있었어요. 한 사흘 정도 만에 갑자기 몇 백만 원의 CDN 비용이 발생한 거예요. 'PNG 이미지를 WebP로 바꾸면 사이즈도 많이 줄어드니까 WebP로 바꿔야 하나? CDN을 떼고 스토리지만 쓸까?' 이런 고민을 하다가 그냥 클라우드플레어 페이지스에 똑같이 이미지를 올리면 되겠다는 생각이 들더라고요. 어떻게 보면 길을 많이 돌아온 거죠. 그래서 별도로 애셋(asset)용 리포지토리를 파고 이걸 그대로 써서 배포를 했습니다. 클라우드플레어 페이지스 같은 경우는 파일 하나당 25메가바이트까지 사용할 수 있더라고요. 그렇게 이것도 이사를 가게 되었습니다. 이 정도 준비를 하고 나니까 25일(크리스마스 당일)이 좀 걱정되더라고요.


25일 어떡하지?

25일 자정에 분명히 많은 트래픽이 몰릴 텐데 펑션을 배포하려니까 너무 느리더라고요. 왜 느린가 생각을 해봤어요. 펑션 같은 경우도 스테이징 서버가 따로 있어서 그 스테이징 슬롯에다가 배포를 하고 이걸 교환하는 작업이 가능해요. 근데 이걸 감안하더라도 너무 느리더라고요. 이미 많은 API 콜이 발생하고 있어서 그런 것 같았어요. 그리고 펑션이 일단 하나만 들어가고 있었는데 '이걸로 감당이 안 되면 어떡할까'라는 걱정도 있었어요. 그래서 펑션 여러 대로 트래픽을 분산하자는 생각을 했습니다. 

사용자 인증

25일 자정 전에 기획이 한 번 바뀌었어요. 처음에는 메시지를 전체 공개할 생각이었어요. 메시지를 전체 공개할 거면 사용자 인증도 필요가 없겠죠. 처음에 로그인이 필요했던 거는 로그인된 사용자의 트리로 리디렉션 시켜주는 역할, 그리고 내가 내 트리의 글을 쓰지 못하게 하는 역할이 다였어요. 그렇기 때문에 구체적인 인증 처리가 불필요했어요. 근데 CS가 많이 들어왔었어요. '내가 여기다 중요한 메시지를 적어놨는데 전체 공개되면 나 정말 죽는다' 이런 식으로 많은 분들이 문의를 남겨주셔서 메시지는 트리 주인만 보이게 바꿨고, 그렇게 되면 당연히 사용자 인증이 필요하겠죠.

그래서 사용자 인증을 하기 위한 Access Token으로 JWT를 사용했어요. 이거를 쭉 바꾸고 GitHub에 빌드해서 올리면 GitHub에서 GitHub Actions로 빌드가 되고 스테이징 슬롯으로 자동 배포가 됩니다. 그리고 스테이징과 프로덕션 교환이 일어나는 배포가 이루어지게 되는데요. 일단 GitHub에서 빌드하는 게 로컬에서 빌드하는 것보다 당연히 느릴 수밖에 없어요. 그리고 스테이징 슬롯으로 자동 배포하는 것도 이미 트래픽이 많이 돌아가고 있는 상황에서 배포를 하려다 보니까 오류가 생기더라고요. 그래서 일단 로컬에서 빌드하고 바로 스테이징 슬롯으로 배포하는 걸로 바꿨어요.

근데 스테이징 슬롯으로 수동 배포하는 것도 굉장히 느리거나 오류가 발생하더라고요. 고민해보니까 펑션에 있는 로드 밸런서만으로 부족하다는 생각이 들었어요. 그래서 Azure Front Door라는 L7 Load Balancer를 사용하게 되었습니다. 이거는 직접 로드 밸런싱을 조절할 수 있어요. 예를 들어 특정 서버에만 트래픽을 몰아준다든지, 가장 응답 속도가 빠른 쪽으로 몰아준다든지. 또 웹 애플리케이션 방화벽이 내장돼 있다고 하니까 악성 트래픽도 거를 수 있지 않을까 해서 사용하게 되었습니다. 그래서 결과적으로 원래 펑션 한 대만 있던 거를 앞에 프론트 도어가 펑션 두 대의 로드 밸런싱을 하는 구조로 변경이 되었습니다.

Azure Front Door (이미지 출처: Microsoft)

로컬에서 빌드해서 트래픽을 펑션 2로 집중을 시키고, 펑션 1에다가 수동 배포를 진행했습니다. 그리고 다시 트래픽을 펑션 1로 집중을 시키고 펑션 2로 수동 배포를 진행했었고요. 그 다음에 다시 트래픽 정상화를 하는 구조로 하니까 배포할 때 오류도 훨씬 줄고 속도도 더 빨라졌어요. 결국에는 25일 자정에 초당 4만 리퀘스트까지 버티면서 잘 살아남았습니다.

본 강연 2 (강현우 님) 🗂️

예찬 님께서 인프라에 대한 부분들을 세팅하고 서버 코드 준비를 다 해주셨다면, 저는 23일 정도에 들어와서 여러 가지 문제들을 발견하고 해결했습니다. 총 3가지 문제가 더 있었어요. 사실 문제라고 생각되는 거는 두 가지고 실험적으로 진행했던 한 가지 내용을 설명드리려고 해요.


또 다른 문제

1) 로그인과 회원가입

일단 첫 번째는 성능 이슈 내 로그인과 회원가입인데요. 이게 문제인 이유는 엄청나게 느려요. 회원가입과 로그인을 하는 데 2분이 걸릴 때도 있었어요. 그런데 저희가 사용 중이었던 API들이 그렇게 큰 볼륨이 아니었어요. 회원가입, 로그인, 메시지 작성, 트리 조회. 문제가 발생했을 당시 트리 조회에서는 메시지를 열람할 수 있는 구조는 아니고 트리에 메시지를 단 사람이랑 이미지 정보만 조회할 수 있는 수준의 API였어요. 근데 여기서 영향을 받은 API는 2개(회원가입, 로그인)였어요. 그러니까 거의 반절이 영향을 받은 거죠. 

왜 그랬냐고 얘기하기 전에 그때 서버가 어떻게 돼 있었는지에 대해서 얘기를 해보려고 해요. 클라이언트 바로 뒤에 서버가 있고 캐시가 있고, 그 뒤에 사용자 컬렉션과 기프트 컬렉션이 따로 있는 상태였어요. 사용자 컬렉션 같은 경우에는 사용자의 회원 정보를 담고 있었고, 트리 컬렉션 혹은 기프트 컬렉션이라고 하는 부분이 트리에 있는 메시지를 조회하거나 작성하는 것들을 담당하고 있었어요.

저희가 메인 데이터베이스로 Cosmos DB를 쓰고 있었는데 이 Cosmos DB에는 여러 가지 API들을 지원을 하고 있어요. 저희는 Cosmos DB For SQL이라는 데이터베이스를 쓰고 있었는데, 데이터를 내부적으로 NoSQL, 그러니까 도큐먼트 형식으로 저장하고 있어요. 다만, 쿼리 언어를 다른 관계형 데이터베이스에서 쓸 수 있게 함으로써 For SQL이라고 이름이 붙은 것 같아요. 데이터를 저장하는 방식은 NoSQL, 데이터를 조회하는 부분은 SQL 쿼리 언어를 채택하고 있어서 두 방식의 장점을 가지고 있는 데이터베이스라고 할 수 있어요. 

근데 SQL 데이터베이스하고 가장 다른 점이 뭐냐면 Constraint라고 하는 제약 사항을 설정하기가 까다로워요. 초기에 설정을 한 번 하면 끝이에요. 새로 만들려면 데이터베이스를 따로 파고, 기존 데이터베이스에서 데이터를 따로 마이그레이션 해야 되는 제약이 있어요.

그 다음에 Cosmos DB만의 특별한 트리거(Trigger)라는 개념이 존재하는데,  이게 문제의 핵심이 될 거예요. 트리거는 프리(Pre) 트리거와 포스트(Post) 트리거로 나누어져 있고, 개발자는 이 트리거라는 함수들을 자바스크립트로 작성하게 되어 있어요(Javascript API). Cosmos DB의 뭔가 특별한 API가 더 있는 것 같았어요. 그리고 이 트리거는 Transaction 단위로 되어 있는데 만약에 트리거가 성공하면 데이터베이스에 대한 액션을 commit하고, 트랜잭션에 실패하면 Rollback을 하는 특별한 기능입니다. 뭐가 됐든 데이터베이스 액션이 실행되기 전에 프리 트리거가 실행되고, 데이터베이스 액션이 실행된 후에 포스트 트리거가 실행됩니다. 3가지 단계가 하나의 트랜잭션 단위로 묶이는 거죠. 

저희는 Unique Constraint를 중간에 설정하는 것이 힘들었고, 사용자 계정을 Cosmos DB 컬렉션에 담고 있었어요. 계정 중복 체크 로직 등을 Post Trigger에 적용하는 부분을 이미 예찬 님께서 진행하신 상태였습니다. 말 그대로 중복 계정이 생성되는 것을 막기 위함이었어요. 즉, 데이터가 중복되어 저장된 경우에는 Post Trigger에서 Transaction을 Revert하는 형식으로 실패하게 만들어서 쓰고 있었습니다.

트리거(Trigger)는 어떤 테이블에 DML 문이 수행됐을 때, 데이터베이스에서 자동으로 동작하도록 작성된 프로그램이에요. '자동'이라는 게 가장 큰 특징이죠.

저희 실제 사용자 회원 가입 요청 모델은 정말 단순했는데요, ID/NickName/Password만 있다고 가정하고 흐름을 따라가 볼게요. 첫 번째로 클라이언트 측에서 회원가입 요청을 보냅니다. 그 다음에 서버에서는 요청 모델의 유효성 검사를 진행합니다. 흔히 닉네임이나 비밀번호가 최소 요구사항을 만족했는지에 대한 검사를 진행해요. 현재 가입하려고 하는 사용자가 데이터베이스에 없는 상태라면 중복된 사용자가 아니라고 서버에서는 판단을 하게 됩니다. 그 이후에 서버는 데이터베이스에 데이터를 넘겨주는데요, 당시 Pre Trigger는 따로 설정하지 않았기 때문에 Pre Trigger에 대한 엑션은 No-Op으로 처리되어서 그냥 넘어가게 될 것입니다. 그 다음 데이터베이스 액션(저장)을 수행하고 Post Trigger가 실행됩니다. 저희가 설정한 Post Trigger는 단순히 저장된 사용자 데이터가 2개 이상일 경우에 에러를 내게 하는 방식의 Trigger지만, 정상적인 가입을 가정하면 Post Trigger 또한 성공적으로 완료될 것입니다. 즉, 사용자 데이터가 잘 저장될 것입니다. 

근데 이상하게도 일련의 과정들 전체가 다 느린 거예요. 심지어 이때가 25일 자정이 24시간도 안 남았을 때였어요. 이걸 일일이 엔드 투 엔드(end-to-end) 트랜잭션으로 분석하든가 테스트를 거쳐서 손으로 찾는 방법도 있었는데 이런 것들은 시간이 안 나더라고요. 근데 또 재미있었던 거는 트리 메시지를 불러오는 것도 Cosmos DB를 사용했는데 그 부분은 되게 빨랐거든요. 캐시의 영향도 있겠지만 왜 사용자만 느릴까 궁금했는데 생각할 시간이 없었어요.

🎄

그래서 저희가 과감하게 “회사에서도 많이 쓰고 있는 관계형 데이터베이스로 이동하자”라고 결론을 내렸어요. 그래서 서버를 중단하지 않고 데이터를 마이그레이션하는 계획을 세우고 진행했는데 사실 그때 제가 왜 그 생각을 했는지 잘 모르겠어요.

우선 무중단 마이그레이션이 뭐냐면 ‘서비스 상태에 영향이 없는 데이터 이동’이라고 정의를 할 수 있는데 이게 말이 쉽지만 개발적으로 고민해야 되는 것들은 여러 가지가 있었어요.

일단 데이터를 복사하는 도중에 이벤트가 발생할 거예요. 사용자가 새로 가입한다든지, 데이터를 읽는다든지 이런 것들. 데이터를 복사하는 와중에 발생한 이벤트를 어떻게 데이터 consistency를 지킬 수 있는가. 그래서 이 마이그레이션을 복사할 때 사용되는 오리진과 그 복사의 대상이 되는 데스티네이션을 어떻게 활용할 것인가를 많이 고민했어요. 그래서 일단 '오리진과 데스티네이션을 동시에 사용하면서 이벤트를 적절히 처리하면 일관성 유지가 가능하지 않을까'라고 저희만의 가설을 세웠어요.

서비스를 중단하지 않고 DB를 옮기는 무중단 마이그레이션. 

만약 마이그레이션 도중에 마이그레이션 Origin(Cosmos DB)에 계정 생성 요청이 들어왔다고 가정을 해볼게요. 만약 아무 처리도 하지 않았다면 최종 목적지인 ‘SQL DB’가 아니라, 더 이상 쓰지 않을 ‘Cosmos DB’에 새로운 계정이 생성됩니다. 저희 목표는 마이그레이션이 종료된 이후에 바로 ‘SQL DB’를 사용하는 거예요. 근데 아무 처리도 안 해주고 마이그레이션을 진행한다면 저희가 원하던 바를 이루지 못하는 문제점이 있었어요.

그래서 2가지 데이터를 동시에 사용해보기로 했어요. 우선 '마이그레이션 도중에 발생하는 계정 생성을 모두 ‘SQL DB’에 저장하게 되면 어떻게 될까'라는 가설이 나왔어요. 이러한 방식을 취하면 ‘마이그레이션 도중에 생성된 계정은 SQL DB로 옮겨지고, 기존 계정들은 마이그레이션 이후에 SQL DB로 옮겨진다'라는 결과가 도출되었어요. 즉, 이러한 가설을 적용한다면 저희가 원하던 바를 이룰 수 있었죠. 결국 저희는 마이그레이션 오리진이 되는
Cosmos DB를 Read Only로 만들고, SQL 쪽을 Write로 둘 다 동시에 사용하는 방안을 채택했어요. 데이터 일관성을 유지하면서 마이그레이션 이후에 계속 SQL을 사용할 준비가 됐다는 거였어요.

그러면 읽기는 어떻게 할 것인가 하면 운영체제 내부에서 작동하는 캐시랑 비슷했어요. 새로운 데이터는 SQL을 쓰기로 했는데, 마이그레이션 도중에 기존 데이터만 참조할 수가 없잖아요. 새로운 계정들은 또 SQL을 참조해야 되는 상황이었어요. 결국에 두 데이터베이스를 모두 읽어야 되는 상황에 직면했어요.

그래서 이런 식으로 진행을 해봤어요. 계정을 읽을 때 첫 번째로 SQL DB에서 한번 거치고 와요. 그러면 데이터가 있거나 없거나 두 가지 상태만 있기 때문에 상태에 따른 값을 잘 반환하겠죠. 근데 SQL에 데이터가 있으면 그 데이터를 그대로 사용해도 되는 상황이에요. 그 이유는 마이그레이션 도중에 SQL에 생성된 계정은 항상 최신을 유지하기 때문인데요, 이건 계정에 대한 데이터 수정(업데이트)이 없기 때문이에요. 근데 만약에 SQL 쪽에 데이터가 없다고 서버가 판단을 내리면 Cosmos DB로 넘어가서 데이터를 참조해요. 그래서 Cosmos DB에 데이터가 있다면 그거를 꺼내서 사용하면 되고 Cosmos DB에도 우리가 사용할 데이터가 없다면 실제로 가입을 안 한 사람이 되는 거겠죠. 결론적으로 신규 사용자에 대한 값도 읽으면서 기존에 있던 사용자 데이터 값도 모두 읽을 수 있는 구조를 채택했어요. 

그래서 이러한 구조들을 사용해서 마이그레이션 계획을 크게 3가지로 나눴는데요. Cosmos DB와 SQL DB를 동시에 사용하는 코드를 배포했어요. 계정 생성/읽기 이벤트를 다 처리한 코드를 먼저 배포했고, Azure 데이터 팩토리를 이용해서 마이그레이션을 진행했고, 마이그레이션이 끝난 계정에 대해서 더이상 Cosmos DB를 사용하지 않는 코드를 배포하는 단계였어요.

제가 강조하고 싶은 건 Azure 데이터 팩토리인데요. 일단 NoSQL에서 SQL로 마이그레이션 하는 거기 때문에 단순히 쿼리로 데이터를 복사하는 건 불가능했습니다. 그래서 총 2가지 방식을 고려할 수 있었어요. 첫 번째는 마이그레이션 코드를 직접 손으로 작성하고 실행하는 방법, 다음에 데이터 팩토리를 이용하는 방법이 있었어요. 근데 첫 번째 방법은 좀 귀찮았어요. 많이 지쳐있었고, 시간도 실제로 없었고.

그래서 Azure 데이터 팩토리를 한번 사용해 봤는데요. 크게 4가지 단계로 나눠져 있더라고요. 데이터를 복사할 소스를 지정하고, 소스가 복사될 위치를 정하고, 타이머 이런 것들을 기타 설정하고, 최종적으로 컨펌을 실행하는 이 4가지 단계로 진행이 되었는데 제가 느꼈던 것 중에서 가장 좀 강력했던 부분은 필드와 이름 매핑이었어요.

Microsoft의 Azure Data Factory

일단 필드 이름하고 타겟은 마스킹을 어느 정도 해두었어요. 복사 오리지널 소스를 선택하고 나면 그거에 대한 데스티네이션을 저희가 일일이 매핑할 수 있는 구조였어요. 실제로 네임은 마스킹이 돼 있지만 실제로 언어의 이름에 대한 소스가 나중에 SQL 쪽에 저장됐을 때 어떻게 저장돼야 되는지 이런 것들을 정의를 할 수 있었어요. 그래서 코드 작성을 할 필요도 없이 마이그레이션을 준비했어요. 더 좋았던 점은 실행 시 실시간 프로그래스를 보여주거든요. 도큐먼트를 읽는 속도, 실제로 SQL에 쓰는 라이트 속도, 데이터가 몇 kb/s 이런 것들. 실질적으로 마이그레이션에 걸린 시간이 2분 이내였어요. 어쨌든 성공했습니다.

2) 중복 트리

이 문제는 저희 팀 전체를 괴롭혔어요. 중복 트리는 사용자가 여러 개의 트리를 가지고 있는 건데요. 논리적인 구조로 볼 때 사용자 계정당 트리는 하나여야 돼요. 근데 한 사람이 여러 개의 트리를 가지고 있는 것처럼 보이는 거예요. 실제로 그런 계정들이었고요.

분석을 해봤더니 회원가입 후에 본인의 트리로 리디렉션을 시켜주고 있는데, 가입 후에 리디렉션되는 트리 주소랑 로그인했을 때 리디렉션되는 트리 주소가 달랐던 거예요. 그럼 ‘이게 왜 문제가 될까’라고 하면 이런 시나리오를 생각할 수 있어요. 일단 사용자는 회원가입 이후에 리디렉션 되자마자 그 링크를 외부로 공유하겠죠. 그리고 그 링크로 사용자들이 메시지를 달아줄 거예요. 근데 알고 보니까 그 계정은 기술적으로 중복 트리에 해당이 되었고, 이용자가 로그인을 다시 했더니 다른 사람들이 쓴 메시지가 사라져 보이는 현상이 발생해요. 근데 실제로 메시지 데이터가 없는 건 아니에요. 회원 가입 후에 리디렉션 되는 주소하고 로그인 후에 리디렉션 되는 주소가 다르기 때문에 아예 다른 트리가 보여진 거였어요.

일단 어느 정도 구조를 살펴보려고 해요. 사용자에 해당되는 사용자 식별 ID로 선물들이 모여 있고, 선물들의 집합을 논리적으로 트리라고 부르고 있어요. 한 계정의 트리가 여러 개가 된다는 구조가 나올 수가 없는데, 같은 아이디와 같은 비밀번호를 가진 사용자 계정이 여러 개 생긴 거였어요. 같은 아이디와 같은 비밀번호인데 식별 아이디가 다 달랐던 거죠. 그래서 트리가 여러 개 생성되어 보이는 현상이 있었고요. 사실 정확히 얘기하면 중복 계정이 맞는데 어느 순간 트위터나 CS나 그런 것들에서 문의가 들어올 때 중복 트리로 불리게 돼가지고 저희도 중복 트리로 부르고 있어요. 실질적으로는 중복 계정이 더 정확한 표현입니다.

중복 트리 문제 관련 문의가 정말 많았다는 게 느껴지는 공식 트위터.

로그인하는 과정을 보면 로그인할 때 아이디를 가지고 오고 하잖아요. 근데 매칭되는 계정 정보 중에 가장 첫 번째 것을 가지고 와요. 마치 'SELECT TOP(1) ~~ FROM ~~'. 근데 문제는 오더링을 따로 하지 않았어요. 그래서 만약에 회원가입할 때 생성되어 있던 식별 아이디가 somethingId2였고, 다음에 로그인할 때 리디렉션 되는 아이디가 UniqueIdentify1이었던 거면 우리가 언급했던 중복 트리 문제를 사용자가 경험하게 되는 형식이었어요.

이러한 문제들을 미뤄왔다가 크리스마스가 한참 지난 다음에 해결을 했는데, 그때는 중단 마이그레이션을 진행했어요. 사용자들이 본인이 가지고 있었던 메시지를 봐야 안 좋은 경험을 빨리 해소할 수 있다고 판단을 했고, 일단 3~4가지 정도의 단계를 세웠어요.

일단 메시지 개수에 따른 마이그레이션을 진행했어요. 메시지 개수가 쫙 나와 있는데 메시지가 가장 많은 계정에다 중복 트리에 해당했던 메시지를 다 몰아주는 형식이었어요. 하지만 로그인은 이런 여러 가지 정보들 중에 가장 첫 번째 걸 가지고 와요. 근데 이거를 근본적으로 해결하지 않으면 또 같은 문제가 발생할 수도 있겠죠. 메시지가 가장 많았던 계정을 ‘주 계정' 이라고 하는데, 주 계정 외에 다른 계정들에 대한 로그인 ID를 모두 난독화했습니다. 이러한 방식을 채택하게 되면서 로그인 아이디가 중복되는 일이 없게 만들었습니다.

그러면 ‘중복 트리에 해당하는 링크에 접속하려면 어떻게 하는가’라고 했을 때, 마지막 단계로 리디렉션 아이디를 하나 추가했어요. 리디렉션 아이디가 Null이 아니라고 하면 클라이언트 측에서 리디렉션 아이디가 있는 주소로 리디렉션을 시키는 거예요. 예를 들어서 사용자가 두 번째 Entity로 접속했을 때 리디렉션 아이디가 널이 아닌 상황이거든요. 그렇게 되면 클라이언트 측에서 리디렉션 아이디 정보에 해당되는 트리 주소로 리디렉션을 시켜주는 그런 작업들을 병행했습니다.

근데 왜 이런 일이 발생했냐고 한다면, 그냥 저희의 예상을 말씀드릴게요. 일단 저희가 처음에 아키텍처를 설명해 드렸는데 여기에 체크 duplicate된 유저하고 트리거가 발생되는 부분을 보려고 해요. 

저희가 서버가 좀 되게 많이 느린 시절이 있었잖아요. 그때 '데이터를 중복 체크하는 부분이 동시간대에 이루어졌다면, 트랜잭션 부분이 동시간대에 치러졌다면 어떻게 됐을까'라고 생각해봤어요. 시점이 다 같게 되면 실제로 데이터가 서로 없을 거라고 생각하고 둘 다 저장을 할 거예요. 그렇기 때문에 중복 계정들이 생겨난 게 아닌가라고 예측하고 있습니다. 

그렇게 마이그레이션들의 여정을 잘 마무리했어요. 그래서 여러 가지를 느꼈어요. 처음에 문제를 분석할 시간도 없었는데, 이 시간이 없었다고 해서 그렇게 후회는 하지 않아요. 갖은 리소스 압박에서도 문제를 빠르게 해결할 수 있었고, 침착함을 많이 길렀던 것 같아요.


한 가지 실험 - 로드 테스팅

제가 24일에 로드 테스팅 비슷한 걸 한 번 걸어봤어요. 로드 테스팅이 맞는지는 잘 모르겠는데 이 부분들은 사실 깊은 내용이 아닐 거예요. 어쨌든 23일에는 마이그레이션이 진행되고 25일 트래픽을 기다리고 있었어요. 근데 기다리고 있는 것만으로는 무서웠어요. 그래서 예찬 님도 다른 방안을 강구했고, 저는 우리가 좀 더 확신할 수 있는 수단이 없을까 고민을 하다가 로드 테스트라는 것을 진행해보았어요. 

이거는 제가 이전에 글 읽다가 ‘나중에 한번 써봐야지’ 했던 툴이었는데요. 바로 K6라는 부하 테스트 툴을 한번 써봤어요. 일단 K6는 그라파나 랩(Grafana Labs)에서 만든 부하 테스팅 툴인데요. 자바 스크립트로 테스트 케이스를 작성할 수 있고 설명 상 CLI Friendly이라고 되어 있는데, 실제로 GUI는 거의 없더라고요. 그리고 'Gorutine으로 작성이 되어 있기 때문에 리소스 최적화가 잘 되어 있다'고 얘기하고 있는데 저는 단순한 관심으로 접근을 했기 때문에 뭐가 좋고를 크게 따지지는 않았어요. 

Grafana Labs K6 홈페이지여러 가지 글들을 읽어보다가 제이미터(Jmeter)가 있는데 왜 굳이 K6를 썼냐는 질문이 있었어요. K6를 사용하기로 했던 데는 크게 세 가지 이유가 있었어요.

일단 제이미터에서 보낼 수 있는 요청수의 한계도 있었고, XML 쪽 방식의 스크립팅도 그렇게 익숙지는 않았어요. 그리고 그라파나 대시보드를 만든 그룹이 만든 툴이니까, 테스트 결과 등을 대시보드로 잘 표현해 줄 거라 믿고 있었어요. 결국엔 시간이 없어서 대시보드랑 연동까지는 못했습니다.

한 가지 더 재밌던 건 쿠버네티스와의 조합을 우리가 기대를 할 수 있더라고요. 좀 실험적이지만, 공식 블로그에 쿠버네티스 대용으로 로드 테스트를 할 수 있는 방법을 설명해 주고 있어요. 그래서 가이드를 한 번 따라해보자고 결심을 했어요. 다만 쿠버네티스로 부하 테스트를 하게 되면 여러 클러스터에 배포된 리소스에서 수집되는 매트릭이 제한된다는 단점이 있어요. 근데 결론적으로 대시보드를 쓸 수 있는 상황은 아니었기 때문에 패스했어요. 간단했던 거는 리소스 파일을 하나 작성해 두고 클러스터에 배포할 스레드 파라미터를 얼마나 줄 것인지를 명시해 두고 어플라이하면 돼서 이 툴을 사용해보기로 결심을 했어요. 

테스트를 하기에 앞서 목적을 세워야 했는데요. 분당 100만 요청 이상이 들어왔을 때 ‘서버가 죽지 않고 살아 있나’가 먼저였어요. 1분 안에 100만 개의 요청이 들어오면 응답이 1분 안에 들어와야 되는 형식의 테스트를 작성을 했었고, 많은 트래픽이 들어왔을 때 우리가 예상했던 Response Code들을 반환하는지, 우리가 원했던 Response Body가 잘 들어오는지 이런 것들을 테스트를 해보려고 했어요. 다만 엔드 투 엔드 레이턴시까지 분석할 시간은 안 났어요.

테스트 케이스를 구성해보려고 하는데 저 혼자 생각하기에는 복잡한 부분이 있어서 어떻게 하면 좋을지 예찬님한테 여쭤봤어요. 수강신청처럼 수강신청 시간대가 되면 그 시간 전후로 엄청나게 조회가 많잖아요. 그런 케이스가 예상됐어서 그 케이스를 구성해보기로 했어요.

일단 12시 전후로 트리 메시지를 조회하는 케이스들을 구성했는데, 트리 메시지를 조회할 때가 되면 통상 새로고침을 많이 할 것이라고 생각했어요. 그런 것들을 구현해보고 싶어서 로그인하고 회원가입을 한 다음에 0.1초당 한 번씩 새로 고침을 하는 계획을 세웠고, 그걸 총 100번 수행하게 했어요. 그래서 1명당 102번의 요청을 진행을 하게 만들었어요. 그 다음에 이렇게 1명에 대한 케이스가 만들어졌으면 램핑업(ramp up) 방식으로 분당 1천 명, 7천명, 만 명 이렇게 들어가다가 최대 분당 5만 명까지 시뮬레이션을 넣었어요. 그래서 이론적으로 계산해 보면 13분 안에 221만 명의 테스트가 이루어지는 형식이었습니다.

실제로 클러스터에 배포한 코드를 보면 엄청 간단해요. 사실 K6에서 제공해 주는 특별한 리소스가 하나가 있었어요. 그래서 그것만 설치를 잘 해주고 저희가 코드 같은 것들만 잘 짜준다면 테스트가 단순하게 배포될 수 있었어요. 

근데 분명히 한계점이 있었어요. 그때 제 개인 컴퓨터로 쿠버네티스를 구성했는데, 당시 6코어 12스레드, 그리고 메모리는 32기가에 K3S Single Cluster로 구성되어 있었어요. K3S는 사용자가 처음에 쉽게 다가갈 수 있는 쿠버네티스 시스템이었어요. 계산을 해 보니, 5만명의 사람 당 약 100번 정도의 요청을 보내게 되는데, 그렇게 되면 이론적으로 500만 요청/min이라는 계산이 나왔어요. 거기다 테스트 병렬 수는 4개까지 구성했기 때문에 최대 2천만 요청/min이라는 계산이 나왔어요. 그러나 이걸 실행하는 클러스터는 단일 기기이고, 분명히 병목현상에 지연 등이 있을 것이라고 생각했어요. 다시 말해 내부 자원이 충분치 않았을 것이죠. 사실 저렇게 요청을 보냈을 때 컴퓨터가 꺼지지 않은 게 다행이라고 생각했어요. 예상했던 테스트 시간보다 많이 지연되기는 했지만, 일단 실행이 되었습니다. 

쿠버네티스의 또 다른 버전, K3S. 가벼움과 간단한 설치가 큰 장점이에요. (홈페이지)

저는 반 성공 반 실패라고 이렇게 결론을 내렸어요. 성공이라고 하면 저희가 스파이크를 찍었을 때 한 서버에 최대 110만 Request/min까지는 달성했고, 그때까지 요청이 중간에 죽거나 서버에서 500 응답으로 반환하는 상태는 없었어요. 그래서 '최소 110만 이상은 버틸 수 있구나'라는 결론을 도출해냈어요. 아쉬웠던 점은 한 노드에 무리한 테스트를 진행했고, 그것 때문에 병목이 많이 발생하지 않았을까 싶어요. 실제로 SSH로 접속을 해보려고 했을 때 좀 많이 느렸어요. 또 테스트 결과 latency 같은 걸 분석해서 최적화할 수 있는 방안을 마련할 수도 있었는데, 리소스에 쫓기다 보니까 이런 것들을 시각화하지를 않았어요. 그래서 자세한 결과를 분석하지 못했다는 단점도 있었어요.

 

인프런과의 Q&A 💬

Q1. Cosmos DB라고 하는 NoSQL을 SQL DB로 전환했는데, 코드상 변경이 거의 없었던 이유는 Entity 프레임워크가 코드상으로 양쪽의 호환을 다 해주기 때문인가요?

이예찬: 코드상 변경이 있었어요. 결국 두 가지 종류의 데이터베이스를 쓰게 바뀐 거기 때문에 그거에 따라서 데이터베이스 컨텍스트도 두 개가 생성이 돼야 해서 코드상 차이점도 분명히 적지 않았고요. 그런데 애초에 그렇게 복잡한 쿼리가 아니었기 때문에 실제로 큰 변경점이 있지는 않았어요.

Q2. Cosmos DB도 Entity Framework를 사용하고 SQL도 Entity Framework를 사용하신 건가요?

이예찬: 맞아요. Cosmos DB는 애초에 RDBMS가 아니기 때문에 굳이 ORM을 사용할 필요는 없었는데, 일단은 쿼리 빌더가 익숙해서 그걸 사용했었어요. 사실 굳이 그걸 쓰지 않아도 충분히 해결할 수 있는 부분이긴 했었는데 그냥 썼었습니다.

강현우: Entity Framework 밑에 프로바이더들이 여러 가지가 있는데 그런 프로바이더를 적절히 사용했던 것 같습니다. 추상화된 DB 프레임워크인데 내부에 끼워넣는 게 Cosmos DB를 넣을 거냐, SQL DB를 끼울 거냐에 따라 달라질 뿐이지 문법적으로는 큰 차이가 없습니다.

Q3. 동시에 트랜지션 2개가 발생한 이유를 말씀해주셨는데, 회원가입 API에서 한 번만 물었는데도 여러 번 발생했다는 가정에서 말씀하신 건가요?

이예찬: 이런 케이스도 있었어요. 핸드폰에서 가입을 시도했는데 안 돼서 컴퓨터에서 가입을 시도했는데 그게 둘 다 이제 마지막으로 트랜지션이 들어가서 굉장히 긴 시간의 트랜지션이 걸린 거죠. 그 분 같은 경우는 패스워드도 결국 달라졌어요. 그러니까 결국에는 사용자가 여러 번 요청한 거예요.

Q4. 무중단 마이그레이션에서 Cosmos DB를 Read Only로 하고 SQL로 하셨다고 하셨는데, 메시지 작성도 SQL로 하시는 건지 궁금해요.

강현우: 저희가 사용자 회원가입과 로그인 API 쪽만 영향을 받고 있었어요. 그런데 실제로 ‘선물'에 해당되는 Cosmos DB 컬렉션은 아무것도 변경하지 않았어요. 메시지를 작성하는 이벤트 같은 것들이요. 왜냐하면 그 당시에 사용자 컬렉션과 선물 컬렉션은 명시적으로 관계가 있는 것이 아니었어요. 선물도 ‘사용자 식별 ID’라는 string 형식의 파티션 키 필드가 있었던 거고, 이 이상으로 복잡한 관계를 가지지 않았어요. 저희는 사용자 컬렉션을 Cosmos DB에서 SQL로 마이그레이션할 때 사용자 식별 ID를 그대로 유지해서 진행했기 때문에 선물과 관련된 필드를 수정할 필요는 없었습니다.

이예찬: 원래 데이터베이스 콜이 두 번 일어났던 거라서 크게 상관없어요. 3600만 건의 데이터를 마이그레이션 하는 것도 리스크가 있기도 했었고요. 굳이 그럴 필요가 없다고 생각했어요.

Q5. 올해도 '내 트리를 꾸며줘'를 하실 계획이 있나요? 아니면 프로젝트를 더 키워갈 생각은 있으신가요?

이예찬: 올해는 최소한의 Internationalization 정도는 추가를 해서 배포를 하지 않을까 싶습니다. 추가적인 기능을 다양하게 붙이거나 그럴 계획까지는 아직 없어요. 일단 지금 발생했던 문제는 다 해결한 상태로 시작할 수 있을 것 같아요.

Q6. 요즘 유사 서비스들이 많이 나왔잖아요. 사실 오리지널이신데 다른 이벤트 날짜에 새로운 사이드 프로젝트로 확장하실 계획도 있나요?

이예찬: 저희도 그 고민을 많이 했어요. 근데 다른 서비스들이 우후죽순으로 나오는 걸 보니까 오히려 저희가 그걸 하면 안 되겠다는 생각이 들더라고요. 일단 산타파이브라는 이름으로 시작했고 '내 트리를 꾸며줘'라는 서비스 자체가 엄청 크게 됐던 거기 때문에. 오히려 우리는 '내 트리를 꾸며줘'를 더 집중을 해서 크리스마스의 아이콘으로 남고 싶다는 생각도 들어요. 특정한 날짜 말고 1년 내내 두고두고 쓸 수 있을 만한 서비스가 없을까 정도는 고민하고 있는 상황이기는 해요.

Q7. Cosmos DB에서 SQL 서버로 옮기실 때 랜덤 아이디 값을 쓰셨잖아요. ID 생성할 때 중복 체크 같은 경우는 따로 하셨나요? 만약 기존에 있는 UUID 항목이라면 그 유저 데이터를 덮어쓰지 않았을까요?

이예찬: 일단 믿고 인서트를 썼어요. 왜냐면 많이 쓸 상황도 아니었고, 어차피 인서트가 실패할 거기 때문에 크게 걱정했던 부분은 아니긴 했어요.

그리고 인서트였기 때문에 덮어쓰지 않았어요. 새로 추가 됐었고. 트리거라는 걸 사용을 해가지고 체크했었는데, 논리적으로는 포스트 트리거가 제대로 작동한다면 이건 데이터베이스 단에서 보장이 되는 거기 때문에 새로운 사용자가 들어오고, 동일한 아이디로 사용자가 또 들어왔다고 하면 그 트리거 단에서 기존의 도큐먼트를 다 검사를 해서 트랜지션을 롤백을 시켰어야 되는 건데 트리거가 간헐적으로 작동을 해요. 그래서 이게 굉장히 좀 큰 문제가 됐어요.

그 트리거의 문제가 또 뭐였냐면 트리거 자체가 테이블 같은 전체 데이터베이스에서 사용자 데이터를 전수 조사하는 거였어요. 사용자의 유니크 아이디 말고 로그인 아이디를 전수 조사하는 거기 때문에 엄청나게 비용이 컸어요. 아마 이것 때문에 늦어졌을 거라는 생각이 들기는 하는데 이 250만 개의 데이터를 다 탐색해가면서 사용자 계정 아이디를 찾아내고 중복인지 아닌지를 판단하는 거였기 때문에 모든 문제가 여기서 출발하지 않았을까 하고 있습니다.

Q8. 저는 이 산타파이브를 사건을 통해 알게 되었거든요. 그 부분에 대한 취약점을 어떻게 해결하셨는지 궁금합니다.

이예찬: 아마 디도스 사건을 말씀하신 것 같은데 사실 그거는 엄밀하게 말씀드려 해결을 못하는 상황이에요. 레이트 리밋 같은 걸 걸어서 할 수는 있겠는데 그거를 설정할 정신이 없었고요. 사실 그 트래픽이 전체 트래픽에 영향을 끼치는 수가 제가 생각하기로는 그렇게 큰 부분이 아니었다고 생각해요. 그때도 워낙 트래픽이 많이 발생하고 있었던 상황이었어요.

사실 사람들이 편의상 디도스라고 부르지만 디도스라고 부르기에는 일단 Distributed가 아니었거든요. 그래서 그 부분에 대해서 법적으로 절차를 하거나 이런 거는 계속 얘기를 하는 중이긴 한데, 그걸 기술적으로 뭔가 딱 막아놓은 상황은 아니에요.

Q9. NoSQL과 RDBMS을 각각 언제 쓰면 좋은지 산타파이브 사례로 말씀해주실 수 있나요?

이예찬: 일단 로그인 같은 경우는 NoSQL 데이터베이스 중에서도 도큐먼트 데이터베이스로 있는 거라서 파티셔닝을 잘 해야 돼요. 제가 생각하기에 로그인 정보는 파티셔닝이 힘든 데이터인 것 같고, 파티셔닝을 엉성하게 할 바에는 SQL 데이터베이스에서 인덱스를 잘 활용하는 게 훨씬 적절하다는 생각이 들었어요.

트리 데이터 같은 경우는 데이터 자체가 중첩돼 있는 것들이 있었어요. 카드에 메시지가 있고 카드의 배경, 색깔 이런 것들도 데이터로 다 저장이 되어 있었어요. 제가 생각하기에는 Aggregate 돼 있는 것들은 Cosmos DB 같은 NoSQL 데이터베이스에 저장하는 게 나았던 것 같고, 사용자 데이터 같이 파티션이 힘든 데이터는 SQL 데이터베이스 이렇게 하는 게 좋았던 것 같아요.

저희가 프로젝트를 하고 Cosmos DB를 회사에서 굉장히 많이 쓰기 시작했거든요. 원래는 모놀리식(monolithic)하게 어떤 거대한 서버가 하나 있고, 통계를 다루는 서버를 분리하고, 사용자의 프리퍼런스 설정을 분리하고, 푸시 알림 같은 경우도 분리해서 따로 되도록 하고 있었어요. 지금은 게임 세이브 파일처럼 해서 Cosmos DB에 저장하는 방식으로 구현을 바꿔놓고 있어요.

Q10. 만약에 프로젝트 시작할 때로 돌아가신다면 NoSQL 대신 RDBMS를 쓰실 건가요?

강현우: 제 생각엔 RDBMS를 쓸 것 같아요. 다만 저희가 파티셔닝을 하는 주 이유는 우리가 특정 데이터가 특정 클러스터로 몰리지 않게 분산을 해주기 위해서잖아요. 그렇게 분산이 필요한 메시지들은 아마 Cosmos DB로 밀고 가자고 할 것 같고, 파티셔닝이 힘들거나 불필요한 정보들은 Cosmos DB로 돌아가지는 않을 것 같아요.

이예찬: 다음에 하더라도 계정 정보는 SQL 데이터베이스 사용하면 될 것 같아요. 사실 선물 정보 같은 경우도 NoSQL 데이터베이스를 그대로 쓸 것 같기는 해요. 그거는 사용자 아이디라는 강력한 파티셔닝 키가 있으니까 그걸 활용하면 될 것 같아요. 스케일링, 스케일 아웃 되는 게 Cosmos DB의 장점이라고 생각했어요. 근데 그렇게 되면 파티션 별로 스케일링이 되면서 물리적으로 서버가 증설되고, 파티션이 물리 서버마다 할당이 되거든요. 할당된 파티션을 빠르게 가져올 수 있기 때문에 그거는 그대로 쓸 것 같아요.

Q.11 많은 사용자들이 산타파이브를 했던 이유는 SNS가 가장 크다고 생각을 했는데, 만드신 입장에선 어떻게 생각하시는지 궁금해요.

이예찬: '우리는 정말 사용자 중심으로 생각을 많이 했다'는 게 큰 결론이에요. 세부적으로 들어가 보면 말씀하신 것처럼 인스타그램에 공유하기 편하게, 사진 한 장으로 캡처했을 때 예쁘게 나오게 하는 걸 컨셉으로 잡았어요. 요즘 MBTI 테스트 같은 거 해서 '나 이거 나왔다'하고 결과를 공유하는 것들이 많잖아요. 그것도 굉장히 파급력이 있었지만, 저희 같은 경우는 이걸 공유해놓고 '나 이거 해줘' 이런 식으로 만든 게 공유를 유발하는 요인이 되지 않았나 생각합니다. 부가적으로 선물을 커스터마이즈 가능하게 한 것도 영향이 있지 않았을까 하는 부분이 있습니다.

Q12. 첫 마케팅은 어떻게 하셨나요?

이예찬: 사실 마케팅이 별로 없었어요. 저희는 완성하고 자정에 각자 트위터에 올리고 인스타그램 올리고 했는데 거기서 바이럴을 탔던 게 거의 끝이었어요. 트위터, 인스타, 그리고 카카오 오픈카톡방. 저희 팀원 중에 현지 님이라고 계시는데 그분이 열심히 알리긴 하셨어요. 오픈카톡방에도 뿌리고 인스타그램에도 올리고 퇴근해서 친구들한테 '야 너도 이거 해봐' 이러고 했는데 거기까지였어요. 나머지는 진짜 바이럴로 쭉 펴져 나갔던 케이스에요.

Q13. 이슈가 터졌을 때 일을 처리하는 플로우가 어떻게 되는지 궁금해요.

이예찬: 사실 뭐가 없어요. 왜냐하면 정말 소수의 팀이었고 당장 눈앞의 일을 처리하기 바빴던 상황이었기 때문에 뭔가 플로우라고 자랑할 만한 게 있지는 않아요. 그냥 누군가 장애를 발견하거나 CS를 받으면 그걸 공유했거든요. 원인을 분석해서 공유해드리고, 작업 진행하고 공유하고 이 정도 수준이었던 것 같아요.

Q14. 백엔드 개발자로서 대용량 트래픽을 수용하는 서비스를 만들려면 무엇을 공부해야 될까요?

이예찬: 저는 다른 회사의 케이스들을 좀 많이 봤던 것 같아요. 사실 직접 경험할 수 있는 상황이 많지 않잖아요. 근데 저는 다른 회사에서 어떤 기술을 왜 사용했는지, 어떤 문제를 해결하기 위해 사용했는지, 어떻게 해결했는지 등 그 키워드들만 알고 있어도 도움이 된다고 생각하거든요. 

예를 들어서 제가 로드 밸런서를 붙였는데 로드 밸런서 존재 자체를 모르는 사람도 분명히  있을 거예요. 그런 경우엔 로드 밸런서가 왜 쓰였고, 어떤 식으로 작동하는지 정도만 알아둬도 충분히 도움이 될 것 같아요. 혼자 공부하는 케이스는 특히 그렇겠죠.

Q15. C#.net이라는 스택이 국내에서 흔한 스택은 아니잖아요. 개발팀을 꾸리실 때의 어려움은 없으셨나요? 그리고 이런 부분들을 어떻게 해결하실 계획인지 궁금해요.

이예찬: C#.net이 시니어를 뽑기 굉장히 애매한 스택이라는 생각이 드는 게, 실질적으로 이걸로 서비스를 많이 돌려봤다고 할 만한 분이 거의 없었어요. 그게 제가 가지고 있던 페인 포인트였던 것 같아요. 그래서 오히려 채용을 할 때 이 부분에 관심만 가지고 있는 분이나, 저연차분을 채용을 해서 같이 배워나가는 식의 선택을 했었던 것 같아요. 

그리고 제가 기술 요구사항에 그냥 오픈해놨어요. 자바 스프링 쓰시는 분들도 지원 가능하다고. 어쨌든 컴파일러가 같다 보니까 비슷한 점도 있을 거고, 스프링에서도 중요한 개념 중에 하나인 Dependency, Injection 같은 개념이 ASP.Net에도 있어서 적응하기 크게 어렵지 않을 것 같다고 생각했어요. 실제로 현우 님을 그렇게 뽑았고요.

당분간 테크 스택을 뭔가 바꾼다거나 할 계획이 있는 건 아니고, 되도록이면 맞춰서 채용하고 싶어요. 당근 같은 경우도 테크 스택을 다양하게 쓴다고 알고 있고요. 나중에 회사 규모가 커지게 되면 자유롭게 스택을 고를 수 있는 회사가 될 수 있지 않을까 생각하고 있습니다.


이번 [주간 인프런] 어떠셨나요?
솔직한 의견을 들려주세요!

유익해요 | 아쉬워요

지난 [주간 인프런] 이 궁금하다면? (클릭)

댓글 2

댓글을 작성해보세요.

  • CHARMING-PARK
    CHARMING-PARK

    이게 벌써 7~8개월 전 얘기라니..
    이제 막 for문을 배우며 고통받아하며 옆 집 예찬이는 저런거 하는데.......하며 알 수 없는 내용들에 한 숨만 내쉬던 저도
    이제는 제 서비스를 다른 현업인들과 함께 만들 수준이 되어서 읽어보니 인프런 팀에서 열일해 주신게 다시 공부하기 좋았습니다.
    (비록 제 친구의 얼굴은 박제가 되었지만요 -- 인기스타라 뭐.....괜찮으려나)

    겨우 로그인 회원가입과 post의 간단한 CRUD만 하는 react 서비스로 개발할 때는 나 편한대로 코딩을 했는데
    유저입장이란 것을 거치면 저렇게 된다는게 새롭네요.

    제 결혼식에 손 수 사용 할 청첩장+ 방명록 웹 앱 을 제작 중 인데 궁금하네요, 앞으로 어떤 미래들이 펼쳐질지..

  • 최민영
    최민영

    저는 그때 왜인지 날아가서 친구들이 남긴 메세지도 못봣는데 문의해도 대답도없고 참 답답햇습니다.
    만들거면 이후 서비스도 좀 신경써주시길