프로그래밍 언어에 대한 글을 쓴다고 했지만… 학업과 회사일을 병행하면서 프로그래밍 언어에 대한 글까지 쓰는것은 사실상 불가능에 가까웠기 떄문에,
그리고 현업에서 일을 하는 데 도움이 되는 글을 쓰는 것도 좋으리라고 판단했기 때문에 지금까지 우리 회사가 어떤 기술적인 선택을, 왜 해 왔는지 설명하는 글을 써 보려고 합니다. 이 아래로는 반말로 작성합니다(사실 미리 써 둔 글들을 대충 정리하고 그대로 옮겨 온 거라 그렇습니다).
TL;DR
1. 스타트업에서 중요한 것은 적절한 수준, 빠른 기능 배포, 그리고 시기에 맞는 변화를 위한 유연성에 대한 사전대응
2. RDBMS라는 선택지는 회사의 수요와 기술의 장단을 따져 잘 도입했다.
3. Hasura의 도입은 추후의 비즈니스 성장을 좌지우지 한 가장 중요했던 결정
4. Flutter의 도입은 거슬리지는 않지만 여전히 아쉬운 부분이 있다.
5. Firebase remote config와 AWS의 도움으로 빠르게 기술 부채를 상환했다.
6. We are hiring
서론: 우리 회사는 무엇을 해야 하는가.
스타트업은 필연적으로 다양한 문제를 해결해야 한다. 그것은 남들이 이미 해결한 것일수도 있고, 우리만이 해결할 수 있는 문제일수도 있다. 후자의 경우 보통 그 스타트업의 핵심 사업 아이템이 된다. 전자의 경우는 기술적인문제가 된다.
그렇다, 애석하지만 기술 중심 스타트업이 아닌, 서비스 중심의 스타트업인 이상 기술 문제가 스타트업의 중심이되긴 힘들다. 이 경우 기술 문재는 대개 중심 문제를 해결하기 위한 도구에 지나지 않게 되고, 따라서 기술 문제는대개 빠르게 해결하고 넘겨야 되는 중간 걸림돌 이상의 의미를 갖기가 힘들다. 바꿔 말하면 이 경우, 사업 아이템이기술적으로는 구현하기 어렵지 않기 떄문에 다른 경쟁자가 시장에 진입하기 이전에 최대한 해결 수준의 격차를 넓히는 것이 중요하다. 이 말은 곧 빠른 배포가 중요하다는 뜻이다.
또 중요하게 기억해야 할 것이 있다. 스타트업의 경우 제대로 성장하고, 수익이나 투자금을 통해 추가로 구성원을고용하는 것이 가능해지기 전까지는 CTO(그러니까 나)가 회사의 유일한 기술 담당자가 되고, 서버부터 클라이언트에 이르기까지 수 많은 문제를 최적의 성능보다는 용납가능한 품질로 최대한 빠르게, 최대한 적은 구현 비용으로 납품해야 한다는 것이다. 이 때 가장 중요한 것이 회사의 핵심 문제를 올바로 파악하는 것이다. 그리고 그들에게서 핵심 기술 문제를 추출해내고 이들을 가장 낮은 비용으로 해결할 방안을 강구해야 한다.
서비스 중심의 스타트업일 경우 클라이언트에서부터 기획하는 것이 많은 경우에 유용하다. 이유는 클라이언트는유저들이 직접 접하는 단말(터미널)이 되고, 이들과 상호작용하는 방법 자체가 곧 서비스 자체를 규정하기 떄문이다. 물론 백엔드는 중요하고 서비스 규모가 커질수록 그 중요도가 매우 커지지만, 초기 단계에서는 대규모의 트래픽을 담당해야 할 일이 별로 없다. 망하거나 문제가 발생하는 것도 일단 본 궤도에 올라가야 고민할 수 있는 문제인것이다.
(구체적인 기획은 삭제)
여기서 우리 서비스는 다음과 같은 기술적 문제를 해결해야 함을 알 수 있다.
1. 복잡한 데이터 구조를 핸들링 할 수 있는 모델의 구현법
2. 서비스의 변경이 매우 잦을 경우 이를 핸들링할 수 있는 서버 API
3. 가성비 높은 백오피스 구현
4. 사용자에게 최대의 만족감을 주면서 다양한 플랫폼을 대응할 수 있는 모바일 앱 개발론
5. 외부에서 사용자 유입을 위한 웹뷰 구현(유니버셜 링크 및 딥 링크에 대응하는 웹뷰 구현을 위한)
서버의 스펙부터 결정: GraphQL
스타트업에서 가장 중요한 것은 보통 클라이언트의 문제를 해결하는 것에서부터 시작한다고 했고, 이 경우 클라이언트와 서버 간 관계를 어떻게 지어주는가를 고민하는 것이 매우 중요하다. 이는 빠른 개발을 위해서도 중요하지만, 서비스 전체의 모델은 곧 서비스를 구현하는 데이터의 구조와도 같다. 문서화는 이 문제를 해결하는 가장 왕도적이고 훌륭한 방법이지만, 개발 기한을 맞추면서 문서를 동시에 작성하기는 힘들고, 개발이 끝난 서비스 구성요소를 추후에 문서화하는 것은 사실상 불가능에 가깝다. 이 경우 차선으로 택할 수 있는 것이 바로 서비스의 구현 자체가 문서화가 되도록 하는 것이다. 이 경우 클라이언트 개발자와 서버 개발자 모두 클라이언트가 서버에 요구하는 데이터의 모델을 어떻게 규정하는가를 보는 것이 추후에 서비스를 파악하는 출발점이 된다.
현재 시장의 주류가 되는 것은 REST API이다(적어도 한국 시장에서는 그렇다). HTTP 프로토콜의 기본 구성요소로 기능의 상당 부분을 정의할 수 있으니 표준 자체가 견고하기도 하고, 범용성이 큰 방법인 만큼 구현을 위한 방법론도 매우 많다(다양한 언어와 다양한 프레임워크를 사용할 수 있다). 하지만 매우 큰 단점이 존재하는데, 그것은 바로 서버의 기능성을 모두 다 구현해야 한다는 점이다.
서버를 서비스에서 직접 구현하는 것은 당연한 것 아니냐고 할 수 있다. 맞는 말이다. 그러나 우리 서비스의 경우굉장히 많은 모델 구성요소를 필요로 했고, 이들에 대응하는 기능성을 일일이 다 개발하면서 기한 내로 출시하는것은 사실상 불가능에 가까웠다. 또한 변경이 잦은데 서버와 클라이언트를 동시에 개발하는 것은(그것도 한 명이) 작업 효율과 주의집중의 context switch비용까지 고려했을 때 여러모로 현명하지 않았다.
때문에 서비스의 데이터 구성요소와 이에 대한 접근 자체를 외부에 노출하는 GraphQL이 여러모로 쓸 만 해 보였다. 데이터만 잘 정의한다면 서버 API개발 자체는 사실상 끝나는 것에 가깝다. 개별 구성요소에 대해서만 동작을정의하면, 나머지는 GraphQL 서버 엔진 구현체들이 알아서 처리해준다. 하지만 여기에는 큰 함정이 있다. 컴퓨터의 자동 로직은 많은 경우에 똑똑하지 않고, GraphQL구현체 자체도 시장에 나온지 오래되지 않아 성숙하지 못했다. 이에 따라 복잡도가 커질수록 서버 개발이 일반적인 개발 방법론보다도 훨씬 비싸진다. 따라서 이 문제를 제대로 해결하려면 다양한 메모리 층위와 알고리즘 최적화를 통해 문제를 해결하는 방법이 있고, 자본을 부어서 해결하는 또 다른 방법이 있다. 회사가 대규모라면 전자의 방법을 택했겠지만 스타트업에게 시간은 그 무엇보다 비싸다. 따라서 우리의 선택은 후자가 되었다.
그러자 AWS Amplify에서 제공하는 Data가 나쁘지 않게 보였다. 연산에 필요한 자원을 프로비저닝할 필요가 없고, 사실상 무한에 가까운 자원을 활용한다는 클라우드의 장점을 최대한 활용하는 클라우드 컴퓨팅의 장점을 십분활용할 수 있으며, 이들의 동작성은 AWS가 보장한다는 데에서 더욱 그렇다. 그러나 해당 서비스를 조사하면서 한계를 매우 많이 발견했는데, 그 중 첫 번째는 개발하기 편한 만큼 제약사항도 매우 많아 우리가 원하는 수준의 복잡도를 구현하기 위해서는 중간에 다양한 AWS Lambda를 구현해야 했으며, 이는 서버 지연시간으로 보나 구현복잡도를 보나 그다지 타당해 보이지 않았다.
AWS Lambda는 내가 대학교 2~3학년때 쯤에 갑자기 시장에 등장해서 많은 관심을 받았다. 그러나 소비자들은곧 이를 외면하기 시작했는데, 가장 큰 이유는 개발의 난해성이었다.
AWS Lambda는 자원 프로비저닝(미리 할당)할 필요가 없다는 것을 강점으로 내세워 자원관리에 지친 여러 사업자들을 유혹했다. 그러나 이는 바꿔 말하면 배포 환경이 어떻게 될지 개발자는 모른다는 뜻이 된다. 따라서 배포 이후에 개발 환경에서 발생하지 않은 이슈들이 매우 많게 되었고, 이를 해결하기 위한 디버거도, 하다 못해 콘솔 기반의 디버깅을 하는 것도 불가능에 가깝다(물론 cloudwatch log를 통해 상당부분 해결할 수 있지만). 따라서 프로비저닝 이외에 모든 게 불편한 괴상한 서비스가 되었고, 곧 개발자들의 외면을 받았다. 현재도 Lambda는 보통AWS의 다양한 관리형 서비스를 중간에 매개하는 글루 코드 정도의 역할을 하고 있다.
Lambda는 또 다른 단점이 있는데, 바로 실행 빈도에 따라서 반응 속도가 다르다는 것이다. AWS lambda의 설명문을 처음에 읽으면, 마치 서버 로직은 AWS에 존재하고 언제나 최적의 속도로 실행될 것 처럼 느껴진다. 하지만 실상은 매우 다른데, 실행 빈도가 낮은 함수는 AWS가 콜드 상태, 즉 배포 중단 상태로 둔다. 늘 실행되지 않는자원을 항상 켜 두는 것은 AWS입장에서 극악의 효율을 보이기 때문이다(실행 시간에 비례해 lambda를 과금하기 때문에 그렇다). 따라서 자주 쓰이지 않는 로직의 경우 반응 속도가 느리고, 적당한 빈도로 실행되는 경우 이 함수가 cold stage에 들어가는 것을 막기 위해 주기적으로 해당 함수를 call 해야 하는데 이는 자본이나 개발 구성요소간 연결성을 파악하는 데 있어서도 매우 큰 낭비이다. 따라서 AWS amplify: data는 최종적으로 선택하지 못했다.
이후로 prisma등 ORM과 GraphQL의 구현을 섞은 다양한 구현체들을 검토했고, 대체로 비슷한 모델을 제시했지만 전부 다 기한 내로 구현하는 것은 불가능해 보였다(우리 서비스를 기획하고 구현하는 데 주어진 시간은 약 6개월 정도였고, 기획 단계에서 이미 2달이 소비되었다). 그러던 와중 정말 다행스럽게도, Hasura라는 기술을 발견하게 되었다.
Hasura: 스타트업은 언제든지 유연하게 변경할 여지를 남겨야 한다.
Hasura를 도입하던 시점에서의 기술적 기반은 다음과 같다. PostgreSQL RDBMS + Haskell API구현체. 현재는 서비스가 더욱 발달해 MSSQL, MySQL, BigQuery등 다양한 Data source를 연결할 수 있지만 현 시점에서도 가장 중대한 기술적 기반은 저 두가지다.
내가 대학교를 다닐 떄는 RDBMS는 마치 구시대의 산물처럼 사람들이 떠들어대던 시기였다. 그러나 나는 이런 시각에 동의하지 않는다. 물론 훌륭한 NoSQL이 많이 등장한 것도 사실이다(개인적인 견해인데, NoSQL은 메타버스나 UX, 햅틱처럼 사람들이 제대로 정의내리기도 전에 먼저 남용되어버린 버즈워드 중 하나라고 생각한다). 그러나 만능의 기술적 선택은 존재하지 않는다. 만약 그런게 있었다면 시장은 이전 세대를 완전히 버리고 빠르게 다음 세대로 이주했을 것이다. 어떤 기술에건 학습 난이도의 측면이건, 성능의 측면이건, 비용의 측면이건 단점은 존재한다. NoSQL에도 다양한 단점들이 존재한다. 그 중 첫 번째가 무결성 검증의 난해성, 두 번쨰가 조직의 실수에대한 대응책 전무라는 점이다.
NoSQL에는 다양한 종류가 존재하지만, 그 중 가장 대중적인 형태가 Key-value 저장소 형태, 다른 하나가 문서기반의 구현체 두 가지다. 전자의 가장 유명한 사례로 카산드라, 후자의 형태로 가장 유명한 것이 MongoDB이다. 하둡 등은 오로지 분석을 위한 서비스로 보는 것이 가장 적절하기 때문에 서비스 구현을 위해서 채택하기에는 적합하지 않다.
이 중 그나마 시장에서 많이 받아들여지고 있는 것이 MongoDB정도인데, 문서를 바로 저장하기 때문에 프론트엔드를 개발하는(그러니까 데이터 관점에서) 개발자가 단 한번의 실수를 하더라도 이를 되돌리기 매우 힘들다. RDBMS는 말하자면 서비스 입장에서 데이터에 타입을 부여하는 것이고, 타입은 프로그래밍 언어 관점에서 수 없이 많은 예측 불가한 문제 중에 사전에 예측 가능한 것으로 바꿔낸 몇 안 되는 사례이다. 이를 포기하면서MongoDB로 갔을 때 얻는 이점은 새로운 컬렉션 추가(RDBMS에서는 테이블에 대응되는) 없이 서비스 변경하기의 용이함, 샤딩 정도인데(이는 엔진 자체의 장점에 가까웠다). 전자의 경우 저장은 쉽지만 후에 검색을 위해 인덱스를 다시 부여할 때 매우 긴 다운타임을 요구해 불가능에 가깝고, 후자의 경우 후술하겠지만 대안 기술에 대체제가 존재하기 때문에 강력한 특장점이라고 보기는 힘들었다.
반면 RDBMS는 데이터베이스에 일반적으로 기대하는 일관성의 면에서도, 서비스의 유연한 확장과 견고한 구조, 다양한 기능성 전체의 면에서 바라봤을때 우리 서비스에 가장 안성맞춤이라고 여겨 선택하게 되었다. 이 때MongoDB에서 취하지 못해 너무나 아쉬웠던 기능이 바로 샤딩이었는데, 놀랍게도 확장을 사용하면PostgreSQL은 샤딩이 가능했다(가장 대표적으로 citus가 있다). 물론 단일 DB에서 테이블의 덩치가 커질수록PostgreSQL은 성능이 떨어지는 단점이 있긴 했지만(인덱싱 전략의 문제 때문이다), 샤딩이라는 해결책으로 이를 해결할 수 있어 보였기에 우리는 최종적으로 DBMS는 PostgreSQL로 선택하기로 했다. 때맞춰 Hasura를 발견한 것은 천운이었다.
Hasura는 Haskell이라는 함수형 프로그래밍 언어를 기반으로 구현되었다. 개발자가 데이터 타입을 선언하고(테이블과 스키마를 정의하고), 이를 기반으로 관계를 설정하고, 인증 토큰에 존재하는 값을 Session variable로 사용해 사용자의 권한을 제한할 수 있다. 가장 중요한 것은, 이 과정에서 코드를 단 1줄도 작성하지 않아도 된다는 것이다. 사실상 일반적인 서버 API를 개발할 때 필요한 일들을 거의 다 핸들링해 주는 셈이다. 물론 이는 AWS data에서도 사용 가능한 것이다. 하지만 위에서 말했듯, Hasura는 PostgreSQL을 사용하고 있었고, 이는 내가 원할떄는 언제든 필요한 기능을 덧붙여 쓸 수 있다는 뜻이다(SQL의 범용성은 매우 높다). 거기다 RDBMS데이터 소스는 관리하기 힘들다면 언제든 RDS/Aurora와 같은 완전관리형 서비스로 책임을 이관할 수 있었다.
거기다 실시간으로 여러 소비자들에게 정보를 제공하기 위한 websocket기반의 subscription, 서버사이드 캐싱, 그리고 호환 client인 apollo의 강력한 기능성을 고려하면, 사실상 서버구현비용을 0으로 내릴 수 있는 매우좋은 선택지로 보였다.
서비스를 런칭한 지 이제 약 2년 정도가 되었다. 그 동안 나는 단 한 순간도 서버사이드 기능구현에 어려움을 겪지않았다. 또한 Index를 걸지 않았을 때를 제외하면 성능 이슈도 겪지 않았다. 아마 내가 이 스타트업에서 가장 잘내린, 향후 2년을 결정한 가장 훌륭한 선택이었던 것 같다. 또한 이를 결정하며 우리 스타트업의 기술적 기조도 결정할 수 있었다. 그것은 바로 ‘언제건 변경 가능한 유연성과 성능 사이의 절충점’이다.
REST API의 구현: 서비스는 내부 서비스만으로 구현되지 않는다.
REST API를 구현하기 싫다면서 갑자기 왜 이 이야기를 꺼내는 것인지 궁금한 사람들이 많을 것이다. 이를 이해하기 위해서는 간단하게나마 튜링 머신에 대해서 이해할 필요가 있다.
튜링의 모델에 따르면, 컴퓨터는 수행할 작업과 이에 필요한 데이터가 기록된 무한의 테이프, 그리고 이를 읽고 처리하는 헤드 두 가지로 구성된다. 전자의 경우 컴퓨터의 핵심 기능 중 하나인 기억, 후자의 경우 처리를 의미한다. 그리고 이는 프로그래밍 언어에 대응했을 때 전자는 변수, 후자는 함수에 대응된다. 서버 또한 클라이언트의 요청을 처리하는 일종의 거대한 컴퓨터로 추상화해 생각할 수 있으며, 이 때 전자의 기능성이 서버의 Database, 후자가 클라이언트의 요청을 처리하는 서버 API에 대응할 수 있다
GraphQL은, 특히 우리 회사의 기술적 선택을 고려하면 데이터 자체를 외부로 공개하는 것에 가깝다. 즉 서버 자체를 하나의 거대한 저장소로 취급하고, 이에 대한 동작 역시 클라이언트에서 규정하기 때문에 사실상 서버는 거대한 메모리로 모델링된 것으로 볼 수 있다. REST API는 이와 달리 서버에서 대부분의 동작을 정의하고 클라이언트에서는 정해진 동작을 다만 따르게 되기 때문에 오히려 서버는 거대한 함수 모음집으로 보는 것이 타당하다. 전자의 경우 클라이언트는 소비자에게 가까운 메모리 층위의 데이터와 동작을 동시에 처리/저장하고, 후자의 경우클라이언트는 단지 데이터만을 저장하고 서버에 구체적인 동작의 모음집들을 수행하도록 의뢰하게 된다.
따라서 단지 서비스 내부의 데이터를 변경하는 것으로 구현이 끝난다면 이 떄는 GraphQL만을 사용해도 충분하다. 그러나 점차 필수가 되는 SSO에 대한 대응은? 결제 기능 구현은? SSO 구현시 애플 플랫폼에 따라붙을 Sign in with Apple ID는?
외부 서비스의 경우 구체적인 데이터 모델을 알 수 없다. 단지 이들도 또 다른 REST API를 정의할 뿐이다. 즉 동작을 중심으로 구동되는 서비스의 경우, 어쩔 수 없이 REST API가 일부나마 필요한 것이다.
따라서 GraphQL과 REST API 사이에 명확한 구분기준을 만들었고, 그것은 ‘동작’을 중심으로 구현되는가, ‘데이터’를 중심으로 정의되는가였다. 이는 일반적인 경우에 ‘외부서비스를 사용하는가’ 여부에 따라 결정되었다.
다음으로 점검해야 할 사항은 서버 개발 언어와 프레임워크를 결정하는 것이었다. 당시 나는Go/Ocaml/Kotlin/Swift/Rust/C++(C는 안 좋아한다. 로우레벨 건드릴 때 어쩔 수 없이 쓰는 쪽에 더 가깝다)/Python/JavaScript-TypeScript/Java등을 다뤄 봤었다. 일단 내가 만지기 싫은 C++을 제외했다(농담이고, 배포 시간과 언어 자체의 개발 난이도를 고려해 배제했다). 추후에 개발자를 구하기 힘들 것 같은Go/Ocaml/Rust도 배제했다. 서버 개발에 잘 쓰이지 않는 Swift도 배제했다. Java는 지나치게 레거시화되었고, 훌륭한 Kotlin이라는 대체재가 존재했기에 배제했다. 남은 것은 Kotlin, JavaScript, TypeScript, Python정도였다.
이쯤 되니 상당히 고민이 된다. 네 언어 모두 시장에서 많이 쓰이고, 사랑받는(적어도 지지층은 확실히 큰)언어다. 넷 중 그 무엇을 골라도 개발자를 추가로 충원하기에도 좋으며, 성능 또한 모두 하한선은 검증된 상태다. 여기서 우리는 다시 요구사항을 명확히 할 필요가 있었다
- 빠른 배포
- 안정적인 개발
- 필요한 최신 기능의 빠른 도입
여기까지 정하고 나자 Kotlin과 Python을 배제할 수 있었다. 코틀린은 물론 너무나 훌륭한 언어고 개인 프로젝트에서는 여전히 사랑해 마지않는 언어 순위에 늘 들어가지만, 빌드를 필수적으로 필요로 하고 다른 언어들에 비해서는 한국에서 서버 개발에 쓰이는 빈도가 그다지 높지 않다고 여겼다. Python은 데이터 사이언스에 지나치게 특화되어 있는데 반해 최근 서버 개발을 위한 기능들은 그다지 많이 발달하지 못했다.
따라서 최종적으로는 TypeScript와 JavaScript가 남았는데, 그 중에서도 후자를 선택했다. 그 이유는 대부분의서버 로직은 Hasura만으로 핸들링이 가능했고, 안전성을 위한 코드를 배제하고 더욱 빠르게 개발할 수 있었으며, 필요하다면 TypeScript로 우선 이주한 뒤 Typed된 또 다른 언어로 이주하기에 매우 적합해 보였기 때문이다. 현재 서비스에는 JavaScript에서 Koa.js를 사용해 서버를 구현했고, 여기에 인증 및 알림 발송을 위해 Firebase admin sdk, 결제를 위해 Iamport 등을 혼용해서 사용중이다. 카카오나 애플 아이디 등으로 로그인한 후에는JWT를 발급하는 일도 REST API에서 한다.
후회하진 않지만 예측하지 못한 문제는 있었다. 바로 최근 버전에서 발생하기 시작한 Memory leak issue이다. Node.js환경을 업그레이드 한 후 문제가 발생했는데, 다만 타입에 관련한 코드를 간결하게 표현하기위해서는 꼭 필요했기 때문에 어쩔 수 없이 사용중이다. 해당 문제는 ECS fargate에서 메모리 leak이 발생할 때대체 인스턴스(task)를 시작하고 현재 요청은 마지막으로 처리한 후 해당 instance를 꺼지게 하는 것으로 해결했다.
웹 대시보드: React admin과의 만남, vue-element-admin으로의 이전
웹 대시보드. 매우 힘든 일이다. 사용자 클라이언트 개발은 돈을 벌어 오고, 서버 개발은 돈을 아낀다. 하지만 관리자층을 위한 웹 대시보드는 직접적으로 수익을 창출하지도 않고, 반면에 가장 방대한 기능성을 요구하기 때문에무척 개발하기 까다로운 일이다. 이 문제는 어떻게 해결해야 할까?
마찬가지로 Hasura와 같이 개발 없이 서비스를 구현할 수 있는 방법을 찾았다. 놀랍게도 있었다. React admin이라는 오픈소스 프로젝트는 React.js, Redux, Redux-saga, React-router 등 이미 유명한 React 기반의 기술들을 기반으로, Django admin과 같이 데이터 모델을 명기하면(여기서는 Data provider라고 불렀다) 데이터를 직접적으로 핸들링할 수 있는 멋진 프로젝트였다. 거기다 기본 디자인 컴포넌트로 Material design을 채택하고 있었는데, 개인적으로 머터리얼 디자인 가이드라인은 사용자 관점에서 그다지 훌륭하다고 생각하지 않지만 디자이너 없이 최소 한도 수준의 디자인을 만들수 있다는 점에서는 괜찮은 선택이었다.
문서를 열심히 따라하다 보니 초기 구현체를 만드는데는 약 3주 정도가 소요된 것 같다(물론 코드는 개판이었지만, 그래도 괜찮았다. 그래도 우리는 코드 구현부를 최대한 줄였으니까). 이후에도 디자인이 예쁘지 않다는 불만은자주 나왔지만 어차피 회사에 디자이너가 있는 것도 아니었고(그래서 심지어는 앱 초기 디자인도 내가 다 했다), 쓰기 불편하다는 요구사항도 직접 결제를 하지 않는 소비자들을 대상으로 하는 제품이니만큼 거의 무시했다(카카오페이 대시보드도 구리긴 마찬가지다). 그러나 점점 서비스에 들어와야 할 벤더가 많아지며 대시보드의 불편함이사실상 서비스의 성장을 가로막게 되며, 우리는 서비스를 구현할 새로운 기술적 대안을 찾아야 했다.
앵귤러와 리액트를 싫어하니 클라이언트의 뷰-모델을 담당하는 라이브러리의 선택지는 사실상 vue.js밖에 남지않았다. 조금 더 자세히 설명하면, 컴포넌트 단위로 모델-스타일-컨트롤러를 분리해서 보기에도, 반응형으로 데이터의 변화와 드로잉을 분리하지 않고 생각하기에도 Vue의 모델이 더 나아보였다(일단 react.js는 너무 verbose하다. 최종 사용자를 위한 제품이 아님을 다시 상기하자).
그래도 서비스를 구현하는 난이도를 더 낮추고 싶었고, 유용한 뷰 컴포넌트와 어드민 대시보드를 구현하는데 필요한 기본 구조는 갖춘 기술을 원했고, 그 결과 vue, element-ui, vue-router를 결합한 vue-element-admin이라는 라이브러리를 찾았다.
기본 골격은 사실상 react admin과 유사하지만, 기존에도 훌륭한 기능성 덕분에 잘 활용해 온 element-ui를 사용했다는 점이 무척 만족스러웠다(사실 지금에야 와서 말하는 거지만, 단순 서버 쿼리로 해결되는 기능성을 구현하는 데는 vue기반의 코드를 작성하는 것과 react admin과 개발 난이도에 큰 차이가 없었다).
앱 클라이언트: Flutter, 가장 큰 도박
앞서 말했듯, 서비스 중심의 스타트업은 클라이언트 개발이 다른 무엇보다도 중요하다(최소한 나는 그렇게 생각한다). 이 때문에 우리는 클라이언트 개발에 다양한 선택지들을 시험하고 그 중 우리 회사의 기술적 수요에 가장 적합한 것을 선택해야 했다. 이를 위해 플랫폼부터 고민하기 시작했다.
모바일 환경을 위해서는 우선 설치형 클라이언트와 웹 앱, 그리고 이 둘의 중간자적 포지션에 해당하는 프로그레시브 웹 앱 세 가지를 생각할 수 있다. 이 중 가장 먼저 포기한 것이 프로그레시브 웹 앱이었다. 물론 에셋과 로직의경계를 불분명하게 만드는 프로그레시브 웹 앱의 개념은 분명 매력적이다. 그러나 모바일 환경은 구글 뿐 아니라애플 플랫폼도 동시에 고려해야 하기 마련이다. 구글 플랫폼만을 중심으로 돌아가는 프로그레시브 웹 앱은 사용자의 직관성 면에서도 여러 모로 좋지 않다고 생각했기 때문에 포기하게 되었다. 최근 몇 년동안 프로그레시브 웹 앱으로 개발된 플랫폼에 일반 유저들이 별로 유입되지 않았다는 사실은 그 방증이라고 생각한다.
웹 앱은 사실상 웹 사이트를 만들자는 것인데, 알림 전송과 같은 서비스야 카카오톡과 같은 플랫폼으로 대체할 수있다손 치더라도 풀 사이즈 디스플레이가 일상이 된 시대에 우리가 원하는 수준의 만족감을 만들어내긴 힘들었다(다양한 네이티브 피처의 사용이 난해하다는 것은 덤이다). 결국 우리는 네이티브 클라이언트 개발을 해야 한다는쪽으로 가닥이 잡혔다.
남은 것은 어떤 형태로 개발을 하는가이다. 우선 빠른 개발을 위해서는 웹 기반 기술로 개발하는 웹뷰/하이브리드앱 정도를 생각할 수 있고, 여기서 최우선순위로 고려한 것은 Ionic + capacitor 조합이었다. 그 다음으로 고려한것은 Flutter/React native와 같은 3rd party lang to native 개발 플랫폼이었다. 애초에 Swift + cocoa/Kotlin + Android API로 대표되는 완전한 네이티브 개발 환경은 고려조차 하지 않았다. 우리는 그럴 시간이 없었으니까.
웹뷰 앱과 네이티브-언어 채널형 크로스플랫폼 개발툴의 가장 큰 비교 우위는 배포 편의성/성능일 것이다. 물론React native + Codepush 조합은 이 둘의 장점을 모두 취하기에 가장 이상적인 솔루션일 수 있었지만, 기존에이미 질릴대로 질려버린 React native 디버깅 경험이 발목을 잡았다(단 한번도 브레이크포인트가 내가 원하는 곳에 걸려 본 기억이 없다). 솔직히 순수 웹뷰 앱은 toss마냥 극강의 웹 프론트엔드 인력과 시간을 갈아넣지 않는 이상 달성하기 힘들 것이라고 생각했기 때문에 하이브리드 앱이 좋은 선택지였겠지만, 생각보다 기술적 자유도가 낮아 원하는 수준의 피처를 개발하기는 힘들다는 것이 발목을 잡았다.
결국 개별 플랫폼 중 무엇을 고르냐는 문제로 약 일주일이 흘러갔다. 더 이상은 기다릴 수가 없었다. 그래서 우리팀은(다시 말해 나는) 개발 환경을 고르는 전략부터 다시 고민했다.
- 가장 장점이 강력한 플랫폼을 쓸 것인지
- 가장 단점이 덜 거슬리는 플랫폼을 쓸 것인지
솔직히 언제나 가장 잘 풀렸을 경우보다는 언제나 가장 잘못될 경우를 우선적으로 생각하는 사람이기에 2를 전략으로 고르기로 했다. 그러자 자연스레 답은 Flutter로 귀결되었다. 그 다음으로 고민해야 할 것은 이것이었다.
“선택한 개발 플랫폼의 단점/다른 플랫폼에 비한 비교열위를 극복할 방법”
사실 이는 React native에 대비한 단점을 생각만 해봐도 단순한 문제였다. 우리는 빠른 배포가 필요했다. 특히나기능 변경이 잦은 스타트업에서 이는 가장 중요한 고려 요소였다. 오랜 시간 고민한 끝에 Codepush와 같이 에셋과 응용 로직을 동일 선상에 두고 원격으로 업데이트 가능한 플랫폼은 Flutter에 도입할 수 없으니, 차선책으로 앱의 기능성을 어느 정도 원격에서 수정할 수 있는지 검토해 보기로 했다.
장고 끝에 Firebase의 Remote config를 사용해, 앱에서 사용할 사용자의 GraphQL쿼리를 사용자 디바이스에저장하고, 서버에서도 실행 가능한 쿼리를 2중으로 제약하여(hasura의 기능이다), 서버 접근에 대한 기능성을 수정하고, 전체 서비스 데이터의 일관성을 해칠 수 있기 때문에 일률적 업데이트를 수행해야 하는 경우에는Remote config를 통해 사용자 클라이언트의 업데이트를 강제하기로 했다.
추가로 개발자를 구할 수 있는가도 문제이기는 했다. 이 부분은 Flutter가 최근 성장세가 가파르다는 것과, 한국에서도 점차 사용층이 늘기 시작했다는 점, 그리고 기본적인 구조가 React와 크게 다르지 않다는 점을 믿고 가기로했다.
최종적으로 내 선택은 어땠을까. 일부의 성공, 그리고 일부의 실패로 평가할 수 있을 것 같다. Dart는 내 생각보다더 괜찮은 언어였다. 전반적인 코드 느낌이 자바스크립트와 자바에서 내가 가장 싫어하는 부분들만 섞은 형태라맘에 안 들었는데, Dart2까지 업그레이드 한 이후로 상당히 효울적으로 개발을 할 수 있었다. 또한 Flutter의 성능이 매우 개선되어 드로잉으로는 거의 네이티브에 준하는 개발을 할 수 있었다(물론 최근에 대두되는 120hz이상디스플레이까지 고려하면 좀 애매하다). 단점으로는 여전히 개발자를 구하기 힘들다는 점, 그리고 데이터 어널리틱스를 기반으로 한 A/B 테스트를 배포하고 수행하는 것은 비효율적이라는 점이다.
서버 마이그레이션: 예상보다 빠른 비즈니스 성장
서비스를 최초로 출시한 것이 2020년 7월 경이었다. 코로나로 여러 산업이 흔들리는 와중에 출시 직후 이벤트를했는데, 상당히 반응이 좋아서 출시 직후 시간당 접속자 수가 약 10000명을 달성하게 되었다. 좋은 신호였지만Flutter의 렌더링 사이클과 GraphQL클라이언트에 대한 이해 부족으로 개별 사용자의 클라이언트 전체가 초당약 600000회의 요청을 보내게 되었다. 당연히 서비스는 아주 빠르게 뻗었고, 당시 테스트로 배포한 상황이라EC2인스턴스를 재부팅해야만 했다.
물론 이후에 앱을 빠르게 재배포하여 문제는 약 이틀만에 해결되었다. 그러나 사용자들을 기다리게 하는 것은 곧잠재적 수익 및 성장 동력에 대한 심각한 위협 요소였고, 우리는 출시 이후 약 2주만에 ECS Fargate/RDS를 기반으로 서버 근간을 이전하기로 했다.
ECS Fargate/RDS조합은 Promising해 보였다. 물론 Fargate는 상당히 비싸다. EC2를 기준으로 단위 자원당비용을 비교하면 2배 정도가 된다. 하지만 Fargate는 언제든 아마존이 재배포할 수 있는 Spot자원을 활용할 경우70퍼센트 가격을 할인해 주므로, 결과적으로는 EC2를 프로비저닝해 쓸 때보다 약 40퍼센트 저렴한 가격 대비성능을 누릴 수 있었고, 결정적으로 자원을 미리 프로비저닝 할 필요가 없다는 점이 무척 매력적이었다. 다음으로RDS인스턴스를 고르는 것 또한 그다지 어렵지 않았다. Hasura dump API를 사용해 덤프 데이터를 다운로드 받고, 해당 쿼리를 RDS인스턴스에서 다시 실행하면 그만이었다.
중요한 것은, 데이터의 일관성을 해치지 않고 이들을 이전할 수 있는 방법이었다. 여러 번 고민해봤지만 이전/테스트/배포는 모두 시간이 걸리는 일이고, 결국 중간에 짧게나마 다운타임이 발생하는 것은 어쩔 수 없는 일이었다. 어널리틱스로 분석한 결과, 우리 서비스는 새벽 3~4시 사이의 접속률이 확정적으로 시간당 0명에 가까웠고, 우리는이 시간에 Remote config를 활용, 강제로 사용자들이 서비스를 사용하지 못하도록 막은 뒤 미리 사전에 연습한배포 프로세스를 실행했다. 결과는 성공적이어서 예정된 30분 보다 이른 시간인 10분만에 이전은 끝났고, 이후 약20분에 거친 정상 동작 테스트 끝에 서비스는 재 런칭했다. 하지만 RDS는 그렇다 쳐도 EC2와ECS사이에 유의미한 사용자의 만족도 차이가 있는지 궁금했는데, Remote config를 기반으로 한 A/B테스트를 통해 측정한 결과두 집단 사이에서 서버 요청 및 응답 시간에는 유의미한 경향성의 차이가 보이지 않았다. 이렇게 배포 시간은 짧았지만, 실제 전체 집단에 적용하기까지는 약 2주의 시간이 걸렸다. 그 동안 처음엔 10퍼센트, 그 다음엔 30 퍼센트, 그 다음엔 70 퍼센트 순으로 사용자들을 테스트했고, 이전 기간 동안 불편을 호소한 고객은 없었다.
그 다음으로 해결해야 할 문제는 데이터베이스 인스턴스의 업데이트 다운타임이었다. 이를 해결할 방법을 찾았지만, 결론적으로 데이터베이스 시스템을 수정할 때 다운타임은 불가피했다. 차선책으로 우리는 AWS RDS Aurora를 검토했는데, 기존 데이터베이스의 읽기 복제본을 만든 다음, 높은 성능의 해당 인스턴스로 Failback하면 100밀리세컨드 이내의 시간 단위로 동기화가 되기 때문에 사실상 적용 가능한 유일한 솔루션으로 보였다.
여기에 관리형 서비스를 사용하며 점점 상승하는 관리 난이도 때문에 DB세션을 납품 기한 내로 정확히 컨트롤하는 것이 힘들었기 때문에, 관리형 RDS session pool 관리 서비스인 RDS proxy를 사용해 데이터베이스에 지나치게 많은 연결이 생성되는 것을 방지했다.
백엔드와 관련된 기능 중 백오피스가 있는데, 간단한 데이터 어널리틱스(즉 BI)의 경우 Redash라는 오픈소스 프로젝트를 도입했고, 전문적인 데이터 어널리틱스의 경우는 Amplitude와 Mixpanel을 활용했으며, 이들에서 발생한 Raw data를 추후에 주기적으로 BigQuery와 같은 대규모 데이터 분석 도구에 재배치하기로 했다(현 시점에서는 여전히 유의미한 데이터가 쌓이지 않기에 포기했다…).
앞으로 해야 할 일
앞으로 해야 할 일은 물론 산더미처럼 많다. 앱 관리자를 뽑기 위해서 앱 코드를 정리한 것이 당장 일주일 전에야끝났다(한달 안에 끝내야 했다). 여전히 웹 대시보드 정리 작업은 손도 못 대고 있다. 그나마 vue와 flutter모두GraphQL클라이언트의 기본 구조가 유사하고, State management 전략 또한 라이브러리들을 사용하면 유사하게 관리할 수 있는 전략이 있기 떄문에 우선순위는 높이기 힘들지만 시간 날 때마다 조금씩 정리해 두고, 다른 개발자를 고용하면 쉽게 해결할 수 있을 것 같다. 남은 것은 AWS의 cloudsearch를 활용한 좀 더 자연스러운 자연어검색기능 구현 정도와 데이터 어널리틱스에 대한 본격적 대비, 그리고 모듈화한 앱 컴포넌트들의 redesign정도인것 같다. 추가 기능 구현 요청이 은 당연히 들어오겠지만, 적어도 현 시점에서 그 동안 쌓아온 기술 부채는 거의 청산한 느낌이다. 그것도 신기능을 꾸준히 개발하면서 이룬 것이니, 충분히 자부심을 가져도 좋을 것 같다.
그래서 새 직원은 어디서 구하지...