코드의 품질에 대한 인식론
요약
본 글은 코드 리뷰 관행의 진화 과정을 추적하며, 단순히 스타일이나 표준 준수 여부를 넘어 코드가 실제로 올바른지 증명하는 것이 어렵다는 점을 지적합니다. 현대 개발에서는 단위 테스트, 자동화된 커버리지 분석, 페어 프로그래밍 등 다양한 '증거'를 결합하여 코드의 품질과 정확성을 확보해야 함을 강조합니다.
핵심 포인트
- 코드 리뷰는 단순한 스타일 검토를 넘어선 진화가 필요하다.
- 실제 시스템은 복잡하여 코드가 완벽함을 증명하기 어렵다.
- 단위 테스트, 커버리지 분석 등 다양한 '증거'의 결합이 중요하다.
- 페어 프로그래밍과 같은 능동적 협업 방식이 효과적이다.
인간 소프트웨어 개발자가 AI가 작성한 코드를 검토할 것이라고 기대해야 할까요? 많은 사람들에게 답은 명확하게 '예'입니다. 그렇지 않으면 어떻게 책임성을 유지할 수 있을까요? 이 생각에 도전하는 사람은 거의 없지만, 과거 코드 리뷰 관행의 많은 장점들이 현대 소프트웨어 개발에서 사라졌습니다.
이 질문 아래에는 또 다른 질문이 있습니다. 우리는 코드가 올바른지 어떻게 알까요? 간단한 함수에 대해서는 때때로 증명에 근접할 수 있습니다. 입력은 경계가 정해져 있고, 동작 방식은 명시되며, 우리는 이를 철저하게 테스트하거나 형식적으로 추론할 수 있습니다. 하지만 실제 시스템은 다릅니다. 사용자들을 가지고 있으며, 완전히 문서화되지 않은 요구사항들, 의존성(dependencies), 그리고 아무도 예상하지 못한 동시성(concurrency) 및 실패 모드(failure modes)를 가집니다. 우리는 그것이 올바르다는 것을 증명할 수 없습니다. 우리가 가진 것은 '증거'이며, 문제는 각 종류의 증거가 얼마나 가치가 있느냐입니다.
본문에서는 코드 리뷰가 어떻게 진화해 왔는지, 각 단계에서 어떤 증거를 제공했는지, 그리고 어디에 미흡했는지를 추적합니다. 그런 다음 현대 개발 프로세스에서 사용 가능한 증거의 종류와 그 안에서의 인간 검토의 한계에 대해 설명합니다.
대면 코드 리뷰
코드 리뷰는 단일한 관행이 아닙니다. 시간이 지남에 따라 변화해 왔으며, 어쩌면 더 나은 방향으로만은 아닐지도 모릅니다. 제가 보험 회사에서 일했을 때, 그들은 각 개발자에게 코드를 인쇄하여 빨간 펜으로 주석을 달게 하는 엄격한 프로세스를 가지고 있었습니다. 리뷰는 모든 개발자가 모인 회의실에서 진행되었고, 검토할 코드 섹션이 하나 있었습니다.
이는 모든 개발자들이 무엇이 작성되고 있는지 볼 수 있게 해주었습니다. 하지만 그 리뷰들은 가르치기보다는 구문(syntax) 확인과 코딩 표준 강제에 더 가까웠습니다. 마치 모든 사소한 표준 위반 사항을 제기하는 적대적인 심문처럼 느껴졌습니다.
장점은 우리가 방에 모여 코드를 논의하고 아이디어를 교류할 수 있었다는 것입니다. 때로는 개발자가 더 나은 구현 방법을 제시하기도 했습니다. 하지만 우리에게 부족했던 것은 단위 테스트(unit tests)나, 소프트웨어의 목적이나 요구사항 충족 여부에 대한 어떠한 논의였습니다. 그것은 별도의 테스트 팀이 담당했습니다.
증거: 검토 과정에서 코드가 표준을 준수했고 여러 사람이 읽었다는 점만 알려주었습니다. 코드 자체가 정확했는지에 대해서는 거의 언급하지 않았습니다. 인간 독자는 스타일 위반 사항을 찾아내는 데 능숙하지만, 코드가 실행될 때만 나타나는 결함을 찾는 것은 서툽니다.
이후 프로젝트에서는 이것이 너무 많은 시간이 소요된다고 판단했습니다. 별도의 테스트 팀 없이 개발자들에게 품질에 대한 책임을 맡겼고, 여기에는 자신들의 코드가 결함이 없도록 보장하는 것이 포함되었습니다. 저희는 단위 테스트(unit tests)를 도입하고, 자동화된 커버리지 분석(automated coverage analysis)을 수행했으며, 커밋 전에 반드시 두 번째 개발자가 검토하도록 의무화했습니다. 실제로는 검토자가 개발자 옆에 앉아 개발자의 컴퓨터에서 코드를 검토했는데, 이는 저희가 모두 같은 사무실에서 일했기 때문에 가능했습니다.
개발자들은 또한 커밋 훨씬 전부터 함께 짝을 이루어(paired up) 코드에 대해 논의하고 검토하기도 했습니다. 협업이 일반적이었습니다. 페어 프로그래밍(Pair programming)은 이를 더욱 발전시켜 지속적으로 만듭니다. 이는 사전 커밋 감사라기보다는 더 능동적이고 지속적인 피드백에 가깝습니다.
이 모델에서의 검토는 개발자가 기능에 대해 설명하고, 작동하는 것을 보여주고, 그것을 커버하는 단위 테스트를 보여준 다음, 비로소 코드를 보여주는 방식으로 진행되었습니다. 코드 검토는 전체 검토의 한 부분일 뿐이었습니다.
증거: 이것은 훨씬 강력했으며, 그 이유는 여러 종류의 증거들을 결합했기 때문입니다. 사람이 기능을 실행하는 것을 지켜보고, 테스트가 통과하며, 커버리지가 측정되고, 두 번째 눈이 코드를 읽어주었습니다. 코드를 읽는 것은 결함을 찾는 신호 중 가장 약했습니다. 하지만 아키텍처를 이해하는 사람이 수행할 경우, 테스트가 볼 수 없는 구조에 대한 판단력을 더해줄 수 있었습니다. 이러한 검토 방식은 통상적으로 행해지기 어려웠습니다.
Git과 지리적 격리
오픈 소스는 버전 관리(source control)에 과제를 안겨주었습니다. 사람들은 더 이상 같은 장소에 모여 있지 않았고, 기여가 전 세계에서 오게 되면서 커밋 전에 동료 옆에 앉아 있는 것이 불가능해졌습니다.
Git은 병합될 수 있는 별도의 브랜치를 장려했고, 이 브랜치가 검토의 단위가 되었습니다. 메인(main)으로 브랜치가 병합되는 풀 리퀘스트(Pull Request, PR)는 검토가 이루어지는 장소가 되었습니다. 디프(diff)는 변경 사항을 식별하고 GitHub와 같은 웹 인터페이스에서 쉽게 표시되었습니다.
오픈 소스에서 시작된 것이 기업 환경에서도 거의 배타적인 접근 방식이 되었습니다. 도구들은 특정 게이트를 자동화했습니다: 회귀(regressions)를 포착하기 위한 단위 테스트 실행, 테스트 커버리지를 유지하기 위한 커버리지 리포트, 그리고 FindBugs와 같은 시스템을 이용한 일반 결함 찾기 등이 그것입니다.
이러한 도구들은 수동 검토의 실패에 대응했습니다. 코드를 육안으로 보는 것은 개발자 교육과 정렬에는 좋았지만, 결함을 찾는 데는 끔찍했습니다. 인간의 검토는 이미 자동화된 도구에 의해 대체되고 있었습니다.
PR 모델 역시 무언가를 깨뜨렸습니다. 그것은 구현에 대한 나란히 논의하는 것을 제거했는데, 저는 이것이 품질 게이트로서 검토보다 더 중요하다고 생각합니다. 왜냐하면 그것이 개발자들에게 어떤 것이 품질인지 교육했기 때문입니다. 또한 PR을 훑어보고 코드가 컴파일할지조차 확인하지 않고 승인하기가 너무 쉬워졌습니다.
증거: PR은 우리에게 디프를 주었고, 이는 변경 사항에 대한 가장 정보력이 낮은 보기였습니다. 실제 증거는 CI/CD로 이동했습니다: 모든 커밋에 대한 빌드, 단위 테스트 및 정적 분석입니다. 사실상 우리는 이미 인간의 눈을 제외한 많은 형태의 증거에 의존하고 있었습니다.
AI와 검토 위기
그렇다면 왜, AI 시대에 우리가 인간의 검토가 그렇게 중요하다고 듣는 것일까요? 왜 AI 거버넌스 접근 방식은 필수적인 인간 코드 검토를 명시할까요?
이유 중 일부는 신뢰입니다. LLM은 환각(hallucinate)을 일으키는데, 이는 무언가를 지어내거나, 좀 더 비관적으로 말하면 거짓말하는 것입니다. 저는 오랫동안 AI 코딩으로 작업해 왔고 그 실패 모드를 목격했습니다. 가장 우려되는 점은 결정론적 테스트가 주어졌을 때, 그것이 테스트를 통과하지만 테스트가 의도했던 정신(spirit)을 깨뜨리는 코드를 작성할 것이라는 점입니다. 이것이 과적합(overfitting)입니다: 목표 달성보다는 테스트를 통과하도록 작성된 코드입니다.
만약 테스트가 10개 있고 모두 녹색(green)이라면 작동하는 시스템처럼 보이고, 당신은 그것을 배포할 수도 있습니다. 하지만 실제로 사용하는 누구에게나 명백하게 고장 난 상태일 수 있습니다.
저는 이 문제를 두 가지 방식으로 다루었습니다. 첫 번째는 이단적입니다: 앱을 수동으로 실행하고 상호작용하는 것입니다. 이것은 '푸딩 속의 증명(proof-is-in-the-pudding)' 테스트입니다. 엄격하거나 객관적이지는 않지만, 최소한의 기준이 됩니다. 두 번째는 더 다양한 단위 테스트를 사용하는 것입니다. 보통 함수당 하나의 테스트를 갖게 되지만, 프로그램 흐름이 동일하더라도 다른 데이터를 사용한 여러 테스트는 코드가 단일 테스트를 속이는 것을 어렵게 만듭니다.
과적합(Overfitting)은 더 깊은 문제를 드러냅니다. 만약 같은 모델이 코드와 테스트를 작성한다면, 요구사항에 대한 오해가 둘 다에 포함될 수 있습니다. 테스트가 통과하는 이유는 코드가 동일한 실수를 인코딩했기 때문입니다. 녹색 결과는 증거처럼 보이지만 실제로는 한 의견을 두 번 말했을 뿐입니다. 이것이 AI 지원 개발의 핵심 문제입니다: AI가 더 많은 실수를 한다는 것이 아니라, 그 실수가 그것들을 잡아내야 하는 검사(check)와 상관관계를 가질 수 있다는 것입니다.
무엇이 증거로 간주되는가
어떤 단일 검사도 '증명'이 아닌 '증거'입니다. 통과하는 단위 테스트는 코드가 테스트가 다루는 사례에 대해 올바르게 동작한다고 말해줄 뿐입니다. 깨끗한 정적 분석(static analysis)은 분석기가 알고 있는 패턴을 피한다는 것을 의미합니다. 어느 것도 소프트웨어가 사용자가 필요로 하는 것을 수행한다고 말해주지는 않습니다. 따라서 질문은 어떤 단일 검사가 신뢰할 수 있느냐가 아니라, 검사들의 집합이 믿음의 합리적인 근거를 어떻게 만드는가입니다.
제 대답은 독립성(independence)입니다. 같은 이유로 실패하는 두 가지 검사는 거의 도움이 되지 않습니다. 만약 다른 이유로 실패한다면, 결함은 그 모든 것을 한 번에 빠져나가야 하며, 추가되는 각각의 검사마다 신뢰도가 빠르게 높아집니다. 목표는 요구사항이 충족되었고 해결책이 신뢰할 수 있다는 합리적인 인식론적 믿음(epistemic belief)을 갖도록 충분히 독립적인 안전장치를 사용하는 것입니다.
독립성은 이분법적이지 않으며, 세 가지 종류가 있습니다. 저자의 독립성이 있습니다: 다른 모델이거나 다른 사람일 수 있습니다. 방법론의 독립성이 있습니다: 코드를 읽는 대신 애플리케이션을 실행하는 것입니다. 그리고 오라클의 독립성, 즉 '정확하다'는 것이 무엇을 의미하는지에 대한 계측 기준에 의해 검사가 측정됩니다. 세 번째가 가장 중요합니다. 완벽하게 독립적인 테스트라도 잘못된 요구사항을 확인하면 여전히 잘못된 것을 확인하고 있을 뿐입니다.
개발 과정에서 얻을 수 있는 증거의 종류와 각각이 무엇을 알려주고 무엇을 알려주지 못하는지 설명합니다.
- 빌드 및 타입 체크(Build and type checks). 코드가 구문적으로 유효하고 내부적으로 일관성이 있습니다. 동작에 대해서는 아무것도 알 수 없습니다.
- 단위 테스트(Unit tests). 커버된 사례에 대해 동작이 올바릅니다. 동일한 저자가 코드와 테스트를 작성했을 경우 과적합(overfitting) 및 공유된 오해에 취약합니다.
- 커버리지 분석(Coverage analysis). 테스트가 어떤 코드를 실행하는지 보여줄 뿐, 의미 있는 것을 확인하는지는 알려주지 않습니다. 약한 단언문(assertions)을 가진 높은 커버리지가 흔하게 나타납니다.
- 기능 및 통합 테스트(Functional and integration tests). 구성 요소들이 함께 작동합니다. 단위 테스트보다 강력하지만, 여전히 누군가의 요구사항 이해에 기반하여 작성됩니다.
- 애플리케이션 수동 실행(Manually running the application). 명백히 고장 난 것을 잡아냅니다. 반복 가능하거나 포괄적이지는 않지만, 자동화된 테스트와 완전히 다른 방식으로 실패합니다.
- 정적 분석(Static analysis) (FindBugs 등). 알려진 결함 패턴이 없습니다. 규칙 밖에 있는 것은 눈치채지 못합니다.
- 순환 종속성 및 복잡도 분석(Cyclic dependency and complexity analysis). 구조에 대한 객관적인 측정 지표: 결합도(coupling), 순환(cycles), 사이클로매틱 복잡도(cyclomatic complexity), 중복(duplication) 등이 있습니다. 코드가 얼마나 변경하기 어려울지 말해주지만, 작동하는지는 알려주지 않습니다.
- 취약점 및 보안 분석(Vulnerability and security analysis). 알려진 취약성 클래스가 없습니다. 전문적이며, 기능 테스트와는 다른 실패 모드를 가집니다.
- 부하 테스트(Load tests). 스트레스 하에서의 동작입니다. 이 목록의 어떤 것도 이를 다루지 못합니다.
- AI 코드 품질 검토(AI code quality review). 측정하기 어려운 단순성과 가독성에 유용합니다.
오직 코드를 작성한 주체의 '맹점(blind spots)'을 공유하지 않을 때만 독립적입니다. 따라서 다른 모델과 다른 프롬프트가 도움이 됩니다.
- AI 인수 테스트 (AI acceptance testing). 에이전트가 사용자가 실제로 사용하는 것처럼 애플리케이션을 실행합니다. 예를 들어 브라우저를 통해 작동하는 방식 등을 확인합니다. 코드가 아닌 외부에서 동작을 검사한다는 점에서 강력하지만, 주어진 인수 기준(acceptance criteria)만큼만 유효합니다.
- 운영 환경 관찰 (Production observation). 텔레메트리(Telemetry), 오류 보고(error reporting), 카나리 배포(canary releases), 롤백(rollback) 등이 있습니다. 일부 증거는 배포 후에만 수집할 수 있으므로, 품질은 PR 경계에서 내리는 판단이 아니라 지속적인 과정입니다.
- 인간 코드 검토 (Human code review). 아래에서 다룹니다.
품질은 정확성(correctness)과 동일한 취급을 받아야 합니다. 테스트는 코드가 오늘 작동한다는 것만을 말할 뿐, 내일 변경하기 얼마나 어려울지에 대해서는 아무것도 알려주지 않습니다. 제가 아는 가장 객관적인 테스트는 '변경 테스트(change test)'입니다. 새로운 에이전트에게 현실적이고 새로운 요구사항을 주고 어떻게 진행되는지를 측정하는 것입니다. 변경 사항을 올바르게 구현하는 데 필요한 노력, 회귀율(regression rate), 그리고 그 변경 사항이 노출시키는 예상치 못한 결합도(unexpected coupling)를 측정합니다. 결과는 코드뿐만 아니라 에이전트 자체를 부분적으로 반영하므로, 이 테스트는 여러 대안적 설계에 동일한 변경을 적용하여 비교하는 방식으로 사용하는 것이 가장 좋습니다. 위에서 언급된 구조적 지표들(structural metrics)은 이를 뒷받침하며, 가독성(readability)과 달리 취향의 문제가 아닙니다.
인간 검토의 한계 (The Limits of Human Review)
이것은 소스 코드에 대한 인간 개입을 반대하는 주장은 아닙니다. 코드가 사람이 작성했더라도, 저는 주관적인 검토보다는 객관적인 품질 측정 기준이 있어야 한다고 생각합니다. 물론 인간은 코드를 심층적으로 분석할 수 있습니다. 문제는 의무적인 인간 검토가 품질을 보장하는 체계적으로 유효한 방법인지 여부입니다. 왜냐하면 시스템 제공에 책임이 있는 우리들은 그 결과에 대해 책임을 지기 때문입니다.
인간은 검토 과정에서 결함을 발견하는가?
시니어 개발자들이 주니어 개발자들의 작업을 검토하는 이유는 신규 개발자들이 신뢰를 얻어야 하기 때문이며, 코드 리뷰는 그 과정의 일부였습니다. 하지만 AI 에이전트들은 더 이상 명백한 오류에만 걸리는 것이 아닙니다. 여전히 오류를 만들지만, 이제는 미묘한 개념적 오류를 포함하면서도 그럴듯해 보이고, 컴파일되며, 테스트를 통과하는 코드를 일상적으로 생성합니다. 명백한 실수는 잡아내기 쉽습니다. 하지만 그럴듯하게 틀린 것들은 diff(차이점)를 빠르게 읽어봐도 놓치기 쉬운 것입니다.
GitHub에서 diff를 육안으로 확인하는 것은 미묘한 결함을 찾거나 소프트웨어 전체에 미치는 영향을 이해하는 데 도움이 되지 않습니다. 코드가 정확하다는 것을 확신하려면, 리뷰어는 요구사항과 전반적인 해결책, 그리고 상세 구현을 모두 이해해야 합니다. 이는 코드를 작성하는 것만큼 오래 걸릴 수 있습니다.
리뷰어들에게 자신이 승인한 코드의 결함에 대한 책임을 지게 한다고 말하는 것은 '확실성'이 얼마나 어려운지 오해하는 것입니다. 이 관행은 단 하나의, 약한 형태의 증거만을 제공하며, 한 사람에게 그들이 만들 시간과 수단이 부족한 판단에 대해 책임을 묻습니다.
피해는 무엇인가요?
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기