40,000줄의 "죽은 코드(Dead Code)"를 삭제했습니다 — 3분 만에 운영 환경이 망가졌습니다
요약
코드 커버리지 도구를 믿고 40,000줄의 죽은 코드를 삭제했다가 운영 환경이 마비된 사례를 다룹니다. 정적 분석 도구가 감지하지 못하는 데이터베이스 기반의 동적 참조(eval)가 미치는 위험성을 경고합니다.
핵심 포인트
- 코드 커버리지 도구는 동적 참조를 완벽히 감지하지 못함
- eval() 등을 이용한 데이터베이스 기반 동적 호출의 위험성
- 정적 분석의 한계를 인지하고 신중한 리팩토링 필요
40,000줄의 "죽은 코드(Dead Code)"를 삭제했습니다 — 3분 만에 운영 환경이 망가졌습니다
우리 모두는 죽은 코드(dead code)를 싫어합니다. 그것은 코드베이스의 잡동사니 서랍과 같습니다. 아무도 그것이 무엇을 하는지 모르고, 몇 년 동안 아무도 건드리지 않았으며, 그저 공간만 차지하며 그 자리에 놓여 있을 뿐입니다.
그래서 제가 코드 커버리지(code coverage) 도구를 실행했을 때, 참조가 전혀 없는 40,000줄이 표시되자 저는 영웅이 된 기분이었습니다. 저는 이 난장판을 정리하려 했습니다. PR(Pull Request) 설명은 말 그대로 다음과 같았습니다: "정리 작업 — 사용하지 않는 코드 제거."
저는 완전히 틀렸습니다.
설정 (The Setup)
우리의 코드베이스는 5년에 걸쳐 유기적으로 성장했습니다. 여러 팀, 여러 번의 재작성(rewrites), 그리고 결코 실행되지 않은 최소 두 번의 "나중에 정리하자" 단계가 있었습니다.
커버리지 보고서는 명확했습니다: 이 함수(functions), 클래스(classes), 그리고 모듈(modules)들은 호출자(callers)가 제로였습니다. 임포트(imports)도 제로였습니다. 참조(references)도 제로였습니다. 도구는 심지어 정확한 파일들까지 보여주었습니다. 저는 확인하는 데 약 20분을 썼습니다. 몇 개의 호출 체인(call chains)을 클릭해 보고, 동적 참조(dynamic references)를 검색했습니다. 아무것도 없었습니다.
저는 PR을 열었습니다. 한 명의 리뷰어는 5분 만에 승인했습니다. 다른 리뷰어는 보지도 않았습니다. 우리는 목요일 오후 4시에 머지(merge)했습니다. 전형적인 상황이었죠.
그다음에 일어난 일
1~3분: 침묵
배포(Deploy)는 초록불이 떴습니다. 모든 테스트를 통과했습니다. 경고(alerts)도 울리지 않았습니다. 저는 생산적이라는 기분을 느끼며 노트북을 닫았습니다.
4분: 첫 번째 페이지
Slack 알림: "체크아웃(Checkout)이 실패하고 있습니다."
"체크아웃이 느립니다"가 아니었습니다. "체크아웃이 이상합니다"도 아니었습니다. 실패하고(Failing) 있었습니다. 즉, 고객들이 물건을 살 수 없다는 뜻이었습니다.
10분: 조사
저는 에러 로그(error logs)를 살펴보았습니다. 스택 트레이스(stack trace)는 제가 방금 삭제한 파일을 가리키고 있었습니다. 하지만 그것은 불가능했습니다. 커버리지 도구는 아무도 그것을 호출하지 않는다고 말했으니까요.
그때 저는 그것을 발견했습니다.
문제: 동적 참조 (Dynamic References)
우리의 "레거시(legacy)" 결제 연동 중 하나는 설정 데이터베이스로부터 결제 프로세서 클래스 이름을 동적으로 생성하기 위해 eval()을 사용했습니다 — 네, _eval_입니다.
# config 테이블에는 다음과 같이 있었습니다: "processor_class": "StripeLegacyProcessor"
processor = eval(f"{config.processor_class}(api_key)")
커버리지 도구는 이를 감지할 수 없었습니다. 참조가 코드가 아니라 *데이터베이스 내의 문자열(string)*이었기 때문입니다. 정적 분석(Static analysis)으로는 데이터베이스 행에 있는 "StripeLegacyProcessor"가 from legacy_payments import StripeLegacyProcessor를 의미한다는 사실을 알 방법이 없었습니다.
하지만 잠깐 — 상황은 더 악화됩니다.
숨겨진 웹 (The Hidden Web)
그 단 하나의 eval()은 빙산의 일각에 불과했습니다. 모든 동적 참조(dynamic references)를 찾아내기 시작하자, 다음과 같은 것들을 발견했습니다:
- 데이터베이스 기반 기능 라우팅 (Database-driven feature routing) — 클래스 이름으로 저장되어 런타임(runtime)에 결정되는 기능 플래그(feature flags)
- 플러그인 시스템 (Plugin system) — 클래스 이름으로 "활성 플러그인"을 나열하는 YAML 파일
- 관리자 대시보드 (Admin dashboard) — 사용자 권한에 따라 동적으로 로드되는 보고서 생성기(report generators)
- 웹훅 핸들러 (Webhook handlers) — JSON 설정을 통해 핸들러 클래스에 매핑되는 URL 경로
각각은 모두 커버리지 도구가 볼 수 없는 문자열 참조였습니다. 그리고 "죽은(dead)" 코드를 삭제할 때마다 각각의 기능이 망가졌습니다.
해결책 (The Fix)
저는 약 2분 만에 PR 전체를 되돌렸습니다(revert). 하지만 피해는 이미 발생했습니다. 결제(checkout) 서비스가 약 15분 동안 중단되었고, 저는 CTO에게 왜 "정리(housekeeping)"를 위한 PR 때문에 수익 파이프라인을 망가뜨렸는지 설명해야 했습니다.
배운 점
1. 커버리지 도구는 거짓말을 한다 (동적 참조 코드에 대하여)
정적 분석(Static analysis)은 정적 참조만 볼 수 있습니다. 만약 코드베이스가 리플렉션(reflection), eval, 메타프로그래밍(metaprogramming), 설정 기반 인스턴스화(configuration-driven instantiation) 등 어떠한 형태의 동적 로딩을 사용한다면, 여러분의 커버리지 보고서는 불완전합니다.
2. "죽은 코드"는 종종 "간접적으로 자신을 호출하는 코드"이다
무언가를 삭제하기 전에 저는 다음과 같은 작업을 했어야 했습니다:
- 클래스/함수 이름과 일치하는 문자열 리터럴(string literals) 검색
- 설정 파일, 데이터베이스 시드(database seeds), 마이그레이션 스크립트 확인
getattr(),eval(),exec(),importlib.import_module()탐색- 해당 코드를 처음 작성한 팀원에게 문의
3. 진짜 죽은 코드 테스트는 정적이 아니라 런타임이다
더 나은 접근 방식:
- 코드 계측 (Instrument the code) — "죽은 것으로 의심되는" 모든 함수에 로깅 (logging) 추가
- 대기 (Wait) — 최소 한 번의 전체 비즈니스 사이클 동안 운영 환경 (production)에서 실행
- 검증 (Verify) — 의미 있는 기간 동안 호출 로그가 0회인 함수만 삭제
- 먼저 피처 플래그 (Feature flag) 적용 — 플래그 뒤에 코드를 숨기고, 플래그를 비활성화한 뒤 장애 발생 여부를 관찰
4. PR 문화의 중요성
내 리뷰어들은 40,000줄의 삭제를 단 몇 분 만에 승인했습니다. 이는 프로세스의 실패입니다. 대규모 삭제는 대규모 추가와 동일한 수준의 정밀 검토를 거쳐야 합니다. 아니, 위험이 눈에 보이지 않기 때문에 더 엄격해야 할지도 모릅니다.
그 후의 결과
우리는 결국 해당 코드베이스를 정리했습니다. 하지만 이번에는 제대로 했습니다. 런타임 계측 (runtime instrumentation)을 추가하고, 2주 동안 기다린 뒤,
실제로 죽은 코드(약 8,000줄)를 식별하여 작고 검증된 배치 (batch) 단위로 삭제했습니다.
죽은 것처럼
보였던 32,000줄은 어땠을까요? 그것들은 모두 동적으로 참조되고 있었습니다. 단 하나도 빠짐없이 말이죠.
교훈
죽은 코드 (Dead code)는 유령의 집과 같습니다. 비어 있는 것처럼 보이지만, 그 안에 무언가가 여전히 살고 있습니다. 벽을 허물기 시작하기 전에, 아무도 집에 없는지 반드시 확인하십시오.
다음에 "사용되지 않는 코드 40,000줄"이라는 문구를 보게 된다면, 저는 제가 아직 코드베이스를 충분히 이해하지 못하고 있는 것이라고 가정할 것입니다.
실제로 죽지 않은 코드를 삭제해 본 적이 있나요? 여러분의 경험담을 댓글로 공유해 주세요.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기