유한 오토마타와 정규 표현식 입문
메모리가 유한한 기계가 할 수 있는 일은 어디까지인가.
1. 계산이란 무엇인가
“계산”이라는 단어는 일상에서 너무 자주 쓰여 오히려 무덤덤하게 들린다. 그러나 계산이론(theory of computation)이 던지는 질문은 결코 무덤덤하지 않다. 어떤 문제가 기계로 풀릴 수 있는가, 그리고 풀린다면 얼마만큼의 자원이 필요한가. 이 질문에 정직하게 답하려면 먼저 “기계”라는 말의 뜻부터 못박아야 한다.
철학적인 만담으로 빠지기 전에 한 가지 영리한 결정을 내려 두자. 우리는 가장 강력한 모델, 예컨대 임의 크기의 메모리를 가진 튜링 기계(Turing machine)부터 시작하지 않는다. 오히려 그 반대다. 가장 단순한 모델—메모리가 유한한 작은 기계—에서 출발한다. 단순한 세계에서 무엇이 가능하고 무엇이 불가능한지 정확히 그려낸 다음, 한 단계씩 능력을 키워 가며 그 경계가 어떻게 변하는지를 추적할 것이다. 작은 사다리부터 차례로 밟는 것이 절벽을 단번에 넘는 것보다 훨씬 멀리 데려다 준다.
그 가장 단순한 모델이 바로 유한 오토마타(finite automaton)다. 신호등의 상태 전환, 자동판매기의 잔돈 계산, 어휘 분석기의 토큰 인식—이런 것들이 본질적으로 유한 오토마타의 작동 방식이다. 너무 단순해 보일지 모르지만, 단순하다는 것은 곧 명료하게 분석할 수 있다는 뜻이기도 하다.
2. 알파벳, 문자열, 언어
기계가 다루는 입력을 형식화해 두자. 알파벳(alphabet) Σ는 유한하고 공집합이 아닌 기호들의 집합이다. 흔히 Σ = {0, 1} (이진 알파벳), Σ = {a, b}, 혹은 Σ = {a, b, ⋯, z}와 같이 쓴다. 문자열(string)은 Σ의 원소를 유한 번 이어 붙인 수열이며, 길이 0인 빈 문자열은 ε로 표기한다. 문자열 w의 길이는 |w|로 쓴다.
정의가 살짝 김빠질 수 있다. “언어”라는 거창한 단어 뒤에 숨은 것이 결국 문자열들의 집합 하나라니. 그러나 이 가벼운 정의가 무거운 일을 한다. 우리가 “문제를 푼다”라고 말하는 행위가 “주어진 입력 w가 어떤 정해진 집합 L에 속하는지 결정한다”라는 행위와 일대일로 대응되기 때문이다. 소수 판별 문제도, 문법 검사도, 컴파일러의 토큰 인식도 모두 이 형태로 다듬을 수 있다. 언어를 다루는 기계를 이해하면 결국 “문제를 푸는 기계”를 이해하게 된다.
3. 결정적 유한 오토마타(DFA)
유한 오토마타의 가장 절제된 형태부터 보자. 결정적 유한 오토마타(DFA, Deterministic Finite Automaton)는 입력 문자열을 왼쪽에서 오른쪽으로 한 글자씩 읽으며, 각 단계에서 “현재 상태”라는 하나의 정보만을 기억한다. 상태의 수는 유한하다. 다음 상태는 현재 상태와 읽은 글자에 의해 유일하게 결정되므로 이름에 “결정적(deterministic)”이라는 수식이 붙는다.
- Q: 상태들의 유한 집합
- Σ: 입력 알파벳
- δ: Q × Σ → Q, 전이 함수(transition function)
- q0 ∈ Q: 시작 상태(start state)
- F ⊆ Q: 받아들임 상태들의 집합(accept states)
전이 함수는 이름은 거창해도 결국 “상태와 글자의 쌍을 다음 상태로 보내는 표”에 불과하다. 머릿속으로 그리려면 다음과 같은 그림이 편하다. 동그라미는 상태, 화살표는 전이, 화살표 머리에 입력 글자를 적는다. 시작 상태에는 어디서 들어오는 화살이 하나 있고, 받아들임 상태에는 이중 동그라미를 두른다.
ab로 끝나는” 언어 L = {w ∈ {a,b}* : w = xab, x ∈ {a,b}*}를 인식하는 DFA를 구성한다. 핵심 통찰은 “직전에 본 글자가 무엇인가”만 기억하면 충분하다는 점이다.
- q0: 아직 의미 있는 접미사 없음(또는 직전 글자가 a, b가 아님)
- q1: 직전 글자가
a - q2: 직전 두 글자가
ab(받아들임 상태)
a b
q0 ─────▶ q1 q0 ─────▶ q0
q1 ─────▶ q1 q1 ─────▶ q2
q2 ─────▶ q1 q2 ─────▶ q0
F = {q2}, q0이 시작 상태다. 잠시 손으로 “aab”, “abb”, “bab”을 추적해 보면 정확히 ab로 끝나는 입력만 q2에서 멈춘다는 사실이 확인된다.
- δ(rk, 0) = rk (0은 카운터를 건드리지 않음)
- δ(rk, 1) = r(k+1) mod 3
4. 정규 언어
이렇게 만든 모델로 표현 가능한 언어들에 이름을 붙여 두자. 이 클래스가 앞으로 몇 강의 동안 우리의 주된 무대가 된다.
정의를 보고 “DFA로만 정의되는 클래스인데 왜 이름을 따로 짓는가” 하는 의문이 들 수 있다. 그 답은 다음 두 강의에 걸쳐 자연스럽게 드러난다. 같은 클래스가 비결정적 모델로도, 정규 표현식으로도, 닫힘 연산의 결과로도 등장한다. 같은 산을 동·서·남쪽에서 각각 올라가 모두 같은 정상에 닿는 셈이다.
5. 정규 표현식
이제 같은 정상에 가는 또 다른 등산로를 살짝 엿본다. 정규 표현식(regular expression)은 언어를 구문론적으로, 즉 작은 표현식을 결합해 서서히 키우는 방식으로 정의한다.
- ∅은 정규 표현식이고, L(∅) = ∅.
- ε은 정규 표현식이고, L(ε) = {ε}.
- a ∈ Σ에 대해 a는 정규 표현식이고, L(a) = {a}.
- R1, R2가 정규 표현식이면 (R1 ∪ R2)도 정규 표현식이고, L(R1 ∪ R2) = L(R1) ∪ L(R2).
- (R1R2)도 정규 표현식이고, L(R1R2) = {xy : x ∈ L(R1), y ∈ L(R2)}.
- (R1*)도 정규 표현식이고, L(R1*) = {x1x2⋯xk : k ≥ 0, xi ∈ L(R1)}. 특히 ε ∈ L(R*) (k=0인 경우).
합집합 ∪, 연결(concatenation), 그리고 클레이니 스타(Kleene star) *—이 셋이 정규 표현식의 세 가지 기본 연산이다. 같은 표현식이라도 괄호와 우선순위 약속에 따라 외양이 달라진다. 통상적으로 *가 가장 강하게 결합하고, 그 다음이 연결, 마지막이 ∪이다.
- (0 ∪ 1)*: 모든 이진 문자열의 집합 Σ*.
- (0 ∪ 1)*1: 1로 끝나는 이진 문자열들.
- 0*10*10*: 정확히 두 개의 1을 포함하는 이진 문자열들.
- (00)*: 짝수 길이의 0-반복 문자열들 {ε, 00, 0000, ⋯}.
- ∅*: 정의에 따라 {ε}이다. 공집합을 0번 이어 붙인 결과는 빈 문자열이라는 묘한 사실이다.
6. 다음 강의 예고
여기까지 우리는 결정적 기계와 구문적 표현식이라는 두 갈래 길을 깔아 두었다. 다음 강의에서는 길을 하나 더 추가한다. 한 상태에서 동시에 여러 곳으로 갈라질 수 있는 비결정적 유한 오토마타(NFA)가 그것이다. 처음에는 “편법 같다”는 느낌이 들겠지만, 놀랍게도 NFA가 인식할 수 있는 언어 클래스는 DFA의 그것과 정확히 같다는 사실을 증명할 것이다. 거기에 정규 표현식까지 같은 클래스로 합류하면, 정규 언어라는 한 봉우리에 세 개의 등산로가 모두 닿게 된다.