SerpApi와 같은 데이터 추출 서비스는 복잡한 웹사이트에서 데이터를 추출하는 데 어려움을 겪고 있으며, 특히 정규 표현식 사용 시 성능 문제가 발생할 수 있습니다. Ruby의 기본 정규 표현식 엔진인 Onigmo는 Ruby 3.2에서 개선되었음에도 불구하고 스캔 시간 측면에서 약점을 보이며, 이는 검색 요청에 지연을 초래합니다. 본 글은 이러한 배경 하에 Ruby 환경에서 사용 가능한 정규 표현식 엔진 대안들을 심층적으로 살펴보고, 각 엔진의 성능을 다양한 벤치마크를 통해 비교 분석하여 최적의 선택지를 모색하고자 합니다. 비교 대상으로는 Google에서 개발되었으며 ReDoS(Regular Expression Denial of Service) 공격 방어에 강한 `re2`와 Rust 언어의 네이티브 정규 표현식 엔진으로 전반적인 속도가 매우 빠른 `rust/regex`가 선정되었습니다. 널리 사용되지만 Ruby 바인딩이 최신이 아니며 JIT 모드 활성화에 문제가 있는 `pcre2`는 비교에서 제외되었습니다.
벤치마크는 rebar 벤치마크의 변형을 사용하여 ruby (Onigmo)
, re2
, rust/regex
세 엔진의 성능을 다양한 시나리오에서 측정했습니다.
* 리터럴(Literal) 패턴 벤치마크에서 rust/regex
는 영어 및 유니코드 텍스트 모두에서 압도적인 속도를 자랑했습니다. 특히 대소문자를 구분하지 않는 매칭에서 Ruby는 현저히 느려졌으며, re2
는 유니코드 텍스트 처리에서 Ruby보다도 저조한 성능을 보였습니다.
* 교대(Alternation)를 포함한 리터럴 패턴에서도 rust/regex
가 선두를 달렸고, re2
, Ruby 순으로 성능이 나타났습니다. Ruby는 교대 패턴 및 대소문자 구분 처리에서 약점을 드러냈습니다.
* 날짜(Date) 추출과 같은 복잡한 정규 표현식 패턴에서는 오토마타 기반의 re2
와 rust/regex
가 백트래커 방식의 Ruby를 크게 능가했습니다.
* Cloudflare ReDoS 취약성 관련 벤치마크는 rust/regex
와 re2
가 Ruby보다 훨씬 빠르게 ReDoS 공격에 저항할 수 있음을 입증했습니다.
* 단어(Words) 검색에서는 rust/regex
가 전반적으로 우수했지만, re2
는 유니코드 인식이 부족하여 특정 상황에서 Ruby보다 느려지는 경우도 있었습니다.
* 제한된 반복(Bounded repeat) 패턴에서 rust/regex
는 가장 뛰어난 성능을 보였고, re2
는 영어 텍스트에서는 양호했으나 유니코드 텍스트에서는 매우 취약했습니다.
* Noseyparker (다중 패턴) 벤치마크에서 rust/regex
를 순차적으로 실행하는 것이 가장 효율적이었고, re2 set
기능도 좋은 성능을 보였습니다. 그러나 rust/regex set
은 광범위한 스코프나 유니코드 모드에서의 \w
사용과 같은 특정 정규 표현식 패턴에 매우 민감하게 반응하여 성능이 크게 저하될 수 있음이 확인되었습니다.
각 엔진의 제한 사항도 명확히 드러났습니다. re2
의 \w, \d, \s, \b
매처는 유니코드 문자를 인식하지 못하고 ASCII 문자에만 작동하며, 반복 횟수 제한이 1000입니다. Ruby의 경우 \w, \d, \s
는 유니코드 인식이 부족하지만 [[:alpha:]]
와 같은 POSIX 문자 클래스를 통해 보완할 수 있으며, \b
는 유니코드 인식입니다. 반복 횟수 제한은 100000입니다. 또한 Ruby는 유효하지 않은 UTF-8 바이트 시퀀스를 포함하는 문자열을 스캔할 수 없습니다. 반면 rust/regex
는 기본적으로 모든 매처가 유니코드 인식이며, 유효하지 않은 UTF-8 문자열도 처리할 수 있지만, 컴파일된 정규 표현식의 크기 제한이 있습니다.
종합적으로 볼 때, `rust/regex`는 Ruby의 기본 정규 표현식 엔진 Onigmo를 대체할 수 있는 가장 빠르고 강력한 대안으로 평가됩니다. `re2`는 유니코드 텍스트를 제외한 대부분의 경우에서 Ruby보다 상당한 성능 향상을 제공하지만, 유니코드 인식 측면에서 한계가 있습니다. `re2`의 `set` 기능은 단일 `re2` 실행보다 항상 빠르다는 장점이 있습니다. `rust/regex`는 개별 정규 표현식 처리 시 유니코드 문제 없이 전반적으로 가장 빠른 성능을 보여주며, 순차적인 `rust/regex` 실행이 `re2 set`보다도 빠릅니다. 그러나 `rust/regex set` 기능은 특정 정규 표현식 패턴에 따라 성능이 크게 저하될 수 있으므로 신중한 테스트 후 사용하는 것이 권장됩니다. 마지막으로, `re2`와 `rust/regex`는 Ruby와 달리 유효하지 않은 UTF-8 바이트 시퀀스를 포함하는 문자열도 성공적으로 파싱할 수 있어, 데이터 추출과 같은 특정 시나리오에서 큰 이점을 제공합니다.