정규표현식 (Regular Expression)

정규표현식(Regular Expression, regex)의 기본 문법부터 NFA/DFA 엔진 내부 동작, 백트래킹과 ReDoS 위험, 리눅스 커널의 lib/glob.c 패턴 매칭과 ftrace 필터, 사용자 공간 도구(grep, sed, awk)의 BRE·ERE·PCRE까지 폭넓게 다룹니다.

전제 조건: Text Search (lib/textsearch) 문서를 먼저 읽으면 커널 패턴 매칭의 맥락을 이해하기 쉽습니다. 정규표현식은 텍스트 처리의 핵심 도구이므로 Bash, ftrace 문서와 함께 보면 더욱 유용합니다.
일상 비유: 정규표현식은 초강력 Ctrl+F입니다. 일반 검색이 "apple"이라는 고정 문자열만 찾는다면, 정규표현식은 "a로 시작하고 숫자 2~3자리 뒤에 .txt로 끝나는 모든 것"처럼 패턴을 기술하여 매칭할 수 있습니다. 리눅스 커널은 성능과 안전성 이유로 내부에서는 단순한 glob 패턴만 사용하지만, 사용자 공간 도구(grep, sed, awk)에서는 정규표현식이 필수 도구입니다.

핵심 요약

  • 패턴 언어 — 정규표현식은 메타문자(Metacharacter)를 조합하여 문자열 패턴을 기술하는 형식 언어입니다. ., *, +, ?, [], () 등이 핵심 구성 요소입니다.
  • 두 가지 엔진 — NFA(Nondeterministic Finite Automaton)는 역추적(Backtracking)을 사용하고, DFA(Deterministic Finite Automaton)는 결정적으로 동작합니다. 대부분의 프로그래밍 언어는 NFA 기반입니다.
  • 커널의 선택 — 리눅스 커널은 lib/glob.c에서 *, ?, [charclass]만 지원하는 간단한 glob 매칭을 사용합니다. 이는 O(n) 보장과 ReDoS 방지를 위한 의도적 설계입니다.
  • BRE vs ERE vs PCRE — grep은 기본 정규표현식(BRE)을, grep -E는 확장 정규표현식(ERE)을, grep -P는 Perl 호환 정규표현식(PCRE)을 사용합니다.
  • ReDoS 주의(a+)+ 같은 중첩 수량자(Nested Quantifier) 패턴은 지수적 백트래킹을 유발하여 서비스 거부(DoS)를 일으킬 수 있습니다.

단계별 이해

  1. 리터럴 매칭abc는 문자열 "abc"를 그대로 찾습니다. 메타문자 없이 사용하면 고정 문자열 검색과 동일합니다.
  2. 메타문자 조합a.c는 "a" 다음 임의 한 글자, 그 다음 "c"를 매칭합니다. .은 줄바꿈을 제외한 임의 문자를 뜻합니다.
  3. 수량자 적용ab*c는 "ac", "abc", "abbc", "abbbc" 모두를 매칭합니다. *은 앞 요소의 0회 이상 반복을 뜻합니다.
  4. 그룹과 대안(cat|dog)는 "cat" 또는 "dog"를 매칭합니다. 괄호는 그룹을 만들고, |는 대안(Alternation)을 나타냅니다.
  5. 앵커로 위치 지정^start는 줄의 시작에서만, end$는 줄의 끝에서만 매칭합니다.

정규표현식 개요

정규표현식(Regular Expression)은 1956년 수학자 스티븐 클리니(Stephen Kleene)가 정규 집합(Regular Set)을 기술하기 위해 제안한 표기법에서 유래합니다. 1968년 켄 톰프슨(Ken Thompson)이 이를 텍스트 편집기 QED에 구현하면서 컴퓨터 과학에 도입되었고, 이후 ed, grep(Global Regular Expression Print), sed, awk를 거쳐 POSIX 표준과 Perl 확장으로 발전했습니다.

형식 언어 이론에서의 위치

촘스키 계층(Chomsky Hierarchy)에서 정규표현식은 Type-3 정규 언어(Regular Language)를 기술합니다. 유한 오토마톤(Finite Automaton)으로 인식할 수 있는 언어의 집합과 정확히 대응하며, 문맥 자유 언어(Context-Free Language, Type-2)보다 표현력이 제한됩니다. 단, 현대 정규표현식 엔진(PCRE 등)은 역참조(Backreference)와 전후방 탐색(Lookaround) 같은 확장을 포함하여, 이론적 정규 언어의 범위를 넘어섭니다.

정규표현식 메타문자 참조표

정규표현식 메타문자 참조표 메타문자 의미 예제 패턴 매칭 결과 . 임의 한 문자 (\n 제외) a.c "abc", "a1c", "a c" * 앞 요소 0회 이상 반복 ab*c "ac", "abc", "abbc" + 앞 요소 1회 이상 반복 ab+c "abc", "abbc" (ac 안됨) ? 앞 요소 0회 또는 1회 colou?r "color", "colour" ^ 줄의 시작 ^Linux 줄 시작의 "Linux"만 $ 줄의 끝 \.conf$ ".conf"로 끝나는 줄 [...] 문자 클래스 (택일) [aeiou] 모음 한 글자 [^...] 부정 문자 클래스 [^0-9] 숫자가 아닌 문자 {n,m} n~m회 반복 a{2,4} "aa", "aaa", "aaaa" | 대안 (OR) cat|dog "cat" 또는 "dog" () 그룹 / 캡처 (ab)+ "ab", "abab", "ababab" \ 이스케이프 \.\*\+ 리터럴 ".*+" 축약 문자 클래스 (Shorthand Character Classes) \d 숫자 [0-9] \D 숫자 아님 [^0-9] \w 단어 문자 [a-zA-Z0-9_] \W 단어 문자 아님 \s 공백 [ \t\n\r\f\v] \S 공백 아님 \b 단어 경계 (Word Boundary) \B 단어 경계 아님 매칭 클래스 부정 클래스 앵커 (너비 0)

메타문자(Metacharacter)

메타문자(Metacharacter)는 정규표현식에서 특별한 의미를 가지는 문자입니다. 리터럴(literal) 문자가 자기 자신만을 나타내는 것과 달리, 메타문자는 매칭 규칙을 제어합니다.

핵심 메타문자 12종

문자역할설명
.와일드카드줄바꿈(\n)을 제외한 임의 한 문자. DOTALL 모드에서는 \n도 매칭
*수량자앞 요소를 0회 이상 반복 (탐욕적). a*는 "", "a", "aa", ... 모두 매칭
+수량자앞 요소를 1회 이상 반복. a+는 "a", "aa", ... 매칭 (빈 문자열 안됨)
?수량자앞 요소를 0회 또는 1회. a?는 "" 또는 "a". 수량자 뒤에 붙으면 게으른(lazy) 모드
^앵커줄의 시작 위치. [^...] 안에서는 부정(negation)의 의미
$앵커줄의 끝 위치. MULTILINE 모드에서는 각 줄의 끝을 매칭
[ ]문자 클래스대괄호 안의 문자 중 하나를 매칭. [a-z]는 소문자 하나
{ }수량자반복 횟수 지정. {n} 정확히 n회, {n,m} n~m회
|대안OR 연산. a|b는 "a" 또는 "b". 우선순위가 낮아 괄호로 그룹화 필요
( )그룹하위 표현식 그룹화 + 캡처. (?:...)은 비캡처 그룹
\이스케이프메타문자를 리터럴로 만들거나, \d, \w 같은 축약 표현 생성

연산자 우선순위(Precedence)

정규표현식의 메타문자에는 프로그래밍 언어의 연산자처럼 우선순위가 있습니다. 우선순위를 이해하지 못하면 예상과 다른 매칭 결과가 나올 수 있습니다.

순위연산자설명예제
1 (최고)\이스케이프\.는 리터럴 마침표
2[]문자 클래스[a-z] 내부는 독자적 규칙
3()그룹화(ab)를 하나의 단위로
4* + ? {}수량자ab+는 a 다음 b 1회 이상
5연접(juxtaposition)순서 결합abc는 a 다음 b 다음 c
6^ $앵커^abc$
7 (최저)|대안 (OR)cat|dog
우선순위 실수 방지: abc|def(abc)|(def)와 같지만, ab(c|d)ef처럼 대안의 범위를 제한하려면 반드시 괄호가 필요합니다. ^cat|dog$는 "cat으로 시작" 또는 "dog으로 끝남"이 되며, "cat 또는 dog인 전체 줄"을 의미하려면 ^(cat|dog)$로 써야 합니다.

메타문자별 동작 예제

# . (점): 줄바꿈 제외 임의 한 문자
echo -e "cat\ncar\ncab" | grep 'ca.'
# 출력: cat, car, cab (모두 매칭)

# [] 문자 클래스 vs . 와일드카드
echo -e "cat\ncar\ncab\nca9" | grep 'ca[tr]'
# 출력: cat, car만 (cab, ca9 제외)

# | 대안: 우선순위 주의
echo "catdog" | grep -E 'cat|dog'     # "cat" 또는 "dog" 포함
echo "catdog" | grep -E '^cat|dog$'   # "cat으로 시작" 또는 "dog으로 끝남"
echo "catdog" | grep -E '^(cat|dog)$' # 전체가 "cat" 또는 "dog"

# ^ 의 이중 의미: 줄 시작 vs 부정
echo "abc123" | grep -o '[^0-9]'     # 숫자 아닌 문자: a, b, c
echo "abc123" | grep '^abc'           # "abc"로 시작하는 줄

이스케이프 규칙

메타문자를 문자 그대로 매칭하려면 백슬래시(\)를 앞에 붙입니다. 셸과 프로그래밍 언어의 문자열 이스케이프가 중첩되므로 주의가 필요합니다. 특히 C 언어에서 정규표현식을 사용할 때는 이중 이스케이프가 필요한 경우가 많습니다.

환경이스케이프 계층리터럴 . 매칭
셸 작은따옴표regex만grep '\.'
셸 큰따옴표셸 + regexgrep "\\."
C 문자열C + regex"\\."
Python raw stringregex만r'\.'
# 셸에서 grep 사용 시 이스케이프 계층
grep '192\.168\.1\.1' /var/log/syslog     # 작은따옴표: 셸 이스케이프 없음
grep "192\\.168\\.1\\.1" /var/log/syslog   # 큰따옴표: 셸 + regex 이중 이스케이프

# 커널 모듈 C 코드에서의 이스케이프
const char *pattern = "192\\.168\\.1\\.1";  /* C 문자열 + regex 이중 이스케이프 */

# 자주 이스케이프가 필요한 문자들
grep 'price: \$[0-9]\+' shop.txt          # $ 리터럴
grep 'version [0-9]\+\.[0-9]\+' log.txt    # . 리터럴
grep -E '^\[ERROR\]' syslog                # [ ] 리터럴

문자 클래스(Character Class)

문자 클래스(Character Class)는 대괄호 [...] 안에 매칭할 문자들의 집합을 나열하는 구문입니다.

기본 문법

구문의미예제
[abc]a, b, c 중 하나[aeiou] — 영어 모음
[a-z]a부터 z까지 범위[A-Za-z] — 영문자
[^abc]a, b, c가 아닌 문자[^0-9] — 숫자 아닌 것
[-]하이픈 리터럴[a\-z] 또는 [-az]
[]]닫는 괄호 리터럴[]abc] — 맨 앞에 위치

POSIX 문자 클래스

POSIX 표준은 로캘(Locale)에 독립적인 문자 분류를 제공합니다. grep[[:class:]] 구문으로 사용합니다.

POSIX 문자 클래스 계층 구조 [:print:] — 인쇄 가능 문자 [:graph:] — 가시 문자 (공백 제외) [:alnum:] — 영숫자 [:alpha:] — 알파벳 [:upper:] A-Z [:lower:] a-z [:digit:] 0-9 (= \d) [:xdigit:] 0-9, A-F, a-f [:punct:] ! " # $ % & ' ( ) * + , - . / : ; < = > ? @ [ \ ] ^ _ ` { | } ~ [:space:] (= \s) 공백 \t \n \r \f \v [:blank:] 공백 + 탭만 [:cntrl:] — 제어 문자 [:print:]에 포함되지 않음 (0x00-0x1F, 0x7F)
# POSIX 문자 클래스 사용 예 (grep)
grep '[[:digit:]]\{3\}-[[:digit:]]\{4\}' contacts.txt    # 전화번호 패턴 000-0000
grep '[[:xdigit:]]\{8\}' dmesg.log                       # 8자리 16진수 (주소값)
grep '[[:upper:]][[:lower:]]*' names.txt                   # 대문자로 시작하는 단어

# ERE에서는 중괄호 이스케이프 불필요
grep -E '[[:digit:]]{3}-[[:digit:]]{4}' contacts.txt

수량자(Quantifier)

수량자(Quantifier)는 앞의 요소가 몇 번 반복되어야 하는지를 지정합니다. 수량자의 기본 동작은 탐욕적(Greedy)으로, 가능한 한 많이 매칭하려 합니다.

수량자 종류

탐욕적게으른소유적의미
**?*+0회 이상
++?++1회 이상
????+0회 또는 1회
{n}{n}?{n}+정확히 n회
{n,}{n,}?{n,}+n회 이상
{n,m}{n,m}?{n,m}+n~m회

탐욕적 vs 게으른 매칭

탐욕적 수량자는 최대한 많은 문자를 먼저 소비한 후, 전체 패턴이 매칭되지 않으면 하나씩 되돌리며(백트래킹) 시도합니다. 게으른 수량자는 반대로 최소한의 문자부터 시도합니다.

탐욕적(Greedy) vs 게으른(Lazy) 매칭 비교 입력: "<b>bold</b> and <i>italic</i>" 입력 문자열: < b > b o l d < / b > a n d < i > i t a l i c < / i > 0 1 2 3 4 5 6 7 8 9 ... ... 31 패턴: <.*> (탐욕적) 1단계: .* 가 < 이후 모든 문자를 소비 (탐욕적 = 최대 확장) 2단계: > 매칭 실패 → 한 글자씩 되돌리며(백트래킹) >를 찾음 결과: "<b>bold</b> and <i>italic</i>" (전체를 하나의 매치로) 패턴: <.*?> (게으른) 1단계: .*? 가 0글자부터 시도 → 바로 > 매칭 시도 2단계: > 매칭 될 때까지 한 글자씩 확장 → "b" 다음의 > 에서 첫 매치 <b> </b> <i> </i> 4개의 개별 매치 소유적(Possessive) *+: 탐욕적처럼 최대 확장하되, 백트래킹 없음 → > 매칭 불가 시 즉시 실패 (PCRE 전용, ReDoS 방지)
# 탐욕적 vs 게으른 실전 비교
echo '<b>bold</b> and <i>italic</i>' | grep -oP '<.*>'
# 출력: <b>bold</b> and <i>italic</i>  (전체 하나)

echo '<b>bold</b> and <i>italic</i>' | grep -oP '<.*?>'
# 출력:
# <b>
# </b>
# <i>
# </i>

그룹과 캡처(Group & Capture)

괄호 ()는 두 가지 역할을 합니다: 하위 표현식을 그룹화하여 수량자를 적용하고, 매칭된 부분 문자열을 캡처하여 역참조(Backreference)에 사용합니다.

그룹 종류

구문유형설명
(pattern)캡처 그룹매칭 결과를 \1, \2 등으로 참조 가능
(?:pattern)비캡처 그룹그룹화만 수행, 캡처하지 않음 (성능 이점)
(?P<name>pattern)명명된 그룹이름으로 참조 가능 (Python/PCRE)
(?<name>pattern)명명된 그룹.NET/Java/PCRE2 구문

캡처 그룹 번호 매기기

캡처 그룹 번호는 여는 괄호 (가 나타나는 순서대로 1부터 할당됩니다. 중첩된 그룹에서는 바깥 괄호가 먼저 번호를 받습니다. 이 규칙을 이해하지 못하면 역참조에서 엉뚱한 값을 참조하게 됩니다.

캡처 그룹 번호 매기기 규칙 패턴: ((a+)(b+))(c+) 입력: "aabbcc" 그룹 1 그룹 2 그룹 3 그룹 4 ( ( a+ ) ( b+ ) ) ( c+ ) \1 "aabb" \2 "aa" \3 "bb" \4 "cc" 번호 할당 규칙 1. 여는 괄호 ( 가 나타나는 순서대로 번호 부여 (왼쪽 → 오른쪽) 2. 중첩 시 바깥 그룹이 먼저 (깊이가 아닌 위치 기준) 3. \0 또는 $0: 전체 매칭 "aabbcc" | 비캡처 (?:...) 는 번호를 소비하지 않음

비캡처 그룹의 성능 이점

비캡처 그룹 (?:...)은 그룹화만 수행하고 매칭 결과를 저장하지 않습니다. 캡처가 불필요한 곳에서는 비캡처 그룹을 사용하면 엔진이 매칭 결과를 메모리에 저장하는 오버헤드가 없어 성능이 향상됩니다. 특히 반복이 많은 패턴에서 차이가 큽니다.

# 캡처 그룹 vs 비캡처 그룹 비교
# 나쁜 예: 캡처할 필요 없는데 캡처 그룹 사용
grep -P '(https?|ftp)://(\w+\.)+\w+' urls.txt

# 좋은 예: 비캡처 그룹으로 변경 (같은 매칭, 더 빠름)
grep -P '(?:https?|ftp)://(?:\w+\.)+\w+' urls.txt

역참조(Backreference)

캡처 그룹이 매칭한 텍스트를 동일 패턴 내에서 \1, \2 등으로 다시 참조할 수 있습니다. 이는 정규 언어의 범위를 넘어서는 기능으로, DFA로는 구현할 수 없고 NFA 백트래킹이 필요합니다. 역참조는 "앞에서 매칭한 것과 동일한 텍스트"를 요구하므로, 같은 패턴이 아니라 같은 결과를 반복 매칭합니다.

# 역참조: 반복되는 단어 찾기
echo "the the quick fox fox" | grep -oP '\b(\w+)\s+\1\b'
# 출력: the the
#       fox fox

# sed에서 캡처 그룹 활용: 날짜 형식 변환
echo "2026-03-23" | sed -E 's/([0-9]{4})-([0-9]{2})-([0-9]{2})/\3\/\2\/\1/'
# 출력: 23/03/2026

# 명명된 그룹 (Python 구문, grep -P)
echo "error: file not found" | grep -oP '(?P<level>\w+): (?P<msg>.+)'
# 출력: error: file not found
역참조와 성능: 역참조가 포함된 패턴은 NP-완전(NP-complete) 문제가 될 수 있습니다. 커널에서 역참조를 지원하지 않는 것은 이런 계산 복잡도 위험 때문입니다.

앵커(Anchor)

앵커(Anchor)는 문자를 소비하지 않고 위치만 매칭하는 제로 너비 단언(Zero-width Assertion)입니다. 일반 문자나 메타문자가 "이 문자를 매칭하라"는 의미라면, 앵커는 "이 위치에 있어야 한다"는 조건을 부여합니다. 앵커는 입력 포인터를 전진시키지 않으므로 매칭 결과에 문자가 포함되지 않습니다.

앵커가 매칭하는 위치 입력: "Hello, World!\nGoodbye!" 줄 1: H e l l o , W o r l d ! \n ^ \A \b \b \b \b $ 줄 2: G o o d b y e ! ^ (MULTILINE) \z \Z ^ \A: 줄/문자열 시작 $ : 줄 끝 \b: 단어 경계 (\w↔\W 전환점) MULTILINE 모드: ^와 $가 각 줄의 시작/끝을 매칭 | 기본 모드: 전체 문자열의 시작/끝만 매칭
앵커의미설명
^줄의 시작MULTILINE 모드에서는 각 줄(\n 뒤)의 시작
$줄의 끝MULTILINE 모드에서는 각 줄(\n 앞)의 끝
\b단어 경계\w\W 사이, 또는 문자열 시작/끝과 \w 사이
\B비단어 경계\b가 아닌 위치
\A문자열 시작MULTILINE과 무관하게 전체 문자열의 시작 (PCRE)
\z문자열 끝전체 문자열의 절대적 끝 (PCRE)
\Z문자열 끝마지막 줄바꿈 앞 또는 문자열 끝 (PCRE)

\b 와 \B의 차이

단어 경계(Word Boundary) \b는 커널 소스 코드 검색에서 매우 유용합니다. 예를 들어 int 타입을 검색할 때 printf, uint32_t, interrupt 같은 단어 안의 "int"를 제외하려면 단어 경계가 필수적입니다. \b\w(단어 문자: [a-zA-Z0-9_])와 \W(비단어 문자) 사이의 경계를 의미하며, \B는 정확히 반대로 같은 유형의 문자 사이(단어 내부)를 의미합니다.

# \b: 단어 경계 — "int" 단어만 매칭
echo "int printf uint32_t interrupt" | grep -oP '\bint\b'
# 출력: int (하나만)

# \B: 비단어 경계 — 단어 내부의 "int"만 매칭
echo "int printf uint32_t interrupt" | grep -oP '\Bint\B'
# 출력: (printf 안의 "int") — pr[int]f

# 단어 경계 활용: "int"라는 단어만 찾기 (printf, uint 등 제외)
grep -w 'int' kernel_source.c        # grep -w = \bint\b 와 동일
grep '\bint\b' kernel_source.c

MULTILINE 모드의 영향

기본적으로 ^$는 전체 입력 문자열의 시작과 끝만 매칭합니다. MULTILINE 모드((?m) 또는 grep의 기본 동작)에서는 \n 뒤의 각 줄 시작과 \n 앞의 각 줄 끝을 매칭합니다. PCRE의 \A\zMULTILINE 모드와 무관하게 항상 전체 문자열의 절대적 시작과 끝을 매칭합니다.

# MULTILINE: 각 줄의 시작에서 #include 찾기
grep -P '(?m)^#include' header.h

# 전체 줄이 주석인 라인만 찾기
grep -E '^[[:space:]]*(//|/\*)' source.c

# 빈 줄 찾기
grep -c '^$' source.c                 # 빈 줄 개수

# 줄 전체가 특정 패턴인 경우만
grep -E '^[[:space:]]*return[[:space:]]+0;[[:space:]]*$' *.c

# \A vs ^: 여러 줄 문자열에서 차이
# \A는 첫 줄의 시작만, ^는 MULTILINE에서 매 줄 시작
printf "line1\nline2\nline3" | grep -cP '^line'   # 3 (매 줄)
printf "line1\nline2\nline3" | grep -cP '\Aline'  # 1 (첫 줄만)

전방탐색/후방탐색(Lookaround)

전방탐색(Lookahead)과 후방탐색(Lookbehind)은 앵커처럼 문자를 소비하지 않으면서 주변 컨텍스트를 확인하는 제로 너비 단언입니다. PCRE와 대부분의 프로그래밍 언어에서 지원하지만, POSIX BRE/ERE에서는 지원하지 않습니다.

구문이름의미
(?=pattern)긍정 전방탐색뒤에 pattern이 있어야 함
(?!pattern)부정 전방탐색뒤에 pattern이 없어야 함
(?<=pattern)긍정 후방탐색앞에 pattern이 있어야 함
(?<!pattern)부정 후방탐색앞에 pattern이 없어야 함
Lookaround 동작 원리 입력: "100px 200em 300px 50%" | 목표: px 앞의 숫자만 추출 1 0 0 p x 2 0 0 e m 3 0 0 p x 5 0 % 긍정 전방탐색: \d+(?=px) "숫자 뒤에 px가 따라와야 함, 하지만 px는 소비하지 않음" 100 300 → "100", "300" 매칭 (px 미포함) 부정 전방탐색: \d+(?!px) "숫자 뒤에 px가 따라오면 안 됨" 200 50 → "200", "50" 매칭 긍정 후방탐색: (?<=\$)\d+ 입력: "$100 200 $300" → "$ 뒤의 숫자" 100 300 → "100", "300" ($ 미포함) 핵심: Lookaround는 "소비하지 않는 확인"으로, 매칭 결과에 포함되지 않으면서 조건을 검증합니다
# 실전 활용: 커널 로그에서 특정 함수 뒤의 에러 코드 추출
grep -oP '(?<=kmalloc failed: )-?\d+' /var/log/kern.log

# 비밀번호 검증: 대문자, 소문자, 숫자 모두 포함 확인
echo "P4ssWord" | grep -P '^(?=.*[A-Z])(?=.*[a-z])(?=.*\d).{8,}$'

# C 코드에서 함수 호출만 찾기 (선언/정의 제외)
grep -P '(?<!^)\b\w+\s*\([^)]*\)\s*;' kernel_module.c

복합 Lookaround 패턴

여러 lookaround를 연결하면 복합 조건을 표현할 수 있습니다. 긍정과 부정 탐색을 조합하여 "A 뒤에 있지만 B 뒤는 아닌" 같은 정교한 조건을 만들 수 있습니다.

# 복합 lookaround: 여러 조건 동시 검증
# "숫자이면서 뒤에 px가 아닌 것" (CSS 값에서 단위 없는 숫자 찾기)
echo "100px 200 300em 50" | grep -oP '\b\d+(?!px|em|rem|%)\b'
# 출력: 200, 50

# 커널 로그에서: "error" 포함하되 "corrected" 뒤에 오는 것은 제외
grep -P '(?<!corrected )error' /var/log/kern.log

# 숫자를 3자리마다 콤마 삽입 (천 단위 구분자)
echo "1234567890" | sed -E ':a;s/([0-9])([0-9]{3})(,|$)/\1,\2\3/;ta'
# PCRE lookaround으로 같은 것: (?<=\d)(?=(\d{3})+(?!\d))

Lookaround 성능 고려사항

Lookaround는 제로 너비이지만 무비용은 아닙니다. 특히 가변 길이 후방탐색(variable-length lookbehind)은 엔진이 현재 위치에서 역방향으로 탐색해야 하므로 비용이 높습니다. POSIX ERE가 lookaround를 지원하지 않는 이유 중 하나이며, 일부 엔진(JavaScript ES2018 이전, RE2)은 후방탐색을 아예 지원하지 않습니다. PCRE에서도 후방탐색 패턴의 길이는 고정되어야 합니다((?<=ab)은 가능하지만 (?<=a+)은 불가).

정규표현식 엔진: NFA vs DFA

정규표현식 엔진은 크게 NFA(Nondeterministic Finite Automaton)와 DFA(Deterministic Finite Automaton) 두 가지 방식으로 나뉩니다. 엔진 선택은 성능과 기능 사이의 근본적인 트레이드오프를 결정합니다.

Thompson NFA vs 백트래킹 NFA

톰프슨이 제안한 원래 NFA 시뮬레이션은 모든 가능한 상태를 동시에 추적하여 O(nm) 시간을 보장합니다. 반면, Perl/PCRE 등의 백트래킹(Backtracking) NFA는 역참조 같은 확장 기능을 지원하기 위해 한 번에 하나의 경로만 탐색하고, 실패 시 되돌아가는 방식을 사용합니다.

NFA vs DFA 엔진 비교: 패턴 a(b|c)*d NFA (비결정적 유한 오토마톤) S0 S1 S2 S3 S4 a ε ε b c ε (반복) d → 수락 ε-전이로 동시에 여러 경로 탐색 가능 상태 수: O(m), 구축: O(m) DFA (결정적 유한 오토마톤) D0 D1 D2 a b,c d 각 상태에서 입력당 정확히 1개 전이 상태 수: O(2^m) 가능, 매칭: O(n) 특성 백트래킹 NFA (Perl/PCRE/Python) DFA (awk/egrep 일부/RE2) 매칭 시간 O(2^n) 최악 (백트래킹) O(n) 보장 구축 시간 O(m) O(2^m) 최악 역참조 지원 (\1, \2, ...) 미지원 Lookaround 지원 미지원 (일부 제한적 지원) 대표 구현 Perl, Python re, PCRE, Java, .NET awk, RE2, Rust regex, Go regexp 메모리 O(m) 고정 O(2^m) 최악 (lazy 구축으로 완화) Thompson NFA: 동시 상태 추적으로 O(nm) 보장 | 백트래킹 NFA: 기능 풍부하지만 O(2^n) 위험 | DFA: O(n) 매칭, 기능 제한 Google RE2: Thompson NFA 기반으로 O(nm) 보장 + 부분적 기능 확장. 안전성이 중요한 환경(웹 서비스)에서 권장
커널과의 연관: 리눅스 커널의 lib/textsearch FSM 알고리즘은 DFA와 유사한 유한 상태 기계 방식으로 패턴을 매칭합니다. 커널은 의도적으로 백트래킹 NFA를 사용하지 않아 매칭 시간의 상한을 보장합니다.

백트래킹(Backtracking)

백트래킹(Backtracking)은 NFA 기반 정규표현식 엔진의 핵심 메커니즘입니다. 패턴의 특정 지점에서 여러 선택지가 있을 때, 하나를 시도하고 실패하면 이전 지점으로 돌아가 다른 선택지를 시도합니다. 이 과정은 재귀적 깊이 우선 탐색(DFS)과 동일하며, 엔진 내부에서 호출 스택이나 명시적 스택을 사용하여 "되돌아갈 지점"을 기록합니다.

백트래킹이 발생하는 상황

백트래킹은 다음 세 가지 상황에서 발생합니다:

스택 깊이와 한계

백트래킹 NFA는 각 선택 지점마다 스택 프레임을 생성합니다. 깊은 중첩 패턴이나 긴 입력에서는 스택 오버플로가 발생할 수 있으며, 이것이 PCRE2의 pcre2_set_depth_limit()이나 Python의 re 모듈 재귀 제한이 존재하는 이유입니다.

백트래킹 과정: 패턴 /a*ab/ 입력 "aab" a* 가 탐욕적으로 "aa"를 소비한 후, "ab"를 매칭하지 못해 되돌아가는 과정 시도 1: a* → "aa" 소비 (탐욕적 최대) a a b a* = "aa" → 남은 입력 "b"에서 "ab" 매칭 시도 → 실패! "b" ≠ "a" 시도 2: 백트래킹 → a* 가 "a" 하나 반환 a a b a* = "a", 패턴의 "ab" = 입력의 "ab" → 성공! a*="a" + "ab" 매칭 완료. 전체 매치: "aab" 백트래킹 탐색 트리 시작: pos=0 a*="aa", pos=2 실패: "b" ≠ "a" 백트래킹 a*="a", pos=1 성공: "ab" 매칭! 이 예제는 2번만에 성공했지만, 중첩 수량자에서는 탐색 트리가 지수적으로 폭발할 수 있습니다 (→ ReDoS)

백트래킹 최적화 기법

엔진 수준에서 백트래킹을 줄이는 대표적인 최적화 기법들입니다:

기법설명효과
앵커 최적화^로 시작하는 패턴은 각 줄 시작에서만 시도시도 횟수 대폭 감소
필수 문자열 추출패턴에서 반드시 포함되어야 할 고정 문자열을 먼저 검색Boyer-Moore로 빠른 사전 필터링
Atomic 그룹(?>...)은 그룹 내 매칭 후 백트래킹 금지지수적 폭발 방지
소유적 수량자a++는 소비한 문자를 반환하지 않음불필요한 재시도 제거
# 백트래킹이 많은 패턴 vs 최적화된 패턴
# 나쁜 예: .*가 불필요하게 많이 백트래킹
echo "name=value;name2=value2" | grep -oP '.*=(.*);'

# 좋은 예: [^=]* [^;]* 로 범위 한정
echo "name=value;name2=value2" | grep -oP '[^=]*=([^;]*);'

# 소유적 수량자 (PCRE): 백트래킹 없이 실패 시 즉시 종료
echo "aaaaaab" | grep -P 'a++b'    # 매칭 (a를 모두 소비 후 b 확인)
echo "aaaaaa" | grep -P 'a++b'     # 실패 (a 반환 없이 즉시 실패)

ReDoS(Regular Expression Denial of Service)

ReDoS(Regular Expression Denial of Service)는 악의적으로 설계된 입력이 백트래킹 NFA 엔진에서 지수적 시간 복잡도를 유발하여 서비스 거부를 일으키는 공격입니다. 웹 애플리케이션의 입력 검증, WAF 규칙, 로그 파싱 등에서 발생할 수 있습니다.

취약 패턴 유형

패턴유형위험 입력원인
(a+)+$중첩 수량자"aaa...!"a+가 "a"를 분배하는 조합이 지수적
(a|a)+$중복 대안"aaa...!"각 "a"마다 두 경로 선택 → 2^n
(a+)*b중첩 반복"aaa...!"a+ 그룹의 반복 횟수 조합 폭발
(.*a){n}와일드카드 반복"aaa...b".*가 소비하는 범위의 조합
ReDoS: (a+)+$ 패턴의 지수적 백트래킹 입력: "aaa!" — $는 줄 끝 매칭, "!"에서 실패 후 모든 분배 조합을 시도 내부 a+ 그룹이 "aaa"를 분배하는 방법 (4가지 → 실패 → 백트래킹): 시도 1: (aaa) → "aaa" 한 그룹 → $ 실패("!" 남음) → 백트래킹 시도 2: (aa)(a) → 두 그룹 → $ 실패 → 백트래킹 시도 3: (a)(aa) → 두 그룹 (다른 순서) → $ 실패 → 백트래킹 시도 4: (a)(a)(a) → 세 그룹 → $ 실패 → 최종 실패 입력 길이 vs 백트래킹 횟수 입력 길이 (n) 백트래킹 횟수 5 10 15 20 25 30 O(2^n) 백트래킹 NFA O(n) DFA/RE2

방어 기법

기법설명예제
중첩 수량자 제거(a+)+a+로 단순화a+$
Atomic 그룹(?>a+)은 백트래킹하지 않음(?>a+)+$ (PCRE)
소유적 수량자a++는 소비한 것을 반환하지 않음a++$ (PCRE)
DFA 엔진 사용RE2, Rust regex 등 백트래킹 없는 엔진Google RE2
타임아웃 설정매칭 시간 상한 설정PCRE2 pcre2_set_match_limit()
앵커링^$로 범위 한정불필요한 .* 제거

실제 ReDoS 사례

ReDoS는 이론적 위협이 아닌 실제 운영 환경에서 반복적으로 발생하는 문제입니다:

ReDoS 방어 전략 상세

Atomic 그룹 (?>...)은 그룹 내부에서 매칭이 완료되면 그 결과를 "잠금"하여 백트래킹을 허용하지 않습니다. 이는 소유적 수량자(++, *+)와 동일한 효과이며, 중첩 수량자 패턴에서 지수적 폭발을 근본적으로 차단합니다.

# Atomic 그룹으로 ReDoS 방어
# 취약: (a+)+$ — 백트래킹 폭발
time echo 'aaaaaaaaaaaaaaaaaaaaa!' | grep -P '(a+)+$'
# → 수 초~수십 초 소요 (입력 길이에 따라 지수적 증가)

# 방어: (?>a+)+$ — Atomic 그룹으로 백트래킹 차단
time echo 'aaaaaaaaaaaaaaaaaaaaa!' | grep -P '(?>a+)+$'
# → 즉시 실패 (백트래킹 없음)

# 근본적 해결: 패턴 단순화
# (a+)+$ → a+$ (동일한 매칭, 중첩 제거)
time echo 'aaaaaaaaaaaaaaaaaaaaa!' | grep -P 'a+$'
# → 즉시 실패
커널의 선택: 리눅스 커널이 lib/glob.c에서 *, ?, [...]만 지원하고 정규표현식을 지원하지 않는 핵심 이유가 바로 ReDoS 방지입니다. 커널 내부에서 지수적 시간 소비가 발생하면 시스템 전체가 멈출 수 있으며, 인터럽트 비활성 컨텍스트에서는 워치독 타임아웃으로 이어질 수 있습니다.

커널 glob 패턴 매칭 (lib/glob.c)

리눅스 커널은 정규표현식 대신 lib/glob.c에 구현된 단순한 glob 패턴 매칭을 사용합니다. glob_match() 함수는 *(임의 문자열), ?(임의 한 문자), [charclass](문자 클래스) 세 가지 와일드카드만 지원합니다.

glob_match() 지원 구문

와일드카드의미정규표현식 대응
*임의 개수의 임의 문자 (0개 포함).*
?정확히 한 문자.
[abc]a, b, c 중 하나[abc]
[a-z]a~z 범위의 한 문자[a-z]
[!abc]a, b, c가 아닌 문자[^abc]
\c리터럴 문자 c\c
glob_match() 매칭 알고리즘 플로차트 pat, str 포인터 시작 *pat 문자 확인 pat == '*' 연속 * 건너뛰기 pat 끝이면 → 성공 아니면 str 각 위치에서 재귀 glob_match(pat+1, str+i) pat == '?' → str++ 진행 pat == '[' [!...] 부정 확인 범위(a-z) 또는 개별 문자 비교 ] 까지 순회 pat == '\' → 다음 문자 리터럴 *pat == *str ? 진행 : 실패 다음 문자 성공 pat과 str 모두 끝 실패 불일치 발견 glob_match() 핵심 특성 • 백트래킹은 * 와일드카드에서만 발생 (중첩 수량자 없으므로 지수적 폭발 불가) • CONFIG_GLOB_SELFTEST: 빌드 시 셀프테스트로 정확성 검증 | 약 130줄의 간결한 구현
/* lib/glob.c — glob_match() 핵심 구현 (v6.x 간략화) */
bool glob_match(char const *pat, char const *str)
{
    for (;;) {
        unsigned char c = *pat++;

        switch (c) {
        case '*':
            /* 연속 * 건너뛰기 */
            while (*pat == '*')
                pat++;
            if (!*pat)
                return true;  /* 패턴 끝이 *면 항상 매칭 */
            /* str의 각 위치에서 나머지 패턴 매칭 시도 */
            while (*str) {
                if (glob_match(pat, str))
                    return true;
                str++;
            }
            return !*pat;

        case '?':
            if (!*str++)
                return false;
            break;

        case '[': {
            /* 문자 클래스 매칭: [!...] 부정, [a-z] 범위 지원 */
            bool inv = (*pat == '!');
            /* ... 범위/개별 문자 비교 로직 ... */
            break;
        }

        default:
            if (c == '\\')
                c = *pat++;
            if (*str++ != c)
                return false;
            break;
        }
    }
}
설계 의도: glob_match()는 의도적으로 수량자 +, {n,m}, 캡처 그룹 (), 대안 |을 지원하지 않습니다. 이를 통해 구현이 간결하고(~130줄), 백트래킹이 *에서만 발생하며, 최악 경우에도 O(nm) 시간을 보장합니다.

ftrace 필터와 glob

ftrace는 커널 함수 추적 도구로, set_ftrace_filterset_ftrace_notrace 파일에 glob 패턴을 써서 추적 대상 함수를 선택합니다. 내부적으로 lib/glob.cglob_match()를 사용합니다.

ftrace 필터 글로브 매칭 경로 사용자 쓰기 echo "sched_*" > set_ftrace_filter tracefs 파일 ftrace_regex_write() 패턴 분석 ftrace_match() glob_match() lib/glob.c 커널 함수 목록에 대한 glob 매칭 sched_fork ✓ sched_exec ✓ sched_yield ✓ do_fork ✗ sys_read ✗ ftrace 필터 패턴 유형 접두사: sched_* → "sched_"로 시작하는 함수 접미사: *_irq → "_irq"로 끝나는 함수 포함: *lock* → "lock"을 포함하는 함수 정확: schedule → 정확히 "schedule" 함수만 (glob 없음 → strcmp) 모듈 지정: :mod:ext4 → ext4 모듈 함수만 복합: *read*:mod:ext4 → ext4의 read 관련 함수
# ftrace glob 필터 실전 사용
cd /sys/kernel/debug/tracing

# 스케줄러 관련 함수만 추적
echo 'sched_*' > set_ftrace_filter

# 여러 패턴 추가 (>> 사용)
echo '*lock*' >> set_ftrace_filter

# 특정 모듈의 함수만
echo ':mod:ext4' > set_ftrace_filter

# 제외 필터: irq 관련 함수 제외
echo '*irq*' > set_ftrace_notrace

# 현재 필터 확인
cat set_ftrace_filter

# 사용 가능한 함수 목록에서 글로브 검색
grep 'sched_' available_filter_functions | head -20

perf probe 이벤트 필터링

perf 도구도 이벤트와 함수 필터링에 glob 패턴을 활용합니다. ftrace와 유사하게 커널 내부 glob_match()를 거쳐 동작합니다.

# perf probe: glob 패턴으로 함수 검색
perf probe -F 'sched_*'         # sched_ 접두사 함수 목록
perf probe -F '*alloc*'        # alloc 포함 함수

# 함수 진입점에 프로브 추가
perf probe 'schedule'
perf probe 'do_sys_open filename:string'

# perf stat: 이벤트 필터링
perf stat -e 'sched:sched_switch' -- sleep 1

# perf record: 특정 함수 호출 기록
perf record -g -e 'probe:schedule' -- sleep 5

# tracepoint 필터 표현식 (정규표현식 아닌 비교 연산)
perf record -e 'sched:sched_switch' --filter 'prev_comm == "nginx"'

tracepoint 필터 표현식

perf의 --filter 옵션은 정규표현식이 아닌 비교 연산 기반 필터를 사용합니다. 이는 커널 내부에서 tracepoint 데이터를 효율적으로 필터링하기 위한 설계로, 정규표현식의 복잡도 위험을 피합니다.

필터 연산자의미예제
==같음prev_comm == "nginx"
!=다름next_pid != 0
>, <, >=, <=비교prev_prio > 100
&비트 ANDflags & 0x1
~glob 패턴comm ~ "kworker*"
# tracepoint 필터 사용 예
perf record -e 'sched:sched_switch' --filter 'prev_comm == "nginx" || next_comm == "nginx"' -- sleep 5

# glob 패턴 필터 (~ 연산자)
perf record -e 'sched:sched_switch' --filter 'next_comm ~ "kworker*"' -- sleep 5

# 우선순위 기반 필터
perf record -e 'sched:sched_switch' --filter 'prev_prio < 100' -- sleep 5

# 커널 함수 glob 검색 후 정규표현식으로 정밀 필터링
perf probe -F '*alloc*' | grep -E '(kmalloc|vmalloc|kzalloc)'
perf probe -F '*lock*' | grep -vE '(unlock|clock)'   # unlock, clock 제외
perf vs grep 필터링: perf probe -F의 glob 패턴은 커널 내부에서 동작하여 효율적입니다. 반면 perf probe -F | grep pattern은 전체 목록을 사용자 공간으로 가져온 후 grep이 필터링하므로 오버헤드가 있지만 정규표현식을 사용할 수 있다는 장점이 있습니다. 실전에서는 두 방법을 조합하는 것이 가장 효과적입니다.

grep: BRE vs ERE

grep은 리눅스에서 가장 많이 사용되는 정규표현식 도구입니다. POSIX 표준은 BRE(Basic Regular Expression)와 ERE(Extended Regular Expression) 두 가지 문법을 정의하고, GNU grep은 추가로 PCRE를 지원합니다.

BRE vs ERE vs PCRE 기능 비교 기능 BRE (grep) ERE (grep -E) PCRE (grep -P) . * ^ $ [ ] 지원 지원 지원 + ? \+ \? (이스케이프 필요) + ? (직접 사용) + ? (직접 사용) | (대안) \| (이스케이프 필요) | (직접 사용) | (직접 사용) () 그룹 \( \) (이스케이프 필요) ( ) (직접 사용) ( ) (직접 사용) {n,m} 반복 \{n,m\} {n,m} {n,m} \d \w \s 축약 미지원 미지원 지원 Lookaround 미지원 미지원 지원 *? +? (게으른) 미지원 미지원 지원 \1 역참조 지원 지원 지원 호출 방법 grep (기본) grep -E / egrep grep -P 실전 권장 일반 검색: grep -E (ERE, 이스케이프 적음) | 고급 기능 필요: grep -P (PCRE) | 고정 문자열: grep -F (fgrep, 가장 빠름) ripgrep (rg): Rust regex 크레이트 기반 (DFA 하이브리드) — 기본 PCRE 미지원이지만 -P 플래그로 활성화 가능 대규모 코드베이스 검색에서 grep보다 수배~수십배 빠름 (병렬 처리 + .gitignore 자동 적용)
# BRE: 메타문자에 이스케이프 필요
grep '\(error\|warning\): .\+' build.log

# ERE: 이스케이프 불필요 (권장)
grep -E '(error|warning): .+' build.log

# PCRE: 고급 기능 사용 가능
grep -P '(?<=ERROR:\s)\d{4,}' syslog              # 후방탐색
grep -P '\b0x[[:xdigit:]]{8,16}\b' dmesg.log        # 커널 주소

# 고정 문자열 검색 (정규표현식 아님, 가장 빠름)
grep -F 'CONFIG_PREEMPT=y' /boot/config-$(uname -r)

# 유용한 grep 옵션 조합
grep -rn 'TODO\|FIXME\|HACK' --include='*.c' .    # 재귀 + 줄번호 + 파일 필터
grep -c '^#include' *.h                             # include 개수 카운트
grep -l 'mutex_lock' kernel/*.c                      # 매칭 파일 이름만

sed/awk에서의 정규표현식

sed (Stream Editor)

sed는 스트림 편집기로, 정규표현식 기반의 텍스트 변환에 특화되어 있습니다. 기본적으로 BRE를 사용하며, -E 옵션으로 ERE를 활성화합니다.

# 기본 치환: s/패턴/대체/
sed 's/oldtext/newtext/' file.txt                  # 각 줄의 첫 매칭만
sed 's/oldtext/newtext/g' file.txt                 # 모든 매칭 (global)

# ERE 모드 (-E)
sed -E 's/([0-9]+)\.([0-9]+)/\2.\1/g' version.txt  # 캡처 그룹으로 순서 바꾸기

# 커널 소스 관련 실전 예제
# CONFIG 옵션 값 추출
sed -n 's/^CONFIG_\(.*\)=y/\1/p' .config

# include 경로 변환
sed -E 's|#include <linux/(.*)>|#include "linux/\1"|' source.c

# 주석 제거 (C 스타일 한줄 주석)
sed 's|//.*$||' source.c

# 여러 줄 패턴: 빈 줄 연속 제거
sed '/^$/N;/^\n$/d' file.txt

# 특정 패턴 사이의 텍스트만 출력
sed -n '/^START$/,/^END$/p' log.txt

awk (패턴-액션 언어)

awk는 ERE를 사용하며, 패턴 매칭과 필드 처리를 결합한 텍스트 처리 언어입니다.

# 기본 패턴 매칭
awk '/error|warning/' build.log                    # error 또는 warning 포함 줄
awk '!/^#/' config.txt                              # 주석 아닌 줄만

# 필드 기반 정규표현식
awk -F: '$3 ~ /^[0-9]+$/ && $3 >= 1000' /etc/passwd  # UID >= 1000 사용자

# 커널 로그 분석
dmesg | awk '/\[[[:space:]]*[0-9]+\.[0-9]+\].*error/i {print NR": "$0}'

# /proc/meminfo 파싱
awk '/^(MemTotal|MemFree|MemAvailable):/ {printf "%-16s %d MB\n", $1, $2/1024}' /proc/meminfo

# 커널 모듈 의존성 분석
lsmod | awk 'NR>1 && $3>0 {printf "%-20s used by %d modules: %s\n", $1, $3, $4}'

커널 빌드 시스템에서의 활용

커널 빌드 과정에서 Kconfig 파일, Makefile, 커널 설정(.config)을 처리할 때 sed/awk가 빈번하게 사용됩니다.

# .config에서 활성화된 모듈만 추출
grep -E '^CONFIG_.*=m$' .config | sed 's/^CONFIG_//;s/=m$//' | sort

# Kconfig에서 depends on 의존성 트리 추출
grep -A1 'config [A-Z]' drivers/net/Kconfig | awk '/^config/{name=$2} /depends on/{print name": "$0}'

# Makefile에서 obj-$(CONFIG_*) 규칙 파싱
grep -E 'obj-\$\(CONFIG_' drivers/net/Makefile | sed -E 's/obj-\$\(CONFIG_([^)]+)\)\s*\+=\s*(.*)/\1 -> \2/'

# 커널 빌드 로그에서 경고 요약
make -j$(nproc) 2>&1 | grep -E 'warning:' | sed -E 's/.*\/([^/]+):[0-9]+:.*/\1/' | sort | uniq -c | sort -rn | head -20

# /proc 파일 일괄 읽기 및 포맷팅
for f in /proc/sys/kernel/sched_*; do
  awk '{printf "%-40s %s\n", FILENAME, $0}' FILENAME="$(basename $f)" "$f"
done

sed vs awk vs grep 선택 가이드

작업최적 도구이유
패턴 포함 줄 찾기grep가장 빠름, 최적화된 매칭
텍스트 치환sed스트림 편집에 특화
필드 기반 처리awk구조화된 데이터 처리에 강함
복잡한 변환awk변수, 배열, 함수 사용 가능
고정 문자열 검색grep -F정규표현식 오버헤드 없이 가장 빠름
고급 regexgrep -PPCRE 기능(lookaround 등) 필요 시

PCRE(Perl Compatible Regular Expressions)

PCRE(Perl Compatible Regular Expressions)은 Perl 5의 정규표현식 문법을 C 라이브러리로 구현한 것입니다. POSIX BRE/ERE를 크게 확장하며, 현대 프로그래밍 언어 대부분의 정규표현식 엔진이 PCRE에 영향을 받았습니다.

PCRE 전용 확장 기능

기능구문설명
게으른 수량자*?, +?, ??최소 매칭 우선
소유적 수량자*+, ++, ?+백트래킹 불가 (ReDoS 방지)
Atomic 그룹(?>...)그룹 내부 백트래킹 금지
명명된 캡처(?P<name>...)이름으로 역참조
조건식(?(cond)then|else)조건부 매칭
재귀 패턴(?R), (?1)패턴 자체 또는 그룹 재귀
유니코드 속성\p{Hangul}유니코드 카테고리 매칭
주석(?#comment)패턴 내 주석
모드 변경(?i), (?m), (?s)인라인 플래그 설정

PCRE vs PCRE2 차이

특성PCRE (libpcre)PCRE2 (libpcre2)
APIpcre_compile() / pcre_exec()pcre2_compile() / pcre2_match()
JIT선택적 (별도 빌드)내장 (기본 지원)
유니코드UTF-8만UTF-8/16/32 모두 지원
매칭 제한pcre_extra.match_limitpcre2_set_match_limit() + depth_limit
메모리 관리malloc 기반커스텀 메모리 콘텍스트
상태유지보수 종료 (2021)활발한 개발

JIT 컴파일 메커니즘

PCRE2의 JIT(Just-In-Time) 컴파일러는 정규표현식 패턴을 네이티브 기계어로 변환합니다. 일반 인터프리터 모드 대비 5~10배 빠른 매칭 속도를 제공하며, 동일 패턴을 반복적으로 사용하는 경우(로그 분석, 네트워크 패킷 필터링 등)에 특히 효과적입니다.

# PCRE 확장 기능 예제

# Atomic 그룹: ReDoS 방지
echo "aaaaaaaaab" | grep -P '(?>a+)+b'        # 즉시 매칭

# 유니코드 한글 매칭
echo "Linux 커널 개발" | grep -P '\p{Hangul}+'  # "커널", "개발"

# 재귀 패턴: 중첩 괄호 매칭 (정규 언어 초월)
echo "func(a, (b, c))" | grep -oP '\((?:[^()]*|(?R))*\)'

# 인라인 모드 변경
echo "Hello WORLD" | grep -P '(?i)hello'     # 대소문자 무시
echo -e "line1\nline2" | grep -P '(?s)line1.line2'  # DOTALL: . 이 \n 매칭

# 복잡한 패턴에 주석 달기 (확장 모드)
grep -P '(?x)
  \b                # 단어 경계
  [0-9]{1,3}        # 1~3자리 숫자
  (\.[0-9]{1,3}){3} # .숫자 3번 반복
  \b                # 단어 경계
' access.log

라이브러리 가용성 확인

# 시스템에서 PCRE/PCRE2 확인
pcre2-config --version 2>/dev/null || echo "PCRE2 미설치"
pkg-config --modversion libpcre2-8 2>/dev/null

# grep이 사용하는 PCRE 버전
grep -P --version 2>&1 | head -1

# PCRE2 설치 (Debian/Ubuntu)
# apt install libpcre2-dev

# PCRE2 설치 (RHEL/Fedora)
# dnf install pcre2-devel

실전 패턴 예제

커널 개발과 시스템 관리에서 자주 사용하는 정규표현식 패턴을 모았습니다.

네트워크 패턴

# IPv4 주소 (단순 버전)
grep -E '[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}' syslog

# IPv4 주소 (엄격한 검증: 0-255 범위)
grep -P '\b(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\b' syslog

# MAC 주소
grep -iE '([0-9a-f]{2}:){5}[0-9a-f]{2}' dmesg.log

# IPv6 주소 (축약형 포함, 간략 버전)
grep -iE '[0-9a-f:]{2,39}' ip6tables.log

커널 로그 패턴

# dmesg 타임스탬프 + 에러 추출
dmesg | grep -E '^\[[[:space:]]*[0-9]+\.[0-9]+\].*([Ee]rror|[Ff]ail|[Ww]arn)'

# 커널 주소 (16진수 8~16자리)
grep -P '\b0x[0-9a-fA-F]{8,16}\b' /var/log/kern.log

# OOM killer 로그
grep -E 'Out of memory|oom-kill|Killed process' /var/log/kern.log

# 커널 패닉/Oops
grep -E 'kernel panic|BUG:|Oops:|Call Trace:' /var/log/kern.log

소스 코드 패턴

# 함수 정의 찾기 (C)
grep -EnP '^[a-zA-Z_]\w*\s+\**[a-zA-Z_]\w*\s*\([^)]*\)\s*\{' *.c

# TODO/FIXME/HACK 주석
grep -rn '(TODO|FIXME|HACK|XXX)(\([^)]*\))?:' --include='*.c' .

# #define 매크로와 값 추출
grep -E '^#define\s+\w+\s+.+' include/linux/*.h

# 구조체 정의
grep -P '^struct\s+\w+\s*\{' include/linux/sched.h

보안 패턴

보안 감사나 침입 탐지에서 사용하는 정규표현식 패턴입니다. 로그 분석이나 소스 코드 리뷰에서 잠재적 보안 문제를 찾는 데 활용됩니다.

# SSH 브루트포스 시도 탐지
grep -E 'Failed password for (invalid user )?(\w+) from [0-9.]+' /var/log/auth.log

# SQL injection 시도 패턴 (웹 로그)
grep -iE "(union\s+select|or\s+1\s*=\s*1|'\s*or\s*'|drop\s+table)" access.log

# 소스 코드에서 잠재적 버퍼 오버플로 함수 찾기
grep -rn '\b(strcpy|strcat|sprintf|gets)\s*(' --include='*.c' .

# 하드코딩된 비밀번호/토큰 탐지
grep -rnE '(password|token|secret|api_key)\s*=\s*"[^"]+"' --include='*.c' .

시스템 관리 패턴

# systemd 서비스 상태에서 실패한 것만
systemctl --failed | grep -E '^\s*●\s+\S+\.service'

# 디스크 사용량 80% 이상인 파티션
df -h | awk 'NR>1 && +$5 >= 80 {print $0}'

# 네트워크 인터페이스 IP 추출
ip addr | grep -oP 'inet \K[0-9.]+(?=/)'

# crontab에서 스케줄 주기 파싱
crontab -l | grep -vE '^(#|$)' | awk '{printf "%s %s %s %s %s → %s\n", $1,$2,$3,$4,$5, substr($0,index($0,$6))}'

Git 패턴

Git은 정규표현식과 고정 문자열 검색을 모두 지원합니다. -G는 정규표현식으로 diff를 검색하고, -S는 고정 문자열의 추가/삭제를 검색합니다(pickaxe). 두 옵션의 차이를 이해하는 것이 중요합니다.

# -G (regex): diff에서 패턴 매칭 (추가 또는 삭제 줄에 포함)
git log -G 'mutex_lock' --oneline

# -S (pickaxe): 문자열 출현 횟수가 변한 커밋 (추가/삭제된 경우)
git log -S 'mutex_lock' --oneline

# 커밋 메시지 패턴 검색
git log --grep='fix.*race' -i --oneline

# 특정 파일 패턴의 변경 이력
git log --all -- '**/sched*.c'

# git grep: 워킹 트리에서 정규표현식 검색 (매우 빠름)
git grep -nE '(WARN|BUG)_ON\(' -- '*.c'

# git blame과 결합: 특정 패턴이 있는 줄의 마지막 수정자
git blame -L '/mutex_lock/,+5' kernel/sched/core.c

시간 복잡도 분석

정규표현식 매칭의 시간 복잡도는 엔진 유형과 패턴 구조에 따라 크게 달라집니다. 여기서 n은 입력 문자열 길이, m은 패턴 길이입니다.

엔진구축매칭 (최선)매칭 (최악)메모리
DFAO(2m)O(n)O(n)O(2m)
Thompson NFAO(m)O(n)O(nm)O(m)
백트래킹 NFAO(m)O(n)O(2n)O(m)
커널 glob없음O(n)O(nm)O(1)
고정 문자열 (KMP)O(m)O(n)O(n)O(m)

언제 어떤 도구를 쓸 것인가

올바른 도구 선택은 성능에 큰 영향을 줍니다. 커널 소스 트리(수만 개 파일, 수천만 줄)에서의 검색은 도구 선택에 따라 수초에서 수분까지 차이가 날 수 있습니다.

상황권장 도구이유
고정 문자열 검색grep -F 또는 git grep정규표현식 오버헤드 없음, git grep은 인덱스 활용
단순 패턴grep -EERE가 충분, 이스케이프 적음
고급 패턴grep -P 또는 ripgreplookaround, 유니코드 필요 시
대규모 코드베이스ripgrep (rg)병렬 처리, .gitignore 자동 적용
커널 내부 매칭lib/glob.c안전성 보장, O(nm)
네트워크 패킷lib/textsearchKMP/BM O(n+m), skb 비선형 지원
실전 관점: DFA의 O(2m) 구축 비용은 이론적 최악이며, 실제로는 lazy construction으로 필요한 상태만 생성하여 완화됩니다. Google RE2가 이 방식을 사용합니다. 커널의 lib/textsearch KMP/Boyer-Moore는 고정 문자열에 특화되어 O(n+m) 이내를 보장합니다.

유니코드와 정규표현식

현대 정규표현식 엔진은 유니코드(Unicode) 문자를 처리할 수 있지만, 바이트 기반 매칭과 코드포인트(Code Point) 기반 매칭은 동작이 크게 다릅니다. 특히 한글, CJK, 이모지 처리에서는 유니코드 인식이 필수적입니다.

유니코드 속성 \p{}

PCRE와 대부분의 현대 엔진은 \p{Property} 구문으로 유니코드 문자 속성에 기반한 매칭을 지원합니다. 이는 [a-zA-Z]로는 처리할 수 없는 다국어 텍스트에 필수적입니다.

유니코드 General Category 계층 \p{L} 문자 Lu 대문자 (A-Z) Ll 소문자 (a-z) Lo 기타 (한글, 한자) Lm 수식어 Lt 타이틀케이스 \p{N} 숫자 Nd 십진수 (0-9) Nl 문자숫자 (Ⅰ,Ⅱ) No 기타숫자 (½,¾) \p{P} 구두점 Pd 대시 (-, —) Ps/Pe 여닫기 ((),[]) \p{S} 기호 Sm 수학 (+,=,<) So 기타 (©,®,♠) \p{Z} 분리자 Zs 공백 Zl/Zp 줄/문단 \p{C} 제어/기타 Cc 제어 문자 Co 사용자 영역 (PUA) 스크립트(Script) 속성 — 문자 체계별 매칭 \p{Hangul} 한글 \p{Han} 한자 \p{Hiragana} 히라가나 \p{Latin} 라틴 \p{Cyrillic} 키릴 유니코드 매칭의 핵심 차이 \w = [a-zA-Z0-9_] (ASCII 모드) vs \w = [\p{L}\p{N}_] (유니코드 모드, 한글 포함) \d = [0-9] (ASCII 모드) vs \d = \p{Nd} (유니코드 모드, 아라비아 숫자 ٣ 포함)
# 유니코드 속성 기반 매칭 (PCRE)
echo "Linux 커널 개발 가이드 v2.0" | grep -oP '\p{Hangul}+'
# 출력: 커널, 개발, 가이드

# 한자 매칭
echo "漢字 テスト" | grep -oP '\p{Han}+'
# 출력: 漢字

# 모든 문자(숫자/공백/구두점 제외) 추출
echo "Hello, 안녕 123!" | grep -oP '\p{L}+'
# 출력: Hello, 안녕

# UTF-8 바이트 경계 주의
# 한글 '가' = 0xEA 0xB0 0x80 (3바이트)
# 바이트 수준 . 은 1바이트만 매칭하므로 유니코드 모드 필요
echo "가나다" | grep -P '.'     # 유니코드: "가"(한 코드포인트)
echo "가나다" | grep -Pa '.'    # ASCII: 0xEA(한 바이트) — 깨진 출력
커널과 유니코드: 커널의 lib/glob.c는 바이트 단위로 비교하므로 유니코드 인식이 없습니다. 파일 시스템 이름 매칭에서 UTF-8 멀티바이트 문자는 각 바이트로 개별 비교됩니다. 이는 의도적 설계로, 커널은 문자 인코딩 해석을 사용자 공간에 위임합니다.

정규표현식 컴파일과 캐싱

정규표현식은 사용 전에 내부 표현(NFA, DFA 또는 바이트코드)으로 컴파일되어야 합니다. 이 컴파일 비용을 이해하면 성능 최적화에 도움이 됩니다.

정규표현식 컴파일 파이프라인 패턴 문자열 "a(b|c)*d" 파싱(Parse) → AST 생성 NFA 구축 Thompson 구성법 DFA 변환 부분집합 구성법 DFA 실행 O(n) 보장 바이트코드 생성 백트래킹 인터프리터 NFA 실행 O(2^n) 가능 JIT 컴파일 (선택) 네이티브 실행 RE2/Go: DFA 경로 → O(n) 보장 PCRE2: 백트래킹 NFA + JIT 선택 가능 Rust regex: 하이브리드 NFA/DFA (lazy DFA) 패턴 캐싱: 컴파일 결과를 재사용하면 매칭마다 재컴파일 비용 제거

컴파일 비용과 캐싱 전략

정규표현식 컴파일은 한 번만 수행하고 결과를 재사용하는 것이 핵심 최적화입니다. grep은 내부적으로 패턴을 한 번 컴파일한 후 각 줄에 재사용합니다. 프로그래밍에서는 루프 바깥에서 컴파일하거나, 컴파일된 객체를 전역으로 유지해야 합니다.

# 나쁜 예 (의사 코드): 매번 컴파일
for line in input:
    if re.match('pattern', line):  # 매 반복마다 컴파일
        process(line)

# 좋은 예: 한 번 컴파일, 재사용
compiled = re.compile('pattern')
for line in input:
    if compiled.match(line):      # 컴파일된 패턴 재사용
        process(line)

보안과 정규표현식

정규표현식은 입력 검증의 핵심 도구이지만, 잘못 작성된 패턴은 오히려 보안 취약점이 됩니다. ReDoS 외에도 정규표현식과 관련된 보안 고려사항이 있습니다.

입력 검증 시 주의사항

실수문제해결
/^valid$/ 미사용부분 매칭으로 우회 가능반드시 ^$로 전체 매칭
.의 과도한 사용예상보다 넓은 범위 매칭구체적 문자 클래스 사용 [a-z0-9]
.* 무한 확장의도하지 않은 입력 허용길이 제한 .{1,100}
멀티바이트 무시UTF-8 바이트로 필터 우회유니코드 인식 엔진 사용

커널 보안에서의 패턴 매칭

# auditd 규칙에서 패턴 매칭
# 특정 경로 접근 감시 (glob 패턴)
auditctl -w /etc/shadow -p rwa -k shadow_access
auditctl -w /etc/passwd -p wa -k passwd_change

# audit 로그에서 패턴 검색
ausearch -k shadow_access | grep -E 'type=SYSCALL.*comm="[^"]+"'

# seccomp 프로필에서 syscall 필터링
# (seccomp은 정규표현식이 아닌 syscall 번호 기반)
# 하지만 프로필 생성 시 strace 출력을 regex로 분석
strace -f -o trace.log ./target_program
grep -oP '(\w+)\(' trace.log | sort -u   # 사용된 syscall 목록 추출

정규표현식 테스트와 디버깅

복잡한 정규표현식은 반드시 테스트해야 합니다. 패턴이 의도대로 동작하는지, 엣지 케이스에서 실패하지 않는지, 성능 문제가 없는지 검증하는 방법을 소개합니다.

단계별 매칭 추적

# grep -c: 매칭 줄 수 카운트 (빠른 검증)
grep -cP 'pattern' test_input.txt

# grep -o: 매칭된 부분만 출력 (패턴 정확도 확인)
echo "test string 123 and 456" | grep -oP '\d+'
# 출력: 123
#       456

# grep --color: 매칭 위치 시각화
echo "Hello World Hello" | grep --color=always 'Hello'

# 패턴 디버깅: 단계적으로 복잡도 높이기
# 1단계: 기본 구조 확인
echo "192.168.1.1" | grep -oP '[0-9]+'
# 2단계: 구분자 추가
echo "192.168.1.1" | grep -oP '[0-9]+\.[0-9]+'
# 3단계: 전체 패턴
echo "192.168.1.1" | grep -oP '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+'

엣지 케이스 체크리스트

정규표현식을 작성한 후에는 다음 엣지 케이스들을 테스트해야 합니다:

체크 항목테스트 입력 예확인 사항
빈 문자열""빈 입력에서 의도대로 실패/성공하는지
경계값최소/최대 길이 입력수량자 범위가 정확한지
특수 문자"a.b" "a*b"메타문자가 리터럴로 처리되어야 하는 곳
줄바꿈"line1\nline2".^$의 멀티라인 동작
유니코드한글, 이모지 입력멀티바이트 문자 처리
성능긴 반복 입력백트래킹 폭발 없는지
부분 매칭패턴 포함한 긴 문자열의도하지 않은 부분 매칭 없는지

커널 glob 셀프테스트

CONFIG_GLOB_SELFTEST=y를 설정하면 부팅 시 glob_match()의 정확성을 자동 검증합니다. 총 51개의 테스트 케이스가 와일드카드, 문자 클래스, 이스케이프, 엣지 케이스를 포괄합니다.

# glob 셀프테스트 활성화 및 확인
# menuconfig에서: Library routines → glob_match() selftest
grep 'CONFIG_GLOB_SELFTEST' /boot/config-$(uname -r)

# 부팅 후 셀프테스트 결과 확인
dmesg | grep 'glob:'
# 정상: "glob: 51 self-tests passed, 0 failed"

커널 설정

정규표현식 및 패턴 매칭과 관련된 커널 설정 옵션입니다. 이 옵션들 사이의 의존 관계를 이해하면 커널 구성을 더 정확하게 할 수 있습니다.

설정설명기본값의존
CONFIG_GLOBlib/glob.c glob 매칭 함수yftrace, perf가 의존
CONFIG_GLOB_SELFTEST부팅 시 glob_match() 셀프테스트nCONFIG_GLOB
CONFIG_TEXTSEARCH텍스트 검색 프레임워크m/ynetfilter가 의존
CONFIG_TEXTSEARCH_KMPKMP 알고리즘 모듈mCONFIG_TEXTSEARCH
CONFIG_TEXTSEARCH_BMBoyer-Moore 알고리즘 모듈mCONFIG_TEXTSEARCH
CONFIG_TEXTSEARCH_FSMFSM 알고리즘 모듈mCONFIG_TEXTSEARCH
CONFIG_FTRACEftrace 프레임워크 (glob 필터 사용)yCONFIG_GLOB
CONFIG_DYNAMIC_FTRACE동적 ftrace (함수별 on/off)yCONFIG_FTRACE
CONFIG_NETFILTER_XT_MATCH_STRINGxt_string 문자열 매칭mCONFIG_TEXTSEARCH
# 현재 커널의 관련 설정 확인
grep -E 'CONFIG_(GLOB|TEXTSEARCH|FTRACE|NETFILTER_XT_MATCH_STRING)' /boot/config-$(uname -r)

# glob 셀프테스트 결과 확인 (CONFIG_GLOB_SELFTEST=y 일 때)
dmesg | grep 'glob:'

# 텍스트 검색 모듈 로딩 확인
lsmod | grep 'ts_'
# ts_kmp, ts_bm, ts_fsm이 로드되었는지 확인

# 런타임 시 textsearch 모듈 수동 로드
modprobe ts_kmp    # KMP 알고리즘
modprobe ts_bm     # Boyer-Moore 알고리즘

# iptables -m string 사용 시 자동 로드
iptables -A INPUT -m string --algo kmp --string "malware" -j DROP
의존 관계 요약: CONFIG_NETFILTER_XT_MATCH_STRING을 활성화하면 CONFIG_TEXTSEARCH가 자동으로 선택되고, 이에 따라 KMP/BM/FSM 모듈도 사용 가능해집니다. CONFIG_FTRACECONFIG_GLOB에 의존하지만, glob은 워낙 기본적이어서 거의 항상 활성화되어 있습니다.

정규표현식 및 패턴 매칭과 관련된 주제를 더 깊이 이해하려면 다음 문서를 참고하세요.

참고 자료