뱅크샐러드는 어떻게 레거시 서비스를 박살 내는가

뱅크샐러드는 앱 출시 이후 약 3년의 세월 동안 폭발적인 성장을 겪어왔습니다. 늘어나는 사용자 수 만큼 더 많은 문제를 해결할 수 있는 서비스로 발전해왔고, 이 과정에서 뱅크샐러드의 기술적인 복잡도는 점점 더 높아졌습니다. 여느 빠르게 성장하는 스타트업이 그렇듯, 뱅크샐러드 역시 비즈니스의 빠른 성장에 집중하여 이러한 복잡도 제어에 있어 미진하게 대처하던 시기가 있었습니다.

그 결과 하나의 마이크로서비스microservice로 시작했던 jg라는 이름의 서버(이하 레거시 서비스)에 주먹구구식 기능 추가가 계속되면서 사실상 뱅크샐러드의 핵심 비즈니스 로직을 담당하는 거대한 모놀리식monolithic 서버가 되어 코드를 추가하기도 수정하기도 어려운 서비스가 되었습니다. 그러나 정작 더 심각한 문제는 이 레거시 서비스의 비즈니스 로직을 정확히 아는 사람이 없다는 점이었습니다. 기획 문서는 있었으나 급변하는 시장 상황 속에서 낡아버린 기획 문서가 대부분이었고, 단위 테스트 커버리지 자체는 높은 편이었으나 정작 가장 까다로운 비즈니스 로직에 대한 테스트는 빈약하여 서비스의 동작을 확신하기에는 늘 자신감이 부족할 수밖에 없었습니다. 설상가상으로 Python과 MongoDB 기반으로 운영된 서비스인 탓에 유연한 타입 체계를 기반으로 빠르게 아이디어를 실험해보기에는 좋았으나, 이렇게 복잡도가 눈덩이처럼 불어난 상태에서는 오히려 그 유연함이 성장의 발목을 잡고 있었습니다.

약 3년 이상 뱅크샐러드의 성장을 견인한 고마운 서비스였지만 저희가 더 빠르게 성장하기 위해서는 이 레거시 서비스가 가진 복잡도를 반드시 제거해야만 했습니다. 저희는 이 거대한 레거시 서비스를 분해decomposition하는 방법을 택했습니다. 복잡도가 높은 서비스 하나를 복잡도가 낮은 서비스 여럿으로 해체하는 방법을 택한 것이죠. 여기서 저희의 첫 번째 고민이 시작됩니다. 이 거대한 서비스를 어떤 모습으로 나눌 것인가.

우리가 일하는 방식에 맞는 서비스 구조를 취하다

거대한 레거시 서비스를 어떤 마이크로서비스로 분해할지 결정하는 일은 전체 프로젝트 과정을 돌이켜볼 때 가장 막막한 일 중 하나였습니다. 레거시 서비스가 거대했던 만큼이나 이 설계 결정을 바꾸는 일은 비쌀 수밖에 없었고 이와 동시에 설계가 늦어지는 비용 역시 컸습니다. [1] 설계가 늦어질수록 레거시 서비스의 분해 작업 역시 늦어질 수밖에 없고, 이렇게 될 경우 더 오랜 시간 뱅크샐러드 서비스 개선이 제한될 수밖에 없다는 뜻이기 때문입니다. 그러나 누구도 이 거대한 레거시 서비스의 모든 비즈니스 로직을 아주 세밀한 수준까지 알지 못했고, 따라서 모든 요구사항이 정확히 드러나지 않은 상황에서 올바른 설계를 결정하기에는 상상 이상의 많은 시간이 필요해 보였습니다. 또한 여기에 시간을 더 쓴다고 과연 모든 요구사항을 정확히 파악하고 판단할 수 있을지에 대한 확신 역시 부족했습니다. 따라서 우리가 모르는 영역이 존재함을, 이러한 불확실성의 영역이 존재함을 염두에 두고 서비스 구조를 도출하기로 했습니다.

이런 불확실성을 제어하기 위해 ‘내가 확실히 알고 있는 무언가’로부터 생각을 시작해봤고, 저희가 당시에 삼은 기준은 ‘우리 조직’이었습니다. 콘웨이의 법칙대로 “소프트웨어의 구조는 이 소프트웨어를 만들어낸 조직의 커뮤니케이션 구조를 따른다”는 말에 착안한 것이죠. 달리 말하면 저희가 새롭게 구상할 서비스 구조를 뱅크샐러드 조직이 일하는 방식에 맞춰 생각해본 셈입니다. [2]

뱅크샐러드는 사용자 임팩트를 만들어낼 수 있는 최적의 규모와 구성을 갖춘 조직 단위인 ‘스쿼드’ 체계로 움직입니다. 스쿼드는 피자 한 판 (저처럼 피자를 좋아하면 두 판) 규모의 조직으로, 뱅크샐러드 각 기능의 오너십을 가지고 주도적으로 실험하고 학습하는 조직입니다. [3] 즉 이 레거시 서비스 분해를 통해 저희가 얻고자 했던 모습인, 각 스쿼드가 제품상에서도, 그리고 기술적으로도 독립된 오너십을 가지고 주도적으로 실험해볼 수 있는 서비스 구조를 목표로 제품·기술·조직 구조상으로도 강하게 결합되고 느슨하게 연결된 다음과 같은 15개의 마이크로서비스 구조를 도출할 수 있었습니다.

각각 연금을 담당하는 annuity, 은행 계좌를 담당하는 bank account, 카드를 담당하는 card, 투자를 담당하는 invest, 가계부를 담당하는 account book, 외부 기관 데이터를 담당하는 organization, 금융 데이터 수집을 담당하는 collect, 자산 화면을 구성하는 financial asset, 환율을 처리하는 currency, 데이터 태깅을 담당하는 tag, 카테고리 데이터를 담당하는 category, 예산을 담당하는 budget, 보험을 담당하는 insurance, 실물 자산을 처리하는 real asset, 대출을 담당하는 loan으로 구성되어있다.
Figure 1 | 총 15개의 마이크로서비스 구성도

구조에 대한 고민이 해결되자 이제 어떻게 분해를 진행할 것인지에 관한 실질적인 고민이 시작되었습니다. 레거시 서비스를 분해하자는 막연한 생각으로 시작했던 프로젝트가 15개의 마이크로서비스를 만드는 작업으로 정의되는 순간 덜컥 저희가 진행하는 프로젝트의 크기를 실감하기 시작했습니다. 이렇게나 많고 복잡한 처리를 수행하던 레거시 서비스를 분해해 다시 만들었을 때, 과연 우리는 어떻게 우리가 성공적으로 분해했는지 알 수 있을까? 어떻게 자신감을 가지고 레거시 서비스를 대체할 수 있을까? 어떻게 하면 장애 없이 이 모든 과정을 진행할 수 있을까? 그리고 이렇게 큰 프로젝트가 과연 성공적으로 흘러가는 중인지 우리는 어떻게 알 수 있을까? 등의 고민이 이어졌습니다.

프로젝트의 복잡도를 제어한다

기술적으로 해결해야 하는 문제가 복잡하고 어려운 만큼, 어떻게든 이 문제를 단순화하여 문제의 난이도를 낮추는 접근이 필요했습니다. [4] 그래서 저희는 크게 세 가지 접근을 취했습니다. (1) 우리가 해결해야 할 문제를 단순화하여 레거시 서비스와 신규 서비스 응답이 얼마나 일치하는지를 측정하는 문제로 바꾼다. (2) 프로젝트의 목표가 아닌 것을 분명히 하고 원칙에 집중한다. (3) 매일, 필요하다면 두 시간에 한 번씩 만나 자주 상황을 공유한다.

우리의 나침반이 되어준 섀도잉

저희가 가장 먼저 시도했던 건 문제를 단순하게 바꾸어 프로젝트의 가시성을 확보하는 일이었습니다. 이를 위해 저희는 섀도잉shadowing을 도입했습니다. 섀도잉은 말 그대로 같은 요청에 대해 기존 서비스와 새로운 서비스의 응답이 일치하는지 비교하는 방법을 뜻합니다.

하나의 요청에 대해 두 서비스의 응답을 비교하여 기존 서비스 응답을 내보내는 것을 묘사하는 이미지
Figure 2 | 섀도잉의 대략적인 흐름

레거시 서비스가 기존과 동일하게 모든 HTTP 요청을 처리하여 적절한 응답을 반환하되, 이때 해당 HTTP 요청을 신규 서비스로도 동일하게 요청을 보내도록 했습니다. 이후 신규 서비스의 응답과 기존 서비스의 응답이 일치하는지 여부를 판단해 이를 스탯statsd을 찍도록 처리했고, 실제로 어떤 값이 어떻게 다른지는 키바나Kibana를 통해 검색해볼 수 있도록 로깅했습니다.

// A
{
  "name": "banksalad",
  "favorites": ["tech", "data", "product"],
  "url": "https://career.banksalad.com/jobs/?category=tech",
}

// B
{
  "name": "banksalad",
  "favorites": ["tech", "impact", "transparency"],
}

예를 들어 레거시 서비스가 어떤 요청에 대해 위 A와 같은 JSON을 응답으로 반환하고 새로운 서비스가 같은 요청에 대해 B와 같은 JSON을 응답으로 반환한다면 이 두 서비스의 응답이 일치하지 않는다고 판단할 수 있습니다. 이런 불일치를 디프diff라 칭합니다. 이러한 디프가 발생한다면 기존 서비스를 새로운 서비스로 대체했을 때 새로운 서비스가 기존 서비스와는 다르게 동작한다는 뜻이 됩니다. 이는 곧 버그나 장애로 이어질 수 있겠죠. 즉, 저희의 목표는 모든 신규 서비스의 디프를 0으로 만드는 것입니다. 기존 서비스의 완전한 대체, 사용자뿐 아니라 뱅크샐러드 구성원도 눈치채지 못할 만큼 기존과 완전히 동일하게 동작하도록 레거시 서비스를 분해하는 게 저희 프로젝트 OKR의 목표objective이고, 최종적으로 발생한 디프가 곧 핵심 결과key result에 해당합니다.

Kibana에서 diff를 확인할 때 보이는 모습을 담은 이미지
Figure 3 | Kibana에서 언제든 발생한 디프를 쉽게 확인할 수 있다

위 화면에서 볼 수 있듯이 키바나를 통해 서비스 중 수집된 모든 디프 로그를 한 건씩 구체적으로 확인하며 기존 서비스와 다른 동작을 쉽게 수정해나갈 수 있었습니다. 사소할 수 있으나 서비스 전면 장애를 일으킬 수도 있는 데이터 타입 체크부터 신규 서비스 JSON에 미포함된 JSON 키 등등 다양한 종류의 실수를 사전에 방지할 수 있었습니다.

.data.stocks.0.principal.amount
		old: 2385384.0
		new: 2385383.99

이번에 신규 서비스를 개발하면서는 MongoDB가 아닌 MySQL을, Python이 아닌 Go를 사용했는데 이 과정에서 MongoDB와 MySQL에서 소수점을 처리하는 방식이나 Python과 Go에서 소수점을 처리하는 방식이 달라 발생한, 개발자들이 쉽게 예상하고 확인할 수 없는 이런 다양한 예외 상황에 대해서도 디프를 통해 확인할 수 있었습니다.

.data.bank_accounts.0.opened_at
		old: 1988-06-14T00:00:00+09:00
		new: 1988-06-14T00:00:00+10:00

특히 기억에 남는 디프 중 하나는 언어별로 기본 타임존 처리 방식이 달라 발생한 디프였습니다. 1988년은 실제로 한국 표준시가 UTC+9가 아닌 UTC+10이었습니다. [5] 이 외에도 UTC 기준시 대비 8시간 27분 차이가 발생하는 등등 타임존과 관련된 다양한 버그가 발견된 바 있습니다. [6] 물론 이런 디프는 실제 서비스에 큰 영향을 끼치지는 않으나 만약 저희가 섀도잉이 아닌 사람을 투입해 직접 신규 서비스 안정성을 테스트했다면 아마도 발견하기 어려운 버그가 아니었을까 생각해봅니다.

이런 식으로 신규 서비스와 레거시 서비스가 얼마나 일치하는지 섀도잉 통해 자신감을 확보하여 점점 더 많은 요청을 신규 서비스에도 보내보도록 조정했습니다. 최초에는 전체 요청 중 1%의 요청만 신규 서비스로 유입 시켜 디프가 발생하는지를 보았고, 소수의 요청에 대해서 유의미한 디프가 발생하지 않으면 좀 더 많은 요청을 보내 더 다양한 데이터에 대해 신규 서비스가 검증받도록 했습니다.

또한 PUT/DELETE 등등 일반적으로 데이터 조작에 해당하는 HTTP 요청은 발생하는 디프와 관계없이 처음부터 모든 요청을 신규 서비스에도 보내도록 조치했는데, 이는 섀도잉과 무관하게 두 서비스(가 서로 다른 DB를 사용하니) 사이의 데이터 정합성을 맞추기 위한 듀얼 라이트dual write 처리 중 하나였습니다.

Chronograf에서 전체 진척도를 확인할 수 있는 대시보드 이미지
Figure 4 | Chronograf로 구축한 대시보드의 일부

이런 방식으로 수집한 디프가 전체 요청 중 얼마나 많은 응답에서 발생하는지 등을 한눈에 수치로 확인할 수 있는 대시보드를 크로노그래프Chronograf를 이용해 구축했습니다. 저희 프로젝트의 목표인 ‘신규 서비스가 레거시 서비스를 얼마나 대체했는지’를 확인할 수 있는 각종 지표를 쉽게 확인할 수 있도록 하여 전체 프로젝트 진척도의 가시성을 높였습니다.

Chronograf 대시보드에서 전체 진척도 외에도 각 서비스별 진척도를 확인할 수 있음을 보여주는 이미지
Figure 5 | 대시보드에서 보여지는 각 서비스 진행 상황

전체 진척도뿐 아니라 서비스별 진행 상황 역시 쉽게 볼 수 있도록 대시보드를 구축했으며 디프뿐 아니라 각 서비스의 gRPC non OK 응답(HTTP의 400/500번대 응답) 수와 HTTP, gRPC timing을 확인할 수 있도록 하여 레거시 서비스와 동일한 응답을 (1) 얼마나 안정적으로 (2) 얼마나 빨리 반환하는지 역시 쉽게 확인하고 대응할 수 있도록 설정했습니다. 섀도잉을 중심으로 프로젝트를 진행하니 레거시 서비스와 얼마나 똑같이 동작하는지 외에도 신규 서비스가 레거시 서비스가 기존에 받던 트래픽을 받더라도 장애가 발생하지는 않는지, 문제가 될 만큼 느려지지는 않는지 등 안정성에 대한 검증 역시 별다른 노력 없이 챙길 수 있었습니다.

이렇게 레거시 서비스를 분해한다는, 어떻게 보면 손에 잘 잡히지 않는 프로젝트의 목표를 수치로 명확히 볼 수 있도록 정의하고 이를 쉽게 확인할 수 있는 대시보드를 만드니 프로젝트 전체의 속도가 매우 빨라졌습니다. 의사결정도 더욱 신속해졌고, 팀원들 역시 상당히 동기부여가 되었습니다. 너무 큰 프로젝트다 보니 우리가 어디까지 왔는지 체감할 수 없어 막연히 느끼던 막막함이 많이 해소되었고, 프로젝트 일정에 대한 불확실성도 상당 부분 줄일 수 있었습니다. 그리고 이 과정에서 OKR 중심으로 일하는 뱅크샐러드의 문화가 얼마나 큰 힘을 가지는지 다시 한번 실감할 수 있었습니다.

목표가 아닌 것과 원칙

프로젝트의 복잡도를 낮추기 위해 저희가 취했던 접근 중 또 다른 하나는 바로 ‘목표가 아닌 것’을 분명히 하고 ‘원칙’을 세운 일이었습니다. 보통 프로젝트를 진행할 경우 목표를 아주 구체적으로 정의하고 기술하기 마련입니다. 목표를 정확하고 구체적으로 정의하는 것은 당연히 중요하지만, 뱅크샐러드에서는 ‘목표가 아닌 것’을 분명히 하는 것 역시 매우 중요하게 생각합니다.

테크 스펙 문서 중 목표가 아닌 것을 발췌한 이미지. 기존 API 명세가 잘못되었더라도 복잡도를 낮추기 위해 이를 변경하지 않겠다는 등 복잡도를 줄이기위한 원칙이 주로 적혀있다.
Figure 6 | 테크 스펙에 명시한 목표와 목표가 아닌 것

이런 식으로 뱅크샐러드에서는 모든 개발 프로젝트마다 목표와 목표가 아닌 것을 테크 스펙 문서에 분명하게 기재합니다. 레거시 서비스를 분해하여 다시 만드는 프로젝트인 만큼 기존에 잘못된 API 설계와 잘못된 네이밍을 고치면서 진행하고 싶은 욕심이 아마 대부분의 개발자분들께는 있을 겁니다. 그러나 레거시 서비스를 장애 없이 분해하는 것만으로도 이미 프로젝트의 복잡도와 난이도가 매우 높은 상황에서 레거시 서비스의 동작을 바꾸는 모든 개선까지 챙기기는 어렵다고 판단하여 이를 목표가 아닌 것에 명시해 프로젝트의 난이도가 높아지는 것을 사전에 막을 수 있었습니다. 또한, 비슷한 취지에서 프로젝트 진행 원칙을 문서로 관리하기 시작했습니다. 프로젝트에 참여하는 인원도 많은 편이었고, 개인이 챙겨야 하는 복잡도가 꽤 높았기 때문에 불필요한 판단 비용과 고민의 시간을 줄이고자 ‘장애 없이 레거시 서비스를 빠르게 분해하는 것’이 우리의 목표라는 공감대 안에서 원칙들을 마련해 프로젝트 전반의 속도를 더할 수 있었습니다.

프로젝트 원칙을 명시한 문서 이미지. 복잡도를 낮추기 위해 세운 원칙과 그 이유가 명시되어있다.
Figure 7 | 프로젝트를 진행하며 세운 원칙 중 일부

이러한 원칙에서 더 나아가 뱅크샐러드 내부에 공감대가 형성된 저희만의 best practices를 바탕으로 go와 gRPC 기반 서버 템플릿을 만들었습니다. 저희가 이러한 템플릿을 만든 이유는 간단한데, 저희가 새롭게 만들어야 하는 마이크로서비스가 15개나 되므로 보일러플레이트 코드 작성 등의 단순 작업을 최대한 줄이고 팀원들이 실제로 해결해야할 문제에 집중할 수 있도록 만들기 위함이었습니다. 이는 단순히 코드 작성 시간을 아낄 수 있는 것 이상의 긍정적인 효과가 있었는데, 보통 이런 프로젝트 설정이나 구조를 잡는 작업은 실수하기 쉬운 반면 잘 드러나지 않아 필요 이상으로 많은 시간을 쏟게 만듭니다. 저희는 15개나 되는 마이크로서비스를 잘 구현하는 일에 좀 더 집중하기 위해 이 기회에 모두의 생산성을 높일 수 있는 템플릿을 만들어 관리 중입니다.

템플릿을 이용해 생성한 파일 목록을 볼 수 있는 이미지이다. 20개 가까이 되는 파일명을 확인할 수 있다.
Figure 8 | 템플릿을 이용해 타이핑 한 번으로 생성한 파일들

결국 핵심은 협업

위에 말한 다양한 시도 덕에 프로젝트 전반의 복잡도와 난이도는 많이 낮아졌음에도 프로젝트를 진행하다 보면 각자 마주하는 문제의 어려움이 상당히 존재하고 보통 이런 어려움은 불확실성의 영역에 있어 미리 알기도 어렵고 빠르게 대응하기도 어렵습니다. 이러한 문제를 해결하기 위해 저희는 프로젝트 기간 동안 매일 약 15분씩의 스탠드업 미팅을 진행했습니다. 만나서 주로 (1) 어제 한 일 (2) 오늘 할 일 (3) 일 처리에 있어 겪는 업무적/개인적 어려움을 각 구성원이 순서대로 말하는 시간인데요. 복잡도가 높은 프로젝트를 진행할 때는 전체 팀원의 힘의 합력을 최대화하는 일이 무척이나 중요합니다. 세 명이 프로젝트를 하면 세 명 만큼의 속도가 나올 것 같지만, 실제로는 기대만큼 속도가 나오지 않는 경우를 종종 목격하셨을 텐데요. 해묵은 레거시 서비스를 분해하는 작업 수준의 복잡도를 지닌 업무에서는 기민하게 서로의 업무 방향을 조정해가며 협업하지 않으면 굉장히 속도가 더뎌지는 것을 종종 목격했습니다.

따라서 저희는 프로젝트 초중반에는 하루에 한 번 만나서 스탠드업 미팅을 진행했고, 프로젝트 막바지에는 두 시간에 한 번씩 만나서 스탠드업 미팅을 진행했습니다. 프로젝트 마무리 단계로 갈수록 프로젝트의 복잡도는 (작성한 코드를 포함해 확인해야 할 대상이 늘었으니) 점점 더 올라가기 마련이기에 끝까지 속도를 잃지 않기 위해 두 시간에 한 번씩 만난다는 다소 파격적인, 어떻게 보면 시간 소모가 심할 것 같은 협업 방식을 제안했었고 결과는 대성공이었습니다. 오히려 실패 비용을 최소화할 수 있었고, 적절한 시점에 적절한 도움을 서로 지원할 수 있었습니다. 어떻게 보면 협업의 복잡도와 비용을 줄인 셈이죠. 덕분에 뒷심을 잃지 않고 프로젝트를 끝까지 잘 마무리할 수 있었습니다.

마치며

난이도가 매우 높은 프로젝트였지만 약 3년의 세월 동안 뱅크샐러드가 걸어왔던 길을 다시 새롭게 만들어, 뱅크샐러드가 앞으로 더 빠르게 성장할 수 있는 토양을 마련하는 보람찬 프로젝트였습니다. 레거시 서비스의 성공적인 분해 이후 한 달이 못 되는 기간에도 벌써 제법 많은 서버 차원의 기능 개선이 이뤄졌고, 레거시 서비스가 자리 잡고 있었다면 모두 이렇게 빨리 개발하여 서비스에 반영할 수 없는 수준의 개선들이었습니다. 또한 뱅크샐러드 내에서 가장 거대하고 복잡했던 레거시 서비스를 분해하는 과정에서 얻은 지혜를 바탕으로 뱅크샐러드에서는 이미 여러 크고 작은 분해 프로젝트가 진행 중입니다. 저희의 경험과 작은 지혜가 이 글을 읽으신 분들에게 도움이 되었으면 합니다. 긴 글 읽어주셔서 고맙습니다.

보다 빠르게 뱅크샐러드에 도달하는 방법 🚀

지원하기