언제까지 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 태그를 제거하려는 상황을 가정해 봅시다.
대상 문자열:
<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 예방 수칙:
- 수량자 중첩(Nested Quantifiers)을 피하십시오. (
(a+)+→a+) - 가능하면 구체적인 문자 클래스를 사용하십시오. (
.*대신[^<]*등) - Python의
re모듈이나 JS 기본 엔진은 백트래킹을 사용하므로, 입력 문자열의 길이를 제한하는 것이 좋습니다.
5. 결론: 도구는 도구일 뿐
정규 표현식은 텍스트 처리의 '스위스 군용 칼'입니다. 강력하지만, 모든 요리에 칼을 쓸 수는 없습니다. 단순히 문자열을 자르거나 특정 문자의 위치만 찾으면 된다면 언어 내장 함수(split, includes)가 훨씬 빠르고 가독성도 좋습니다.
하지만 복잡한 패턴 매칭이 필요한 순간, 오늘 다룬 내용을 기억해 내신다면 수십 줄의 코드를 단 한 줄로 줄이는 쾌감을 맛보실 수 있을 겁니다. 작성한 정규식은 반드시 Regex101 같은 사이트에서 테스트하고, 설명(Explanation) 탭을 확인하여 성능 이슈가 없는지 점검하는 습관을 기르시기 바랍니다.
Post a Comment