Ramp에서 머신러닝 워크플로의 모노레포는 편리한 공유 공간에서 팀이 '일종의 늪'이라고 부르는 상태로 변해 있었다. 버전이 고정되지 않은 전이적 의존성이 담긴 requirements.txt가 머신마다 달라졌고, 포매팅은 일관되지 않았으며, 자동화된 테스트는 없었고, 타입 애너테이션은 누락되었거나 미묘하게 틀려 있었다. 엔지니어들은 무언가를 깨뜨릴까 봐 코드를 건드리기를 꺼렸고, 원래 목표였던 속도는 거의 멈춰버렸다.

팀은 저장소를 다시 작성하는 대신 네 가지 도구를 그 자리에서 도입했다. 잠긴 재현 가능한 의존성을 위한 poetry, 포매팅과 린팅을 위한 ruff, 전체 커버리지를 좇기보다 영향력이 큰 공유 유틸리티를 테스트하기 위한 pytest, 점진적 타입 검사를 위한 mypy였다. 운영 중인 코드베이스에서 도입을 가능하게 만든 요령은 모든 것을 먼저 고치지 않은 것이었다. 자동 포매터를 저장소 전체에 한 번 실행해 단일 커밋으로 만들고, 그 커밋의 해시를 .git-blame-ignore-revs에 추가해 blame 이력을 오염시키지 않게 했다.

린팅과 타입 검사에서는 CI를 켜기 전에 수백 개의 기존 오류를 고치는 대신, 기존 위반마다 '# noqa'와 '# type: ignore' 주석을 기계적으로 생성한 뒤 새로 들어오는 것에만 강제 적용을 켰다. ruff의 'RUF100' 규칙과 mypy의 'warn_unused_ignores'는 해당 코드가 실제로 고쳐진 뒤 쓸모없어진 억제 주석을 표시했기 때문에, 무시 목록은 줄어들기만 할 수 있었고 조용히 썩어들어갈 수는 없었다.

그 결과 코드베이스는 명령으로 '깨끗해진' 적은 없지만 첫날부터 앞으로 들어오는 코드에 대해서는 강제되었고, 개발자들이 자연스럽게 오래된 파일을 건드리면서 부채는 줄어들었다.

기존 위반은 예외로 인정하고 새 코드에만 강제 적용

전면 재작성이나 대규모 일괄 정리는 강제 적용을 시작하기도 전에 몇 주를 소모했을 것이므로, 팀은 소급해서가 아니라 앞으로 들어오는 코드에 대해 강제했다.

초기 억제 주석을 기계적으로 생성한 덕분에 끝이 보이지 않던 수동 정리가 한 번만 수행하면 되는 자동화 가능한 단계로 바뀌었다.

사용되지 않는 억제 주석을 표시한 덕분에 부채는 줄어들기만 할 수 있었다. 오래된 위반을 고치면 이제는 무의미해진 무시 주석이 드러나고 CI가 이를 잡아냈기 때문이다.

재포매팅을 하나의 커밋으로 합치고 git blame에서 제외한 덕분에, 어떤 줄을 실제 이유로 마지막에 건드렸는지 확인할 수 있는 능력이 유지되었다.

코드베이스에 표준을 강제하기 전에 코드베이스가 깨끗할 필요는 없다. 기존 위반을 한 번 모두 침묵시키고 새 코드에만 엄격히 강제하면, 사람들이 어차피 오래된 파일을 건드리면서 백로그는 줄어든다.

팀은 코드베이스가 앞으로 품질이 유지되는 상태에 도달했다고 보고했다. poetry의 잠금 파일은 개발자 머신과 CI 전반에 일관된 환경을 제공했고, ruff는 병합 전에 중복 딕셔너리 키 같은 실제 버그를 잡아냈으며, mypy는 단위 테스트가 다루지 못한 호출 지점 전반의 시그니처 변경 회귀를 잡아냈다. 이 글은 목표를 도구 자체가 아니라 대규모 변경을 할 수 있는 신뢰의 회복으로 규정하며, 팀이 이 부담을 기여자에서 도구로 더 옮기는 방안을 여전히 검토하고 있었다고 언급한다.

근거 확인하기

출처

  1. Increasing velocity by modernizing a Python codebase Ramp Engineering · builders.ramp.com ·