자연 언어 와 프로그래밍 언어

 

자연 언어 처리 : 김영택.권혁철.옥철영.서영훈.이상조.윤덕호.강승식.이호석.윤성 희.이하규.심광섭 공저, 교학사, 1994, page 39~51

 

1. 자연 언어와 프로그래밍 언어의 성격

2. 프로그래밍 언어 처리

3. 언어 처리 기술

 

 

자연언어와 달리 컴퓨터 프로그래밍을 위하여 특별히 개발된 FORTRAN, PASCAL, C 등의 언어를 인공언어 (artificial language) 혹은 프로그래밍 언어 (programming language) 라 부른다. 프로그래밍 언어는 애당초 기계에서의 처리를 염두에 두고 인위적으로 만들어 졌기 때문에 자연언어에 비하여 훨씬 처리하기 쉬운 언어에 속한다. 그러나 프로그래밍 언어 설계의 이론적 배경이 되고 있는 형식언어 (formal language) 이론이 원래 자연언어를 설명하기 위하여 연구되었던 것이기 때문에 프로그래밍 언어 설계의 궁극적인 모델은 바로 자연언어였다고 말할수 있다. 따라서, 자연언어와 프로그래밍 언어의 차이를 이해하는 한편, 컴퓨터에서의 프로그래밍 언어의 처리 방법에 대하여 살펴보는 것은 앞으로의 논의를 이해하는데 도움이 될 수 있을 것이다.

1. 자연 언어와 프로그래밍 언어의 성격

인류가 언제부터 지국상에 생존하였고 언제부터 말을 하였는지 정확하게는 알 수 없지만, 적어도 몇 만년 전부터 인류는 이 지구상에 흩어져 살아왔고, 말을 사용한 역사 역시 몇 만년 전부터라는 설이 있다.

특히,인류는 지구 전역게 걸쳐 흩어져 살면서 나름대로의 문화를 발전시켜왔고 지역 간의 통신 수단도 빈약하였기 때문에, 인류가 사용해온 언어는 긴 세월 동안 지역별로 다양하게 발전하여 오늘날 지구상에는 200 가지가 넘는 언어가 존재하는데, 이 가운데 40 가지 가량이 표기 체계인 글을 갖고 있다고 한다. 이들 언어들이 바로 자연언어이며, 따라서 자연언어의 역사는 곧 인류의 역사라고 까지 말할수 있을 것이다.

반면, 프로그래밍 언어는 컴퓨터 출현과 더불어 나타나 컴퓨터의 발전과 함께 다양화 되고 고급화 되어 왔으니, 불과 수십년의 짧은 역사를 갖고 있다. 형식언어 (formal language) 이론에서는 자연언어를 문맥의존 언어 (Context Sensitive) 로 분류하며, 프로그래밍 언어는 문맥자유 언어 (Context Free) 로 분류하고 있다. 문맥자유 언어란 문맥의존 언어의 특수한 경우에 해당하기 때문에, 프로그래밍 언어가 자연언어의 범주안에 들고 있음을 알 수 있다.

형식언어 이론에서는 문법 G 를 G = (N, T, P, S) 로 정의한다. N 은 문법 기호에 해당하는 비단말 기호 (nonterminals) 집합을 의미하며, T 는 위에 해당하는 단말 기호 집합 N 의 원소 가운데 하나이어야 하는데, 자연언어의 경우 대개 문장을 나타내는 문법 기호가 시작 기호가 된다. 끝으로 P 는 시작 기호 S 로부터 시작한 유도 (derivation) 의 과정을 통하여 단말기호의 열 (string) 이 얻어지기까지 적용될수 있는 생성규칙 (production rule) 들의 집합을 의미하며, 생성규칙은 문법규칙이라고도 한다. 즉, 형식언어 이론에서는 문법 기호의 집합, 어휘의 집합, 생성규칙의 집합을 정의하고, 문법규칙 집합의 원소 가운데 하나를 시작 기호로 정의한다면 하나의 문법이 정의되었다고 보는 것이다.

한 문법이 정의되었을 때, 시작 기호로부터 시작하여 생성규칙의 반복적 적용을 통하여, 단말기호와 비단말 기호가 섞인 열 (string) 들을 얻어나갈 수 있으며, 궁극적으로 모든 비단말 기호가 사라지고 단말 기호들만이 남게 되었을 때 이 단말기호, 즉 어휘들의 열을 그 문법의 문장 (sentence) 이라 부른다.

그런데 문법들은 다시 그 생성규칙이 얼마나 자유로운 형태를 갖는가, 혹은 제약된 형태를 갖는가에 따라 네가지의 등급으로 분류된다. 가장 자유로운 정도가 큰 문법을 무제약 문법 (unrestricted grammar) 이라고 부르며, 여기에 차례로 제약을 가하여 규칙의 자유도를 낮추워 나감에 따라 문맥의존 문법, 문맥자유 문법, 정규문법 (regular grammar) 들이 얻어진다. 이 가운데 자연언어나 프로그래밍 언어의 처리와 관련하여 가장 많은 주목을 받는 것은 문맥의존 문법과 문맥자유 문법의 두가지이다.

문맥의존 문법에서의 생성규칙은 아래와 같은 형태를 갖는다.

     

위의 생성규칙은 비단말 기호 N 의 원소인 A 가 공백 (λ) 이 아닌 적당한 단말 및 비단말 기호열 W 로 대치될수 있다는 뜻이다. 단, 이 생성규칙이 적용되기 위해서는 비단말 기호 A 의 앞과 뒤에 각각 φ 와 ψ 로 규정된 단말 및 비단말 기호열과 일치되는 내용이 있어야 한다. 즉, 생성규칙의 적용은 생성규칙에 의하여 치환될 문법 기호 뿐만 아니라 그 문법 기호의 앞뒤에 위치하는 구절의 내용인, 이른 바 문맥에 영향을 받고 있음을 말한다.

반면에 문맥자유 문법에서의 생성규칙은 아래와 같은 형태를 갖는다.

     

생성규칙의 이같은 형태는 문맥의존 문법의 생성규칙에 φ = λ, ψ = λ 인 제약이 가해진 경우에 해당하며, 이는 곧 생성규칙의 적용이 문맥에 의하여 제한되지 않음을 의미한다. 따라서, 문맥자유 문법은 문맥의존 문법의 특수한 경우로 볼수 있으며, 제약이 가해진 만큼 문법의 자유도가 떨어져 표현력은 약화되지만, 대신 처리하기는 보다 쉬워지는 것이다.

프로그래밍 언어는 인위적으로 만들어진 언어이기 때문에 처리의 용이성을 중시하여 문맥자유 문법으로 설계되었으며, 특히 문법의 모호함이 없도록 주의를 기울여 만들어졌다. 따라서, 프로그래밍 언어의 분석을 위한 효율적인 알고리즘이 많이 개발되어 프로그래밍 언어로 작성된 프로그램을 같은 내용의 기계어 프로그램으로 번역해주는 컴파일러 (compiler) 에 이용되고 있다. 심지어, 컴파일러의 개발 과정중 상당부분이 자동화되기까지 하여, 문법 설계만 하여 입력으로 주면 컴파일러의 골격을 만들어 주는 도구 (tool) 프로그램들이 나와 있는 상태이다.

반면에, 자연언어는 인류 역사 속에서 생겨나 발전되어 온 탓에 인위적인 세심한 설계가 없이 자연 발생적으로 만들어졌다는 점에서 처리에 많은 어려움을 갖게된다. 형식언어 이론에서는 자연언어가 문맥의존 문법에 해당한다고 보고 있다. 그러나 문맥의존 문법을 효율적으로 처리하여 주는 알고리즘이 아직 개발되어 있지 못한 상태이기 때문에, 현재 자연언어처리를 위한 노력들은 일단 문맥자유 문법을 기본으로 하여, 여기에 문맥 의존적 요소를 처리할수 있는 기능을 부가하는 방향으로 이루어지고 있다.

문맥자유 문법의 예로서 프로그래밍 언어의 산술문에 나타나는 수식에 대한 문법을 정의하여 보자. 수식 <expression> 은 아래와 같은 문법에 의하여 연산자 우선 순위와 좌결합 우선의 성질을 갖는 사칙 연산의 형태를 가질수 있다.

여기서 기호 '|' 는 동일한 좌변을 갖는 생성규칙들을 모아 간결하게 표현하기 위한 것으로, 예를들어 'A → α | β | γ' 는 'A → α', 'A → β', 'A → γ' 의 세 개의 생성규칙을 함께 표현하는 것이다.

따라서 위의 문법은 모두 아홉 개의 생성규칙으로 이루어져 있으며, 비단말 기호 집단 N 은 {<expression>, <term>, <factor>} 가 단말 기호 집단 T 는 {+, -, *, /, (, ), id, number} 가 된다. 또 S 는 <expression> 이고, P 는 위의 생성규칙들의 집합이다.

이 문법에 의하여 생성가능한 문장의 예로 id * (id + number) 가 있을수 있다. 이 문장이 생성되는 과정은 다음과 같다.

시작 기호인 <expression> 에서 시작하여, 각 단계마다 아홉가지 생성규칙 가운데 하나의 적용을 통하여 밑줄친 부분이 고쳐지며, 최종적으로 단말 기호들만의 열이 되어 더 이상 생성규칙을 적용할 수 없게 된다. 이러한 과정을 유도 (derivation) 라 하며, 이처럼 유도 과정을 통하여 시작 기호로부터 생성될수 있는 단말 기호열을 그 문법의 문장이라 하는 것이다.

2. 프로그래밍 언어 처리

프로그램을 작성할 때는 먼저 적절한 프로그래밍 언어를 선정해야 하며, 프로그래밍 언어가 결정되면 해당 언어는 구문 (syntax) 과 의미 (semantics) 가 있기 때문에, 이들 구문과 의미에 맞추어 프로그램을 작성하게 된다.

프로그래밍 언어 가운데는 기계가 직접 이해할 수 있는 기계어나 어셈블리어 같은 저급 언어도 있지만, 대개의 경우 사용하기 편리한 구문과 의미를 갖춘 고급 언어 (high level language) 를 사용하게 된다. 고급 언어로 작성된 프로그램은 구문과 의미에 맞게 작성되었다 해도, 기계에서 직접 이해하고 실행할 수가 없기 때문에, 먼저 번역기에 의하여 같은 기능을 하는 기계어 프로그램으로 번역되어야 하며, 번역 결과 생긴 기계어 프로그램인 목적 코드가 컴퓨터에 의하여 처리되어야 비로소 결과가 생성되게 된다. 이같은 과정을 도식화 하면 그림 1 과 같다.

그림 1 고급 언어 프로그램의 처리 과정

그림 1 에서의 번역기는 일종의 소프트웨어이며 목적 코드 역시 소프트웨어이다. 컴퓨터는 이들 두 개의 소프트웨어를 가동하여 데이터를 처리하여 결과를 얻게 되는 것이다. 이 번역기를 컴파일러 (compiler) 라 하며, 컴파일러의 구성을 보면 자연언어처리 시스템의 일종인 기계번역 시스템과의 유사함을 관찰할수 있어 흥미롭다.

컴파일러의 구조는 대략 그림 2 와 같은 몇 단계로 구성되어 있다.

그림 2   컴파일러의 구조

렉시칼 분석 단계에서는 프로그램을 차후 단계에서의 처리 단위가 되는 토큰 (token) 들로 분리해 낸다. 토큰의 종류로는 각종 키워드 (key words), 연산자 등의 특수 문자들, 그리고 기호 (symbol) 와 수치 등이 있다. 여러 가지 기호나 수치들은 모두 동일한 기호 토큰 또는 수치 토큰으로 표현되는 대신, 기호의 내용 혹은 수치 값과 같은 정보를 기호 테이블 (symbol table) 등의 각종 테이블에 저장하여 두고, 그 인덱스 정보를 토큰에 함께 부여하여 필요할 때 참조할수 있게 한다.

예를들어, 그림 3 의 (a) 와 같은 원시 문장에 대하여 렉시칼 분석을 수행하면 (b) 와 같은 토큰들을 얻어낼수 있을 것이다. TID, TASSIGN 등의 토큰은 물론 기계 안에서는 적절히 배정된 정수값으로 표현되게 된다. 이 때 (c) 와 같은 기호 테이블이나 (d) 와 같은 수치 테이블에 함께 만들어져 나중에 TID 토큰이나 TNUM 토큰에 대한 구체적인 정보가 필요할 때 이용할수 있게 한다.

ALPHA := BETA * (GAMMA + 128)

(a)

TID1, TASSIGN, TID2, TMUL, TLPAREN, TID3, TADD, TNUM, TRPAREN

(b)

번호

명칭

.....

 

번호

값

.....

1

ALPHA

.....

 

1

128

.....

2

BETA

.....

 

2

.....

.....

3

GAMMA

.....

 

3

.....

.....

...

.....

.....

 

...

.....

.....

(c)                                                     (b)

그림 3   렉시칼 분석과 테이블 관리

구문 분석 단계에서는 입력 프로그램이 구문에 합당한가를 검사하며, 합당한 경우 그 분석 구조를 보여주는 파싱 트리를 만들어주고 합당하지 못할 경우에는 원인을 찾아 오류메시지 (error message) 를 출력해주게 된다.

그림의 4 는 간단한 수식 'A * B + C' 에 대한 파싱 트리를 보여주고 있다. 기호 A, B, C 는 모두 렉시칼 분석 단계에서 TID 로, 연산자, '*' 와 '+' 는 TMUL 과 TADD 로 분석된 상태이다.

그림 4   파싱 트리의 예

의미 분석 단계에서는, 구문 분석 결과 얻어진 파싱 트리가 갖는 의미를 분석하여 준다. 이를 위하여 의미 분석 단계에서는, 파싱 트리의 각 노드를 찾아다니면서 분기된 형태 및 내용에 따라 그에 상응하는 약속된 의미를 부여하게 된다. 이 때 의미의 일관성을 확인하여 잘못된 경우 오류를 발생시키기도 하므로, 의미 분석 단계에서는 문맥 의존적인 측면도 일부 반영하고 있다고 볼 수 있으며, 기계어 단계에서는 문맥 의존적인 측면도 일부 반영하고 있다고 볼수 있다. 기계어 목적 코드로의 번역을 목적으로 하는 컴파일러의 경우, 의미 분석의 결과로 중간 코드를 생성하게 된다. 예를들어, 수식 'C * D' 에 대해서는 다음과 같은 중간 코드가 생성될 수 있을 것이다.

중간 코드는 컴파일러 내부에서만 임시로 이용하는 기계어보다 다소 간단한 형태로 정의된 언어로서, 코드의 생성을 쉽게하는 한편 기계가 바뀌어 목적 코드의 기계어 체계가 바뀌는 경우에도 컴파일러의 이식 (porting) 을 쉽게 하기 위하여 사용된다.

마지막 단계인 코드 생성 단계에서는, 이들 중간 코드를 해당 컴퓨터의 기계어로 생성하여 컴퓨터가 처리할수 있는 목적 코드를 생성한다. 요즘은 컴파일러 구성 기술이 크게 발달함에 따라, 목적 코드의 생성을 시간 또는 코드 길이의 측면에서 최적화하는 많은 기법들이 개발되어 이 단계에서 사용되기도 한다.

컴파일러의 구조를 좀 더 상세히 논하는 경우, 중간 코드 생성 단계를 의미 분석 단계에서 분리해내고, 중간 코드 생성 단계와 목적 코드 생성 단계 사이에 코드 최적화 단계를 설정하여 여섯 단계로 나누기도 한다.

3. 언어 처리 기술

자연언어와 프로그래밍 언어의 처리 과정을 살표보면 유사한 성격이 많으며, 특히 자연언어를 위한 기계번역 시스템과 프로그래밍 언어를 위한 컴파일러의 번역 과정은 매우 흡사하다고 볼수 있다.

번역 과정의 첫 단계는 컴파일러에서는 렉시칼 분석 단계이며, 기계번역에서는 어휘 분석 단계인데, 렉시칼 분석 단계에서 원시 문장에 나타나는 변수나 연산자들을 식별하는 과정은, 어휘 분석 단계에 형태소들을 분리해내고 각 형태소의 품사를 결정하는 과정과 크게 다르지 않음을 알수 있다.

또한 컴파일러는 프로그래밍 언어의 구문 (syntax) 을 이용하여 파싱을 하며, 기계번역에서는 자연언어의 문법 규칙 (grammar rule) 을 이용하여 파싱을 하는데, 그림 5 의 자연언어 문장에 대한 파싱 결과를 보면 그림 4 에서 이미 소개하였던 프로그래밍 언어에 대한 파싱 결과의 성격이 흡사함을 쉽게 알수 있다.

그림 5   자연언어의 파싱 트리

앞에서 설명한 바와 같이, 자연언어는 프로그래밍 언어와 달리 문맥의존 언어로 간주되나, 처리의 어려움을 고려하여 일반적으로 문맥자유 문법을 기본 틀로 삼아 처리하면서, 여기에 문맥 의존적 요소의 처리를 부가하는 접근 방법을 취하기 때문에, 이같은 처리의 유사성은 더욱 커지게 된다.

다만, 프로그래밍 언어의 경우 기계에서의 편리한 사용을 위하여 인위적으로 설계된 것이기 때문에, 동일한 입력에 대하여 두 가지 이상의 유효한 분석 결과를 내게 되는, 이른 바 모호성 현상이 애초부터 배제되어 있는 반면에, 자연언어의 경우 그렇지 못하다는 점이 큰 차이가 된다.

그림 6   파싱 트리와 모호성

그림 6 의 두 파싱 트리는 모두 동일한 한국어 구절 '착한 영희의 친구' 에 대한 분석 결과인데 (a) 는 영희가 착함을, (b) 는 영희의 친구가 착함을 나타낸다. 이는 자연언어 고유의 특성으로서, 사람조차도 어느쪽이 옳은지는 문맥이나 발화 상황에 의해서만 정확히 알수 있을 뿐이다.

따라서, 자연언어 처리 시스템은 컴파일러와는 달리 구문 분석에 있어서 모호성을 발견하고 처리하는데 역점을 두게 되어, 좀 더 복잡한 접근 방법을 사용하게 된다. 그러나 이러한 차이에도 불구하고 자연언어 처리 시스템과 컴파일러의 구문 분석 단계는 매우 유사성을 가지는 것이 사실이다.

파싱을 위한 다양한 기술과 방법이 개발되어 있으며, 이들에 대해서는 뒤에서 설명하기로 한다. 파싱의 근본 이론에는 큰 차이가 없지만, 언어마다 성격의 차이가 있고, 이를 반영하면서 파서의 효율을 극대화하려는 노력 속에서 다양한 파싱 기법이 개발된다고 볼수 있다.

컴파일러의 의미 분석 단계 및 코드 생성 단계는 자연언어 기계 분석에서의 의미 분석 단계 및 목적 언어 생성 단계에 대응되게 되는데, 프로그래밍 언어와 자연언어의 특징을 감안하여 여러 가지 기술을 사용하고 있지만, 근본 이론은 크게 다르지 않다고 볼수 있다. 다만 번역의 목표가 또 다른 자연언어와 기계번역 시스템의 경우가, 기계어를 번역 목표로 하는 컴파일러보다는 고려할 사항이 훨씬 많아 복잡해지는 것은 사실이다.

또한, 기계 번역에서는 사전의 역할이 큰데 비해, 컴파일러에서는 각종 테이블의 역할이 중요함을 말해 둔다.

결론적으로, 프로그래밍 언어는 컴퓨터를 이용하여 자료를 처리하는 단순한 목적을 위하여 인위적으로 개발된 것으로서, 자연언어와 같은 광범위한 표현이나 구조를 허용하고 있지 못한 반면, 모호성 등의 문제를 발생시키지 않아 처리에 큰 어려움이 없는 간단한 언어이다. 그러나, 형식언어 이론의 입장에서 볼 때 프로그래밍 언어는 자연언어의 한 범주에 속한다고 볼수 있으며, 두 종류 언어 사이에는 많은 공통점이 존재하게 된다. 따라서, 프로그래밍 언어의 처리를 위한 컴파일러 개발의 경험은, 자연언어처리 기법의 개발에도 많은 도움을 주어온 것이 사실임을 명심해야 할 것이다.

또한, 자연언어를 처리하는데 프로그래밍 언어가 중요한 도구가 되고 있음도 간과할수 없는 사실이다.