ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 파이프라인을 자동화했더니 부채가 쏟아졌다 — 갚는 비용의 단위는 항목 수가 아니라 판단 횟수다
    IT 2026. 9. 13. 21:00

    🎧 오디오 개요 — 파이프라인을 자동화했더니 부채가 쏟아졌다 — 갚는 비용의 단위는 항목 수가 아니라 판단 횟수다

    20분 50초 — 기술 부채를 갚는 비용의 단위는 항목 개수가 아니라 맥락을 새로 불러오는 판단 횟수다. 스무 개를 안고 가면 실행할 때마다 항목마다 판단해 20 곱하기 실행 횟…. 화면은 이 글의 인포그래픽 한 장 고정 — NotebookLM 오디오 개요로 생성

    손으로 하던 일을 파이프라인으로 묶는 중이다. 자동화 자체는 어렵지 않았다. 어려운 건 그 과정에서 전체를 처음부터 끝까지 훑게 된다는 점이었다. 사람이 돌릴 때는 "아 이건 원래 이럴 때 한 번 더 눌러 줘야 해" 하고 넘어가던 것들이, 기계가 돌리려니 전부 명시적으로 처리해야 할 항목이 됐다. 그렇게 스무 개 남짓한 부채 목록이 눈앞에 쌓였다.

    여기서 갈림길이 생긴다. 안고 가면 부채마다 예외 처리 루틴을 하나씩 달아야 하고, 그 루틴들이 또 파이프라인의 일부가 된다. 하나씩 갚으면서 가면 자동화 자체가 하염없이 늦어진다. 어느 쪽도 공짜가 아니다. 그래서 공개 자료를 뒤졌는데, 커뮤니티의 답이 하나로 모이지 않는다는 게 첫 번째 발견이었다. 오히려 서로 정면으로 어긋나 보이는 답이 세 갈래로 나온다.

    먼저, 이 부채가 비싼가 — 숫자부터 본다

    결정을 하려면 이자율을 알아야 한다. Adam Tornhill과 Markus Borg가 2022년에 낸 Code Red: The Business Impact of Code Quality는 실제 상용 코드베이스 39개, 파일 3만 737개를 놓고 코드 품질과 개발 시간의 관계를 쟀다. 결과는 이렇다. 품질이 낮은 코드는 결함이 15배 많았고, 같은 이슈를 해결하는 데 평균 124% 더 긴 시간이 걸렸다. 그리고 가장 아픈 항목은 예측 가능성이었다 — 최대 사이클 타임이 9배로 벌어졌다. 평균이 두 배 느려지는 것보다, 언제 끝날지 모르는 상태가 되는 게 계획을 망친다.

    주관적 체감으로도 뒷받침된다. 2024년 Stack Overflow 개발자 설문에서 기술 부채(technical debt — 빠른 구현을 위해 나중에 갚기로 하고 미뤄 둔 설계·구현 빚)의 양은 전문 개발자가 꼽은 좌절 요인 1위였고, 응답자의 62.4%가 이를 지목했다. 개인 기여자(62.3%)와 관리자(62.4%)가 거의 같은 비율로 답했다는 게 흥미롭다. 흔히 "위에서는 부채를 모른다"고 하지만, 적어도 현장 관리자는 알고 있었다.

    그러니 "안고 가도 별거 아니다"는 선택지는 일단 접힌다. 문제는 지금 갚을지, 나중에 갚을지, 어느 것부터 갚을지다.

    커뮤니티의 답이 세 갈래로 어긋난다

    이 질문에 커뮤니티는 서로 다른 대답을 내놓는데, 셋 다 근거가 탄탄해서 어느 하나를 틀렸다고 하기가 어렵다.

    첫 번째 — 즉시 갚아라. Martin Fowler의 설계 체력 가설(design stamina hypothesis)은 내부 품질을 포기하고 얻는 속도 우위가 오래가지 않는다고 본다. Fowler가 못 박은 문장이 인상적이다. "개발자들은 몇 주 안에 저품질 코드가 자신을 상당히 느리게 만든다는 걸 발견한다(Developers find poor quality code significantly slows them down within a few weeks)." 그는 품질과 비용의 트레이드오프가 성립하는 구간, 즉 "활주로"가 생각보다 아주 짧다고 덧붙인다. 몇 달이 아니라 몇 주다.

    두 번째 — 하나씩 갚지 말고 한 번에 쓸어라. Google의 Software Engineering at Google은 팀별 점진적 정리를 정면으로 부정한다. "아무도 예산 없는 지시를 좋아하지 않는다(nobody likes unfunded mandates)"는 게 이유고, 더 결정적인 이유는 따로 있다. "유기적 마이그레이션은 완전히 성공하기 어려운데, 부분적으로는 엔지니어들이 새 코드를 짤 때 기존 코드를 예시로 삼기 때문이다." 남아 있는 옛 패턴이 새 옛 패턴을 낳는다. 그래서 Google은 대규모 변경(LSC, Large-Scale Change — 저장소 전체를 가로지르는 기계적 일괄 변경)을 전담 도구로 처리한다. scoped_ptr를 std::unique_ptr로 바꾼 작업은 하루에 독립 변경 700건 이상, 파일 1만 5천 개 이상을 건드렸고, 가장 큰 삭제 작업은 사흘 동안 10억 줄 이상을 지웠다.

    세 번째 — 절대 한 번에 바꾸지 마라. 같은 Fowler가 교살자 무화과 패턴(strangler fig pattern — 낡은 시스템을 통째로 갈아엎지 않고 새 시스템을 그 위에 조금씩 키워 결국 대체하는 방식)에서는 정반대를 말한다. "우리는 옛 시스템이 뭘 하는지 아니까, 똑같이 하는 새 시스템을 새 기술로 만들면 된다"는 발상이 대개 실패한다는 것이다. 이유는 시간이다 — "심각한 IT 시스템을 교체하는 데는 오랜 시간이 걸리고, 사용자들은 새 기능을 그때까지 기다려 주지 않는다." 버려질 걸 알면서도 과도기 구조물을 짓는 비용을 감수해야 하며, "점진적 접근의 낮아진 리스크와 앞당겨진 가치가 그 비용을 넘어선다"고 결론짓는다.

    어긋남이 풀리는 지점 — 셋은 다른 질문에 답하고 있다

    세 답을 나란히 놓고 한참 봤는데, 모순이 아니었다. 각자 다른 종류의 부채를 전제하고 있었다. 구분선은 두 개다. 하나는 이번 작업이 그 코드를 실제로 지나가는가, 다른 하나는 그 변경이 기계적인가 판단이 필요한가.

    diagram

    다이어그램 설명. 이 그림이 보여주는 건 "안고 가느냐 갚느냐"가 애초에 이지선다가 아니었다는 것이다. "이번 작업이 이 코드를 실제로 지나가는가"라는 질문에서 먼저 갈리고, 지나가지 않는 쪽만 다시 "기계적으로 한 번에 바꿀 수 있는가"로 갈린다. 그래서 최종 선택지는 셋이 된다 — 지금 갚거나, 일괄 처리하거나, 장부에 적어 둔다. 앞의 세 커뮤니티 답이 각각 이 세 자리에 정확히 하나씩 들어간다. Fowler의 "몇 주 안에 발목 잡힌다"는 지금 지나가는 코드 이야기고, Google의 일괄 변경은 기계적으로 바꿀 수 있는 것 이야기고, 교살자 무화과는 판단이 필요해서 한 번에 못 바꾸는 것 이야기다. 놓치기 쉬운 함정은 "지나가지 않는다"를 성급하게 판정하는 것이다 — 이번 실행에서 그 코드가 예외 경로로 한 번이라도 밟힌다면 그건 지나가는 것이고, 지금 갚는 쪽이 싸다.

    "지금 지나가는 것부터"라는 규칙에는 이미 정식 이름이 있다. Kent Beck의 한 줄이 그것이다 — "변경을 쉽게 만들고, 그다음에 쉬워진 변경을 하라(make the change easy, then make the easy change)." Fowler는 이를 준비 리팩터링(preparatory refactoring)이라 부르며, 새 기능이 들어갈 자리의 구조부터 먼저 손보고 기능을 얹는 순서를 권한다. 핵심은 갚는 범위가 지금 손대는 자리로 한정된다는 점이다. 별도의 정리 프로젝트를 여는 게 아니다.

    어느 것부터 갚나 — 부채의 양이 아니라 부채가 밟히는 빈도

    목록이 스무 개면 순서를 정해야 한다. 여기에 대해 가장 실용적인 답을 내놓은 건 CodeScene 진영의 핫스팟(hotspot) 방법이다. 코드의 복잡도만 보지 않고 버전 관리 이력상 얼마나 자주 바뀌었는지를 곱해서 우선순위를 매긴다. 결과가 극단적이다 — 우선순위가 높게 잡힌 핫스팟은 전체 코드의 2~3%에 불과한데, 전체 커밋의 11~16%가 그 좁은 영역에서 일어난다.

    뒤집어 말하면 이렇다. "대부분의 파일은 긴 꼬리에 있고, 이는 거의 손대지 않는 코드라는 뜻이다." 안 건드리는 코드의 부채는 이자가 붙지 않는다. 리팩터링은 그 자체로 위험하고 비싼 활동이니, 밟히지 않는 자리에 그 비용을 쓰는 건 순손실이다. 이게 내가 찾던 답에 가장 가까웠다 — 전부 갚을 필요가 없다는 게 게으름이 아니라 근거 있는 전략이라는 것이다.

    그래서 부채 목록을 다시 정렬했다. 기준은 "이게 얼마나 지저분한가"가 아니라 "파이프라인이 이걸 몇 번 밟고 지나가는가"다. 실행할 때마다 밟히는 것은 이자가 매일 붙고, 분기에 한 번 밟히는 것은 사실상 무이자다.

    비용의 단위는 부채 항목 수가 아니라 판단 횟수다

    여기까지 정리하고 나서야 처음 질문이 왜 답하기 어려웠는지 알았다. 나는 비용을 부채 항목 수로 세고 있었다. 스무 개를 안고 가면 예외 루틴이 스무 개 생기고, 갚기로 하면 작업이 스무 개 생긴다. 어느 쪽이든 스무 개니까 답이 안 나온 것이다.

    실제 비용의 단위는 항목 수가 아니라 사람이 판단을 내려야 하는 횟수다. 같은 스무 개라도 처리 방식에 따라 판단 횟수가 전혀 다르게 붙는다.

    처리 방식 판단이 필요한 시점 스무 개일 때 총 판단 횟수
    안고 간다 (예외 루틴) 실행할 때마다, 항목마다 20 × 실행 횟수
    하나씩 갚는다 항목마다 한 번씩, 맥락을 새로 불러와서 20
    지나가는 것만 갚는다 이미 그 코드를 보고 있을 때 지나가는 개수만큼, 맥락 로딩 0
    기계적으로 일괄 처리한다 도구를 만들 때 한 번 1

    이 표가 왜 중요한지는 Google의 기준선 하나가 잘 보여준다. LSC 문서는 "변경이 500곳 이상의 수정을 요구한다면, 엔지니어가 자동 변경 생성 도구를 배워서 실행하는 편이 대개 더 효율적이다"라고 못 박는다. 500이라는 숫자 자체보다, 손으로 하는 비용과 도구를 만드는 비용이 교차하는 지점이 존재한다는 사실이 중요하다. 그 지점을 넘으면 "하나씩 성실하게"는 성실한 게 아니라 비싼 것이다.

    그리고 "안고 간다"의 진짜 비용도 여기서 드러난다. 예외 루틴 하나를 붙이는 비용은 한 번이지만, 그 루틴이 옳게 동작하는지 확인하는 판단은 실행할 때마다 다시 붙는다. 곱셈으로 늘어나는 항이 여기 하나 있고, 나머지 선택지에는 없다.

    내 파이프라인에서 실제로 이렇게 갈렸다

    추상적인 이야기만 하면 못 믿을 테니, 최근에 직접 겪은 걸 하나 적는다. 홈 서버에서 돌리는 파이프라인 중에 글 초안을 넣으면 부속 자산을 만들어 본문에 붙여 주는 게 있다. 생성이 외부 서비스 호출이라 실패가 잦아서, 재시도 큐와 타이머로 묶어 뒀다.

    어느 날 큐 상태를 봤더니 초안 다섯 편의 자산 열 건이 전부 done이었다. 그런데 초안을 열어 보니 하나도 붙어 있지 않았다.

    diagram

    다이어그램 설명. 이 그림은 부채를 안고 가기로 한 결정이 어떻게 조용해지는지를 보여준다. "자산 생성 성공"에서 출발해 "초안에 붙이기 시도"로 가고, 거기서 두 갈래로 나뉜다. 성공하면 본문에 임베드가 생기지만, 실패하면 오류를 필드에만 적고 상태는 그대로 done으로 둔다. 이 설계 자체는 의도적이었다 — 붙이기가 실패했다고 자산을 다시 만들면 외부 서비스의 일일 한도를 태우니까, "만든 건 만든 거다"로 처리한 것이다. 판단으로서는 타당했다. 문제는 그 타당한 판단이 만든 상태와 결과의 어긋남을 아무 데도 드러내지 않았다는 점이다. 놓치기 쉬운 함정이 바로 여기다 — 예외를 처리했다는 것과 예외를 보이게 뒀다는 것은 전혀 다른 일이고, 조용히 성공하는 예외 처리는 부채를 파이프라인의 정상 동작으로 승격시킨다.

    원인은 더 시시했다. 자산 생성은 격리된 파이썬 환경에서 돌았고, 그 환경에는 업로드에 필요한 브라우저 자동화 라이브러리가 없었다. 생성은 되고 업로드는 죽는 구조였다. 이건 파이프라인이 매 실행 밟고 지나가는 자리였으니 앞의 규칙대로 즉시 갚아야 할 부채였고, 실제로 그렇게 했다.

    다만 갚은 방식이 예상과 달랐다. 나는 "붙이기가 실패하면 작업을 done으로 넘기지 않는다"로 고칠 줄 알았는데, 그렇게 하면 재시도가 자산을 다시 만들어 한도를 태운다. 그래서 실제로 넣은 건 두 가지였다 — 매 실행 앞에서 필요한 것들이 실제로 기동하는지 확인하는 사전 점검, 그리고 상태 출력에 "만들어졌지만 안 붙은 건수"를 따로 세는 줄이다. 부채를 없앤 게 아니라 계기판을 달았다.

    안고 갈 것에는 상한과 계기판을 단다

    이 "계기판" 접근에 정식 근거가 있다. Google SRE 책의 toil(고역 — 서비스를 굴리는 데 필요하지만 수동·반복적이고 자동화 가능하며 지속적 가치가 없는, 서비스 규모에 비례해 늘어나는 작업) 장이다. 여기서 SRE가 내건 목표는 "toil을 0으로 만든다"가 아니라 "운영 작업을 각 SRE 시간의 50% 미만으로 유지한다"는 상한이다. 그리고 그 이유를 이렇게 적는다. "toil은 방치하면 팽창하는 경향이 있고, 순식간에 모두의 시간을 100% 채울 수 있다."

    핵심은 허용하되 계량한다는 것이다. 안고 가기로 한 부채는 사라지지 않으니, 그게 얼마나 자라는지 보이는 자리에 숫자로 띄워 둔다. 내가 상태 출력에 "미반영 건수"를 붙인 게 정확히 이 형태였다. 참고로 이 발상은 기준선 잠금(baseline lock — 지금 존재하는 위반 목록을 스냅샷으로 박아 두고 그 목록에 없는 신규 위반만 차단하는 방식)과 형제다. 둘 다 "지금 있는 건 인정하되, 늘어나는 건 막는다"는 같은 자리에 서 있다.

    여기에 위안이 하나 붙는다. Fowler의 기술 부채 사분면(technical debt quadrant)은 부채를 의도적/무심결과 신중한/무모한의 두 축으로 나눈다. 파이프라인을 자동화하다가 쏟아져 나오는 부채는 대개 무심결이면서 신중한(inadvertent-prudent) 칸에 들어간다. Fowler가 그 칸을 설명하며 쓴 문장이 이렇다. "어떤 프로젝트에서는 최선의 설계 접근이 무엇이었는지 이해하기까지 1년간의 프로그래밍이 걸릴 수 있다." 즉 훌륭한 팀도 다 만들고 나서야 더 나은 설계를 알아본다는 뜻이고, 이 칸의 부채는 부주의의 증거가 아니라 이해가 자란 증거다.

    이게 왜 중요한가? 부채 더미를 보면 "내가 그동안 엉망으로 해 왔구나"라는 반응이 먼저 오는데, 그 감정이 판단을 왜곡한다. 죄책감은 전부 갚기를 향하고, 전부 갚기는 앞의 핫스팟 데이터가 말해 주듯 대부분 낭비다. 실제로 SEI(카네기멜런대 소프트웨어공학연구소)의 현장 조사에서도 응답자의 65%가 정의된 기술 부채 관리 관행 자체를 갖고 있지 않았고, 부채의 최대 원천으로 지목된 건 지저분한 코드가 아니라 아키텍처 선택이었다. 대부분의 팀이 애초에 이 결정을 체계적으로 하지 않고 있다는 뜻이기도 하다.

    정리 — 물어야 할 질문은 "갚을까 말까"가 아니다

    공개 자료를 훑고 남은 건 처방전이 아니라 질문의 교체였다. "부채를 안고 갈까, 갚을까"는 답이 나오지 않는 질문이다. 어느 쪽이든 항목 수가 같으니까. 대신 세 개를 순서대로 묻는다.

    첫째, 이번 작업이 그 코드를 지나가는가. 지나가면 지금 갚는다 — 맥락이 이미 머릿속에 있어서 판단 비용이 사실상 0이고, Fowler의 몇 주짜리 활주로가 그 근거다. 둘째, 기계적으로 한 번에 바꿀 수 있는가. 가능하면 도구를 만들어 일괄 처리한다 — 판단 한 번으로 끝나고, Google은 그 교차점을 500곳으로 잡았다. 셋째, 나머지는 장부에 적고 상한을 건다. 없애는 게 아니라 자라지 못하게 묶는 것이고, 그 숫자는 반드시 눈에 보이는 자리에 있어야 한다.

    그리고 안고 가기로 한 부채에 대해 마지막으로 확인할 게 하나 있다. 그 예외 처리 루틴이 조용히 성공하고 있지는 않은가. 내 파이프라인이 열 건을 전부 done으로 보고하면서 아무것도 붙이지 않았던 것처럼, 잘 만든 예외 처리는 부채를 감추는 데도 똑같이 유능하다. 안고 가도 된다. 다만 안고 있다는 사실이 보이는 상태로 안고 가야 한다.


    참고한 공개 자료:


    이 글은 생성형 AI의 도움을 받아 작성되었습니다. 원본 자료를 기반으로 AI가 초안을 생성하고, 작성자가 검토·편집하였습니다.

Designed by Tistory.