Stripe의 모노레포 개발 환경
원문은 Nelson Elhage님이 에 게재했습니다. 이 블로그 구독하기
저는 2012년부터 2019년까지 약 7년간 Stripe에서 일했습니다. 그 기간 동안 여러 세대에 걸친 Stripe의 개발 환경, 즉 엔지니어들이 매일 코드를 작성하고 테스트하는 데 사용하던 도구들을 직접 사용하고 개선하는 데 기여했습니다. Stripe는 그 개발 경험을 설계하고 구축하는 일을 꽤 잘 해냈다고 생각하며, 퇴사한 이후로도 그 환경의 특징들을 친구와 동료들에게 반복해서 설명하는 제 모습을 발견하곤 했습니다.
이 글은 제가 기억하는 그 환경의 주요 특징들을 기록하려는 시도입니다. 또한 이러한 선택들의 배경과 제약, 동기에 대해서도 되짚어 보려 합니다. 당시 맥락에서는 좋은 선택이었다고 생각하지만, 그 선택들은 비즈니스와 기술적 맥락에 깊이 영향을 받은 것이었기에 다른 팀에서는 다른 변형이 필요할 것입니다.
몇 가지 전제할 점이 있습니다. 거의 5년이 지났고, 전체적인 그림은 확신하지만 세부적인 내용 중 일부는 분명 잘못 기억하고 있을 것입니다. 또한 Stripe는 계속 발전해 왔을 것이 확실하므로, 이 글이 오늘날 Stripe의 개발 경험을 대변한다고 주장하지 않습니다.
더불어, 여기서 설명하는 많은 구성 요소에 기여하긴 했지만 전체적인 공로를 주장하려는 것은 아닙니다. 여기에 담긴 모든 것은 비전을 구상하는 과정부터 구현에 이르기까지 수많은 뛰어난 엔지니어들의 기여로 여러 해에 걸쳐 진화해 온 결과물입니다.
Stripe의 맥락
제가 이야기하는 시점에 Stripe 코드베이스의 대부분은 Ruby로 작성되어 있었고, 하나의 거대한 모노레포에 담겨 있었습니다. 이 코드베이스는 여러 서비스를 지원했으며, 서비스 간에 코드를 광범위하게 공유했습니다. Stripe에는 모노레포 밖에, 다른 언어로 작성된 서비스들도 많았고 그 수는 계속 늘고 있었지만, 일반화하자면 비즈니스 핵심과는 거리가 멀었습니다. 특히 Stripe API는 거의 전적으로 모노레포 안의 단일 Ruby 서비스였습니다. 여기서 다루는 툴링과 경험은 주로 모노레포에서의 개발을 지원하기 위해 설계되고 구축된 것입니다. 다른 서비스와 언어는 지원하더라도 부수적으로 지원될 뿐이었습니다.
Stripe의 툴링은 여러 팀과 개인이 이어받아 만들고 유지해 왔지만, 이 글에서는 그 제작자와 소유자를 통칭해 “developer productivity” 팀(약칭 “devprod”)이라 부르겠습니다. 이는 제가 재직하던 마지막 몇 년간 그 팀의 이름이었습니다.
툴링에 대한 투자
Stripe는 제가 합류한 뒤 비교적 이른 시점에 내부 툴링과 생산성을 전담하는 팀 인력을 만들었고, 이는 결국 이 도구들을 만든 팀이 되었습니다. 하지만 마지막 3~4년 동안 그 팀은 규모가 크게 커지며 본격적으로 자리를 잡았습니다. 이 팀에는 매우 시니어한 개인 기여자를 포함해 꾸준히 우수한 엔지니어들이 배치되었습니다. 앞으로 설명할 어떤 기술적 선택보다도, 이 팀의 존재 자체와 개발 툴링의 안정성과 신뢰성에 대한 투자(팀이 어느 정도 커져 당장의 불을 끄는 일을 넘어 계획하고 투자할 여유가 생긴 조금 뒤 시점부터)가 Stripe 개발 경험의 성공을 이끈 가장 큰 요인이었다고 생각합니다. 기술적 선택과 엔지니어링은 분명 중요하지만, 제대로 작동하려면 충분한 인력과 적절한 지속적인 유지보수가 뒷받침되어야 합니다.
특히 팀과 코드베이스가 성장하고 진화하는 과정에서 환경의 이러한 신뢰성과 안정성은 반복해서 등장하는 주제가 될 것입니다. 아무리 훌륭한 툴링이라도 “환경을 디버깅하느라 하루를 날리는 일이 빈번한” 상황을 메우기란 어렵고, 따라서 이 부분을 제대로 해내는 것만으로도 다른 거의 모든 결정에 앞설 수 있습니다. 많은 설계 결정은 devprod 팀이 툴링을 더 쉽고 일관되게 지원하고 모니터링할 수 있게 하고, 문제를 개별 엔지니어에게 떠넘기는 대신 중앙에서 디버깅하고 해결할 수 있도록 하는 것을 목표로 내려졌습니다.
개발 환경의 아키텍처
개발 코드는 클라우드에서 실행된다
개발 환경에서 결정적인 질문이 하나 있습니다. 개발 중인 코드를 개발자의 노트북에서 로컬로 실행할 것인가, 아니면 중앙에서 프로비저닝된 환경 안의 인스턴스나 컨테이너에서 원격으로 실행해야 하는가 하는 문제입니다.
수년간 Stripe 엔지니어들은 개발자 선호도와 시기와 환경에 따라 무엇이 동작했는지에 대한 특수한 결정과 세부 사항에 따라 두 가지 방식을 혼용했습니다. 개발 생산성 팀이 하나의 공식 환경을 지원하기로 결정했을 때, 우리는 Stripe 클라우드 환경(프로덕션 환경 외부)의 개발자별 인스턴스(“devbox”)를 지원하기로 정했습니다. 개발 과정에서는 minitest 테스트를 실행할 때나 대화형 테스트 및 실험을 할 때 모두 코드가 devbox에서 실행되었습니다.
이 개발자 인스턴스들은 Stripe의 표준 구성 관리 툴링으로 프로비저닝되었고 일시적이었습니다. 하나의 명령으로 인스턴스를 제거하고 새로 프로비저닝할 수 있었으며(웜 스페어를 일정 수량 유지해 두어 이 작업은 보통 매우 빨랐습니다), 레지스트리가 어떤 엔지니어에게 어떤 인스턴스가 활성화되어 있는지를 추적해 툴링이 활용할 수 있게 했습니다. 모든 devbox는 모든 엔지니어가 ssh로 접근할 수 있었고, 이는 협업과 환경 문제 디버깅을 용이하게 했습니다.
이 설계 덕분에 대부분의 환경 구성 문제는 툴링 팀이나 서비스 소유자가 중앙에서 해결할 수 있었고, 개발자들은 대체로 자신의 개발 환경을 최신 상태로 유지해야 한다는 걱정을 할 필요가 없었습니다.
새로운 의존성 추가
Stripe는 API나 다른 서비스를 위해 의존성을 끊임없이 추가하고 재작업했습니다. 예를 들어 API에 rate limiting을 구현하려면 Redis 클러스터가 필요했습니다. 개발 코드가 노트북에서 실행되는 환경에서는 모든 노트북에 Redis를 설치하고 설정까지 해야 한다는 의미였습니다. 개발자 노트북의 구성을 업데이트하는 일은 까다로웠고, 종종 엔지니어들 사이의 구전으로 해결되곤 했습니다. “master를 pull 받았는데 이상한 에러가 나는데 어떻게 고치는지 아는 사람 있어?”라는 질문에 동료가 적절한 설정 명령을 보내주는 식이었습니다. 다른 방법으로는 개발자들이 정기적으로 실행하는 임의의 스크립트에 Redis를 설치하는 brew 명령을 슬쩍 끼워 넣어 문제를 해결하려 하기도 했는데, 이 역시 깨지기 쉬웠고 주기적으로 다른 도구들의 속도를 느리게 만들기도 했습니다.
devbox 모델에서는 Redis를 추가하는 팀이 이미 프로덕션에서 이를 구성할 책임이 있었고, devbox에서도 설치되어 실행되도록 적절한 Puppet 구성을 추가할 수 있었습니다. 사용자가 이 새로운 경로에서 문제를 겪더라도 해당 팀이나 devprod 멤버가 그의 devbox에 직접 ssh로 접속해 문제를 디버깅한 뒤, 다른 사용자에게 문제가 확산되지 않도록 Puppet이나 Ruby 소스를 직접 업데이트할 수 있었습니다.
제가 Stripe에 재직하던 대부분의 기간 동안 새로운 기능과 인프라 변경으로 인해 끊임없이 어떤 형태로든 새로운 의존성이 생기거나 기존 의존성의 설정이 바뀌었습니다. devbox로 표준화한 것은 해당 팀들의 작업을 수월하게 했을 뿐 아니라, 인프라 변경이 다른 사람의 개발 경험을 망가뜨리는 빈도를 대폭 줄여 주었고, 두 가지 모두 엄청나게 가치 있는 일이었습니다.
에디터와 소스 컨트롤
개발 중인 새 코드를 실행하기 전에 먼저 소스 컨트롤을 통해 코드에 접근하고 편집할 수 있어야 합니다.
Stripe에서는 코드가 클라우드에서 실행되었음에도 git 체크아웃과 에디터는 개발자 노트북에 로컬로 존재했습니다. Stripe가 여러 가지 이유로 이 방식을 택했는데, 그 이유는 다음과 같습니다.
다양한 에디터와 IDE 환경 지원
일부 에디터(예:
emacs나vim)는 SSH 세션을 통해 어느 정도 무리 없이 실행할 수 있고, 일부(예: VS Code나tramp를 사용한 emacs)는 원격 파일 시스템을 상대로 로컬 UI를 실행하는 것을 지원하지만, 많은 에디터는 그렇지 못합니다. 코드를 노트북에 둠으로써 Stripe는 개발자들이 원하는 어떤 에디터든 계속 사용할 수 있게 했습니다.지연 시간
Stripe에는 전 세계에 개발자들이 있었지만, 당시에는 아직 미국 외 지역에 대규모 인프라를 유지하고 있지 않았습니다. 200ms가 넘는 네트워크 지연을 사이에 두고 코드를 편집하는 일은 고통스럽습니다. 전 세계에 devbox를 구축하는 것도 선택지였지만 여러 가지 이유로 복잡했을 것입니다.
소스 코드의 내구성과 파일 시스템 성능
신뢰할 수 있는 소스 코드를 노트북에 두고 devbox 밖에 둠으로써 실행 환경을 일시적이고 휘발적인 것으로 취급하기가 훨씬 쉬워졌습니다. 이는 다시 환경을 최신 상태로 유지하고 시간이 지남에 따라 환경이 달라지는 것을 방지하는 데 매우 도움이 되었습니다.
소스 코드를 devbox와 별개인 네트워크 접근 가능한 저장소(예: EFS나 EBS 볼륨)에 보관할 수도 있었겠지만, 복잡성 비용을 차치하더라도 그런 옵션들은 지연 시간이 비교적 높고, 소스 코드는 (최소한) 빠른 파일 메타데이터 조회에 의존하는 경향이 있습니다. NFS 위나 EBS에서의
git작업은 대대적인 튜닝과 최적화 없이는 고통스러울 정도로 느린 경향이 있습니다.
자동 동기화
하지만 코드를 노트북에 두고 클라우드에서 실행하는 방식은 새로운 과제를 제기합니다. 편집된 코드가 어떻게 노트북에서 devbox로 옮겨지는가 하는 문제입니다.
제가 합류하기 전부터 Stripe에는 파일 와처와 rsync를 이어 붙인 “sync” 스크립트가 있었습니다. 로컬 체크아웃의 변경 사항을 감시해 devbox로 복사하는 스크립트였습니다. 예전에는 개발자들이 별도의 터미널이나 tmux 창에서 이 스크립트를 임시방편으로 수동으로 시작하고 모니터링했습니다.
결국 개발 생산성 팀이 이 도구를 인수해 대대적으로 투자했으며, 주로 “다듬기”에 집중해 다른 개발자들에게 매끄럽고 거의 눈에 띄지 않게 만드는 것을 목표로 했습니다.
특히 동기화가 별도의 설정이나 개입 없이 암시적으로 일어나도록 만들었습니다. IT 팀과 협력해 모든 개발자 노트북에 sync 스크립트를 launchd 서비스로 설치했고, 앞서 언급한 레지스트리를 이용해 올바른 devbox를 자동으로 찾도록 했습니다.
또한 오류와 네트워크 문제에 대한 신뢰성과 자가 복구에도 크게 투자했습니다. “내부적”이지만 중요한 개선 중 하나는 파일 와칭을 위해 watchman으로 옮긴 것이었습니다. 테스트 결과 지금까지 찾은 파일 와처 중 압도적으로 가장 견고했으며, 업데이트 누락이나 파일 와처가 “멈추는” 빈도를 크게 줄여 주었습니다. 또한 잠시 후에 설명할 더 고급 기능들도 활용할 수 있었습니다.
전반적으로 이러한 투자는 대체로 성공했습니다. 대부분의 개발자는 코드 동기화를 그저 “당연한 것”으로 여길 수 있었고, 이를 고민하거나 디버깅해야 하는 일이 거의 없었습니다.
동기화의 한 가지 단점은 자동화된 마이그레이션 도구나 단순한 린터, 코드 생성 도구와 같이 “코드에 작용하는 코드”를 작성하기 어렵게 만든다는 점입니다(Stripe는 결국 다양한 이유로 체크인해야 하는 소수의 코드 생성 컴포넌트에 의존하게 되었습니다). 이는 제가 재직하는 동안 다소 골칫거리로 남았고, 우리는 여러 전략을 혼합해 대처했습니다.
- 개발자 노트북에서 실행하고 환경 문제를 감수한다
- devbox에서 실행한 뒤 어떻게든 생성된 파일을 “다시 동기화”한다. 스크립트를 “sync-back” 래퍼 안에서 실행해 파일을 노트북으로 다시 복사하도록 요청할 수 있는 작은 프로토콜이 있었지만, 여전히 다소 어색하고 사용성이 떨어지며 가끔 신뢰할 수 없는 방식이었습니다.
devbox의 HTTP 서비스
Stripe의 코드 상당 부분은 Stripe API를 비롯해 HTTP 서비스 안에서 실행되었습니다. 개발 생산성 팀은 이러한 서비스 개발을 지원하기 위한 툴링을 구축했으며, 그 내용은 다음과 같습니다.
- DNS는
*.$username.stripe-dev.com형태의 호스트 이름을 해당 사용자의 현재 devbox로 해석했습니다 - 각 devbox에서는 SSL을 종료하고, 클라이언트 인증서 기반의 인증/인가를 처리하며, 서비스 이름을 적절한 로컬 인스턴스에 매핑하는 프론트엔드 서비스가 실행되었습니다
- Stripe의 각 서비스에는 로컬 포트가 정적으로 할당되었고, 프론트엔드는 이를 이용해 트래픽을 라우팅했습니다.
- 예를 들어 API 서비스에 포트 3000이 할당되었다면, 프론트엔드는
api.nelhage.stripe-dev.com을localhost:3000으로 포워딩했습니다
- 별도의 프록시 서비스가 각 포트에서 대기하다가, 요청이 있을 때 백엔드 Ruby 서비스를 시작했습니다.
- 따라서
localhost:3000에 대한 첫 요청 시 API 서비스가 시작되고, 이후에는 계속 실행되면서 이후 요청들을 직접 처리했습니다.
- 따라서
- 이들 서비스는 devbox에서 Stripe의 autoloader를 사용했기 때문에 필요한 경우에만 Ruby 코드를 로드했고, 그 결과 시작 시간이 빨랐습니다.
- autoloader는 또한 어떤 소스 파일이 어떤 서비스에서 로드되었는지를 추적하고 파일 시스템 변경을 감시했습니다. 디스크에서 로드된 파일이 변경되면 해당 파일을 사용하는 서비스는 변경 사항을 반영하기 위해 자동으로 재시작되었습니다.
이 인프라의 순효과는 개발자가 코드를 변경한 뒤 수동으로 재시작할 필요 없이 거의 즉시, 고정된 공유 가능한(Stripe 내부에서) URL에서 새 코드가 반영된 서비스 사본에 접근할 수 있다는 것이었습니다. 이 URL은 새로운 devbox를 프로비저닝하더라도 안정적으로 유지되었습니다. 또한 이 기능은 거의 모든 내부 서비스에서 동작했지만, 각 devbox는 해당 개발자가 실제로 사용하는 서비스만 실행했기 때문에 CPU와 메모리 부하를 줄일 수 있었습니다.
pay 명령
devprod는 또한 다양한 devbox 기능과 워크플로에 통합적으로 접근할 수 있게 해주는 커맨드라인 도구인 pay를 만들고 유지했습니다.
devbox에는 ssh(pay ssh로 래핑됨)를 통해 접근할 수 있었지만, pay는 가장 일반적인 사용 패턴들도 래핑했습니다.
pay test ...는 devbox에서minitest테스트를 실행하고(내부적으로는ssh를 사용) 출력과 종료 상태를 로컬로 돌려보냈습니다.pay curl은curl을 래핑해 devbox의 서비스에 대해 수동curl명령을 실행하는 헬퍼를 제공했으며, 이는 API 엔드포인트를 즉석에서 수동으로 테스트할 때 종종 유용했습니다.pay typecheck는 Sorbet을 래핑해 devbox에서 코드의 타입을 검사했습니다
동기화 장벽
이 도구들이 제공한 편의성에 더해, 또 다른 핵심 기능은 소스 동기화 과정과의 통합이었습니다.
소스 코드에 대해 동작하는 pay 명령은 sync 프로세스와 통신해 원격으로 코드를 실행하기 전에 동기화가 적절히 “따라잡힌” 상태인지 확인했습니다. 이 기능은 동기화의 투명성을 크게 개선한 것이었습니다. 예전에는 파일을 편집한 뒤 동기화가 완료되기 전에 테스트를 시작해, 결국에는 새 코드를 테스트하고 있다고 믿으면서 이전 버전에 대해 테스트를 실행하는 것이 흔한 “함정”이었습니다. 이는 종종 변경 사항이 동작하지 않는다는 잘못된 믿음으로 이어졌고, 헛수고와 큰 좌절을 초래하곤 했습니다.
워크플로를 노트북에서 시작함으로써 pay 하위 명령은 에디터와 동일한 파일 시스템 상태를 본다는 것이 보장되었고, 동기화와 협력함으로써 devbox가 그 상태 “이후”의 상태를 보게 할 수 있었습니다.
이러한 “sync barrier”에 대한 단순한 접근 방식은 (예컨대) pay test가 새로운 동기화를 촉발하도록 하는 것으로, 아무것도 변경하지 않았더라도 워크플로에 답답한 지연을 추가합니다. watchman의 clock 기능을 활용함으로써 우리는 더 나은 방식을 구현해 불필요한 왕복을 피할 수 있었습니다. 엔지니어가 파일을 편집하고 테스트를 실행하는 일반적인 경우, 이벤트 타임라인은 다음과 같이 진행됩니다.
- 사용자가 파일을 편집하고 저장합니다
pay sync가watchman에 의해 깨어납니다. 파일 시스템 clock을 기록하고rsync를 시작합니다- 사용자가
pay test명령을 시작합니다 pay test가pay sync에 동기화가 따라잡힐 때까지 기다려 달라고 요청합니다- 이에 대해
pay sync는 먼저 파일 시스템 clock을 다시 한 번 확인합니다 - 파일 시스템이 이전 저장 이후 변경되지 않았으므로, 이 타임스탬프는 현재 진행 중인
rsync와 연결된 타임스탬프와 일치합니다 pay sync는 이제pay test에 알리기 전에 이미 진행 중인rsync명령이 끝날 때까지만 기다리면 된다는 것을 알게 됩니다
watchman clock을 사용하면 이벤트 순서가 바뀌어도 견고하게 동작합니다. pay test 호출이 동기화 전, 도중, 후에 일어나더라도 올바른 동작을 얻을 수 있습니다.
이러한 pay sync 조정은 동기화 프로세스의 상태를 가시화하는 유용한 훅도 제공했습니다. 개발자 노트북은 말 그대로 노트북이므로 자주 오프라인이 되었다가 다시 온라인이 됩니다. 따라서 동기화 실패나 동기화가 오랫동안 지연되는 현상이 정상일 수 있어, sync 스크립트에서 오류 통계를 유의미하게 수집하기가 어렵습니다. 하지만 사용자가 devbox에서 동작할 pay 명령을 실행한다면, 이는 사용자가 동기화가 정상이기를 기대하고 있다는 좋은 증거가 됩니다. 따라서 동기화 프로세스에 대한 sync-barrier 호출이 실패하거나 타임아웃되면, Stripe의 중앙 예외 트래커에 오류를 보고하기에 적절한 순간이 됩니다. 그 보고를 통해 devprod는 동기화 시스템과 사용자 경험의 전반적인 상태를 모니터링할 수 있었습니다.
LSP와 에디터 툴링
위에서 언급했듯이 Stripe 엔지니어들은 원하는 에디터를 자유롭게 사용할 수 있었지만, 2019년 무렵 개발 생산성 팀은 VS Code에 투자하기로 결정했습니다. 이를 선호 에디터로 지정하고 특히 그 환경을 위한 툴링에 투자했습니다.
이 작업 중 일부는 커서 아래의 특정 테스트에 대해 pay test를 실행하는 지원과 같이 작지만 의미 있는 삶의 질 개선을 제공하는 내부 플러그인으로 구성되었습니다.
하지만 더 실질적인 개선은 Stripe의 Ruby 타입 체커인 Sorbet이 성숙해지고 LSP 서버 구현을 갖추면서 이루어졌습니다(LSP는 Language Server Protocol의 약자로, 서버가 “정의로 이동”이나 자동완성과 같은 언어 인식 기능을 다양한 에디터가 활용할 수 있는 방식으로 제공할 수 있도록 인터페이스를 정의합니다).
Stripe는 VS Code가 Sorbet LSP 서버를 ssh를 통해 devbox에서 실행하고, 로컬 LSP 서버처럼 stdin과 stdout으로 통신하도록 구성했습니다. 이렇게 함으로써 Sorbet은 devbox의 대용량 RAM을 활용할 수 있었고(LSP 서버는 일반적으로 대규모 코드베이스에서 메모리를 많이 사용하며 Sorbet도 예외가 아니었습니다), Sorbet 팀이 모니터링하고 디버깅하며 테스트하기 더 쉬운 Linux 환경에서 실행될 수 있었습니다.
전반적으로 이 접근 방식은 꽤 잘 동작했습니다. LSP 서버는 일반적으로 어느 정도의 지연을 감내해야 하므로, 추가적인 네트워크 홉과 파일 동기화로 인한 지연은 대체로 문제가 되지 않았습니다. 예를 들어 LSP 프로토콜은 사용자가 에디터에서 변경은 했지만 아직 디스크에 저장하지 않은(극히 흔한) 경우를 처리하도록 설계되어 있어, 에디터가 관련 편집 내용을 직접 서버로 보내게 됩니다. 이 과정은 결과적으로 파일 동기화의 일부 지연에 대해서도 자동으로 견고성을 제공합니다.
회고
제 생각에 여기서 설명한 툴링은 꽤 멋졌고, 꽤 잘 동작했으며, Stripe의 많은 엔지니어들에게 효과적이고 생산적인 개발 경험을 만들어 주었습니다. 여기서는 툴링을 형성했다고 생각하는 Stripe 조직과 기술 스택의 몇 가지 세부 사항에 대해 되돌아보고자 합니다. 다른 조직에서 개발자 툴링을 고민하고 있다면 고려해 볼 만한 영역들입니다.
조직 규모
여기서 설명한 툴링은 Stripe 엔지니어링 팀이 수백 명에서 1,000명 이상으로 성장하던 시기에 걸쳐 개발되었습니다. 그 기간 동안 devprod는 1 FTE 미만에서 열댓 명 규모의 팀으로 성장했습니다. 여기서 설명한 툴링의 상당 부분은 devprod가 3명 이상이 된 후반부 규모에 가까울 때 만들어졌습니다.
이러한 규모, 즉 devprod의 규모와 나아가 툴링에 10명의 FTE를 투입할 수 있을 정도의 전체 조직 규모는 우리의 선택에 중요한 요인이었습니다. 저는 꽤 복잡한 커스텀 툴링을 많이 설명했지만, 이를 만들고 유지보수할 충분한 엔지니어와 그 투자가 보상을 받을 만큼 충분한 “고객” 엔지니어가 필요했습니다. 유지보수와 신뢰성에 대해 다시 한 번 강조하고 싶습니다. 노트북에서 파일을 동기화하기 위해 파일 와처와 rsync를 이어 붙이는 일은 쉽습니다. 첫 버전은 2012년까지 이미 존재했고 초기 투자도 하루 미만이었습니다. 궁극적인 마법은 그런 도구가 존재했다는 사실이 아니라, 사용자 입장에서 신경 쓸 필요가 없을 정도로 충분히 신뢰할 수 있었고, 성장과 변화하는 요구 속에서도 계속 신뢰할 수 있는 상태로 유지되었다는 점에 있었습니다.
게다가 Stripe는 빠르게 성장하고 있었습니다. 빠르게 성장하는 조직에서는 새로운 엔지니어가 최소한의 설정 고통으로 바로 “그냥 동작하는” 개발 환경을 갖추는 것이 훨씬 더 중요합니다. 비교적 정적인 조직에서는 엔지니어들이 도구의 특성과 우회 방법을 익힐 수 있고, 이는 대체로 일회성 비용입니다. 빠르게 성장하는 조직에서는 그 비용을 모든 신규 입사자와 그들을 교육하는 엔지니어들이 끊임없이 부담하게 되므로, 안정성과 매끄러움에 대한 투자의 수익이 커집니다.
이러한 특성은 “devbox” 모델의 동기에 큰 부분을 차지했습니다. 노트북에서의 로컬 개발은 장점이 있지만, 환경을 중앙에서 관리하거나 사용자의 디버깅을 돕기가 훨씬 어렵고, 따라서 거의 필연적으로 개별 개발자의 더 많은 전문성과 참여를 요구합니다. 중앙에서 프로비저닝되고 일시적인 devbox로 옮겨가면서 우리는 유지보수의 대부분을 중앙화할 수 있었고, 대부분의 문제를 해결하는 1차 디버깅 조언으로 “master를 pull 받고 devbox를 다시 생성하세요”라는 일괄적인 처방을 제시할 수 있었습니다.
코드베이스
앞서 언급했듯이 이 인프라는 시간적으로나 코드베이스 전반에 걸쳐 비교적 일관된 아키텍처와 패턴을 가진 대규모 모노레포를 지원했습니다. 이러한 안정적인 대상이 있었기에 개발 생산성 팀은 환경 및 코드베이스와 상당히 깊이 통합되어 사용자 친화적인 기능을 제공하는 공유 툴링(예: autoloader와 devbox 서비스 매니저)을 만들 수 있는 지렛대를 확보했습니다. 훨씬 더 다양한 언어와 런타임, 패턴이 포함된 환경에서는 어느 하나에 그렇게 특화하는 것이 의미가 없고, 인프라 팀은 사용자 코드를 더 불투명한 상자로 취급하는 보다 범용적인 툴링을 구축해야 합니다.
게다가 다양한 역사적, 비즈니스적, 기술적 이유로 Stripe의 코드베이스는 여러 면에서 상당히 강하게 결합되어 있었습니다. 개발 생산성 팀의 우리에게는 그것이 쉽게 혹은 빠르게 어떤 식으로든(예: 더 많은 수의 마이크로서비스로) 분해되지 않을 것이 분명했습니다. 우리는 모노레포 내에서 모듈성과 추상화를 위한 도구와 패턴에 지속적으로 투자했지만, Ruby 모노레포 구조 전반의 “끈끈함”에 대한 확신 역시 특화된 툴링에 대한 대규모 투자를 정당화했습니다.
Stripe 내부에서도 이 선택은 다소 논쟁적이었고 항상 만장일치였던 것은 아니었음을 밝혀 둡니다. 코드와 비즈니스 가치의 대부분이 Ruby 모노레포에 있었음에도, Stripe에는 여전히 인프라 컴포넌트나 특화된 파이프라인의 일부를 실행하는 다른 코드베이스가 다수 존재했고, 이들은 일반적으로 툴링 및 인프라 팀으로부터 더 적은 지원을 받았으며 이는 반복적인 긴장의 원인이 되었습니다.
모노레포가 특히 Ruby로 작성되었다는 사실 역시 우리의 많은 결정을 세부적으로 형성했습니다. 예를 들어 소스와 아티팩트가 더 분리된 컴파일 언어였다면 우리는 다른 방향으로 갔을 수도 있습니다. 게다가 Stripe의 모노레포는 (우리가 아는 한) 현존하는 가장 큰 Ruby 코드베이스였고, 이는 우리가 종종 독자적으로 해결해야 했고 기존 툴링이 이런저런 방식으로 우리의 규모를 지원하기 어려워했다는 것을 의미합니다.
맺으며
엔지니어링 조직이 성장함에 따라 개발자 생산성을 유지하는 일은 어렵습니다. 조직과 코드베이스가 성장하면 엔지니어 1인당 생산성이 어느 정도 떨어지는 것은 거의 불가피하지만, 그 효과를 정량화하는 것은 거의 불가능합니다.
과제는 조직의 모든 수준에서 발생하며, 기술적인 경우만큼이나 사회적, 조직적인 경우도 많습니다. 제가 모든 해답을 가지고 있다거나 Stripe가 이 과제의 모든 측면에 대해 최적의, 혹은 심지어 “일관되게 충분히 좋은” 해결책을 찾았다고 주장할 생각은 없습니다. 하지만 2019년 무렵에는 Stripe가 개발자 툴링에 충분히 투자한 덕분에 여러 면에서 더 작은 성장 단계의 때보다 중앙값에 해당하는 개발자 경험을 개선했다고 생각하며, 여기서는 그 경험을 뒷받침한 기본적인 선택과 인프라를 전달하려 했습니다.
마지막으로, 개발 경험은 물론 이야기 전체 중 일부에 불과합니다. 코드와 기능의 전체 수명 주기는 CI와 코드 리뷰를 거쳐 궁극적으로 프로덕션 배포로 이어지며, 그곳에서 다시 관찰되고 디버깅되며 진화합니다. 그 시스템들에 대해 쓰는 일은 적어도 이 글만큼 긴 추가 글이 필요할 것입니다.
글을 무작위로 읽기
댓글
로그인하고 댓글 남기기