코드는 나빠지는 데 한계가 없다 (번역)
원문: https://zachkehs.com/blog/theres_no_limit_to_how_bad_code_can_get/
핵심 요약(TL;DR): “가라앉는 배” 같은 은유가 코드베이스를 설명하는 데 자주 쓰이지만, 이는 오해를 부른다. 코드 품질이 가상의 바닥에 도달하기 훨씬 전에 비즈니스가 먼저 가라앉는다. 기술 부채에는 파산(bankruptcy)이 없고, 깔끔한 리셋도 없다. 그래서 “끝”을 암시하는 은유는 잘못된 안도감을 준다.
소프트웨어는 추상의 영역에 있다. 건물이나 다리처럼 그 본질을 보고 만질 수 있는 물리적 영역에 있지 않다. 건물에 영원히 층과 방을 계속 더하면 무너진다. 소프트웨어에는 그런 제약이 없다. 코드는 언제나 더 나빠질 수 있다. 언제나 새로운 간접층(indirection layer)을 쌓거나, 성능을 한 단계 더 떨어뜨릴 수 있다.
1: 가라앉는 배에 탑승하다
10년이 넘었을 때, 나는 처음으로 지독하게 추한 레거시 코드베이스와 마주했다. 대학을 갓 나와 “Software Development Engineer”로 아마존에 입사했고, 주문 처리(processing orders) 관련 코드를 소유한 팀에서 일했다. 겉보기에 이 시스템이 해야 할 일은 단순해 보였다. 주문 처리란 무언가를 데이터베이스에 쓰고, 다른 팀들이 소유한 서비스들을 호출해 유효성 질의를 하거나 그쪽 장부를 갱신하는 것이었다. 동료들과 나는 이 시스템을 충분히 구현하고 유지·발전시키는 데 스물몇 명의 유능한 엔지니어면 족하다고 추산했다. 그런데 우리 조직은 수백 명이었고, 시스템은 그렇게 크고 복잡해져서 전체가 어떻게 돌아가는지 배우는 것 자체가 불가능한 상태가 되어 있었다.
이 조직에서는 몇 년 이상 머무는 일이 드물었고, 조직의 암묵지(institutional knowledge)는 침식되어 있었다. 그 결과 코드는 “유령 묘지(haunted graveyards)”로 가득했다. 두려움 때문에 기존 시스템을 단순화하려는 (보상받지 못할) 시도는 억눌렸다. 주문 유형마다 무엇을 해야 하는지에 대한 비즈니스 규칙은 오래전에 퇴사한 사람들이 정한 것이었고, 그 규칙은 가끔 ‘살아있는 문서(living document)’라며 자부하던 완전히 낡은 파일에서 발견되곤 했지만, 대개는 우리가 찾을 수 있는 어디에도 기록되어 있지 않았다. 직접 동작을 추적하는 것도 쉽지 않았다. 시스템의 많은 부분이 팀 경계 너머에 있었고, 그 코드에는 쉽게 접근할 수 없었기 때문이다.
어떤 생소한 프로세스가 주문에 일어나야 하는데 일어나지 않으면, 우리 호출기(pager)는 의존성 그물망 속 누군가가 불행하다고 분노하며 알려왔다. 이 피드백 메커니즘 덕분에 시스템은 떠 있긴 했지만, 바꾸기는 어렵고 성능은 처참했다. 그럼에도 최신 아마존 제품과 기능을 지원하기 위해 새로운 층이 끊임없이 얹혔다. 지속 불가능해 보였고, 이 느낌이 나와 다른 사람들이 기술 부채를 갚지 않는 조직을 묘사할 때 “가라앉는 배” 은유를 꺼내게 만든다.
그런데 이들이 그럴듯하게도, 아키텍처를 고치려는 시도는 끊임없이 있었다. 보통 이런 식이었다. 새 매니저나 시니어 엔지니어가 조직에 합류하고 “상황이 나쁘다”고 관찰한다. 조직의 리더십도 동의하고 개선을 원하지만, 가용한 엔지니어가 없어서 각 개선 시도에는 새 엔지니어와 팀을 추가하는 일이 따라붙는다.
재설계(re-architecture)는 언제나 실패했다. 시스템을 제대로 이해하려면 수 년의 연구가 필요했고, 시스템은 끊임없이 바뀌고 있었다. 그렇게 오래 걸려서 해법을 설계하는 것은 정치적으로(조직 내에서) 실행 불가능하므로, 당연히 모든 개선 시도자는 불완전한 정보로 작업해야만 했다. 이 조급함의 일부는 내부에서 나온다. 그렇게 뻔히 아픈 아키텍처의 대대적 해법을 설계하려는 사람은 부분적으로 승진을 원하기 때문이다 (승진을 너무 오래 기다리고 싶지는 않을 테니).
각 사이클은 늘 같은 방식으로 끝났다. 새 시도의 잔해가 우리 아키텍처에 영구히 이식(graft)되고, 주도하던 엔지니어는 기어이 받은 승진과 함께 떠났다. 늘어난 인원은 남는다. 마이그레이션 계획이 너무 아프고 인기가 없어서 실제로 끝내는 일은 불가능했기 때문이다. 그 사이클은 내가 입사하기 훨씬 전부터 계속되던 대로 반복되었다. 가라앉는 배에는 끝이 없어 보였다.
2: 그래서 어디서 끝나는가?
떠난 지 약 3년쯤 지나서, 그 팀의 옛 동료와 통화를 했다. 그는 6년이라는 인상적인 근속 후 최근 퇴사했고, 그 사이클이 또 한 번 완주되는 걸 지켜봤다. 우리는 서로 쓴잔을 나누듯 위로했고, 둘 다 몇 년 차이를 두고 퇴사했음에도 그 조직을 묘사할 때 같은 “가라앉는 배” 은유를 꺼냈다.
“어디서 끝나나? 어떻게 끝나나?” 그가 내 생각을 궁금해하며 물었다.
그 질문과 은유는 나에게 자꾸 걸렸고, 나는 그 질문이 두 가지를 혼동하고 있음을 깨달았다. 우리가 말하는 게 코드인가, 아니면 회사인가?
비즈니스는 가라앉을 수 있다. 나쁜 소프트웨어가 비즈니스에 실제 드래그가 되는 건 사실이지만, 그게 얼마나 중요한지는 여러 요인에 달렸다. 아마존처럼 현금흐름이 풍부한 회사는 내부의 부식이 한동안 어슬렁거려도 최종 손익에 의미 있는 타격이 가기 전까지 버틸 수 있다. 반면 비즈니스 모델이 소프트웨어 품질에 더 민감한 다른 회사에게는, 나쁜 소프트웨어가 경쟁자에게 배가 뚫리는 순간을 맞이하라는 잠복 초대장이 될 수 있다 (그리고 그렇다고 LLM이 이걸 바꿔주지는 않는다).

하지만 코드에 대해서라면, 가라앉음에는 끝이 없다. 그것은 무한히 가라앉는 배다. 코드가 얼마나 나빠질 수 있는지에 한계가 없기 때문이다. 당신은 무너지려는 건물에서 탈출한 게 아니다. 그 건물은 끊임없는, 영원히 끝나지 않는 붕괴 상태에 있다. “끝”을 암시하는 말을 쓰는 것 자체에 문제가 있다.
소프트웨어는 추상의 영역에 있다. 건물이나 다리처럼 그 본질을 보고 만질 수 있는 물리적 영역에 있지 않다. 건물에 영원히 층과 방을 더하면 무너진다. 소프트웨어에는 그런 제약이 없다. 코드는 언제나 더 나빠질 수 있다. 언제나 새로운 간접층을 쌓거나, 성능을 한 단계 더 떨어뜨릴 수 있다.
교조적인 사람들은 소프트웨어가 충분히 나빠지면 완전히 작동을 멈출 수 있다고 지적할 것이다. 실제로 그런 파괴적 변경은 빠르게 리버트된다. 반면 그 코드를 나쁘게 만든 그 수천 개의 이전 변경들은 리버트되지 않는다. 소프트웨어는 계속 ‘작동’한다. 리버트할 단일 파괴적 변경이 없는 경우는, 나쁜 소프트웨어의 부풀어 오르는 비용이 그 이익을 압도하거나, 아무것도 고장 내지 않고는 아무것도 배포할 수 없어 개발 속도가 0에 수렴하는 경우다. 이 모든 경우에, 코드가 어떤 가상의 바닥에 닿기 훨씬 전에 비즈니스가 먼저 죽는다 (그러니 바닥이 있다는 듯이 행동하지 말라).
3: 기술 부채에는 파산이 없다
나쁜 소프트웨어가 비즈니스에 주는 드래그는 실재하는 위협이고, 좋은 조직이 코드 품질에 신경 쓰는 이유다. 소프트웨어 품질에는 급격한 실패 임계점이 없으므로 흔히 “기술 부채(technical debt)”로 묘사되는데, 이는 더 나은 은유일 수 있다 (부채는 영원히 복리로 불어날 수 있으니까) 하지만 역시 완벽하진 않다.
부채에는 ‘끝’이 있다. 파산이 강제 리셋이기 때문이다. 소프트웨어에서 그에 대응하는 것은 전면 리라이트(full rewrite)인데, 이것은 거의 불가능한 선택이다 (Joel Spolsky의 고전).
아마존 같은 초대형 기업이 취할 수 있는 가장 근접한 옵션은 내가 “사이드 채널(side-channel)”이라 부르는 것이다. 새로운, 완전히 분리된 시스템을 특정 새 유즈케이스에 필요한 최소 기능만으로 빌드할 팀을 갈라놓는 것이다. 이후로는 새 유즈케이스를 이 단순화된 별개 시스템으로 보낼 수 있는 선택지가 생긴다. 중요한 건, 구 시스템은 반드시 남아 유지보수되어야 한다는 점이다 (그게 ‘끝’이 아니다). 모든 구 유즈케이스가 여전히 존재하고, 미래에 어느 쪽을 쓸지 결정할 때마다 조직 차원의 새로운 고통이 반복되기 때문이다. 파산처럼 판을 싹 밀어버리는 것과는 거리가 멀다.
소프트웨어에 대한 잘못된 멘탈 모델이 나쁜 결정을 낳는다. ‘하드 리셋’ 탈출구가 존재한다고 믿으면, 기술 부채를 나중으로 미루는 게 그렇게 나빠 보이지 않는다. 리라이트가 곧 있을 거라는 믿음은 오늘의 결정을 더 나쁘게 만든다. 오늘의 결정권자는 탈출구가 없다는 사실을 이해하지 못하기 때문이다.
“붕괴하는 건물”이나 “가라앉는 배” 같은 은유는 소프트웨어에 적합하지 않다. 그럼에도 우리는 소프트웨어를 뭐가 다른지 강조하기 위해 그 은유들을 일부러 뒤틀어 받아들일 수 있다. 건물은 무한히 붕괴하는 중이다. 배는 무한히 가라앉는 중이다. 당신의 프로젝트 매니저를 깨워 기술 부채를 다루도록 강제할 자연적 제약은 존재하지 않는다. 소프트웨어는 우리가 가라앉음을 막으려 노력을 들일 때만 고품질로 남는다. 양동이를 집어 들자.
[덧붙임] Simon Willison의 코멘트, “리라이트가 성공하는 경우는 정말 드물다”
(기술 부채가 감당 불가능해지면 갈아엎고 처음부터 다시 짜자는 반응에 답하며)
내 경험상 그게 통하는 경우는 정말 드물다.
상황을 재현해보자. 당신은 옛 시스템이 기술 부채에 되돌릴 수 없이 잠겼다고 선언한다. 처음부터 리라이트할 팀을 꾸린다. 작업이 시작된다.
한편 옛 시스템은 여전히 움직이는 표적(moving target)이다. 핵심 비즈니스를 돌리고 있으니 변경은 계속 필요하다. 그걸 담당하는 개발자들은 자기 시스템이 곧 폐기될 걸 알기에, 새 기능을 추가하되 최소한의 노력만 들일 유인밖에 없다. 기술 부채는 계속 쌓인다.
한편 새 시스템을 만드는 팀은 야심차고 아마 약간은 순진하다. 초반 속도는 좋다, 그린필드이니까, 하지만 시간이 지나면 드러난다. 자신들이 대체하려는 것의 동작과 범위를 완전히 이해하는 사람은 아무도 없다는 게. 잘 문서화되고 잘 테스트된 시스템이라면 애초에 교체할 필요가 없었을 테니까…
가치를 전혀 전달하지 못한 채 몇 달(혹은 몇 년)이 흐르면 “이제 배포하라”는 압박이 온다. 그래서 새 시스템은 구 시스템이 담당하던 것의 일부만 처리하는 상태로 출시된다, 아니면 흔히, 이제 사실상 방치된 구 시스템으로는 만들기 너무 어려웠던 어떤 새 기능을 위해 출시된다.
…그래서 이제 프로덕션에는 두 개의 시스템이 존재한다. 아무도 건드리고 싶지 않은 삐걱대는 구 시스템, 그리고 극소수의 프로덕션 기능만 처리하고 나머지 80%는 ‘언젠가 구 시스템을 대체할’ 비활성 코드로 가득한 신 시스템.
운이 정말 좋다면, 회사가 아직 새 시스템에 대한 인내심을 잃지 않아 작업을 계속 허용해줄 것이다. 이 모든 게 오래 걸릴수록, 그리고 구 시스템이 프로덕션에 남아 버티고 작동하는 날이 길어질수록, “우선순위가 바뀌었다”며 신 시스템 전면 교체 작업이 중단될 위험이 높아진다. 결과적으로 하나 있던 시스템이 둘이 된다.
이 과정을 책임감 있게 완수하는 방법에 대해 내가 읽은 최고의 글은 Will Larson의 Migrations: the sole scalable fix to tech debt다.
앞으로 이런 상황을 마주친다면 내 강한 권고는 이렇다. 구 시스템에 자동화 테스트를 최대한 많이 보강하고(shore up), 그다음 타겟형 리팩터가 원하는 모양까지 갈 수 있는지 보라. 내 직감에, 많은 경우 이 방법이 그린필드 교체의 세이렌 노래보다 훨씬 높은 성공 확률을 갖는다.
(토론 스레드의 또 다른 통찰, hyperpape의 코멘트: “이해할 수 없는 비즈니스 로직을 가진 시스템을 보고 ‘재설계하면 명확해질 것’이라고 생각하는 게 이해가 안 된다. 재설계하려면 먼저 이해해야 한다. 재설계는 세련되게 들리지만, 기존 시스템의 기능을 이해하는 일은 노가다처럼 들린다”, 그리고 schneems의 보충: 리팩터를 검증하려면 프로브를 심어 와이드 이벤트 스토어에 덤프하고, 리팩터된 코드를 병렬로 no-op 실행해 결과를 대조하라.)