나무모에 미러 (일반/밝은 화면)
최근 수정 시각 : 2025-01-29 22:54:13

어휘 분석

렉서에서 넘어옴

'''이론 컴퓨터 과학
{{{#!wiki style="display: inline-block; font-family:Times New Roman, serif;font-style:italic"'''
{{{#!wiki style="margin: 0 -10px -5px; min-height: calc(1.5em + 5px)"
{{{#!folding [ 펼치기 · 접기 ]
{{{#!wiki style="margin: -5px -1px -11px"
<colbgcolor=#a36> 이론
기본 대상 수학기초론{수리논리학(논리 연산) · 계산 가능성 이론 · 범주론 · 집합론} · 이산수학(그래프 이론) · 수치해석학 · 확률론통계학 · 선형대수학
다루는 대상과 주요 토픽
계산 가능성 이론 재귀함수 · 튜링 머신 · 람다대수 · 처치-튜링 명제 · 바쁜 비버
오토마타 이론 FSM · 푸시다운 · 튜링 머신(폰노이만 구조) · 정규 표현식 · 콘웨이의 생명 게임 · 형식언어
계산 복잡도 이론 점근 표기법 · 튜링 기계^고전, 양자, 비결정론적, 병렬 임의접근 기계^ · 알고리즘 · 자료구조 · 알고리즘 패러다임(그리디 알고리즘, 동적 계획법)
정보이론 데이터 압축(무손실 압축 포맷 · 손실 압축 포맷) · 채널 코딩(채널 용량) · 알고리즘 정보 이론(AIT) · 양자정보과학
프로그래밍 언어이론 프로그래밍 언어(함수형 언어 · 객체 지향 프로그래밍 · 증명보조기) · 메타 프로그래밍 · 유형 이론 · 프로그래밍 언어 의미론 · 파싱 · 컴파일러 이론
주요 알고리즘 및 자료구조
기초 정렬 알고리즘 · 순서도 · 탐색 알고리즘
추상적 자료형 및 구현 배열^벡터^ · 리스트^연결 리스트^ · 셋(set)^레드-블랙 트리, B-트리^ · 우선순위 큐^, 피보나치 힙^
수학적 최적화 조합 최적화 외판원 순회 문제 · 담금질 기법 · 유전 알고리즘 · 기계학습
볼록 최적화 내부점 방법 · 경사하강법
선형계획법 심플렉스법
계산 수론 및 암호학 밀러-라빈 소수판별법 · Pollard-rho 알고리즘 · 쇼어 알고리즘 · LLL 알고리즘 · 해시(MD5 · 암호화폐 · 사전 공격(레인보우 테이블) · SHA) · 양자 암호
대칭키 암호화 방식 블록 암호 알고리즘(AES · ARIA · LEA · Camellia) · 스트림 암호 알고리즘(RC4)
공개키 암호화 방식 공개키 암호 알고리즘(타원 곡선 암호 · RSA) · 신원 기반 암호 알고리즘(SM9)
계산기하학 볼록 껍질 · 들로네 삼각분할 및 보로노이 도형^Fortune의 line-sweeping 알고리즘^ · 범위 탐색^vp-tree, R-tree^ · k-NN
그래프 이론 탐색^BFS, DFS, 다익스트라 알고리즘, A* 알고리즘^ · 에드몬드-카프 · 크루스칼 알고리즘 · 위상 정렬 · 네트워크 이론
정리
정지 문제대각선 논법 · 암달의 법칙 · P-NP 문제미해결 · 콜라츠 추측미해결
틀:이산수학 · 틀:수학기초론 · 틀:컴퓨터공학 }}}}}}}}}


1. 개요2. 명칭3. 토큰과 렉심
3.1. 주요 토큰타입
4. 종류
4.1. 정규 렉서
4.1.1. 정규 문법의 NFA 변환
4.2. 문맥 자유 렉서
5. 렉서 제너레이터
5.1. 목록
6. 관련 문서

1. 개요

Lexical analysis / Lexing

주어진 입력을 사전에 정해진 규칙에 맞게 문법적으로 의미를 가지는 최소 단위인 토큰으로 분리하고 각 토큰의 종류에 맞게 적절한 분류를 수행하는 과정.

2. 명칭

이론언어학이론 컴퓨터 과학에서 동시에 다루어지는 주제인 만큼 여러 명칭이 공존하고 있다. 특히 컴퓨터공학 쪽에선 스캐닝(scanning), NLP 측에선 토큰화(tokenization) 등의 용어도 렉싱(lexing)만큼 자주 쓰이는 편. 정리하면 다음과 같다.
의미 용어
어휘 분석 영어 lexical analysis, lexing, lexical tokenization, tokenization, tokenizing[u], scanning
한국어 어휘 분석, 낱말 분석[u], 렉싱, 토큰화, 스캐닝
어휘 분석의 단위 영어 lexical token, token, word[u]
한국어 어휘[u], 토큰
어휘의 종류 영어 token type, token name, lexical category[u]
한국어 토큰 분류[u], 토큰 타입
문장 내 실제 어휘 영어 lexeme, span[d]
한국어 어휘소[u], 렉심
어휘 분석을 수행하는 기계 또는 프로그램 영어 lexical analyzer, lexer, tokenizer, scanner, segmenter[u]
한국어 어휘 분석기, 렉서, 토크나이저, 스캐너
어휘 분석을 수행하는 기계 또는 프로그램을 생성하는 기계 또는 프로그램 영어 lexical analyzer generator[u], lexer generator
한국어 어휘 분석기 생성기[u], 렉서 제너레이터

언어학의 lexicalization과 lexicon, lemmatization 및 기타 형태론이나 NLP 과정들과는 다르다. 렉싱의 핵심은 형태소를 형태소를 추론하는 것이 아닌 단지 주어진 문장의 segmentation이기 때문.

위에서도 언급했지만 lex- 형태의 용어는 주로 언어학 연구에서 파생된 것들이 대부분으로, 조금 더 엄밀한 명칭인 동시에 타 표현들에 비해 lexer, lexing 등의 줄임말 표현이 있기에 실무에서도 자주 쓰이는 편. 또한 타 용어들에 비해 프로그래밍 언어의 컴파일러로써의 성격도 강하다. token- 형태의 용어는 주로 NLP 분야, 즉 입력이 자연어일 경우인 맥락일 가능성이 높지만 후술할 token을 대체할 단어가 없기에 덩달아 자주 쓰인다. scan- 형태는 흔히 scanf, java.util.Scanner 등으로 연상되듯 주로 손으로 짠(handwritten) 렉서를 의미하며 완성된 프로그래밍 언어보단 문장의 일부분을 파싱하는 용도로 쓰인다. 렉서 제너레이터가 scanner generator등의 형태로 쓰이지 않는 것도 이러한 뉘앙스 때문.

토큰과 토큰 타입, 렉심은 화자와 맥락에 따라 혼용되면서도 사전적으로는 다른 의미이다. 렉심의 경우 인식된 개별 토큰과 추상적으로 대응되는 입력의 일부분을 가리키지만, span은 입력 문자열의 시작과 끝 인덱스를 의미하는 조금 더 구현 친화적인 용어.

본 문서에서는 컴퓨터공학 및 컴파일러 설계적 관점을 우선하며, 용어 또한 실질적으로 현실에서 자주 쓰이는 영어 음차 용어들(렉서, 파서, 토큰, 렉심, 제너레이터 등)의 사용을 우선한다.

3. 토큰과 렉심

토큰은 문장의 최소 구성단위로, 프로그래밍 언어에서는 대체로 키워드(keyword), 식별자(identifier), 상수(constant), 문자열(string), 연산자(operator), 구분자(punctuation), 공백(whitespace) 등등이 있다. 일례로 다음의 C 언어 코드를 보자.

#!syntax cpp
int mins = 24 * 60;

24, 60 등은 상수이고 int는 키워드일 것이다. 마찬가지로 위 코드를 개별 토큰으로 전부 분리해 보자.
토큰 타입 {{{#!wiki style="margin: -15px -10px" 키워드 }}} {{{#!wiki style="margin: -15px -10px"<tablewidth=100%><tablebordercolor=#c5c2cb,#585260> 공백 }}} {{{#!wiki style="margin: -15px -10px"<tablewidth=100%><tablebordercolor=#aa573c> 식별자 }}} {{{#!wiki style="margin: -15px -10px"<tablewidth=100%><tablebordercolor=#c5c2cb,#585260> 공백 }}} {{{#!wiki style="margin: -15px -10px"<tablewidth=100%><tablebordercolor=#2a9292> 연산자 }}} {{{#!wiki style="margin: -15px -10px"<tablewidth=100%><tablebordercolor=#c5c2cb,#585260> 공백 }}} {{{#!wiki style="margin: -15px -10px"<tablewidth=100%><tablebordercolor=#be4678> 상수 }}} {{{#!wiki style="margin: -15px -10px"<tablewidth=100%><tablebordercolor=#c5c2cb,#585260> 공백 }}} {{{#!wiki style="margin: -15px -10px"<tablewidth=100%><tablebordercolor=#2a9292> 연산자 }}} {{{#!wiki style="margin: -15px -10px"<tablewidth=100%><tablebordercolor=#c5c2cb,#585260> 공백 }}} {{{#!wiki style="margin: -15px -10px"<tablewidth=100%><tablebordercolor=#be4678> 상수 }}} {{{#!wiki style="margin: -15px -10px"<tablewidth=100%><tablebordercolor=#955ae7> 구분자 }}}
렉심 <rowcolor=#fff><nopad> i <nopad> n <nopad> t <nopad> <nopad> m <nopad> i <nopad> n <nopad> s <nopad> <nopad> = <nopad> <nopad> 2 <nopad> 4 <nopad> <nopad> * <nopad> <nopad> 6 <nopad> 0 <nopad> ;
인덱스 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18

이 때 키워드, 공백, 식별자 등의 유형을 토큰 타입이라고 하며, mins, 60 등의 구체적인 값을 렉심이라고 한다. 하나의 토큰 타입은 주어진 형식 언어의 정의에 따라 0개 이상의 렉심을 가질 수 있는데, 예를 들어 24, 60 등은 서로 다른 렉심이지만 둘다 같은 토큰 타입(상수)에 해당한다.

근본적으로 개별 토큰은 토큰 타입과 렉심으로 구성된다.

[math({\rm Token} = ({\rm TokenType},{\rm Lexeme}))]


실제로 렉서를 설계할 때는 최적화 및 에러 보고 등을 위해 소스에서 해당 렉심의 위치를 인덱스(pos)나 인덱스 범위(span) 형태로 기록하기도 하며, evaluation 단계에서 각 렉심에 대응되는 실제 값 정보[12]를 추가하기도 한다.

예를 들어 토큰 타입이 BOOL인 토큰이라면 가능한 렉심 값은 true 또는 false 둘 중 하나가 될 것이다. 이 값이 둘 중 무엇이냐에 따라 코드의 실행 결과가 달라지기에 각 토큰에 어떤 렉심인지 기록해둬야 할 것이다. 반면에 해당 토큰 타입에 해당하는 렉심이 단 하나뿐인 경우는 최적화 등을 위해 렉심 부분을 생략하기도 한다. 즉, 렉심은 빼고 개별 토큰에 토큰 타입 정보만 남기는 것. 주로 키워드, 구분자 등과 같이 고정적인 문자열인 경우에 토큰 타입만 사용한다.

이제 위 예시를 (첫 문자 인덱스, 토큰 타입, [렉심])의 형태로 순서대로 나타내면 다음과 같을 것이다.
(0, KEYWORD_INT), (3, WS), (4, IDENT, "mins"), (8, WS), (9, OP_EQ), (10, WS), (11, LIT_NUM, "24"), (13, WS), (14, OP_AST), (15, WS), (16, LIT_NUM, "60"), (18, PUNCT_SEMI)

이와 같이 입력 소스를 개별 토큰들의 배열로 만든 것을 토큰 스트림(token stream)이라고 한다.

3.1. 주요 토큰타입

컴퓨터 언어프로그래밍 언어에서 흔히 쓰이는 토큰타입들의 예시와 체계. 몇몇 토큰타입은 다른 토큰타입의 부분집합으로 서술하는데, 결정적 유한 상태 기계를 만드려면 각 토큰의 토큰타입은 반드시 하나씩일 수밖에 없기 때문에 불필요해 보이지만, 이런 분석은 추후 렉싱이 끝나고 구문 분석, 포맷팅, 신택스 하이라이팅, 린팅 등 다양한 정적 분석 작업에서 유용한 정보로 활용될 수 있다. 이를테면 키워드 토큰만 초록색으로 색칠해 보여주도록 하는 식.

4. 종류

4.1. 정규 렉서

정규 문법을 통해 기술할 수 있는 토큰들만 허용하는 렉서. 후술할 문맥 자유 렉서에 비해 제한적이고, 시간 복잡도는 [math(\mathcal O(n))]으로 매우 빠르다. 특히 대부분의 렉서 제너레이터가 정규 렉서를 지원하므로 제너레이터의 최적화를 사용하면 손으로 짠 거의 대부분의 렉서보다 성능이 좋게 나온다. 따라서 렉서 단에서 뭔가 복잡한 로직이 필요하거나 복잡한 문법 때문에 어쩔 수 없이 문맥 자유 렉서를 짜야 하는 경우가 아니라면 대부분의 프로그래밍 언어는 정규 렉서를 사용한다.

4.1.1. 정규 문법의 NFA 변환

흔히 렉서 제너레이터를 사용할 때 정규 표현식을 사용하지만, 엄밀히 말해 정규 표현식은 정규 언어의 확장(superset)으로, 백트래킹 등의 구현이 들어가면 당연히 [math(\mathcal O(n))] 시간 안에 결정가능하지 않다. 정규 렉서가 인식할 수 있는 언어는 정규 언어(regular language)로, 형식 언어의 일종이다. 이런 형식 언어를 형식 문법(formal grammar)으로 나타냈을 때 아래의 형태를 가지는 것을 정규 문법(regular grammar)이라고 하고, 이는 정규 언어를 유도(derive)함이 증명되어 있다.

[math(\begin{array}lA\to t\;|\;\epsilon\\A\to tB\;\;{\rm or}\;\;Bt\end{array}\quad t\in\Sigma\;A,B\in V_{NT})]


이때 [math(A\to tB)] 형태의 문법을 우정규(right-regular) 문법, 우선형(right-linear) 문법이라 하고 [math(A\to Bt)] 형태의 문법을 좌정규(left-regular), 좌선형(left-linear) 문법이라고 한다. 모든 좌선형 문법은 우선형 문법과 동치 관계에 있음이 증명되어 있으므로 이후로는 우선형 문법을 기준으로 서술한다.

이를 실제로 나열해 보면 굉장히 긴 생성 규칙의 집합이 되는데, 이를 행렬(구현에서는 점프 테이블)로 나타내면 좋을 것이다. 다음과 같은 전이 함수를 갖는 비결정적 유한 상태 기계(NFA)를 생각해 보자.

[math({\rm NFA_G}=(Q,\Sigma,\delta,q_0\in Q,F)\quad\delta:Q\times(\Sigma\cup\{\epsilon\})\mapsto\mathcal P(Q))]


상태 집합 [math(Q)]가 반드시 논터미널 기호 집합인 [math(V_{NT})]와 같을 것 같지만 꼭 그렇지는 않다. 터미널 기호나 공문자열로 끝나는 생성 규칙의 경우 별도의 추가 상태를 정의해야 하기 때문. 일반적으로 다음 규칙에 따라 정규 문법을 NFA로 변환할 수 있다.
  1. [math(q_0=S)]로 한다. 즉, 시작 기호가 곧 시작(entry) 상태가 된다.
  2. [math(Q)]에 [math(V_{NT})]를 전부 넣는다. 즉, 모든 논터미널 기호를 별도의 상태로 만든다.
  3. [math(F)]는 공집합으로 둔다.
  4. [math(\forall p\in P_G)]에 대해
    • [math(p:A\to tB)] 꼴이라면, [math(A)]에서 [math(t)]를 만났을 시 [math(B)]로 가는 전이를 추가한다. 다시 말해, [math(B\in\delta(A,t))]가 되도록 한다.
    • [math(p:A\to t)] 꼴이라면, 새 상태 [math(A')]을 [math(Q)]와 [math(F)]에 추가하고 [math(A'\in\delta(A,t))]가 되도록 한다.
    • [math(p:A\to\epsilon)] 꼴이라면, [math(A)]를 [math(F)]에 추가한다.

이렇게 구해진 NFA는 주어진 정규 문법과 정확히 대응한다. 유한 길이의 문자열을 시작부터 읽어들였을 때, 최종 상태 [math(q_f)]가 [math(F)]의 원소라면 허용되는 토큰, 그렇지 않고 다른 상태라면 오류 토큰인 것.

4.2. 문맥 자유 렉서

5. 렉서 제너레이터

렉서를 손으로 작성하는 경우도 있지만, 대부분의 경우는 렉서 코드를 생성하는 렉서 제너레이터를 사용한다. 사용 이유는 파서 제너레이터와 비슷한데, 렉서를 손으로 작성하는 것에 비해 개발 시간이 현저히 빠르고, 그만큼 언어 스펙 수정에 대응하기도 편하며, 대부분의 경우 모든 path에 대한 경우를 컴파일 타임에 체크하므로 에러가 없고 무결하며, 무엇보다 손으로 작성된 대부분의 렉서보다 최적화가 쉬워 성능이 훨씬 빠르기 때문이다.

5.1. 목록

6. 관련 문서


[u] 자주 쓰이는 용례가 아님.[u] [u] [u] [u] [u] [d] 뜻이 100% 일치하지 않음.[u] [u] [u] [u] [12] 이를테면 12.3 이라는 렉심이 있을 때, 이는 단지 1, 2, ., 3이라는 4개의 문자들의 나열일 뿐이다. 이를 숫자로써 파싱하고 부동소수점 방식으로 변환해 12.3이라는 숫자 값으로 메모리에 저장해야 실제 연산을 할 수 있을 것이다.[13] 언어에 따라 역참조 연산자로도 쓰인다.[비트부정] [거듭제곱] [거듭제곱] [비트부정]