Monday, October 20, 2025

텍스트 패턴의 언어, 정규 표현식 깊이 보기

우리가 매일 사용하는 거의 모든 디지털 서비스의 이면에는 방대한 양의 텍스트 데이터가 흐르고 있습니다. 웹 서버의 로그 파일, 사용자 데이터베이스, 소스 코드, 심지어 이메일 한 통까지 모두 텍스트로 이루어져 있죠. 이 거대한 텍스트의 바다에서 원하는 정보를 정확히 찾아내거나, 특정 규칙에 따라 데이터를 가공하고, 유효성을 검사하는 작업은 모든 개발자와 시스템 관리자에게 주어진 숙명과도 같은 과제입니다. 이때, 마치 텍스트를 위한 스위스 군용 칼처럼 등장하는 강력한 도구가 바로 정규 표현식(Regular Expression, 줄여서 Regex 또는 RegExp)입니다.

정규 표현식을 처음 접하는 사람들은 복잡하고 암호 같은 문법에 압도당하기 쉽습니다. /^(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$/ 와 같은 표현식을 보면, 이것이 과연 인간이 읽고 쓸 수 있는 언어인지 의문이 들기도 합니다. 하지만 이 암호문의 각 기호가 가진 의미와 규칙을 하나씩 이해하기 시작하면, 텍스트를 다루는 방식에 혁명적인 변화가 일어납니다. 단순한 문자열 검색(Ctrl+F)을 넘어, '패턴'을 검색하고 조작하는 새로운 차원의 문이 열리는 것입니다.

이 글은 정규 표현식이라는 강력한 언어를 여러분의 것으로 만들어 드리기 위해 작성되었습니다. 단순히 문법을 나열하는 것을 넘어, 각 구성 요소가 어떤 철학을 가지고 디자인되었는지, 그리고 이들이 어떻게 유기적으로 결합하여 복잡한 패턴을 만들어내는지를 심도 있게 다룰 것입니다. 기초적인 개념부터 시작해 고급 기술인 룩어라운드(Lookaround), 재귀 패턴, 성능 최적화 팁까지, 실무에서 마주할 수 있는 다양한 시나리오와 함께 정규 표현식의 세계를 탐험해 보겠습니다. 이 여정이 끝나면, 여러분은 더 이상 텍스트 앞에서 막막함을 느끼지 않고, 자신감 있게 데이터를 지배하는 개발자로 거듭날 수 있을 것입니다.

1. 정규 표현식의 기본 철학: '무엇'이 아닌 '어떻게'

정규 표현식을 배우기 전에 가장 먼저 이해해야 할 것은 그 기본 철학입니다. 일반적인 검색은 '무엇(what)'을 찾을지 명시합니다. 예를 들어 "apple"이라는 단어를 찾는 것은 매우 간단합니다. 하지만 정규 표현식은 '어떻게(how)' 생긴 문자열을 찾을지를 정의합니다. 이것이 바로 '패턴 매칭'의 핵심입니다.

예를 들어, "세 자리 숫자"를 찾고 싶다고 가정해 봅시다. 일반 검색으로는 "000", "001", "002", ..., "999"까지 천 번을 검색해야 합니다. 하지만 정규 표현식으로는 \d{3} 이라는 단 하나의 패턴으로 이 모든 경우를 표현할 수 있습니다. 여기서 \d는 '숫자 하나'를, {3}은 '3번 반복'을 의미합니다. 이처럼 정규 표현식은 구체적인 문자열이 아닌, 문자열이 가진 구조와 규칙을 기술하는 언어입니다.

이러한 접근 방식은 다음과 같은 강력한 이점을 제공합니다.

  • 유연성: 예측할 수 없는 다양한 형태의 데이터를 일관된 규칙으로 처리할 수 있습니다. (예: 다양한 형식의 전화번호, 날짜)
  • 간결성: 수십, 수백 줄의 코드로 처리해야 할 복잡한 문자열 검사를 단 한 줄의 정규 표현식으로 끝낼 수 있습니다.
  • 재사용성: 잘 만들어진 정규 표현식 패턴은 다른 프로젝트나 다른 종류의 데이터에도 쉽게 재사용될 수 있습니다.

이제부터 우리는 이 '패턴을 기술하는 언어'의 구성 요소들을 하나씩 배워나갈 것입니다. 모든 것은 가장 기본적인 벽돌, 즉 '리터럴(Literals)'과 '메타 문자(Metacharacters)'에서 시작됩니다.

2. 정규 표현식의 구성 요소: 원자와 분자

정규 표현식은 화학의 원자와 분자처럼 기본적인 요소들이 결합하여 복잡한 구조를 만드는 것과 유사합니다. 가장 작은 단위인 '원자'에 해당하는 것이 바로 개별 문자와 메타 문자이며, 이들이 모여 '분자'와 같은 복잡한 패턴을 형성합니다.

2.1. 리터럴 (Literals): 가장 단순한 형태

리터럴은 정규 표현식에서 가장 직관적인 부분입니다. 특별한 의미를 가지지 않는 모든 일반 문자는 '자기 자신'을 의미합니다. 예를 들어, 정규 표현식 cat은 정확히 "cat"이라는 문자열과 일치합니다. hello world는 "hello world"와 일치합니다. 이는 우리가 일상적으로 사용하는 '찾기' 기능과 동일하게 작동합니다.


텍스트: The quick brown fox jumps over the lazy dog.
정규식: fox
결과: "fox" (일치)

2.2. 메타 문자 (Metacharacters): 패턴에 생명을 불어넣는 특수 기호

정규 표현식의 진정한 힘은 '메타 문자'에서 나옵니다. 메타 문자는 일반적인 문자로 취급되지 않고, 특별한 규칙이나 의미를 부여받은 문자들입니다. 이들을 어떻게 조합하느냐에 따라 정규 표현식의 표현력이 결정됩니다.

메타 문자는 그 기능에 따라 여러 그룹으로 나눌 수 있습니다. 하나씩 자세히 살펴보겠습니다.

2.2.1. 문자 클래스 (Character Classes)

문자 클래스는 '이 위치에는 이러한 종류의 문자 중 하나가 올 수 있다'를 정의합니다. 특정 문자 하나가 아닌, 문자의 집합 또는 범위를 지정할 때 사용됩니다.

메타 문자 설명 예시 매칭되는 문자열
. (마침표) 개행 문자(\n)를 제외한 모든 단일 문자와 일치합니다. (단, 특정 옵션 설정 시 개행 문자도 포함 가능) c.t "cat", "cot", "c@t", "c5t" 등
[] (대괄호) 대괄호 안에 포함된 문자 중 '하나'와 일치합니다. 범위 지정([a-z], [0-9])도 가능합니다. gr[ae]y "gray", "grey"
[^] (부정 문자 클래스) 대괄호 안에서 ^가 맨 앞에 오면, 괄호 안에 포함된 문자를 '제외한' 모든 단일 문자와 일치합니다. [^0-9] 숫자가 아닌 모든 문자 (예: "a", "%", " ")
\d 하나의 숫자(Digit)와 일치합니다. [0-9]와 동일합니다. \d\d\d "123", "987", "000"
\D 숫자가 아닌(Non-Digit) 모든 문자와 일치합니다. [^0-9]와 동일합니다. \D\D "ab", "$%", " " (공백 2개)
\w 단어 문자(Word character)와 일치합니다. 영문자, 숫자, 언더스코어(_)를 포함합니다. [a-zA-Z0-9_]와 동일합니다. \w\w\w\w "user", "pass", "var_1"
\W 단어 문자가 아닌(Non-Word character) 모든 문자와 일치합니다. [^a-zA-Z0-9_]와 동일합니다. \W "@", "#", " ", "!"
\s 공백 문자(Whitespace character)와 일치합니다. 스페이스( ), 탭(\t), 개행(\n, \r) 등을 포함합니다. hello\sworld "hello world", "hello\tworld"
\S 공백 문자가 아닌(Non-Whitespace character) 모든 문자와 일치합니다. \S+ 공백이 없는 연속된 문자열 (예: "word", "http://example.com")

예를 들어, 한국의 휴대전화 번호 형식 중 하나인 '010'으로 시작하고, 중간에 4자리, 마지막에 4자리 숫자가 오는 패턴을 찾는다고 가정해 봅시다. 문자 클래스를 사용하면 010-\d\d\d\d-\d\d\d\d와 같이 표현할 수 있습니다. 이는 아직 비효율적이지만, 패턴의 기본 구조를 잡아가는 첫걸음입니다.

2.2.2. 앵커 (Anchors): 위치를 지정하는 닻

앵커는 문자 자체와 일치하는 것이 아니라, 문자열 내의 특정 '위치'와 일치합니다. 마치 배가 닻을 내려 위치를 고정하듯, 앵커는 패턴이 시작되거나 끝나야 할 위치를 명시적으로 지정하여 매칭의 정확도를 높입니다.

  • ^ (Caret): 문자열의 시작 부분과 일치합니다. 대괄호 [] 안에서 사용될 때와는 전혀 다른 의미이므로 주의해야 합니다. 예를 들어, ^cat은 "caterpillar"에서는 "cat"과 일치하지만, "tomcat"에서는 일치하지 않습니다.
  • $ (Dollar): 문자열의 끝 부분과 일치합니다. 예를 들어, cat$는 "tomcat"에서는 "cat"과 일치하지만, "caterpillar"에서는 일치하지 않습니다.
  • \b (Word Boundary): 단어의 경계와 일치합니다. 단어 경계란, 단어 문자(\w)와 단어 문자가 아닌 문자(\W) 사이의 위치, 혹은 문자열의 시작/끝 위치를 의미합니다. \bcat\b는 "the cat sat"에서는 "cat"과 일치하지만, "tomcat"이나 "caterpillar"에서는 일치하지 않습니다. 오직 'cat'이라는 독립된 단어만 찾아냅니다.
  • \B (Non-word Boundary): 단어의 경계가 아닌 위치와 일치합니다. \Bcat\B는 "tomcat"의 "cat" 부분과 일치합니다.

앵커를 사용하면 의도치 않은 부분 매칭을 방지할 수 있습니다. 예를 들어 사용자 ID가 "admin"인 것을 찾고 싶은데, "administrator"도 함께 검색되는 것을 원치 않을 때, ^admin$ 패턴을 사용하면 정확히 "admin"이라는 문자열만 찾아낼 수 있습니다.

2.2.3. 수량자 (Quantifiers): 반복의 미학

수량자는 바로 앞의 문자나 그룹이 몇 번 반복될 수 있는지를 지정합니다. 앞서 본 \d\d\d\d와 같은 비효율적인 패턴을 간결하게 만들어주는 핵심적인 도구입니다.

수량자 설명 예시 동일한 의미
* 앞의 요소가 0번 이상 반복되는 경우와 일치합니다. ab*c {0,} (ac, abc, abbc, abbbc...)
+ 앞의 요소가 1번 이상 반복되는 경우와 일치합니다. ab+c {1,} (abc, abbc, abbbc...)
? 앞의 요소가 0번 또는 1번 나타나는 경우와 일치합니다. (선택적) colou?r {0,1} (color, colour)
{n} 앞의 요소가 정확히 n번 반복되는 경우와 일치합니다. \d{4} 정확히 4자리 숫자
{n,} 앞의 요소가 최소 n번 이상 반복되는 경우와 일치합니다. \d{2,} 최소 2자리 이상의 숫자
{n,m} 앞의 요소가 최소 n번, 최대 m번 반복되는 경우와 일치합니다. \w{3,5} 3~5글자의 단어 문자

이제 수량자를 이용해 앞서 다룬 휴대전화 번호 패턴을 개선해 보겠습니다. 010-\d\d\d\d-\d\d\d\d010-\d{4}-\d{4}로 훨씬 간결하게 표현할 수 있습니다. 만약 중간 번호가 3자리 또는 4자리일 수 있다면 010-\d{3,4}-\d{4}와 같이 유연하게 대처할 수도 있습니다.

탐욕적(Greedy) vs. 게으른(Lazy) 수량자

한 가지 매우 중요한 개념은 기본적으로 수량자는 '탐욕적(Greedy)'이라는 사실입니다. 이는 가능한 한 가장 긴 문자열을 찾으려고 시도한다는 의미입니다.


텍스트: <h1>Title</h1>
정규식: <.*>

위 예제에서 우리의 의도는 <h1></h1>을 각각 찾는 것이었을 수 있습니다. 하지만 .*는 탐욕적으로 행동하기 때문에, 첫 번째 <에서 시작하여 마지막 >까지 가능한 모든 문자를 집어삼킵니다. 결과적으로 <h1>Title</h1> 전체가 하나의 일치 항목으로 잡히게 됩니다.

이러한 문제를 해결하기 위해 '게으른(Lazy)' 또는 '최소 일치(Minimal)' 수량자를 사용합니다. 수량자 뒤에 ?를 붙이면 게으른 버전이 됩니다.

  • *?: 0번 이상, 최소한으로 일치
  • +?: 1번 이상, 최소한으로 일치
  • ??: 0번 또는 1번, 최소한으로 일치 (?와 동일하게 동작)
  • {n,}?: 최소 n번, 최소한으로 일치

텍스트: <h1>Title</h1>
정규식: <.*?>
결과:
1. "<h1>"
2. "</h1>"

게으른 수량자 .*?는 첫 번째 <를 만난 후, 가능한 가장 빨리 >를 찾아 매칭을 멈춥니다. 따라서 의도했던 대로 HTML 태그를 각각 찾아낼 수 있습니다. 이 Greedy와 Lazy의 차이점을 이해하는 것은 복잡한 텍스트 파싱에서 매우 중요하며, 성능에도 큰 영향을 미칠 수 있습니다.

3. 패턴의 조합과 확장: 그룹화와 분기

지금까지 배운 요소들을 조합하면 상당히 유용한 패턴들을 만들 수 있습니다. 하지만 더 복잡하고 구조적인 패턴을 다루기 위해서는 패턴의 일부를 하나의 단위로 묶거나, 여러 가능한 패턴 중 하나를 선택하는 기능이 필요합니다.

3.1. 그룹화와 캡처 (Grouping and Capturing)

소괄호 ()는 정규 표현식에서 여러 가지 중요한 역할을 수행합니다.

  1. 우선순위 지정: 수학에서 괄호가 연산의 순서를 정하듯, 정규 표현식에서도 괄호는 패턴의 적용 범위를 지정합니다. 예를 들어, ha|ba는 "ha" 또는 "ba"를 의미하지만, (h|b)a는 "ha" 또는 "ba"를 의미하며, 이는 ha|ba와 동일합니다. 그러나 (abc)+는 "abc"라는 문자열 시퀀스가 1번 이상 반복되는 것("abc", "abcabc", ...)을 의미하는 반면, abc+는 "ab" 뒤에 "c"가 1번 이상 반복되는 것("abc", "abcc", ...)을 의미합니다. 괄호가 수량자의 적용 대상을 명확히 해준 것입니다.
  2. 캡처링 (Capturing): 괄호로 묶인 부분에 일치하는 문자열은 나중에 다시 사용할 수 있도록 '캡처'되어 메모리에 저장됩니다. 이를 '캡처 그룹(Capture Group)'이라고 부릅니다. 캡처된 문자열은 정규 표현식 내부에서는 '역참조(Backreference)'를 통해, 외부 프로그래밍 언어에서는 매칭 결과 객체를 통해 접근할 수 있습니다.

역참조 (Backreferences)

역참조는 \1, \2와 같은 형태로 사용되며, n번째 캡처 그룹에서 일치한 문자열을 다시 참조합니다. 이는 반복되는 패턴을 찾을 때 매우 유용합니다.


텍스트: <h1>Title</h1> <p>Content</p>
정규식: <([a-z][a-z0-9]*)>.*?<\/\1>

위 정규식을 분석해 봅시다.

  • <([a-z][a-z0-9]*)>: <로 시작하고 >로 끝나는 태그를 찾습니다. 태그 이름(h1, p 등)은 괄호로 묶여 첫 번째 캡처 그룹(\1)이 됩니다. 태그 이름은 알파벳으로 시작하고([a-z]), 이후 알파벳이나 숫자가 0번 이상 반복([a-z0-9]*)되는 규칙을 가집니다.
  • .*?: 태그 사이의 내용과 일치합니다. (게으른 수량자 사용)
  • <\/\1>: 닫는 태그를 찾습니다. </ 다음에, 첫 번째 캡처 그룹에서 찾았던 바로 그 태그 이름(\1)이 오고 >로 끝나는 패턴입니다.

이 정규식은 <h1>...</h1>이나 <p>...</p>처럼 여는 태그와 닫는 태그의 이름이 동일한 쌍만 정확히 찾아냅니다. <h1>...</p>와 같이 짝이 맞지 않는 경우는 일치하지 않습니다. 이것이 바로 역참조의 힘입니다.

역참조는 중복된 단어를 찾거나(\b(\w+)\s+\1\b), CSV 파일에서 특정 필드가 다른 필드와 동일한 값을 갖는 행을 찾는 등 다양한 용도로 활용될 수 있습니다.

비캡처 그룹 (Non-capturing Groups)

때로는 그룹화의 우선순위 지정 기능만 필요하고, 굳이 문자열을 캡처할 필요는 없을 때가 있습니다. 불필요한 캡처는 메모리를 낭비하고 성능을 저하시킬 수 있습니다. 이때 사용하는 것이 비캡처 그룹 (?:...) 입니다.

예를 들어, 웹사이트 주소에서 "http" 또는 "https" 부분을 처리하고 싶다고 가정해 봅시다. (http|https)를 사용하면 이 부분이 캡처 그룹 1이 됩니다. 만약 프로토콜 부분은 캡처할 필요가 없고, 도메인 이름만 캡처하고 싶다면 (?:http|https)를 사용하는 것이 더 효율적입니다. (?:http|https):\/\/([\w.-]+)라는 패턴에서 괄호로 묶인 도메인 이름(([\w.-]+))이 첫 번째 캡처 그룹(\1)이 됩니다.

3.2. 분기 (Alternation)

파이프 기호 |는 'OR' 논리와 같습니다. A|B는 A 또는 B와 일치함을 의미합니다. 이는 여러 가지 가능한 패턴 중 하나를 선택해야 할 때 사용됩니다.


텍스트: I like cats. I like dogs. I like birds.
정규식: cat|dog|bird
결과:
1. "cat"
2. "dog"
3. "bird"

분기는 그룹화와 함께 사용될 때 더욱 강력해집니다. 예를 들어, 파일 확장자가 jpg, jpeg, png, gif 중 하나인 파일을 찾고 싶다고 가정해 봅시다. \.jpg|\.jpeg|\.png|\.gif라고 쓸 수도 있지만, 그룹화를 이용하면 더 깔끔하게 표현할 수 있습니다.

\.(?:jpg|jpeg|png|gif)$

  • \.: 마침표(.)는 메타 문자이므로, 실제 마침표 문자와 일치시키기 위해 이스케이프(\) 처리합니다.
  • (?:...): 확장자들을 비캡처 그룹으로 묶습니다.
  • jpg|jpeg|png|gif: 가능한 확장자들을 |로 연결합니다.
  • $: 문자열의 끝과 일치시켜, "image.jpg.txt"와 같은 파일이 잘못 매칭되는 것을 방지합니다.

4. 고급 기술: 보이지 않는 경계를 탐색하는 룩어라운드

정규 표현식의 고급 기능 중 가장 강력하고 유용한 것 중 하나가 바로 '룩어라운드(Lookaround)'입니다. 룩어라운드는 앵커와 마찬가지로 실제 문자와 일치하는 것이 아니라, 특정 위치의 앞(lookbehind)이나 뒤(lookahead)에 특정 패턴이 존재하는지 여부만 '확인'하고, 실제 매치 결과에는 포함시키지 않는 '제로 너비 단언(Zero-width Assertion)'입니다. 쉽게 말해, "A 다음에 B가 오는 경우의 A를 찾아라"와 같은 조건을 표현할 수 있게 해줍니다.

룩어라운드에는 네 가지 종류가 있습니다.

4.1. 긍정형 전방 탐색 (Positive Lookahead): (?=...)

A(?=B) 형태는 'B가 뒤따라오는 A'와 일치합니다. 매칭 결과에는 A만 포함되고, B는 조건 검사에만 사용됩니다.

예제: 금액이 표시된 문자열에서 'USD'가 뒤따라오는 숫자 부분만 추출하고 싶을 때 사용합니다.


텍스트: Price: 150USD, 200EUR, 300USD
정규식: \d+(?=USD)
결과:
1. "150"
2. "300"

만약 룩어라운드 없이 \d+USD를 사용했다면 "150USD", "300USD"가 매칭되었을 것입니다. 룩어라운드를 사용함으로써 우리는 순수한 숫자 값만 깔끔하게 추출할 수 있습니다.

4.2. 부정형 전방 탐색 (Negative Lookahead): (?!...)

A(?!B) 형태는 'B가 뒤따라오지 않는 A'와 일치합니다. 특정 패턴을 제외하고 싶을 때 매우 유용합니다.

예제: 'q' 다음에 'u'가 오지 않는 경우를 찾고 싶을 때 (일반적으로 영어에서 'q' 다음에는 'u'가 오지만, 예외적인 단어를 찾을 때)


텍스트: Iraq, Qatar, quit, sequence, qintar
정규식: q(?!u)
결과:
1. "q" (in "Iraq")
2. "q" (in "qintar")

quit이나 sequence의 'q'는 뒤에 'u'가 오므로 매칭되지 않습니다.

4.3. 긍정형 후방 탐색 (Positive Lookbehind): (?<=...)

(?<=B)A 형태는 'B가 앞에 있는 A'와 일치합니다. 긍정형 전방 탐색과 방향만 반대입니다.

예제: 통화 기호($, €, £) 바로 뒤에 있는 숫자 값만 추출하고 싶을 때 사용합니다.


텍스트: Item A: $50, Item B: €75, Item C: 100
정규식: (?<=[$€£])\d+
결과:
1. "50"
2. "75"

100은 앞에 통화 기호가 없으므로 매칭되지 않습니다. 주의: 많은 정규 표현식 엔진(특히 JavaScript의 구형 버전)에서 후방 탐색은 고정 길이 문자열만 허용하는 제약이 있습니다. 즉, (?<=\w+)\d+와 같이 수량자(+, *)를 포함한 패턴은 후방 탐색 내에서 사용할 수 없는 경우가 많습니다. 하지만 최신 엔진에서는 가변 길이 후방 탐색을 지원하는 경우도 늘고 있습니다.

4.4. 부정형 후방 탐색 (Negative Lookbehind): (?<!...)

(?<!B)A 형태는 'B가 앞에 오지 않는 A'와 일치합니다.

예제: 마이너스 부호(-)가 앞에 붙지 않은 숫자, 즉 양수만 찾고 싶을 때 사용합니다.


텍스트:
Revenue: 1000
Expense: -500
Profit: 500
정규식: (?<!-)\b\d+\b
결과:
1. "1000"
2. "500"

(?<!-)는 앞에 하이픈이 없는 위치를 확인합니다. -500500은 앞에 -가 있으므로 매칭에서 제외됩니다. \b는 숫자가 다른 단어의 일부가 되는 것을 방지하기 위해 추가했습니다 (예: "item500"에서 500이 매칭되는 것을 방지).

룩어라운드는 여러 개를 중첩하여 복잡한 조건을 만들 수도 있습니다. 예를 들어, 강력한 비밀번호 정책을 정규 표현식으로 검증하는 경우를 생각해 봅시다.

  • 최소 8자 이상
  • 최소 하나의 소문자 포함
  • 최소 하나의 대문자 포함
  • 최소 하나의 숫자 포함

이 모든 조건을 하나의 정규 표현식으로 표현할 수 있습니다.

^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$

  • ^: 문자열 시작
  • (?=.*[a-z]): 전방 탐색을 사용해 문자열 어딘가에 소문자가 있는지 확인합니다. (실제로는 문자를 소비하지 않고 위치만 확인)
  • (?=.*[A-Z]): 대문자가 있는지 확인합니다.
  • (?=.*\d): 숫자가 있는지 확인합니다.
  • .{8,}: 위의 모든 조건이 충족되었다면, 전체 문자열이 8자 이상인지 확인합니다. 이 부분이 실제로 문자열을 소비하며 매칭을 수행합니다.
  • $: 문자열 끝

이처럼 룩어라운드는 'AND' 조건을 효과적으로 구현하여, 정규 표현식의 표현력을 한 차원 높여주는 매우 강력한 도구입니다.

5. 실전 예제: 정규 표현식 활용 사례

이론을 배웠으니 이제 실제 업무에서 정규 표현식을 어떻게 활용할 수 있는지 구체적인 예제를 통해 살펴보겠습니다.

5.1. 이메일 주소 유효성 검사

이메일 유효성 검사는 정규 표현식의 단골 예제입니다. 완벽한 이메일 정규식(RFC 5322 표준을 따르는)은 매우 복잡하지만, 실용적인 수준에서 99%의 이메일 주소를 검증할 수 있는 패턴은 다음과 같습니다.

/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/

이 패턴을 분해해 봅시다.

  • ^: 문자열 시작
  • [a-zA-Z0-9._%+-]+: 로컬 파트(local part, '@' 앞부분). 영문 대소문자, 숫자, 그리고 특수문자(._%+-)가 1번 이상 반복됩니다.
  • @: 리터럴 '@' 기호
  • [a-zA-Z0-9.-]+: 도메인 이름. 영문 대소문자, 숫자, 그리고 하이픈(-), 마침표(.)가 1번 이상 반복됩니다.
  • \.: 리터럴 '.' 기호. 최상위 도메인(TLD)을 구분합니다.
  • [a-zA-Z]{2,}: 최상위 도메인(TLD). 영문 대소문자가 최소 2번 이상 반복됩니다. (예: com, net, kr, museum)
  • $: 문자열 끝

이 정규식을 사용하면 대부분의 유효한 이메일 주소를 찾아내고, "user@domain", "user.name@sub.domain.co.kr" 등은 통과시키고 "test@.com", "test@domain.", "@domain.com" 과 같은 잘못된 형식은 걸러낼 수 있습니다.

5.2. URL 파싱

URL에서 프로토콜, 도메인, 경로, 쿼리 스트링 등의 구성 요소를 추출해야 할 때 정규 표현식과 캡처 그룹은 매우 유용합니다.

/^(https?):\/\/([^\/\s]+)(\/[^\s]*)?$/

텍스트: https://www.example.com/path/to/resource?id=123&page=2

이 정규식을 적용하면 다음과 같이 캡처 그룹을 얻을 수 있습니다.

  • 전체 일치: `https://www.example.com/path/to/resource?id=123&page=2`
  • 그룹 1 (프로토콜): `https`
    • (https?): 'http' 뒤에 's'가 0번 또는 1번 오는 패턴을 캡처합니다.
  • 그룹 2 (호스트/도메인): `www.example.com`
    • ([^\/\s]+): 슬래시(/)나 공백(\s)이 아닌 문자가 1번 이상 반복되는 부분을 캡처합니다.
  • 그룹 3 (경로 및 쿼리): `/path/to/resource?id=123&page=2`
    • (\/[^\s]*)?: 슬래시(/)로 시작하고 그 뒤에 공백이 아닌 문자가 0번 이상 오는 부분을 캡처합니다. 이 전체 그룹은 선택적(?)이므로, "https://www.example.com" 처럼 경로가 없는 URL도 매칭됩니다.

이렇게 추출된 각 그룹을 이용하여 프로그래밍 로직에서 URL의 각 부분을 쉽게 처리할 수 있습니다.

5.3. 로그 파일 분석

웹 서버 로그는 정규 표현식이 빛을 발하는 대표적인 분야입니다. 대량의 비정형 텍스트에서 의미 있는 정보를 추출해야 하기 때문입니다. 예를 들어, 일반적인 Nginx 접근 로그 한 줄은 다음과 같습니다.

127.0.0.1 - - [10/Mar/2024:13:55:36 +0900] "GET /api/v1/users?page=2 HTTP/1.1" 200 512 "-" "Mozilla/5.0"

여기서 IP 주소, 요청 시간, HTTP 메소드, 요청 경로, 상태 코드, 응답 크기를 추출하는 정규 표현식은 다음과 같이 작성할 수 있습니다.

/^(\S+) - - \[([^\]]+)\] "(\S+) (\S+) \S+" (\d{3}) (\d+) ".*" ".*"$/

  • ^(\S+): (그룹 1: IP 주소) 줄 시작에서 공백이 아닌 문자열. `127.0.0.1`
  • - - \[([^\]]+)\]: (그룹 2: 타임스탬프) 리터럴 ` - - [` 와 `]` 사이의, `]`가 아닌 모든 문자. `10/Mar/2024:13:55:36 +0900`
  • "(\S+) (\S+) \S+": (그룹 3: HTTP 메소드, 그룹 4: 요청 경로) 큰따옴표 안에서 공백으로 구분된 세 덩어리 중 앞의 두 개를 캡처. `GET`, `/api/v1/users?page=2`
  • (\d{3}): (그룹 5: 상태 코드) 공백 뒤의 세 자리 숫자. `200`
  • (\d+): (그룹 6: 응답 크기) 공백 뒤의 하나 이상의 숫자. `512`

이 정규식을 로그 파일 전체에 적용하면, 각 요청에 대한 구조화된 데이터를 손쉽게 얻어 통계를 내거나 분석하는 데 사용할 수 있습니다.

5.4. 데이터 정리 및 변환 (CSV -> JSON)

간단한 CSV 형식의 데이터를 JSON으로 변환하는 작업도 정규 표현식을 활용하면 스크립트로 쉽게 자동화할 수 있습니다. 예를 들어 다음과 같은 CSV 데이터가 있다고 가정합시다.

name,age,city
John Doe,30,New York
Jane Smith,25,London

각 줄을 파싱하여 캡처 그룹으로 만드는 정규식: `^([^,]+),([^,]+),([^,]+)$`

이후 프로그래밍 언어의 '찾아 바꾸기(replace)' 기능과 결합하여 JSON 형식으로 변환할 수 있습니다. 예를 들어, JavaScript에서는 다음과 같이 활용할 수 있습니다.


const csvLine = "John Doe,30,New York";
const regex = /^([^,]+),([^,]+),([^,]+)$/;
const jsonString = csvLine.replace(regex, `{\n  "name": "$1",\n  "age": $2,\n  "city": "$3"\n}`);
console.log(jsonString);

/* 출력:
{
  "name": "John Doe",
  "age": 30,
  "city": "New York"
}
*/

여기서 $1, $2, $3는 캡처 그룹 1, 2, 3에 해당하는 값을 참조하는 대체 문자열(replacement string)입니다. 이처럼 정규 표현식은 데이터 추출뿐만 아니라 변환 작업에도 매우 효과적입니다.

6. 성능 최적화와 함정 피하기

정규 표현식은 강력하지만, 잘못 사용하면 심각한 성능 문제를 일으킬 수 있습니다. 특히 '치명적인 백트래킹(Catastrophic Backtracking)'이라 불리는 현상은 시스템을 마비시킬 수도 있으므로 반드시 이해하고 피해야 합니다.

6.1. 치명적인 백트래킹 (Catastrophic Backtracking)

이 현상은 정규 표현식 엔진이 일치하는 항목을 찾기 위해 가능한 모든 경로를 탐색하다가 경우의 수가 기하급수적으로 늘어나는 상황을 말합니다. 주로 중첩된 수량자와 불분명한 패턴이 결합될 때 발생합니다.

악명 높은 예시: (a+)+$

이 패턴은 "a"가 하나 이상 반복되는 그룹이 다시 하나 이상 반복되는 것을 찾습니다. "aaaa" 와 같은 문자열은 잘 찾지만, 만약 일치하지 않는 문자열, 예를 들어 "aaaaaaaaaaaaaaaaaaaaaaaaaaab"를 입력하면 어떻게 될까요?

엔진은 (a+)를 어떻게 나눌지 모든 경우의 수를 시도합니다. 예를 들어 "aaaa"는 `(a)(a)(a)(a)`, `(aa)(a)(a)`, `(a)(aa)(a)`, `(aaa)(a)`, `(a)(a)(aa)`, `(aa)(aa)`, `(aaaa)` 등으로 해석될 수 있습니다. 문자열이 길어질수록 이 조합의 수는 폭발적으로 증가합니다. 마지막에 'b' 때문에 결국 매칭은 실패하지만, 실패를 확인하기까지 엔진은 이 모든 조합을 시도하느라 엄청난 시간을 소모하게 됩니다. 이를 '백트래킹'이라고 합니다.

이러한 문제를 피하기 위한 몇 가지 팁이 있습니다.

  1. 구체적으로 작성하기: .* 처럼 지나치게 관대한 패턴 대신, [^"]* (큰따옴표가 아닌 문자의 연속)처럼 가능한 한 구체적인 패턴을 사용하세요.
  2. 불필요한 캡처 그룹 피하기: 캡처가 필요 없다면 비캡처 그룹 (?:...)을 사용하세요. 백트래킹 시 저장하고 복원할 상태가 줄어들어 성능에 도움이 됩니다.
  3. 중첩된 수량자 주의: (a*)*, (a|b)*, (a+)+ 와 같이 모호한 중첩 수량자는 백트래킹의 주범입니다. 패턴을 재고하여 이를 피하는 방법을 찾아야 합니다.
  4. 소유 수량자(Possessive Quantifiers) 사용: 일부 정규 표현식 엔진(Java, PCRE 등)은 소유 수량자를 지원합니다. *+, ++, ?+ 와 같이 수량자 뒤에 +를 붙입니다. 이는 탐욕적 수량자처럼 최대한 많이 일치시키되, 일단 일치하고 나면 절대 백트래킹을 통해 문자를 '포기'하지 않습니다. 이는 엔진에게 "일단 이만큼 가져갔으면 뒤돌아보지 마"라고 지시하는 것과 같아서, 불필요한 백트래킹을 원천 차단하는 효과가 있습니다. 예를 들어, (a+)+ 대신 (a++)+를 사용하면 백트래킹이 발생하지 않습니다.

6.2. 정규 표현식 엔진의 차이 (Flavors)

모든 정규 표현식이 동일하게 동작하지는 않는다는 점을 기억하는 것이 중요합니다. 언어와 도구마다 사용하는 정규 표현식 엔진이 다르며, 이로 인해 문법이나 지원하는 기능에 미묘한 차이가 있습니다.

  • PCRE (Perl Compatible Regular Expressions): PHP, Python, R, Nginx 등에서 널리 사용되는 매우 강력하고 기능이 풍부한 엔진입니다. 룩어라운드, 소유 수량자 등 대부분의 고급 기능을 지원합니다.
  • JavaScript: ECMAScript 표준에 따라 정의됩니다. 과거에는 후방 탐색 등 일부 기능이 지원되지 않았지만, 최신 버전(ES2018+)에서는 대부분의 고급 기능이 추가되었습니다.
  • POSIX (BRE & ERE): grep, sed, awk와 같은 전통적인 유닉스 도구에서 사용됩니다. 기본 정규 표현식(BRE)과 확장 정규 표현식(ERE)으로 나뉘며, \d, \w 와 같은 단축 문자 클래스나 룩어라운드 같은 고급 기능은 지원하지 않는 경우가 많습니다.
  • Java, .NET: 자체적인 강력한 정규 표현식 엔진을 가지고 있으며, PCRE와 유사한 풍부한 기능을 제공합니다.

따라서, 특정 환경에서 정규 표현식을 작성할 때는 해당 환경의 엔진이 어떤 문법과 기능을 지원하는지 공식 문서를 확인하는 습관이 중요합니다.

결론: 꾸준한 연습이 최고의 스승

정규 표현식은 처음에는 암호처럼 보일지라도, 그 규칙과 철학을 이해하면 텍스트 데이터를 다루는 능력을 비약적으로 향상시키는 강력한 도구입니다. 리터럴과 메타 문자라는 기본 단위에서 시작하여 수량자, 그룹, 앵커를 조합하고, 룩어라운드와 같은 고급 기술을 활용함으로써 이전에는 수십 줄의 코드로도 어려웠던 작업을 단 한 줄의 패턴으로 해결하는 경험을 하게 될 것입니다.

하지만 정규 표현식은 눈으로만 익힐 수 있는 기술이 아닙니다. 가장 중요한 것은 직접 부딪히고, 실패하고, 디버깅하며 자신의 것으로 만드는 과정입니다. Regex101이나 RegExr과 같은 온라인 정규 표현식 테스트 도구를 적극적으로 활용하세요. 이 도구들은 여러분이 작성한 패턴이 어떻게 동작하는지 시각적으로 보여주고, 각 부분에 대한 상세한 설명을 제공하여 학습 과정을 크게 단축시켜 줍니다.

오늘 당장 여러분이 처리해야 할 로그 파일, CSV 데이터, 혹은 소스 코드에 정규 표현식을 적용해 보세요. 간단한 패턴부터 시작하여 점차 복잡한 문제에 도전하다 보면, 어느새 텍스트의 바다를 자유롭게 항해하는 자신을 발견하게 될 것입니다. 정규 표현식은 단순한 기술이 아니라, 개발자로서 문제에 접근하고 해결하는 방식을 바꾸는 새로운 사고방식입니다.


0 개의 댓글:

Post a Comment