. 분석 전략

문법강조 기능은 굉장히 복잡한 기능이고 다량의 복잡한 코드를 작성해야 하며 기존 코드와도 긴밀히 연관된다. 나중에 완성된 코드를 보면 알겠지만 작성된 코드를 이해하는 것도 쉽지 않을 정도로 어려운 기능이다. 이런 복잡한 작업을 할 때는 처음부터 설계를 잘 해야 두 번 작업을 피할 수 있을 뿐만 아니라 차후의 유지나 확장에도 유리하다. 무턱대고 코딩에 들어갈 수 있을 정도로 간단한 기능이 아니므로 충분히 생각한 후 작업에 들어가야 한다.

누가 언제 어떻게 문서를 분석할 것인지, 그리고 분석된 결과는 어떻게 화면에 출력할 것인지 등 문법강조와 관련된 모든 문제점들을 충분히 검토해보도록 하자. 기능의 구현뿐만 아니라 속도, 코드의 양, 다른 기능과의 충돌 여부, 이후의 확장성까지 고려해야 한다. 다음은 ApiEdit가 문법강조 기능을 구현하기 위한 전략들이며 이후 코딩의 지침이 된다.

분석 주체

분석 대상이 되는 문서 내용은 buf에 저장되므로 구문 분석은 ApiEdit 컨트롤이 직접 해야 한다. 그러나 컨트롤이 이 작업을 하기에는 코드량이 너무 많으므로 분석 전담 객체를 따로 두는 것이 좋을 것 같다. CParse라는 이름의 C++ 객체를 정의하고 이 클래스에 구문 분석에 필요한 모든 코드를 작성할 것이다. 컨트롤은 이 객체를 멤버로 가지고 있다가 필요할 때 구문 분석을 하도록 지시하기만 한다. 이후부터 구문 분석을 전문적으로 하는 객체를 분석기(Parser)라고 칭한다.

지원 문법

텍스트 편집기로 작성할 수 있는 문서의 종류는 거의 무한대에 가깝다. C, 파스칼, 어셈블리, 베이직, HTML, XML, 자바, C#, SQL, Perl, Python 등등 알고 있는 것만 해도 수십 가지나 되며 계속 새로운 문서 포맷이 만들어지고 있다. 여기서 포맷이라는 용어는 문서의 물리적인 포맷이 아니라 논리적인 포맷을 의미한다. 각 포맷별로 적용되는 문법이 다른 것은 당연하다. C에서는 switch가 키워드이지만 HTML에서는 일반 문자열이 되고 주석을 표시하는 방법이나 스크립트 블록 표기법도 제각각이다.

편집기는 이런 포맷들에 적용되는 문법에 따라 문서를 해석하는 방법을 달리 해야 한다. C전용 편집기나 HTML 전용 편집기를 만들 생각이 아니라면 문서의 포맷에 따라 문법강조 방식을 바꿀 수 있도록 작성해야 할 것이다. 하지만 문서 포맷은 계속 늘어나고 있으므로 모든 문서 포맷을 다 지원한다는 것은 사실상 불가능하다. 대신 문법 분석 루틴을 편집기의 코드와 분리함으로써 새로운 포맷에 대한 지원을 쉽게 추가할 수 있도록 준비해두도록 한다.

각 포맷에 대해 분석기를 따로따로 만들어 포맷당 분석기 하나를 작성할 것이다. 형태적으로는 CParse 클래스로부터 파생되는 클래스의 객체가 각 포맷에 대한 분석기가 된다. 각 분석기는 CParse가 정의하는 방법대로 구문 분석을 하며 ApiEdit에게 분석결과를 넘겨 주도록 통일된 인터페이스를 유지함으로써 임의의 포맷에 대해서도 분석기를 추가로 덧붙일 수 있게 된다.

분석 시점

구문 분석을 하는 궁극적인 이유는 결국은 화면에 출력하기 위해서이므로 분석을 하는 시점은 그리기 직전인 OnPaint가 가장 적당하다. 그러나 매번 화면에 출력할 때마다 분석을 다시 할 필요는 없으므로 분석이 되어 있지 않은 경우에만 문서를 다시 분석하도록 한다. 이미 분석이 완료된 문장은 더 이상 분석할 필요가 없다. , OnPaint에서 분석을 하기는 하지만 그릴 때마다 분석을 하는 것이 아니라 문서가 변경된 후 처음 그려질 때 한 번만 분석해놓으면 된다.

적용 시점

분석된 결과를 적용하는 시점은 한 줄을 출력하는 DrawLine 함수이다. 분석 정보는 줄단위로 작성되므로 한 줄을 출력할 때 분석된 결과를 보고 각 단어의 스타일값에 맞는 색상을 조사해 출력하면 된다. 구문을 분석하는 시점과 결과를 적용하는 시점이 분리되어 있는 셈이다. 분석은 문서변경 후 한 번만 하지만 적용은 화면출력시마다 하게 된다. 그래서 분석 시간이 좀 걸리더라도 적용 속도가 빠르도록 하는 것이 유리하다.

분석 단위

블록의 개념이 있는 모든 문서 포맷은 단 하나의 문자입력으로도 전체 문서 내용이 완전히 바뀔 수 있다. 예를 들어 C 소스에서 /문자 다음에 *가 입력되면 블록 주석의 시작이라는 뜻이며 이후의 모든 줄은 */를 만나기 전에는 주석으로 처리되어야 한다. 이 상태에서는 중간에 키워드나 상수가 있어도 모두 주석으로 처리된다. 삭제할 때도 마찬가지로 /* 단어 중 한 문자가 삭제되면 이후 모든 줄의 주석이 풀리게 되므로 모든 줄의 구문 분석을 다시 해야 한다. 즉 한 문자의 삽입, 삭제에 의해 이후의 모든 줄들이 영향을 받을 수 있다.

그렇다고 해서 구문 분석을 위해 문서 전체를 스캐닝하면서 통째로 분석할 수는 없다. 이렇게 하면 매번 문서의 처음부터 분석을 해 와야 하므로 번거로울 뿐만 아니라 속도가 느려짐은 물론이다. 또한 분석 단위가 문서가 되면 함수끼리 작업을 분담할 수 없게 되므로 설계상으로도 불리하다. 그래서 구문 분석은 항상 줄단위로 수행한다. 즉 줄 처음에서 모든 분석 정보를 초기화하고 다시 분석을 하는 것이다. , 블록 주석과 같이 여러 줄에 영향을 미치는 경우를 위해 이전 줄의 분석결과(Context)를 참조해야 한다.

분석 범위

문서가 바뀔 때마다 문서 전체를 다 분석할 필요는 없다. 당장 화면출력에 필요한 만큼만 분석하면 된다. 예를 들어 1000줄 문서의 100~150줄까지 출력한다면 150줄까지만 분석한 후 출력하면 된다. 나머지 뒤 부분은 스크롤, 윈도우 크기 변경 등의 동작에 의해 화면에 보이기 전에는 분석할 필요가 없다. 분석 범위는 현재 화면에 보이는 첫 줄에서부터 화면 끝줄까지이다.

문자열의 삽입, 삭제시는 분석 정보가 무효화되는데 이때마다 문서를 다시 분석해야 한다. 한 글자에 의해서 문서의 구조 전체가 바뀔 수도 있기 때문이다. 그러나 편집은 화면 안에서 일어나는 일이고 그 화면의 끝까지만 재분석하면 되므로 빠른 속도로 구문 분석을 처리할 수 있다. 만약 문서변경시마다 전체를 다 재분석한다면 편집속도는 형편없이 느려질 것이다.

 

이상이 ApiEdit의 문법강조 기능 작성을 위한 일종의 작전들이다. 나는 이 전략을 먼저 수립하는데 꽤 많은 시간을 투자했지만 그 대가로 코딩에는 그다지 많은 시간이 걸리지 않았으며 이 원칙 내에서 문제점을 해결하고 기능을 개선하는데 상당한 자유를 누릴 수 있었다. 당장 이 전략들이 다 이해가 되지 않겠지만 코드를 보면 이 전략들이 어떻게 코드에 반영되었는지 알게 될 것이다.