정규식 엔진처럼 순수한 알고리즘은 생성형 테스트로 버그를 잡는 데 매우 적합합니다. 실제로 구버전 Rust regex 라이브러리에서 정규식과 입력값을 무작위로 섞어 비교하는 방식으로 알려지지 않은 버그를 찾아낼 수 있었습니다. 하지만 이 방법은 모든 경우의 수를 검증해 주는 마법이 아닙니다. 내가 원하는 특정 버그를 타게팅하여 테스트를 구성했기 때문에, 일반적 검증력이라는 측면에서는 약점이 있습니다.
정규식 엔진은 ‘진짜 정답’을 아는 오라클이 있어도, 생성형 테스트와 함께 사용할 때 비로소 그 위력을 발휘합니다.
Rust 정규식 버그, 퍼징으로 잡는 솔직 후기 및 한계점 정리
1. 작고 까다로운 예제가 버그를 잡는다
거대한 데이터를 던져주면 버그가 나올 거라 생각하기 쉽지만, 실제로는 반대가 많습니다. 버그는 보통 문자 집합의 특정 조합이나 기능 간의 미세한 상호작용에서 튀어나옵니다. 마치 복잡한 기계의 톱니 하나만 틀어지면 전체가 고장나는 것과 같습니다.
따라서 5GiB짜리 파일보다는, 특정 문자 몇 개만 섞어 만든 아주 짧은 문자열이 더 나쁜 일을 만듭니다. 모든 글자가 같은 문자열은 약하지만, 무작위하게 섞인 소수의 글자 조합은 예상치 못한 경로를 탐지합니다.
입력 데이터는 크기가 아니라 ‘다양 한 분포’가 중요합니다. 문장 구조나 기능 사용 빈도가 불규칙할수록 숨은 결함을 찾기 쉽습니다.
2. 오라클 설계, 두 개의 엔진을 서로 견제하게 하기
알고리즘 테스트의 정수는 ‘정답 비교’입니다. 같은 문제를 다른 방식으로 풀어낸 두 구현체가 있는지 확인해야 합니다. 정규식의 경우, 한쪽이 표준 regex이고 다른 쪽은 내부 구조가 다를 수 있는 regex_lite나 단순 비교용 코드를 만들 수 있습니다.
두 엔진이 같은 입력에 대해 다른 결과를 반환하는 순간, 어디에 문제가 있는지는 아니어도 최소한 ‘버그가 있다’는 사실은 확정됩니다. 이때 단순히 실패하길 바라며 아무렇게나 생성기만 돌리는 것은 효율이 떨어집니다. 오라클이 믿을 만한 정답을 출력하도록 시스템을 미리 설계해야 합니다.
독립적인 두 가지 구현체를 만들어 결과를 교차 검증하는 것이 생성형 테스트의 성공 여부를 가르는 기준입니다.
3. 문자열과 정규식 생성의 실전 팁
시작할 때 가장 큰 고민은 ‘어떤 문자를, 어떤 규칙으로 쓸 것인가’ 하는 것입니다. 이를 두고 고민하다가 결국 단순한 의사난수 생성기로 해결했습니다. 여기에는 두 가지 비결이 숨어 있습니다.
첫째, 캐릭터 선택을 두 단계로 나눕니다. 우선 전체에서 쓸 문자 전체를 고르고, 그 안에서 다시 부분집합을 쪼개서 사용합니다. 예를 들어 알파벳 26개 중 a와 b만 들어가는 긴 문자열도 만들어 보는 겁니다. 둘째, 정규식 기능(반복, 와일드카드 등)을 켜거나 끄는 것만 아니라, 각각에 가중치를 부여합니다. 특정 기능이 자주 나타나도록 무게중심을 옮기면, 그 기능과 다른 기능의 충돌 지점을 더 집중적으로 몰아칠 수 있습니다.
문장의 길이나 복잡한 질서를 통제하지 않아도, ‘분포 자체’를 조작함으로써 특정 코드 경로에 집중적으로 부하를 걸 수 있습니다.
4. 구버전에서 잡은 버그와 최신 버전의 결과
이 실험의 목표는 구버전 Rust regex의 특정 합성 오류를 재현하는 것이었습니다. 정규식 .abb|b로 zabb라는 입력을 주면, 본래는 zabb 전체가 매칭되어야 하는데 오류 버전에서는 b만 반환하는 문제를 가지고 있었습니다.
생성형 테스트를 돌려보니, 이 목표 버그를 찾는 것보다 다른 매칭 관련 버그가 먼저 포착되었습니다. 이는 오히려 좋은 신호로할 수 있는데, 테스트 환경을 안정적으로 구축했다는 증거이기 때문입니다. 반면, 최신 버전에서 테스트를 반복해 봤더니 아무 버지도 찾을 수 없었습니다.
버그 패치 전후로 테스트 결과를 비교하면, 수정이 실제로 당한 인과 파악에 도움이 됩니다. 최신 버전에서 깨끗한 결과가 나오면 안심해도 됩니다.
5. 이 퍼저의 한계와 왜 중요한가
솔직히 말하면, 저는 이미 목표 버그가 무엇인지, 그리고 이 퍼저가 그 버그를 찾아낼 수 있다는 것을 알고 실험을 시작했습니다. 즉, ‘무작위성이 강력하다’는 주장에 대한 완벽한 증명보다는, ‘이러한 테크닉이 실제로 적용 가능하다’는 데모에 가깝습니다.
단순한 단위 테스트에도 예외 처리가 있듯, 퍼저가 놓치는 버그는 항상 존재합니다. 특히 퍼저가 반복해서 실패하는 구간은 테스트 설계 자체에 결함이 있을 가능성이 큽니다. 일단 버그를 찾았더라도, 즉시 수정하기보다는 퍼저를 개선하여 유사한 버그들을 더 많이 낚아채는 대로를 선택해야 합니다.
생성형 테스트는 ‘구원 대상’이 아니라, 시스템의 신뢰도를 높이는 ‘층층적 방어선’의 일부로 봐야 합니다.
6. 개발자가 해야 할 다음 액션
지금 즉시 거대한 자동화 설비 투자를 생각하지 마세요. 대신 기존 메인 코드베이스 안에 숨어 있는 순수 함수들 중, 결과를 쉽게 비교할 수 있는 것부터 골라보세요.
1. 비교 가능한 두 버전(또는 오라클 코드가 아닌 ‘부수 효과 없는 정리 코드’)을 준비합니다. 2. 입력 값의 분포를 의도적으로 왜곡하여 특정 경로를 강조하는 논리 구현합니다. 3. 실패 사례가 나오면, 그 패턴을 일반화하여 단위 테스트로 고정합니다.
이 작업을 통해 코드 품질에 대한 자신감이 붙고, 향후 리팩토링 과정에서도 자신 있게 움직일 수 있게 됩니다.
작은 함수 하나에서도 ‘오라클 비교’를 적용하기 시작하면, 전체 시스템의 테스트 전략이 다른 차원으로합니다.
자주 묻는 질문
=
🔗 함께 읽으면 좋은 글: (SEO title 40-60 characters Korean, includes keyword “개인연금저축