. 취소 레코드

제일 먼저 결정해야 할 사항은 사용자의 편집 동작을 어떻게 기억할 것인가 하는 자료구조의 문제이다. 취소 요구가 있을 때 가장 최후의 편집 동작부터 역으로 취소해야 하므로 사용자의 편집 동작은 LIFO(Last In First Out) 방식의 스택에 저장해야 한다. 스택을 구현하는 방법은 배열, 연결 리스트 등이 있는데 여기서는 메모리 요구량이 작은 배열을 사용하기로 한다. 편집 동작 하나는 여러 가지 요소로 구성되므로 구조체로 표현할 수 있다. ApiEdit.h 헤더 파일에 다음 구조체를 선언한다.

 

struct UndoRecord

{

     BYTE action;

     BYTE status;

     int pos;

     union {

          TCHAR *data;

          int *dest;

     };

};

enum { UR_NONE, UR_INSERT, UR_DELETE, UR_MOVE };

enum { UR_MAKING, UR_CANCELED };

 

이 구조체의 배열로 취소 레코드 스택을 구성할 것이다. 하나의 배열요소는 한 번의 편집 동작을 기억하는데 각각의 레코드는 편집 동작을 역재현할 수 있는 모든 정보를 완전하게 포함해야 한다. 이 레코드들의 집합은 문서의 전체 편집 과정을 나타내게 된다. 각 멤버의 의미에 대해 알아 보자.

action

편집 동작의 종류를 지정하는 정수값이며 0~4 중 하나의 값을 가지는데 이 값들은 바로 아래쪽에 열거형으로 선언되어 있다. 최초 UR_NONE의 값을 가지며 이 값은 아직 아무 동작도 일어나지 않은 빈 레코드라는 뜻이다. 문서에 문자열을 삽입할 때는 UR_INSERT, 삭제할 때는 UR_DELETE의 값을 가지며 문자열을 이동할 때는 UR_MOVE의 값을 가진다. 이 동작들 외에 문자열을 다른 문자열로 바꾸는 대체 동작이 있기는 하지만 삭제 후 삽입과 같은 의미이므로 굳이 별도의 동작으로 정의하지는 않았다.

텍스트 편집기의 편집 동작은 이렇게 세 종류로 크게 구분되는데 편집 데이터의 포맷이 단순하기 때문에 다행히 동작의 종류가 그리 많지는 않다. 만약 워드프로세서라면 동작의 종류가 훨씬 더 많아질 것이다. 텍스트 외에도 틀 조작, 그림 삽입, 개체의 속성 편집, 도표 편집 등 복잡한 편집을 하기 때문에 수십 가지의 동작이 필요해진다.

status

레코드의 현재 상태를 나타내는 값이며 가능한 값은 둘 중 하나이다. 이 값이 UR_MAKING이면 작성중이거나 작성이 완료된 레코드이며 UR_CANCELED이면 취소된 레코드이다. 사용자가 편집할 때는 UR_MAKING의 상태를 가지며 편집을 취소하면 UR_CANCELED가 되고 재실행하면 다시 UR_MAKING이 된다. 이 값은 취소/재실행 코드의 곳곳에서 여러 가지 조건 판단에 사용되는데 어떻게 사용되는지는 잠시 후 보게 될 것이다.

pos

편집된 문서상의 오프셋이다. 어느 위치에서 문자열을 삽입했는지, 삭제된 위치는 어디인지를 기억한다. 가장 쉽게 이해되는 멤버이다.

data

삽입 삭제시에 편집한 문자열을 보관하는 문자열 버퍼이다. 문자열을 삭제할 때 삭제된 문자열을 따로 저장해두어야만 취소시 원래대로 삽입해 넣을 수 있다. 삭제된 문자열은 이미 문서에서 사라졌기 때문에 레코드에 따로 저장해놓지 않으면 어떤 문자열이 삭제되었는지 알 수 없을 것이다. 그래서 삭제할 때 이 멤버에 원본 문자열을 저장해 둔다.

문자열을 삽입할 때도 삽입된 문자열을 이 멤버에 저장해두어야 한다. 삽입한 문자열은 문서상에 이미 기록되어 있으므로 사실 취소를 위해 문자열을 따로 저장해 둘 필요가 없으며 삽입 위치와 길이만 기록해두면 된다. 예를 들어 123 오프셋에 대한민국 문자열을 입력했다면 pos 123을 기억해두고 8바이트 길이임을 저장해두기만 하면 취소시 123 오프셋에서 8바이트를 삭제하면 된다. 삽입 취소를 위해서는 문자열을 굳이 저장해놓지 않아도 될 것 같다.

하지만 취소한 동작을 재실행할 때를 위해서 문자열을 저장해 둘 필요가 있다. 삽입을 취소하면 삭제가 되지만 이 삭제를 재실행하면 다시 삽입이 되며 이때는 문서상에 문자열이 남아 있지 않으므로 원본 문자열을 알 수가 없게 된다. 그래서 삽입할 때도 재실행을 위해 원본 문자열을 이 멤버에 저장해 둘 필요가 있다.

편집된 문자열의 길이는 미리 예측할 수 없다. 한꺼번에 100자를 삭제할 수도 있고 10000자를 클립보드에서 붙여넣을 수도 있다. 그래서 이 멤버는 레코드를 작성할 때 필요한 크기만큼 동적으로 할당되며 만약 이미 할당된 크기보다 더 큰 메모리가 필요하면 재할당한다.

dest

문자열을 이동할 때 이동된 위치와 문자열의 길이를 저장하는 크기 2정수형 배열이다. 문자열을 마우스로 드래그해서 이동할 때는 문서상의 문자열 위치만 바뀔 뿐이며 문자열은 여전히 남아 있으므로 문자열 자체를 저장할 필요는 없다. 재실행할 때도 위치만 정확하게 추적할 수 있으면 원본 문자열을 얻을 수 있다. 이동시 저장할 필요가 있는 정보는 이동 시작위치 오프셋인 pos와 이동된 오프셋 위치, 그리고 이동 문자열의 길이다. pos는 따로 멤버가 있으므로 별 문제가 되지 않고 이동된 오프셋과 길이를 어딘가에 저장해야 하는데 그 위치가 바로 dest 정수형 배열이다.

dest[0]가 이동된 오프셋 위치, dest[1]이 이동된 문자열의 길이이다. 이 멤버는 삽입, 삭제시에는 사용되지 않으며 문자열 이동시에만 사용되기 때문에 data와 공용체로 선언되어 있다. 이 두 정보는 동시에 사용되지 않기 때문에 action 값에 따라 같은 기억 장소를 다른 용도로 사용해도 무방하다. 단 이 멤버에 메모리를 할당할 때 최소한 정수 2개를 담을 수 있는 크기인 8바이트 이상을 할당해야 한다.

공용체에 익숙하지 않은 사람은 이 구조가 무척 복잡해보일지도 모르겠다. 공용체를 쓰지 않는다면 이동 기록을 위해 dest, len이라는 멤버를 추가로 선언해서 사용해야 하는데 이렇게 되면 모든 편집기록 레코드의 크기가 8바이트 더 늘어난다. 편집 레코드는 수천~수만 개까지 늘어날 수 있기 때문에 이 크기는 결코 무시할 수 없다. 코드가 조금 복잡해질지 모르겠지만 메모리 절약을 위해 공용체를 써야 하는 가장 전형적인 예에 해당한다.

 

취소 레코드의 구조는 비교적 직관적으로 이해가 될 것이다. 이동동작이 조금 복잡하기는 하지만 크기를 줄이기 위해서는 어쩔 수 없다. 이 구조체는 가급적이면 크기를 줄이기 위해 data 멤버에 할당된 메모리 양을 별도의 멤버에 저장하지 않으며 _msize 함수로 실시간 조사를 할 것이다. 편집은 사람이 직접 하는 것이므로 그다지 빠를 필요가 없지만 취소 레코드의 크기는 가급적이면 작은 것이 좋다. 여기서는 속도를 희생하고 크기를 최대한 줄이는 방식을 채택했다.

UndoRecord 하나는 구조체 자체의 크기 12바이트(실제 10바이트이지만 컴파일러의 정렬 기능에 의해 12바이트로 확장된다) data 멤버에 기본적으로 할당되는 크기 10바이트를 합쳐 최소 22바이트가 된다. 앞으로 작성하게 될 취소/재실행 코드의 주 내용은 이 레코드를 관리하는 것이다. 그러므로 복잡한 코드를 좀 더 쉽게 이해하려면 이 레코드의 멤버에 대해 잘 숙지하고 있어야 하며 아예 외운 후 실습을 진행하는 것이 좋을 것 같다. 이 레코드에 편집 동작이 어떻게 기록되는지 예를 들어 보면서 레코드 구조를 정리하도록 하자.

다음 예는 시작이 반이다라는 문자열을 입력한 예이다. 삽입, 삭제 동작은 공백과 개행코드에 의해 분리되도록 하였으므로 두 개의 취소 레코드가 생성된다.

첫 번째 레코드의 경우 삽입 동작이므로 action UR_INSERT가 되면 pos 0, data시작이 가 된다. data는 문자열 저장에 필요한 만큼 적당한 크기를 갖도록 할당되어 있으며 status UR_MAKING이다. 편집을 취소하면 위쪽 레코드부터 읽어서 삽입의 반대 동작인 삭제를 하게 될 것이다. 다음은 삽입과 삭제 그리고 또 삽입을 한 예를 보자. 김상형 바보다.를 먼저 입력한 후 뒤쪽의 .을 삭제하고 아니다.를 입력하면 다음과 같은 4개의 취소 레코드가 생성된다.

이 동작을 모두 취소하면 삭제-삽입-삭제-삭제 되어 빈 문서로 돌아가게 된다. 다음은 좀 복잡한 이동의 경우를 보자. 코리아 파이팅을 입력한 후 파이팅을 드래그해서 코리아 앞으로 이동한 경우의 예이다.

2번 레코드는 오프셋 7에 있는 길이 6의 단어를 오프셋 0으로 옮겼음을 기억한다. 이동 위치는 dest[0]에 기록되고 문자열의 길이는 dest[1]에 기억된다. 편집 레코드에 어떤 식으로 편집 동작이 기록되는지까지는 아직 몰라도 된다. 여기서는 레코드의 각 멤버가 어떤 정보를 저장하는지만 파악해두고 코드를 작성하다 보면 레코드 조작법을 알게 될 것이다.