Skip to content
03 / Field note

서버 없이 앱 수십 개의 데이터를 어떻게 모을까? - 난테 스튜디오 아키텍처 4편

관리할 서버 없이 Cloudflare Workers와 D1로 앱 62개의 이벤트를 모읍니다. 이벤트를 두 군데로 보내는 이유, 외부 데이터를 미러링하는 방법, Worker끼리 부를 때 생긴 함정을 정리했습니다.

난테 스튜디오 앱은 대부분 서버가 필요합니다. 사용자를 구분해야 하고, 앱에서 일어난 일을 기록해야 하고, 외부 데이터를 받아 보여 줘야 하니까요. 그런데 서버를 관리할 사람은 없습니다. 그래서 백엔드는 모두 Cloudflare Workers 위에 올렸습니다. 지금 백엔드 쪽 Worker 설정이 44개, D1 데이터베이스가 35개, 크론이 붙은 Worker가 25개입니다.

그런데 올해 4월 말에는 앱 이벤트가 Firebase에만 쌓이고 있었습니다. 에이전트가 "새 버전이 이벤트를 제대로 보내고 있나?"를 확인하려 해도, 혼자서는 그 데이터를 조회할 방법이 없었어요. 데이터는 있는데 확인해야 하는 쪽이 열어 볼 수 없는 상태였습니다.

지금은 이벤트를 두 군데로 보냅니다. 오래 쌓아 두고 광고 성과를 재는 쪽은 Firebase와 BigQuery가 맡고, 몇 초 전 이벤트를 에이전트가 바로 확인하는 쪽은 저희 D1이 맡습니다. 지난 7일 동안 앱 62개가 이벤트를 보냈고, 하루에 2만 1천에서 2만 9천 건이 들어왔습니다. 이번 글에서는 데이터가 어떤 길로 들어오고, 외부 데이터는 어떻게 보여 주고, Worker끼리 부를 때 어떤 함정이 있었는지 공유하려고 합니다.

이벤트는 어떤 길로 들어올까

앱은 안드로이드, 플러터, iOS마다 공통 분석 모듈을 씁니다. 화면을 열거나 버튼을 누르는 이벤트가 생기면, 이 모듈이 같은 이벤트를 두 곳으로 보냅니다.

앱 이벤트 하나가 Firebase와 BigQuery, 그리고 수집 Worker와 D1 이벤트 테이블 두 길로 나뉘는 흐름

그림 1. 이벤트 하나가 두 길로 나뉩니다. 위쪽은 기록과 광고 성과용, 아래쪽은 에이전트가 바로 확인하는 용도입니다.

  • Firebase Analytics: 원본 이벤트를 오래 보관하고 BigQuery로 내보냅니다. Google Ads가 입찰에 쓰는 전환과 광고 수익 연결도 여기서 나옵니다.
  • 저희 수집 Worker: 앱마다 따로 발급한 수집 키로 이벤트를 받아 D1의 이벤트 테이블에 한 줄씩 넣습니다. 몇 초 안에 조회할 수 있습니다.

광고 수익도 이벤트로 들어옵니다. 광고가 한 번 노출될 때마다 광고 SDK가 그 노출의 수익을 알려 주는데, 앱은 이 값을 이벤트 하나로 보냅니다. 지난 일주일 동안 하루 3천 건 안팎이 들어왔어요. 덕분에 "이 앱의 사용자 한 명이 광고로 얼마를 버는가"를 추정이 아니라 측정한 값으로 볼 수 있습니다.

10월 20일부터 26일까지 하루 이벤트 수와 그날 이벤트를 보낸 앱 수를 보여 주는 막대 그래프

그림 2. 10월 20일부터 26일까지 하루 이벤트 수입니다. 막대 위 숫자는 그날 이벤트를 보낸 앱 수입니다.

왜 D1에 따로 모을까

D1 이벤트 테이블은 원래 한 번 없앴던 것입니다. 4월 23일에 한 번 정리했는데, 8일 뒤인 5월 1일에 다시 만들었어요. BigQuery 내보내기가 아직 켜져 있지 않아서, 에이전트가 혼자 쿼리할 방법이 없었기 때문입니다.

지금은 역할이 나뉘어 있습니다. BigQuery는 과거 데이터를 보는 창고이고, D1은 지금 일어나는 일을 확인하는 보조 테이블입니다. 새 빌드를 올린 뒤 "이벤트가 들어오나?", "광고 노출 이벤트가 찍히나?"를 확인할 때 D1을 봅니다.

에이전트는 nst analytics query로 이 테이블에 SQL을 직접 보냅니다. 대신 Worker가 쿼리를 먼저 검사합니다. SELECT와 WITH만 받고, 이벤트 테이블을 참조해야 하고, 세미콜론이 있으면 거절합니다. 에이전트가 만든 SQL이 테이블을 바꾸는 일은 코드가 막습니다.

사용자는 어떻게 구분할까

로그인도 Worker 하나가 맡습니다. 모든 앱이 같은 인증 Worker를 쓰고, 앱 ID로 사용자 풀을 나눕니다. 처음 앱을 연 사용자에게는 가입 없이 익명 계정을 만들어 주고, 나중에 Google이나 Apple 계정을 연결하면 같은 사용자 ID에 붙입니다.

발급하는 토큰은 수명이 짧은 서명 토큰이라, 다른 Worker는 인증 Worker를 부르지 않고 공개 키만으로 검증할 수 있습니다. 등록되지 않은 앱 ID로 요청하면 404를 돌려줍니다. 새 앱을 붙일 때는 앱 목록에 한 줄을 넣으면 끝이에요.

외부 데이터는 어떻게 보여 줄까

유가, 공공 데이터, 스토어 정보처럼 외부 API에서 받아 앱에 보여 주는 데이터가 있습니다. 이런 데이터는 앱이 외부 API를 직접 부르지 않게 합니다. 규칙은 한 줄입니다. 앱에 보이는 외부 데이터는 전부 저희 D1에 복사해 두고 크론으로 갱신합니다.

외부 데이터를 KV 캐시, D1 복사본, 외부 API 순서로 읽고 받은 값을 D1에 다시 쓰는 흐름

그림 3. 외부 데이터를 읽는 순서입니다. 캐시, 저희 D1, 외부 API 순서로 찾고, 외부 API에서 받은 값은 D1에 다시 씁니다.

  1. 먼저 KV 캐시를 봅니다. 가장 빠릅니다.
  2. 없으면 D1에 복사해 둔 데이터를 봅니다. 행마다 언제 복사했는지 기록돼 있습니다.
  3. 그래도 없으면 외부 API를 부르고, 받은 값을 D1에 씁니다.
  4. 크론은 자주 쓰이는 데이터를 미리 채우고, 가장 오래된 행부터 다시 받습니다.

이렇게 하면 외부 API가 멈춰도 앱 화면은 멈추지 않습니다. 데이터가 조금 오래될 뿐이에요. 하루 호출 수가 정해진 공공 API도 앱 사용자 수와 상관없이 크론이 정한 만큼만 부르게 됩니다.

Worker끼리는 어떻게 불러야 할까

Worker가 많아지면 Worker끼리 부를 일도 많아집니다. 처음에는 다른 Worker를 그 Worker의 공개 주소로 불렀는데요. 같은 계정의 Worker가 공개 주소로 서로를 부르면, 부르는 쪽이 크론으로 실행될 때 Cloudflare가 요청을 막습니다. 에러 1042나 엉뚱한 404가 돌아오고, 요청은 상대에게 닿지 않아요.

이 문제로 세 번 고생했습니다.

  • 모니터링 Worker가 멀쩡한 Worker들을 "다운"으로 판단했습니다.
  • 분석 화면이 다른 Worker의 데이터를 읽지 못했습니다.
  • 주식 뉴스 알림 Worker는 실행할 때마다 404를 받았는데, 로그에는 "검증된 헤드라인 없음"으로만 남았습니다.

세 번 모두 문제가 보인 곳만 고쳤기 때문에, 같은 실수가 다른 Worker에서 또 나왔습니다. 그래서 지금은 린트가 저장소의 Worker 설정 파일을 전부 찾아서, 코드에 다른 Worker의 공개 주소가 있으면 푸시를 막습니다. Worker끼리는 서비스 바인딩으로만 부릅니다. 검사 대상은 목록에 등록된 Worker가 아니라 저장소를 훑어 찾은 Worker라서, 새 Worker도 만들어지는 날부터 검사를 받습니다.

자주 빠지는 함정 두 가지

D1을 쓰면서 에이전트가 여러 번 빠진 함정이 있습니다.

  • 시간 단위: 이벤트 테이블의 시각은 초 단위입니다. 밀리초라고 생각하고 1000을 곱해 조건을 걸면, 데이터가 가득한 테이블에서 결과가 0건으로 나옵니다.
  • 로컬과 원격: wrangler d1 execute는 옵션을 주지 않으면 로컬 시뮬레이터에서 실행됩니다. 원격 데이터베이스에 쓰려면 --remote를 붙여야 하는데, 붙이지 않아도 "성공"이라고 나옵니다.

두 함정 모두 "결과가 비었다"로 나타납니다. 그래서 스튜디오에는 규칙이 하나 있습니다. 빈 결과는 "없음"이 아니라 "모름"입니다. 도구를 의심하고 조건 없이 다시 조회해 보기 전에는, 데이터가 없다고 결론 내리지 않습니다. 화면에서도 읽지 못한 값은 0이 아니라 빈칸으로 보여 줍니다.

Workers로 백엔드를 운영하고 있다면

먼저, 확인하고 싶은 데이터가 에이전트 손이 닿는 곳에 있는지 보세요. 콘솔을 열어야만 볼 수 있는 데이터는 결국 사람을 기다리게 합니다. 저희는 그 이유 하나로 없앴던 테이블을 다시 만들었습니다.

다음으로, Worker끼리 공개 주소로 부르는 코드가 있는지 찾아보세요. 사용자 도메인에서 테스트하면 잘 되기 때문에 크론에서 실패해도 알아채기 어렵습니다.

마지막으로, 외부 API를 앱에서 바로 부르고 있다면 복사본을 하나 두는 게 좋습니다. 외부 API가 멈추는 날, 앱 화면이 같이 멈추는지 아닌지가 여기서 갈립니다.

마치며

서버 없이 백엔드를 운영한다는 건, 서버를 신경 쓸 일이 없다는 뜻이지 아무 일도 없다는 뜻은 아니었습니다. 문제는 대부분 서버가 아니라 데이터가 어디에 있고 누가 볼 수 있는지에서 생겼습니다.

돌아보면 한 번 없앤 테이블을 8일 만에 다시 만든 일이 가장 많은 걸 알려 줬습니다. 데이터는 있는 것만으로는 부족하고, 그 데이터를 확인해야 하는 쪽이 직접 열어 볼 수 있어야 했습니다.

다음 글에서는 이 데이터를 근거로 AI가 광고 예산을 어떻게 움직이는지 다룹니다. 작은 팀에서 서버 없이 앱 여러 개를 운영하고 있다면 조금이나마 도움이 되었으면 합니다.

난테 스튜디오 아키텍처 시리즈의 4편입니다. 전체 목록은 1편에 있습니다.