AI가 작성한 코드, 리뷰 없이 프로덕션에 쓸 수 있을까?
AI가 생성한 코드를 프로덕션 환경에서 쓰려면 어떤 조건이 필요할까요? 이 질문에 답하기 위해 실험을 진행했고, 제 생각이 크게 바뀌었습니다. 기존의 "AI 코드는 반드시 리뷰해야 한다"는 입장에서 "AI 코드는 반드시 검증해야 한다"로 전환된 거죠.
여기서 말하는 리뷰란 코드를 한 줄 한 줄 읽는 행위이고, 검증이란 코드가 정말 맞는지 확인하는 것입니다. 리뷰, 자동화된 도구 제약, 또는 둘 다를 통해서 말이에요.
실험: 단순한 FizzBuzz 문제로 시작
AI 코딩 에이전트에게 간소화된 FizzBuzz 문제를 풀도록 했습니다. 그 다음 생성된 코드가 여러 제약 조건을 통과하는지 반복해서 검증했어요.
1. Property-based 테스트 통과 (부록 B 참조)
일반적인 테스트는 특정 입력값에 대한 특정 출력값만 확인합니다. 하지만 property-based 테스트는 훨씬 더 넓은 범위의 값들을 검증하죠. 이를 통해 요구사항이 정말 충족되는지 확인할 수 있습니다.
여기엔 다음이 포함됩니다:
- 예외가 발생하지 않는지 확인
- 레이턴시가 충분히 낮은지 확인
2. 뮤테이션 테스트 통과 (부록 C 참조)
뮤테이션 테스트는 일반적으로 테스트 스위트를 확충할 때 쓰입니다. 하지만 우리 테스트가 정확하다면, 반대로 이를 활용해서 코드를 제약할 수 있어요. 코드가 요구사항 이상의 것을 하지 않도록 보장하는 거죠.
3. 부작용(Side Effect) 제거
코드가 예상치 못한 부작용을 일으키지 않아야 합니다.
4. 타입 체크 및 린팅
Python을 사용하므로 타입 체크와 린팅도 강제했습니다. (다른 언어라면 이 단계가 필요 없을 수도 있어요)
결과: 사람이 코드를 직접 보지 않아도 괜찮을까?
이 모든 검증을 거친 후라면, 생성된 코드를 직접 읽지 않고도 신뢰할 수 있다고 생각합니다. 물론 이 모든 검증을 통과하면서도 여전히 틀린 코드가 존재할 수는 있지만, 그런 경우는 매우 드물고 우연히 마주치기도 어려워요.
유지보수성은 어떻게 되나요?
처음엔 생성된 코드의 유지보수성을 걱정했습니다. 하지만 생각해보니, 이 관점에서는 유지보수성과 가독성이 그리 중요하지 않을 수도 있겠더라고요. 컴파일된 코드처럼 취급하면 되는 거죠.
현재의 한계
지금 당장은 이런 검증 체계를 구축하는 데 드는 비용이 코드를 직접 읽는 비용보다 클 수 있습니다. 하지만 이렇게 기초선을 다져두면, AI 에이전트와 도구가 개선되면서 점점 그 비용을 줄여갈 수 있을 거예요.
실제로 이런 검증들을 직접 구현해보고 싶다면 [fizzbuzz-without-human-review](https://github.com) 리포지토리를 참고하세요. Python으로 이 모든 체크를 어떻게 구현하는지 볼 수 있습니다.
부록 A: 관련 연구
형식 검증(Formal Verification)
AI 생성 코드가 정말 작동하는지 보장하기 위해 형식 검증이 제안되어 왔습니다. 하지만 실제로 적용하기엔 상당한 노력이 필요하죠. 아마도 중간 지점이 있을 겁니다. 오류율이 충분히 낮아서 마음 놓고 잘 수 있는 수준 말이에요.
JustHTML 사례
JustHTML은 대규모 유닛 테스트 스위트를 통해 AI 생성 코드를 기초 있게 다루는 방식으로 큰 호응을 얻었습니다. 초기 버전에는 작가가 지적한 한계가 있었는데, 일부 코드가 "테스트 데이터를 정확히 따라 하는" 문제였어요. 하지만 "LLM 모델이 개선되면서" 이런 일은 덜 흔해졌습니다. 그들은 코드를 직접 읽어서 이를 감지했지만, property-based 테스트가 이런 전략을 충분히 억제할 수 있다고 생각합니다. 따라서 사람의 리뷰가 필요할 일이 줄어들 거예요.
Software Factory 프로젝트
한 팀은 "Software Factory"라는 개념으로 아무도 코드를 리뷰하지 않는 환경을 만들고 있습니다. 그들의 기술에 대해 깊이 있게 생각해본 건 아니지만, 이 개념은 너무 관련이 있어서 언급하지 않을 수 없네요.
부록 B: Property-based 테스트 입문
일반적인 소프트웨어 테스트는 특정 입력에 특정 출력을 확인합니다:
def test_returns_fizzbuzz_for_multiples_of_3_and_5(n: int) -> None:
assert fizzbuzz(15) == "FizzBuzz"
assert fizzbuzz(30) == "FizzBuzz"
Property-based 테스트는 훨씬 더 넓은 범위의 값을 대상으로 실행됩니다. 아래는 Hypothesis를 사용한 예시인데, 3과 5의 배수 100개를 반-무작위로 생성해서 테스트하고, 0이나 극도로 큰 수 같은 "흥미로운" 케이스를 선호합니다:
@given(n=st.integers(min_value=1).map(lambda n: n * 3 * 5))
def test_returns_fizzbuzz_for_multiples_of_3_and_5(n: int) -> None:
assert fizzbuzz(n) == "FizzBuzz"
특정 입력값 테스트 대비 이 방식은 시스템의 특정 "속성"이 정말 유지되는지 훨씬 더 확신할 수 있게 해줍니다. 다만 속도가 느리고, 실행마다 결과가 다르며, 복잡도가 높다는 단점이 있어요. 더 자세한 정보는 [Hypothesis 문서](https://hypothesis.readthedocs.io/)를 참고하세요.
부록 C: 뮤테이션 테스트 입문
mutmut 같은 뮤테이션 테스트 도구는 코드를 작은 단위로 변경합니다. 연산자를 바꾸거나 상수를 살짝 조정하는 식으로요. 그 다음 테스트 스위트를 다시 실행합니다. 테스트가 실패하면 "뮤턴트"가 "죽었다"(좋음)고 하고, 통과하면 "생존했다"(안 좋음)고 합니다.
예를 들어 다음 코드를 봅시다:
def double(n: int):
print(f"DEBUG n={n}")
return n * 2
def test_doubles_input():
assert double(3) == 6
print(f"DEBUG n={n}")를 print(None)으로 뮤테이션하면 test_doubles_input이 여전히 통과하므로 뮤턴트가 생존합니다. 이를 해결하려면 부작용을 제거하거나 이에 대한 테스트를 추가해야 해요.
부록 D: 감사의 말
이 글의 초안에 피드백을 주신 Taha Vasowalla와 다른 리뷰어분들께 감사드립니다.