. 취소 레코드의 상한값

앞에서 정했던 레코드 관리 원칙 중 세 번째 원칙은 메모리의 낭비를 없애기 위해 레코드 크기의 상한값을 두는 것이었다. 레코드 크기의 총합이 일정 용량에 이르면 잘라버리자는 원칙이다. 이 원칙은 아직 구현되지 않았는데 모든 기능을 완성했으므로 이제 구현해보자.

문서를 계속 편집하면 취소 레코드는 편집 동작을 기억하기 위해 계속 쌓여 가므로 메모리를 계속 소비하게 된다. 취소 레코드는 편집 동작에 대한 모든 것을 다 포함하기 때문에 보통 문서 자체의 크기보다 4~5배 정도 더 크다. 만약 1MB의 문서를 편집했다면 4~5MB 정도의 취소 레코드가 생성되며 삽입, 삭제, 이동을 빈번하게 했다면 이 크기는 더욱 커질 것이다.

배보다 배꼽이 더 커진 상황인데 이 레코드들을 모두 유지한다면 문서를 최초 입력하기 전의 상태로 완벽하게 복구할 수 있다. 하지만 이런 기능을 원하는 사용자는 별로 없다. 실행취소 기능은 고작해야 몇 십 번 이전의 편집 동작까지만 실질적으로 유효하며 몇 천 번 전에 입력 또는 삭제했던 편집을 취소할 경우란 무척 드물다. 취소 레코드 보유량이 많으면 많을수록 취소할 수 있는 거리는 늘어나겠지만 그로 인한 메모리 사용 증가와 전체적인 시스템 성능 저하라는 반대 급부가 있으므로 적당한 선에서 취소 레코드의 최적 보유량을 결정해야 한다.

아주 오래 전에 기록해놓은 취소 레코드는 최근의 레코드에 비해 실용성이 떨어지므로 삭제해도 별 상관이 없다. 사용자가 천 번을 편집했다면 그 중 앞쪽 500개의 레코드는 뚝 잘라서 버려도 500 Undo를 해보기 전에는 이 사실을 눈치챌 수도 없다. 그렇다고 해서 10번 편집했는데 5개를 잘라서 버리면 금방 들통날 것이다. 그래서 취소 레코드의 적당한 보유량을 잘 결정해야 한다.

취소 레코드의 보유 상한선을 지정하는 기준에는 두 가지가 있다. 우선 취소 레코드의 개수를 기준으로 할 수 있는데 예를 들어 1000개까지만 허용한다면 1000개가 넘을 때 앞쪽 레코드를 잘라버리는 것이다. 두 번째로 총 용량을 기준으로 할 수 있는데 취소 레코드의 총 용량이 100KB가 되면 절반 정도를 버린다. 어떤 기준을 취소 레코드의 상한선으로 정하는 것이 과연 옳을까?

취소 레코드의 상한선을 정하는 주된 이유는 메모리를 절약하자는 것이므로 총 용량을 기준으로 하는 것이 일단은 합리적이다. 100개의 레코드가 고작 10KB밖에 안되는 경우도 있고 10개의 레코드가 1MB를 넘을 수도 있기 때문에 레코드의 개수는 사용 메모리량을 정확하게 반영하지 못한다. 그러나 개수도 무시할 수는 없다. 이런 경우를 생각해보자. 용량만 100KB 상한선을 두었는데 사용자가 10KB, 30KB, 80KB의 텍스트를 세 번 삭제했다고 하자. 그러면 최초 10KB를 삭제한 레코드는 사라지므로 두 번밖에 취소를 못하게 되는 셈이다.

그래서 ApiEdit는 일단 총 용량을 기준으로 하되 개수에 대해서도 최소한의 보유량을 지정할 수 있도록 하였다. 즉 일정 개수 이상의 레코드가 허용 용량을 초과할 때에 한해서만 취소 레코드의 일부를 삭제하도록 한다. 설사 용량을 넘었더라도 최소한의 개수만큼은 유지한다. 이 처리를 위해 다음 두 변수를 선언하였다.

 

class CApiEdit

{

     ....

     int UndoSize;

     int UndoMin;

 

UndoSize는 취소 레코드의 허용 총 용량이며 UndoMin은 최소한 보유해야 할 취소 레코드의 개수이다. SetDefaultSetting에서 이 변수들을 다음과 같이 초기화한다.

 

void CApiEdit::SetDefaultSetting()

{

     ....

     UndoSize=100*1024;

     UndoMin=100;

}

 

허용 총 용량은 100KB로 초기화했으며 최소 레코드의 개수는 100개이다. 100KB 정도면 웬만큼의 편집 동작을 기억하기에는 비교적 충분한 용량이다. 텍스트 편집기라면 1MB 정도로 늘려도 별 상관은 없고 한 윈도우에 ApiEdit를 여러 개 사용한다면 좀 더 줄여야 할 것이다. 이 용량의 최적값은 ApiEdit를 어떤 용도로 사용하는가에 따라 달라지는 일종의 설정값이다. 그래서 호스트에서 이 값을 변경할 수 있도록 해야 할 필요가 있으며 다음 함수들을 추가한다.

 

class CApiEdit

{

     ....

     int GetUndoSize() { return UndoSize; }

     void SetUndoSize(int aSize);

     int GetUndoMin() { return UndoMin; }

     void SetUndoMin(int aMin);

 

Get 함수는 모두 인라인으로 작성했으며 Set 함수는 다음과 같이 작성하였다.

 

void CApiEdit::SetUndoSize(int aSize)

{

     UndoSize=max(aSize,1024);

}

 

void CApiEdit::SetUndoMin(int aMin)

{

     UndoMin=max(aMin,10);

}

 

호스트가 지정하는 크기로 변경하되 총 용량은 최소한 1KB 정도가 되도록 했으며 레코드 개수는 최소한 10개가 되도록 하였다. 이 설정값이 변경되면 현재 작성되어 있는 취소 레코드의 개수도 조정해야 하겠지만 복잡하기 때문에 그렇게 하지 않았다. Set 함수에서 변수값만 바꿔 놓으면 바로 다음 편집시에 이 값이 적용되므로 무리하게 동기화를 할 필요까지는 없다. 이 상한값에 따라 레코드를 관리하는 작업은 새로 레코드를 추가하는 NextRecord 함수에서 담당한다.

 

void CApiEdit::NextRecord()

{

    int i,j,size, size2,len;

 

     nowur++;

 

     if (nowur==URSize-1) {

          URSize+=64;

          pUR=(UndoRecord *)realloc(pUR,sizeof(UndoRecord)*URSize);

          memset(pUR+URSize-64,0,sizeof(UndoRecord)*64);

     }

 

    for (size=0,i=0;;i++) {

        if (pUR[i].action == UR_NONE)

           break;

        size += sizeof(UndoRecord)+_msize(pUR[i].data);

    }

 

    if (i > UndoMin && size > UndoSize) {

        for (size2=0,i=0;;i++) {

           size2 += sizeof(UndoRecord)+_msize(pUR[i].data);

           if (size2 > UndoSize/2)

               break;

        }

 

        for (j=0;j<=i;j++) {

            if (pUR[j].data)

               free(pUR[j].data);

        }

 

        len=nowur-i;

        memmove(pUR,&pUR[i+1],sizeof(UndoRecord)*len);

        memset(pUR+len,0,sizeof(UndoRecord)*(nowur-len));

        nowur=len-1;

    }

}

 

취소 레코드 전체를 순회하며 레코드의 총 용량과 개수를 구한다. 그리고 개수가 최소 개수보다 더 크고 총 용량이 허용 용량을 초과했으면 앞쪽 레코드를 적당히 정리한다. 용량이 절반이 되는 지점을 찾아 그 이전의 레코드는 모두 삭제하도록 하였다.

취소 레코드의 총 용량을 구하는 루프는 취소 레코드가 아주 많다면 시간이 다소 오래 걸릴 수도 있다. 속도를 향상시키려면 꼭 필요할 때만 상한선 점검을 하도록 별도의 함수로 분리하거나 아니면 NextRecord 함수에 bTestLimit 같은 인수를 두어 불필요한 상한선 점검을 하지 않도록 수정하면 된다. 예를 들어 Redo에서 NextRecord를 호출할 때는 레코드 용량에 변화가 없으므로 굳이 상한선 점검을 하지 않아도 된다. 이 함수는 결국 사용자의 느린 키 조작에 의해 호출되므로 일단은 그런 처리를 생략하였다.