안녕하세요. 어카운트 팀의 청명입니다☀️

뜨거운 여름 쏘카를 타고 전국으로 떠나려는 고객분이 많아지고 있습니다. 모든 사람이 자유롭고 행복하게 이동하는 세상을 만들어 가는 쏘카는 다가오는 성수기 트래픽에 대응할 수 있도록 서비스를 개선했습니다. 개선을 하며 얻은 여러 경험 중에서 이번 글은 변동성이 높은 상황에서 실현 가능한 일정을 산정 하는 방법에 대한 이야기를 담았습니다.

규모가 큰 프로젝트의 일정을 산정해야 하는 분, 프로젝트의 완성도를 높이고 싶은 분, 목표의 우선순위가 자주 바뀌어 우선순위 산정에 어려움을 겪는 분이라면 이번 글이 도움이 되리라 생각합니다.


목표

매년 이동이 증가하는 성수기는 쏘카에게 중요한 이벤트입니다. 성수기에 차량 수요가 폭발적으로 증가함을 다년간 축적된 데이터로 알고 있어 성수기 전에 전반적인 시스템의 정비를 완료해야합니다. 회사 비즈니스가 빠르게 성장하고 있어 성수기 트래픽은 매년 순증하고 있습니다. 성수기 트래픽에 대응하기 위해 지금까지 사용해 온 임시방편이 한계에 도달하여 2023년 성수기 전 지속 가능한 근본적인 해법을 찾아야 하는 상황이 되었습니다. 이를 해결 하기 위해 대대적인 개편 계획을 세웠습니다.

해결해야할 문제는 크게 세가지 범주로 나뉩니다.

트래픽 증가가 master DB 부하를 증가시키는 문제

[그림] - write DB의 요청량 write DB의 요청량 [그림] - read DB의 요청량 read DB의 요청량

쏘카 서비스는 트래픽이 증가하면 MasterDB의 부하가 같이 늘어나는 구조였습니다. 트래픽 처리에는 scale out 또는 scale up 전략을 사용할 수 있지만 쏘카는 master DB에만 부하가 몰려 scale up만 적용해왔습니다. 매년 부하가 늘어남에 따라 scale up 전략은 한계에 도달했고 효율적인 대응을 위해 scale out 전략을 사용할 수 있도록 아키텍쳐를 수정하기로 결정했습니다.

거대한 모놀리식(Monolithic) 아키텍쳐 문제

쏘카는 빠른 성장을 하며 정리하지 못한 기술 부채가 있어 하나의 모놀리식 구조의 서비스도 존재합니다. 큰 규모의 모놀리식 서비스는 변경이 작아도 적용부터 배포까지 매우 많은 비용이 들어가기에 개선이 쉽지 않은 구조입니다. 변경이 용이한 구조로 만들기 위해 쏘카 메인 서비스에서 어카운트 서비스를 호출하는 구조로 변경이 필요했습니다.

MSA(MircroService Architecture)로 분리된 개별 프로젝트가 인증관련 로직을 직접 처리하는 문제

몇개의 서비스는 모놀리식 아키텍쳐의 문제를 해결하기 위해 MSA로 분리되어 있었습니다. MSA로 분리된 서비스마다 인증로직을 직접 구현해 사용하다보니 변경을 여러 서비스에 반복 적용해야하는 문제가 있었습니다. 개별 처리 중인 인증로직을 하나의 서비스로 통합해 변경을 한곳에 적용할 수 있도록 계획을 세웠습니다.

급변한 상황

해결해야 할 큰 문제를 식별하고 치열한 플래닝을 거쳤습니다. 주니어가 많아서 예측이 힘든 저희팀 특성상 한 일감에 대해 스토리포인트를 산정하는 것이 어려워 이번 스프린트에 꼭 해내야 하는 것이 무엇인지 선정하고 이를 완수할 수 있도록 진행하는 것으로 플래닝을 진행했습니다. 그 결과로 2달의 개발기간을 산정했고 성수기인 7월 이전 운영에 배포하는 원대한 계획을 세웠습니다. 목표를 달성하면 앞으로 걱정 없이 성수기를 보내리라는 달콤한 희망에 빠졌습니다.

하지만 이상과 현실은 매우 달랐습니다. 시장의 속도는 저희가 상상한 것보다 빨랐고 회사는 이 속도에 대응하기 위해 새로운 도전을 하고 있는 상황이었습니다. 여행수요를 높이기 위한 쏘카 스테이 프로젝트 출시에 맞춰 대규모 이벤트가 계획되었고 런칭시점은 예상한 성수기보다 빨랐습니다.

기존 계획대로 진행하면 쏘카스테이 런칭 시점에 대규모 장애 발생은 불보듯 뻔한 일이었습니다. 당시 팀 플래닝을 모두 마치고 ‘해보자. 할 수 있다. 아자아자!’하며 의기투합하던 자리에서 본부장님이 했던 말씀이 아직도 생생합니다.

본부장: 일정 한달 당깁시다.

어카운트: [그림] - 어카운트팀의 속마음 어카운트팀의 속마음

쏘카 스테이 오픈으로 계획했던 일정이 절반으로 줄었습니다. 일정은 줄었지만 부하분산은 반드시 해내야만 하는 일이 됐습니다. 변경 전에 세웠던 일정은 아래와 같습니다.

페이즈 내용
페이즈1 쏘카 메인 서비스에서 인증 코드 분리
페이즈2 그외 나머지 서비스 인증 코드 분리
페이즈3 성능최적화

비즈니스 환경은 급변합니다. 특히 시간 제약은 언제나 겪는 문제입니다. 시간은 부족하고, 사람은 없고, 할 일이 많은 것은 누구나 겪습니다. 이런 어려움 속에서도 결과를 만들어야합니다. 프로젝트의 중요 요소가 시간 / 자원 / 해야할 일이라면 제어할 수 있는 단 한가지 요소는 해야할 일입니다. “해야할 일을 조정하면 돌파할 수 있지 않을까?”라는 생각이 들었습니다.

페이즈1

최초 계획을 다시 검토했지만 모든 게 중요해 보이고, 설상가상 팀으로 들어오는 요청도 많아 팀장님의 리소스를 많이 사용할 수 없었습니다. 그렇다면 저와 동료는 야근과 특근, 개발자의 열정으로 목표를 달성해야 할까요?

초반에야 그럴 수도 있겠지만, 프로젝트 기간 내내 지속할 수는 없는 방법입니다. 일을 줄일 수는 없지만 우선순위를 정할 수는 있었습니다. 프로젝트에서 꼭 해야 하는 것과 하면 좋은 것으로 우선순위를 분리할 수 있지 않을까? 라는 생각이 들어 최초에 세웠던 계획을 분류했습니다.

페이즈 내용 우선순위
페이즈1 쏘카 메인 서비스에서 인증 코드 분리 꼭 해야 하는 것
페이즈2 그외 나머지 서비스 인증 코드 분리 하면 좋은 것
페이즈3 성능최적화 하면 좋은 것

기존의 문제는 트래픽이 증가하면 master DB의 부하가 증가하여 전체 API 응답이 지연 되고 장애가 발생하는 것이므로 페이즈1에서는 master DB의 부하를 낮추는 것이 중요했습니다. 두가지 해결책이 있었는데 하나는 쏘카 메인 시스템에서 DB의 부하를 분산처리 하는것이고, 다른 방법은 인증을 별도 서비스로 분리해 근본적인 개선을 해내는 것이었습니다.

당장의 목표를 생각하면 메인 시스템에서 DB 부하 분산 처리를 하는것이 효율적 이겠지만 근본적인 해법은 아니라 판단했습니다. 하나의 거대한 시스템이다보니 코드를 고치는것도 고친 코드를 배포하는 것도 경험이 적은 저희에게는 너무 어려운일이었기 때문입니다.

그렇게 메인 시스템에서 인증 서비스를 분리하자는 목표를 세우고 구현 과정에서 또 다른 문제를 만났습니다.

쏘카는 현재 모놀리식으로 이뤄진 거대한 시스템을 각 도메인별로 분리하는 시도를 하고 있습니다. 각 마이크로시스템간 통신은 gRPC를 표준프로토콜로 이용하고 있고 내부 구현은 많은 요청에 대응하기 위해 비동기 처리를 지향하고 있습니다.

[그림] - 쏘카 마이크로 서비스 표준 아키텍쳐쏘카 마이크로 서비스 표준 아키텍쳐

저희는 시니어 개발자인 팀장 한명과 저를 포함한 주니어 둘로 구성된 소규모 팀입니다. 저와 같이 작업하는 동료의 경험을 모두 합쳐도 1년이 채 되지 않는 그야말로 찐주니어로 구성된 팀입니다.

이런 저희에게 gRPC 와 비동기 처리는 개념으로는 알지만 실제 사용하기에는 너무 높은벽이었습니다.

팀장님의 도움을 받아 해결할 순 있겠지만 앞으로 남은 일정을 생각하면 모든 문제를 그렇게 해결할 수는 없었고 지금부터 학습해 적용하기엔 짧은 일정안에 완료를 확신할 수 없었습니다.

어떻게 돌파하면 좋을까요?

이런 상황에서 초반에 설정한 목표는 좋은 길잡이가 됐습니다. 이번 페이즈에서 정복하고자 한 문제는 ‘DB 부하 분산’이고 근본적인 개선을 위해 ‘아키텍쳐 개선’을 하는 것입니다. 이에 반해 쏘카 표준 마이크로 서비스가 정의한 gRPC나 비동기 처리는 성능향상을 지향하는 기술이기에 이번 작업의 목표와는 다소 거리가 있다고 생각했습니다.

일반적인 http protocol 과 동기방식으로 처리하는 것은 경험이 적은 저희도 충분히 잘할 수 있는 처리방식이기에 아래 그림과 같이 소화할 수 있는 방식의 아키텍쳐를 구상했습니다.

[그림] - 계정 마이크로 서비스 아키텍쳐계정 마이크로 서비스 아키텍쳐

우선 정복할 수 있는 방법으로 문제를 해결하고 gRPC 및 비동기 처리는 시간을 두고 천천히 진행하기로 결정했고 이 접근방법은 짧은시간내 문제를 해결하는데 큰 도움이 됐습니다. 부하 분산과 성능개선 모두 달성해야 하는 목표라는 하나의 커다란 문제일 때는 gRPC와 비동기 처리를 위해 학습비용을 쓰거나 다른 시니어 개발자의 도움을 받아야 하지만 작은 문제로 나누고 그 문제에 집중한 덕분에 주니어 개발자가 다룰 수 있는 수준으로 문제를 해결하여 ‘할 수 있을까?’에서 ‘해 볼만 하다’ 는 자신감으로 이어지게 됐습니다.

페이즈2

전체 프로젝트에서 가장 많은 작업을 해야했던 페이즈2가 찾아왔습니다. 남아있는 시간에 비해 작업해야할 서비스가 매우 많았고 서비스를 수정해도 생전 처음보는 서비스의 배포까지 완료하려면 시간이 많이 필요했습니다. 배포를 제외한 수정만으로도 시간이 부족해 작업의 총량을 줄일 필요가 있어 우선순위를 파악해보기로 했습니다.

[그림] - read와 write의 요청수 차이read와 write의 요청수 차이

분석 결과 api 요청은 read write api로 나눌 수 있었고 그 중에서 read 요청수가 write보다 압도적으로 높다는 것을 확인했습니다.

이번 페이즈에서 저희가 집중해야 하는것은 개발자 생산성이었습니다.

시간이 많지 않기 때문에 비용 대비 효과가 큰 것을 선택했습니다. 꼭 달성해야 하는 목표인 ‘DB 부하분산’ 관점에서 read 부하는 꼭 해결해야 하는 문제 write는 하면 좋은 작업으로 분류할 수 있었습니다. 이런 판단으로 페이즈2 는 아래와 같은 우선순위를로 결정했습니다.

페이즈 내용
페이즈2-1 그외 나머지 서비스 인증 코드 분리 [read]
페이즈2-2 그외 나머지 서비스 인증 코드 분리 [write]

위 기준에 따라 분리대상 api를 부하를 많이 받고 있거나, 앞으로 많은 부하를 받게될 예정인 api 위주로 분류한 끝에 최초에 총 50여개의 api와 5개의 서비스에서 10개의 api와 3개의 서비스를 수정하는 것으로 페이즈 목표를 줄일 수 있었습니다. api의 개수가 줄다보니 작업은 여유로워졌고, 변경해야하는 서비스의 개수가 줄어서 테스트해야하는 환경을 구성하는 비용과 테스트 진행 시간도 줄일 수 있었습니다. 그러다보니 시간상 압박 받을 것이란 처음 예상과 달리 여유로운 작업이 가능해져 심적으로 여유롭고 안전하게 작업할 수 있었습니다.

해당 페이즈 특성상 반복적인 업무가 많았고, 수정해야하는 서비스에 대한 이해도가 늘어가다보니 시간이 갈수록 업무 퍼포먼스도 향상되었습니다. 이를 통해서 하면 좋은 일이라 분류했던 작업도 진행할 수 있다라는 결정을 했고 write 분리까지 진행하며 성공적으로 페이즈를 마무리할 수 있었습니다.

페이즈3

페이즈2를 성공적으로 마무리하고 최초 목표였던 master DB 부하 분산은 성공적으로 달성했습니다. 하지만, 호락호락하지 않은 성수기 대응 프로젝트에는 새로운 작업이 필요했습니다.

예상치 못한 추가작업

여행 수요를 충족시키기 위한 쏘카스테이 오픈 후에 이를 홍보하기 위한 대규모 이벤트를 진행한다는 청천벽력 같은 소식이 들려왔습니다.

프로모션 내용은 정말 파격적이었습니다. 추첨을 통해 누구라도 알만한 특급호텔을 만원대의 가격으로 경험해볼 수 있고 다양한 매체를 통해 이뤄지는 대규모 마케팅은 지금까지의 목표였던 ‘DB 부하분산’ 만으로는 부족했습니다. 부하를 더 효과적으로 처리할 수 있는 방안을 찾아야했고 팀 내부 논의 끝에 ‘캐시를 도입해 해결해보자!’라는 결론에 도달했습니다.

캐시 도입 과정에서 이전까지 진행한 페이즈로 만들어진 새로운 아키텍쳐 덕을 많이 봤습니다. 이전이라면 모든 인증로직을 사용하는 서비스를 수정해야했지만, 이제는 관리하는 서비스에만 캐시를 적용하면 됐습니다. 하지만, 캐시를 도입한 후에 예상한만큼의 속도 개선을 달성하지 못했습니다. 놓친 부분이 무엇이 있을지 분석이 필요했습니다. 여러 포인트를 하나씩 확인하며 분석을 진행하던 중 캐시를 도입한 테이블의 데이터를 확인한 결과 엄청난 것을 목격했습니다. 하루 read 요청이 무수히 많이 들어오는 저희 DB 테이블에서 쏘카가 지금까지 지나온 10년의 데이터가 계속해서 쌓이고 있었습니다. 불필요하게 쌓이며 성능이슈를 만들던 데이터를 이관해 지금 사용하고 있는 데이터만 남길 수 있도록 작업하는 데이터 최적화가 필요했습니다.

페이즈 내용
페이즈1 쏘카 메인 서비스에서 인증 코드 분리
페이즈2-1 그외 나머지 서비스 인증 코드 분리 [read]
페이즈2-2 그외 나머지 서비스 인증 코드 분리 [write]
페이즈3 캐시 적용 & 데이터 최적화

이번 페이즈는 페이즈1, 2를 수행하며 높아진 자신감과 변경된 아키텍쳐를 통해 과감하게 일정을 구성하고 작업할 수 있었습니다. 최초 목표 설정 시 ‘주니어 두명이 과연 해낼 수 있을까?’하는 불안감은 그간 여러차례 진행된 페이즈 결과물이 배포될때마다 전체 개발조직에 안정감을 주었고 동료의 신뢰라는 소중한 자산을 쌓을 수 있었습니다.

이제 막 실무에 투입된 주니어 개발자에게 ‘대규모 트래픽을 소화할 수 있도록 개선해주세요’는 정복하기 어려운 일이었지만 ‘인증을 담당하는 로직을 별도 서비스로 분리하기’ ‘Read는 slave DB 로 처리하기’ 와 같은 작은 목표는 두렵지 않았고 작은 목표를 점진적으로 정복하며 정해진 일정내 목표한 것 이상을 해낼 수 있었습니다.

데모

프로젝트를 돌아보니 페이즈 성공 여부를 판단할 수 있는 근거를 얻을 수 있던 것은 주기적으로 진행한 데모 덕분이었습니다. 페이즈1의 gRPC를 http로 변경할 수 있게 한 에피소드를 간략하게 소개하며 얻었던 장점에 대해 이야기해 보려고 합니다.

세상에 망한 데모는 없다

사내 규약이었던 gRPC를 사용해 저희의 서비스를 구성했을 때의 데모는 완성이 되지 않은 상태에서 진행했습니다. gRPC로 인해 불안정한 데모를 할수 밖에 없었고, 결국에는 오류가 발생해 목표한 데모 시나리오를 진행할 수 없었습니다.

하지만, 정말 망한 데모일까요?

이 데모를 통해 페이즈1 방향을 수정 할 수 있었고, 돌이켜보면 완성도를 높일 수 있었던 가장 큰 계기였습니다. 그렇게 판단한 이유는 두가지입니다.

제 3자의 눈을 빌려 부족한 부분을 확인할 수 있습니다.

  • 목표를 향해 같이 개발하는 분과 몰두하다 보면 시야가 좁아집니다. 그러다보니 객관적으로 부족한 부분이 무엇인지 파악하기가 어렵습니다. 이러한 상황을 객관적으로 바라볼 수 있는 가장 효과적인 방법이 데모입니다.
  • 다양한 관점에서 피드백을 받아 더 완벽한 애플리케이션을 만들었습니다. 동료가 전해준 메모리 사용문제, QA 담당자가 전해준 ‘고객 관점에서 느꼈을 때 동작이 불친절할 수 있다’와 같은 피드백을 통해 어떻게 개선하면 좋을지에 대한 의견을 수렴했습니다.
  • 또한 경험이 부족한 저희에게 큰 도움이 되는 개발 관련 피드백은 앞으로 이뤄질 성능테스트에서 큰 도움이 됐고 이를 통해 성공적인 성능최적화를 진행할 수 있었습니다.

지금 어떤 상태인지를 모두에게 알릴 수 있습니다.

  • 상태를 공유하는 것은 정말 중요합니다. 데모는 어떤 상태인지를 알리는 최고의 방법입니다. 미리 세운 계획에 비해 얼마나 달성했는지 혹은 달성하지 못했는지를 공유하고 해당 프로젝트와 관계있는 프로젝트의 경우 어떻게 계획을 산정하면 좋을지를 확인할 수 있습니다.
  • 여러 의견을 통해 결정하기 어려운 사항을 결정할 수 있습니다. 데모가 실패한 원인으로 팀의 기술성숙도에 맞지 않는 기술을 선택해 속도가 중요한 프로젝트에서 속도를 못내고 있는 상황에서 결정을 주저하고 있던 저희에게 익숙한 기술을 사용하는 것이 나을 수 있다는 선택을 할 수 있게 해줬습니다.
  • 글이나 말로 확인하는 것보다 전달이 쉽고 이해가 빠릅니다. 슬랙을 통해 여러번 설명하고, 왜 필요한지에 대해서 커뮤니케이션하는 비용을 한번의 데모로 보다 효율적으로 의견을 전달할 수 있습니다.

물론, 데모라는 것 자체가 매우 큰 장벽으로 다가옵니다. 저희도 그랬습니다. 하지만 가장 필요한 장치입니다.

‘개발하기 바쁜데 데모 준비를 어떻게하고 발표는 어떻게 하지..’라는 핑계로 데모를 미루는 게 어떨까하는 생각도 많이 했습니다. 이 문제는 준비하는 시간이 필요하다는 게 허들이라 생각했고 ‘준비를 줄이고 한 것만을 보여주자!’의 마인드 셋을 가졌습니다. 따로 자료를 준비하는 것이 아닌 코드와 실행 과정을 시연하며 설명했고, 이 과정 자체만으로도 충분히 장점이 있습니다.

그러니 여러분, 절대 데모하세요.

결과

이런 과정을 거쳐서 무슨 결과를 만들어냈을까요?

[그림] - DB 부하분산(파란색 : master, 노란색 : read)DB 부하분산(파란색 : master, 노란색 : read)

[그림] - cache layer 적용 후 DB 조회 쿼리 요청 수cache layer 적용 후 DB 조회 쿼리 요청 수

이제는 성수기가 돼도 안절부절하지 않아도 되는 쏘카가 됐습니다.

프로젝트를 돌아보니 분할정복을 통해 얻은 장점 외에도 의도하지 않았지만 얻은 장점이 많습니다.

분할정복

하나의 커다란 문제를 분할해 정복하면서 거대하고 모호한 목표보다는 작고 확실한 목표를 설정하여 구체적인 목표의식을 갖고 일할 수 있었고 막연했던 두려움을 없앨 수 있었습니다. ‘아키텍쳐를 개선한다’라는 모호함은 두려움으로 이어지지만 ‘반복되는 코드를 관리가능한 서비스로 분리한다’는 쉽게 다가갈 수 있습니다. ‘성능개선을 해야한다’는 두려웠지만 ‘DB 부하를 분산 한다’는 확실했습니다. 작은 목표를 지향하며 일하는 과정에서 오버엔지니어링 하지 않을 수 있다는 장점도 확인했습니다.

변화에 유연한 대응

짧은 주기로 목표를 정복해 나가며 변화에 능동적으로 대응할 수 있었습니다. 작은 단위로 일감을 나누고 정복했기에 할 일이 추가되거나 변경되어도 덜 민감하게 받아들일 수 있었습니다. 하나의 커다란 목표를 정복하는 과정에 변화가 발생하면 전체가 영향받지만 작게 나눠 목표를 정복하니 이미 완료된 페이즈 결과물은 수정하지 않을 수 있었습니다. 이를 통해 프로젝트의 안정성을 챙기면서도 스프린트 종료 결과에 따라 이후 목표를 조정하며 진행할 수 있었습니다.

명백한 롤백지점

자연스러운 롤백시점이 생겼다는것도 큰 장점이었습니다. 많은 서비스를 한번에 배포하면 사소한 문제에도 당황하여 어디가 문제인지 파악하기 어렵고, 롤백 지점도 찾지 못하는 경우가 생깁니다. 이런 경우에 당황하지 않고 전 페이즈 버전으로 롤백을 진행할 수 있어 가장 신경이 많이 쓰이는 배포에도 대처 가능한 여유를 얻을 수 있었습니다.

마무리

함께 작업했던 동료와 이번 프로젝트를 돌아보며 ‘저희가 이걸 어떻게 했을까요?’라고 말하며 놀랍니다. 저희의 경험이 글을 읽는 여러분께도 도움이 되길 바랍니다. 과정을 짤막하게 정리하면 다음과 같습니다.

  • 일정이 수정되어야할 때마다 해야만하는 것과 하면 좋은 것을 구분해 목표를 설정한다.
  • 큰 프로젝트를 달성가능한 목표로 분리해 페이즈를 나누어 정복한다.
  • 데모를 통한 현재 상황 인지와 완성도 높힌다.
  • 페이즈를 통해 우리가 어떤 컨디션인지 파악한다.

이렇게 쏘카의 대규모 트래픽 대응, 프로젝트 플래닝 편을 마무리하겠습니다. 어카운트팀이 여러분 계정의 안전과 속도를 위해 열심히 힘쓰고 있다는 사실을 기억해주세요!

마지막으로 치열한 고민을 통해 문제를 헤쳐나가는 과정에 동참하고 싶다면 쏘카에 지원해주세요!!! 언제나 환영할 준비가 되어있습니다.