개발자의 퇴근 시간, 정규식(RegEx)이 결정한다: 기초부터 ReDoS 방지까지

언제까지 substring, indexOf, split을 남발하며 스파게티 코드를 짜고 계실 건가요? 로그 파일에서 특정 에러 코드만 추출하거나, 복잡한 사용자 입력 패턴을 검증해야 할 때, 100줄짜리 if-else 지옥을 단 1줄로 줄여주는 마법이 있습니다. 바로 정규 표현식(Regular Expression, RegEx)입니다.

물론, 처음 정규식을 접하면 마치 외계어처럼 보일 수 있습니다. 하지만 이 '외계어'를 해독하는 순간, 여러분의 텍스트 처리 속도와 야근 횟수는 반비례하게 될 것입니다. 이 글에서는 정규식의 기초 문법은 물론, 주니어 개발자들이 가장 많이 실수하는 Greedy 탐색 문제, 그리고 실제 서비스 장애를 유발할 수 있는 ReDoS(정규식 서비스 거부) 보안 이슈까지 시니어의 관점에서 깊이 있게 다룹니다.

1. 정규식의 해부학: 암기가 아니라 '이해'입니다

정규식을 무작정 외우려 들면 3일 뒤에 까먹습니다. 엔진이 어떻게 문자를 읽어 들이는지 그 메커니즘을 이해해야 합니다. 정규식은 크게 리터럴(있는 그대로의 문자)메타 문자(기능을 가진 문자)의 조합으로 이루어집니다.

기본적인 메타 문자는 아래와 같습니다. 이것만 알아도 웬만한 검색은 가능합니다.

메타 문자 설명 예시
. 줄바꿈을 제외한 모든 문자 1개 c.t → cat, cut, c@t
^ / $ 문자열의 시작 / 끝 (앵커) ^Top (Top으로 시작), End$ (End로 끝남)
[] 문자 집합 (내부 문자 중 하나) [abc] → a 또는 b 또는 c
[^] 부정 문자 집합 (내부 문자 제외) [^0-9] → 숫자가 아닌 모든 것

더 자세한 문법은 MDN 공식 문서나 사용 중인 언어(Python, Java 등)의 도큐먼트를 참고하는 것이 가장 정확합니다. 여기서는 실무에서 헷갈리는 포인트만 짚고 넘어가겠습니다.

2. 주니어가 가장 많이 하는 실수: Greedy vs Lazy

"분명 괄호 안의 내용을 가져오라고 짰는데, 문장 전체를 가져와요."
이런 하소연을 들었다면 100% 탐욕적(Greedy) 수량자 때문입니다.

기본적으로 정규식의 수량자(*, +)는 가능한 가장 길게 매칭하려고 노력합니다. HTML 태그를 제거하려는 상황을 가정해 봅시다.

실패 케이스 (Greedy)
대상 문자열: <div>Hello</div> <span>World</span>
사용 패턴: <.*>

우리의 의도는 <div>, </div> 등을 각각 잡는 것이지만, 정규식 엔진은 첫 번째 <부터 마지막 >까지 한 번에 잡아버립니다. 결과적으로 전체 문자열이 통째로 선택됩니다.

해결책: 게으른(Lazy) 수량자 사용

수량자 뒤에 ?를 붙이면 "가능한 가장 짧게" 매칭을 멈춥니다. 이를 Lazy Match라고 합니다.

<.*?>  /* ?를 추가하여 첫 번째 >를 만나는 순간 멈춤 */

이 작은 물음표 하나가 파싱 로직의 정확도를 결정합니다. 데이터 스크래핑이나 로그 분석 시 필수적인 테크닉입니다.

3. 고급 기법: 그룹화와 전후방 탐색(Lookarounds)

단순 매칭을 넘어, 데이터를 "추출"하거나 "검증"만 하고 싶을 때 그룹화와 탐색 기능을 사용합니다.

3.1. 캡처링 그룹 vs 비-캡처링 그룹

소괄호 ()는 기본적으로 매칭된 내용을 메모리에 저장(캡처)합니다. 나중에 $1, group(1) 등으로 꺼내 쓸 수 있죠. 하지만 단순히 묶기만 하고 저장은 필요 없다면 성능 낭비가 발생합니다. 이때 비-캡처링 그룹 (?:...)을 사용하세요.

/* 캡처링 (메모리 사용) */
(https|http)

/* 비-캡처링 (단순 그룹화, 성능 우수) */
(?:https|http)

3.2. 전후방 탐색: 비밀번호 검증의 핵심

"특수문자를 포함해야 하지만, 비밀번호에 포함되지는 않아야 한다?" 말이 어렵죠? 전후방 탐색은 커서는 이동하지 않고 조건만 검사할 때 사용합니다. 이를 '제로 너비 단언(Zero-width assertions)'이라고 부릅니다.

  • 긍정형 전방 탐색 (?=...): 뒤에 패턴이 있어야 함 (e.g., Windows (?=10) -> 뒤에 10이 오는 Windows만 매칭)
  • 부정형 전방 탐색 (?!...): 뒤에 패턴이 없어야 함 (e.g., Windows (?!XP) -> 뒤에 XP가 오지 않는 Windows만 매칭)

이 기능은 특히 복잡한 패스워드 정책(숫자 포함 필수, 특수문자 포함 필수 등)을 검증할 때 위력을 발휘합니다.

4. 실전! 시니어의 30% 팁: ReDoS를 조심하라

많은 튜토리얼이 정규식의 '기능'만 알려주고 '위험성'은 경고하지 않습니다. 잘못 짠 정규식 하나가 여러분의 서버 CPU를 100%로 만들고 서비스를 다운시킬 수 있습니다. 이를 ReDoS (Regular Expression Denial of Service)라고 합니다.

대표적인 위험 패턴은 중첩된 수량자입니다.

위험한 패턴 예시
(a+)+

이 패턴에 aaaaaaaaaaaaaaaaaaaaX 같은 문자열을 넣으면, 정규식 엔진은 엄청난 횟수의 역추적(Backtracking)을 시도하며 멈춰버립니다. 실제로 Cloudflare의 2019년 전 세계적 서비스 장애 원인도 잘못 작성된 정규식 하나였습니다.

ReDoS 예방 수칙:

  1. 수량자 중첩(Nested Quantifiers)을 피하십시오. ((a+)+a+)
  2. 가능하면 구체적인 문자 클래스를 사용하십시오. (.* 대신 [^<]* 등)
  3. Python의 re 모듈이나 JS 기본 엔진은 백트래킹을 사용하므로, 입력 문자열의 길이를 제한하는 것이 좋습니다.

5. 결론: 도구는 도구일 뿐

정규 표현식은 텍스트 처리의 '스위스 군용 칼'입니다. 강력하지만, 모든 요리에 칼을 쓸 수는 없습니다. 단순히 문자열을 자르거나 특정 문자의 위치만 찾으면 된다면 언어 내장 함수(split, includes)가 훨씬 빠르고 가독성도 좋습니다.

하지만 복잡한 패턴 매칭이 필요한 순간, 오늘 다룬 내용을 기억해 내신다면 수십 줄의 코드를 단 한 줄로 줄이는 쾌감을 맛보실 수 있을 겁니다. 작성한 정규식은 반드시 Regex101 같은 사이트에서 테스트하고, 설명(Explanation) 탭을 확인하여 성능 이슈가 없는지 점검하는 습관을 기르시기 바랍니다.

Post a Comment