
레거시 코드베이스에서 AI를 안전하게 사용하는 방법
요약
레거시 코드베이스에 AI 에이전트를 도입할 때 발생할 수 있는 위험성과 이를 방지하기 위한 전략을 다룹니다. AI가 코드의 표면적 정보만으로 추론할 때 발생하는 부작용을 경고하며, 안전한 현대화를 위한 세 가지 핵심 규칙을 제시합니다.
핵심 포인트
- 테스트 우선(Tests first) 원칙 준수
- 변경 사항을 최소화하는 작은 디프(Small diffs) 유지
- 코드 수정 전 비즈니스 규칙을 먼저 발견할 것
아무도 건드리고 싶어 하지 않는 시스템을 물려받았습니다.
그 시스템은 10년이나 되었습니다. 3,000줄짜리 클래스 파일들이 존재합니다. 명명 규칙(Naming convention)은 4년 차쯤에 바뀌었습니다. 원작자는 6년 차에 떠났습니다. 테스트 커버리지(Test coverage)는 "웃기지도 않음"과 "프레임워크를 임포트하는 테스트가 딱 하나 있음" 사이 어딘가에 있습니다. 그리고 이제 경영진은 AI 에이전트(AI agents)를 발견했고, 당신이 3분기까지 그 모든 것을 "현대화"하기를 원합니다.
규칙 없이 AI를 풀어놓았을 때 다음에 어떤 일이 벌어질지 당신은 알고 있습니다. AI는 자신이 이해하지 못하는 급여 계산 로직을 다시 작성합니다. 하위 시스템(Downstream system)의 버그를 보완하기 위해 존재했던 기묘한 조건문을 "정리"해 버립니다. 함수를 새로운 헬퍼 파일(Helper file)로 옮기면서 기존 메서드가 가지고 있던 부작용(Side effect)을 조용히 삭제합니다. 아름다워 보이지만 회사의 두 번째로 큰 수익원을 망가뜨리는 4,000줄짜리 디프(Diff)를 만들어냅니다.
이것은 AI의 문제가 아닙니다. 이것은 레거시(Legacy) 문제입니다. 레거시 코드는 사고처럼 보이지만 사실은 그렇지 않은 결정들로 가득 차 있습니다. AI는 표면을 보고 표면에 대해 추론합니다. 레거시의 추론은 표면 아래에 존재합니다.
여기서 AI를 잘 사용하는 방법이 있습니다. 다만 그것은 그린필드(Greenfield) React 앱에서 사용하는 방식과는 다릅니다. 더 느리고, 더 절제되어야 하며, 제대로 해냈을 때 더 큰 보람을 느낍니다. 이 글은 그 절제에 관한 것이며, 제 생각에 "AI가 내 레거시 코드베이스를 개선했다"와 "AI가 내 레거시 코드베이스를 다른 사람의 문제로 만들었다"의 차이를 만드는 세 가지 규칙을 중심으로 작성되었습니다.
세 가지 규칙은 다음과 같습니다: 테스트 우선(Tests first), 작은 디프(Small diffs), 그리고 **어떤 변경을 하기 전 비즈니스 규칙 발견(Business rule discovery before any change)**입니다.
각 규칙을 자세히 살펴보겠습니다.
레거시 코드가 AI의 가정을 깨뜨리는 이유
현대의 AI 에이전트들은 인터넷에 아주 많이 존재하는 코드 형태를 다루는 데 매우 뛰어납니다. React 컴포넌트, Express 핸들러(Handlers), 일반적인 명명 규칙을 가진 CRUD 엔드포인트(Endpoints) 등이 그렇습니다. 코드의 형태만 보고도 동작을 대부분 추론할 수 있는 모든 것이 해당됩니다.
레거시 코드는 그 반대입니다. 레거시의 형태는 당신에게 거짓말을 합니다.
calculateTotal이라는 함수는 합계를 다시 계산하고, 인보이스(invoice)를 제자리에서 변경(mutate)하며, 감사 이벤트(audit event)를 발생시키고, 고객이 환불을 받을지 여부를 결정하는 플래그(flag)를 설정할 수도 있습니다. 이 중 그 어떤 것도 이름에는 나타나 있지 않습니다. 타입 시그니처(type signature)에도 나타나 있지 않은데, 대개 타입 시그니처 자체가 존재하지 않기 때문입니다. 당신이 이 사실을 아는 유일한 이유는, 3년 전 누군가가 이를 알지 못해 해고되었기 때문입니다.
AI 에이전트가 calculateTotal을 볼 때, 에이전트는 이름을 봅니다. 본문(body)을 봅니다. 하지만 부수 효과(side effect)에 의존하는 7개의 하위 소비자(downstream consumers), 플래그가 설정되지 않으면 깨져버리는 하나의 크론 잡(cron job), 또는 매 분기 감사 로그를 읽는 SOC 2 감사관은 보지 못합니다.
이것이 핵심적인 실패 모드(failure mode)입니다. 모델이 거짓말을 하거나 환각(hallucination)을 일으키는 것이 아닙니다. 모델은 불완전한 정보로부터 올바르게 추론하고 있는 것입니다. 문제는 레거시 시스템에서는 거의 모든 정보가 불완전하다는 점입니다. 시스템의 동작은 코드, 데이터베이스, 운영 런북(runbook), 2019년의 지원 티켓, 전임 엔지니어의 Slack DM, 그리고 우연히 아직 그곳에서 일하고 있는 세 명의 조직적 지식(institutional knowledge)에 걸쳐 인코딩되어 있습니다.
가이드라인(rails) 없이 그러한 환경에 AI를 풀어놓는 것은 "빠르게 움직이는 것"이 아닙니다. 그것은 도박입니다. 그리고 그 판돈은 다음 달 결산 시점까지는 조용히 유지됩니다.
아래의 세 가지 규칙은 그 도박에 가이드라인을 치기 위해 존재합니다. 이 규칙들은 당신의 속도를 늦추지 않습니다. AI의 속도를 늦추는 것이며, 이는 하나의 기능(feature)입니다.
규칙 1: 항상 테스트를 먼저 (Tests First, Always)
AI가 생성한 그 어떤 변경 사항도, 당신이 건드리고 있는 영역의 현재(current) 동작을 고정(pin down)하는 테스트가 있기 전에는 레거시 코드베이스에 들어갈 수 없습니다.
이것은 품질에 관한 것이 아닙니다. 레버리지(leverage)에 관한 것입니다. 테스트는 AI의 출력값과 비교할 수 있는 무언가를 제공합니다. 테스트가 없다면, 당신에게는 "차이점(diff)이 그럴싸해 보인다"라는 신호 외에는 아무것도 남지 않으며, 이는 레거시 환경에서 가질 수 있는 최악의 신호입니다.
여기서 당신이 작성하는 테스트는 교과서적인 의미의 단위 테스트 (Unit Test)가 아닙니다. 이것은 Michael Feathers의 저서 _Working Effectively with Legacy Code_에서 사용된 용어인 **특성 테스트 (Characterization Tests)**입니다. 특성 테스트는 함수가 '올바른 일'을 하는지 검증하는 것이 아닙니다. 함수가 '현재 하고 있는 일'을 하는지 검증합니다. 오늘 코드가 무엇을 하든, 테스트는 정확히 그 동작을 단언 (Assert) 합니다.
그 루프(Loop)는 다음과 같습니다:
- 변경하려는 가장 작은 함수나 메서드를 선택합니다.
- 현실적인 입력값을 사용하여 해당 함수를 호출하고, 당신이 바라는 결과값이 아닌 '실제' 결과값을 단언하는 테스트를 작성합니다.
- 테스트를 실행합니다. 통과하는지 확인합니다. (만약 통과하지 못한다면, 코드가 틀린 것이 아니라 테스트가 틀린 것입니다. 테스트를 수정하세요.)
- 이제 AI가 리팩터링 (Refactor) 하도록 둡니다.
- 테스트를 다시 실행합니다. 여전히 통과한다면, 동작이 보존된 것입니다.
이것은 AI가 지루한 부분을 실제로 도와줄 수 있는 지점입니다. 특성 테스트 자체를 생성하는 것은 AI에게 매우 적합한 작업입니다. 모델은 "여기에 함수가 있으니, 가시적인 입력과 출력을 모두 다루는 12개의 테스트 케이스를 작성해줘"라는 요청을 수행하는 데 탁월합니다. 당신은 AI에게 코드를 이해하라고 요구하는 것이 아닙니다. 열거 (Enumerate) 하라고 요구하는 것입니다.
의도적으로 특정 언어에 종속되지 않은 짧은 예시입니다:
characterization-test-loop.txt
당신: 여기 billing/penalties에 있는 calculateLatePenalty 함수가 있습니다.
15개의 테스트 케이스를 생성하세요. 각 케이스에 대해 현실적인 입력값을 제공하고
...
이 루프에서 흥미로운 점은 이것이 당신의 **신뢰 보정 (Trust Calibration)**에 미치는 영향입니다. 당신은 AI의 차이점 (Diff)이 깔끔해 보인다는 이유로 신뢰하는 것을 멈추게 됩니다. 대신, 당신이 먼저 작성한 계약 (Contract)을 통과했기 때문에 AI를 신뢰하기 시작하게 됩니다.
테스트 불가능한 코드(데이터베이스 접근, 네트워크 호출, 시간 의존적 동작 또는 전역 상태(Global State)가 하드코딩된 코드)의 경우, 문제는 더 까다로워집니다. 솔직한 답변은, 특성화(Characterization)가 가능해지기 전에 먼저 심(Seam)을 도입해야 할 수도 있다는 것입니다(단일 의존성 주입(Dependency Injection) 지점을 만들거나, now() 호출을 스텁(Stub)화할 수 있는 함수로 감싸는 것 등). 그 심(Seam) 자체가 심 지점 주변에 자체적인 테스트를 갖춘, AI의 도움을 받은 첫 번째 작은 변경 사항이 됩니다. 네, 느립니다. 하지만 그 대안은 조용히 망가진 결제 시스템입니다.
경고
리팩터링(Refactor) 후 테스트가 실패한다고 해서 AI가 테스트 코드를 다시 작성하게 두지 마세요. 그것이 테스트의 존재 이유입니다. 테스트가 실패한다면, 리팩터링이 잘못되었거나 처음부터 테스트가 잘못된 것입니다. 조사하십시오. 단언문(Assertion)을 삭제하여 증상을 없애려 하지 마세요.
규칙 2: 항상 작은 차이(Small Diffs)를 유지하라
두 번째 규칙은 지키기 가장 어려운 규칙입니다. 왜냐하면 AI는 진정으로 거대한 변경 사항을 만들어낼 능력이 있고, 거대한 변경 사항은 생산적인 것처럼 느껴지기 때문입니다.
하지만 그렇지 않습니다. 레거시 코드베이스에서 차이(Diff)의 크기는 곧 리스크입니다. AI가 건드리는 모든 줄은 AI보다 시스템을 더 잘 이해하는 사람이 검토해야 하는 줄입니다. 사람이 한 시간 동안 주의 깊게 검토할 수 있는 줄의 수는 대략 일정합니다. 따라서 200줄의 차이는 검토 가능합니다. 하지만 2,000줄의 차이는 불가능합니다. 대충 훑어보게 될 것이고, 레거시 리팩터링을 대충 훑어보는 것이 바로 버그를 배포하는 지름길입니다.
핵심적인 규율은 모든 AI 지원 변경 사항을 검토 가능한 하나의 작업 단위로 제한하는 것입니다. 저만의 대략적인 기준은 다음과 같습니다: 변경된 줄은 300줄 미만, 건드리는 파일은 3개 이하, 그리고 단일한 명명된 목표를 가질 것. 만약 AI가 더 많은 일을 하려고 한다면, 작업을 분할하십시오.
이것은 당연하게 들립니다. 하지만 어려운 이유는 AI가 스스로 멈추겠다고 자원하지 않기 때문입니다. 당신이
대부분의 레거시 함수는 실제로는 '코드 (code)'가 아닙니다. 그것들은 '인코딩된 비즈니스 규칙 (encoded business rules)'입니다. calculateShippingDiscount 함수는 사실 할인 계산에 관한 것이 아닙니다. 그것은 2018년에 물류 파트너와 협상된 정책이며, 2021년에 규제 기관에 의해 수정되었고, 2023년에는 아무도 재현할 수 없는 예외 케이스 (edge case)를 처리하기 위해 당황한 엔지니어가 패치(patch)를 적용한 결과물입니다. 현재의 코드는 이러한 모든 결정 사항들의 '기록 (record)'이지, 그 결정들에 대한 '설명 (explanation)'이 아닙니다.
AI가 calculateShippingDiscount를 볼 때, AI는 수학적 계산을 봅니다. 정책을 보지 못합니다. 따라서 당신이 "이 함수를 단순화해줘"라고 말한다면, AI가 만들어내는 결과물은 수학적으로는 더 깔끔해 보일지 모르지만, 조용히 정책을 위반할 수도 있는 버전입니다.
해결책은 프롬프트 (prompt)를 더 똑똑하게 만드는 것이 아닙니다. 해결책은 리팩터링 (refactor)을 하기 '전에' 비즈니스 규칙을 추출하고, 해당 규칙을 실제로 알고 있는 사람을 통해 이를 확인하는 것입니다.
제가 사용하는 프로세스는 다음과 같습니다:
- 함수를 선택합니다. 직접 읽거나, 더 좋은 방법은 AI에게 읽게 하여 구조화된 규칙 목록을 생성하도록 하는 것입니다.
- AI가 코드에서만 도출된 규칙들을 평이한 영어(plain English)로 번호가 매겨진 목록을 생성하게 합니다. 각 규칙은 기술적 지식이 없는 사람도 읽을 수 있는 형태여야 합니다.
- 이 목록을 도메인 지식을 가진 사람(시니어 엔지니어, 프로덕트 오너, 재무 파트너, 고객 지원팀 등)에게 가져가서, 각 규칙에 대해 "이것이 여전히 맞습니까?"라고 묻습니다.
- 답변은 다음 네 가지 중 하나일 것입니다: 예 (규칙이 최신임), 예-하지만-강화가 필요함, 아니오-이것은 3년 동안 잘못되어 있었음, 또는 아무도-왜-추가되었는지-기억하지-못함.
- 이제 당신은 출처(provenance)가 명확한 규칙 목록을 갖게 되었습니다. '이제' AI가 리팩터링을 수행할 수 있습니다. 왜냐하면 새로운 코드가 기존 코드가 아닌, 명시적인 사양 (specification)을 기준으로 측정되기 때문입니다.
효과적인 프롬프트의 짧은 예시는 다음과 같습니다:
business-rule-extraction-prompt.txt
여기에 calculateShippingDiscount 함수가 있습니다.
리팩터링하지 마세요. 개선 사항을 제안하지 마세요. 다음을 표시하지 마세요
...```
AI는 이 작업에 진정으로 탁월합니다. AI는 300줄에 달하는 중첩된 조건문 (nested conditionals)을 읽고, 사람이 검토할 수 있는 12개의 문장으로 요약해 줄 수 있습니다. 이러한 종류의 요약을 실제 도메인 전문가 (domain expert)에게 전달하면, 다음 리팩터링 (refactor) 시 반드시 보존해야 하는 사장된 규칙 (dead rules), 오해된 규칙 (misunderstood rules), 그리고 핵심적인 역할을 하는 규칙 (load-bearing rules)들을 찾아낼 수 있습니다.
여기서 진정한 가치가 발생하는 순간이 있습니다. 저는 이 과정을 여러 번 반복하며 도메인 전문가가 "잠깐만요, 그 작업은 2022년에 중단했는데 왜 코드는 여전히 플래그 (flag)를 확인하고 있죠?"와 같은 반응을 보이는 것을 목격했습니다. 그 단 한 문장은 그 어떤 리팩터링보다 가치가 있습니다. AI가 버그를 찾아낸 것이 아닙니다. AI의 요약과 정책을 기억하고 있는 사람 사이의 대화가 버그를 찾아낸 것입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기