개인적으로 저는 잘 사용하지 않지만, DBNavigator 컴포넌트는 데이터베이스 애플리케이션에서 정말 유용한 컴포넌트입니다. TDBNavigator의 역할은 현재 연결되어 있는 데이터셋의 레코드 단위 운영을 위한 컴포넌트로서, 아시다시피 데이터셋의 앞으로/뒤로, 최초/최종 등의 작업을 위한 UI를 간단히 구성해줍니다.
그런데, 델파이/C++빌더의 XE2 이후 버전들부터는, 이 TDBNavigator가 TClientDataSet과 연결해서 사용하면, 데이터 량이 많을 때 엄청나게 느려지는 문제가 있습니다. 체감적으로 말하자면, 레코드 건수가 대략 1000건이 넘게 되면 데이터를 처음 불러올 때나 데이터 앞뒤로 이동할 때 좀 느리다는 것이 느껴지고, 레코드가 많으면 많을 수록 비례해서 더욱 더 느려집니다.
실제로 어느 정도나 느려지는지 궁금하실 거 같은데요. 제 PC 기준으로, 레코드가 4만건 정도 있는 데이터셋의 경우, 레코드를 바로 앞, 뒤로 이동하는 데에 대략 5초가 걸립니다. 이 데이터를 초기에 로드하는 데에는 실제로 데이터를 불러오는 데 소요되는 시간 외에 추가로 15초 정도가 더 걸립니다. 이건 당연히 사용자가 절대로 허용할 수 있는 수준이 아니지요.
이런 성능 문제가 발생한 것은 TDBNavigator에 추가된 새로운 기능 때문입니다. XE 버전까지는 없던 것이 XE2 버전에서 TDBNavigator에 ApplyUpdate 지원 기능이 추가되면서 기존의 버튼들 외에 ApplyUpdates 및 CancelUpdate 버튼 두개가 더 추가되었습니다. 이 버튼들은 디폴트 상태에서는 숨겨져 있고 VisibleButtons 속성에서 세팅해줘야만 보이기 때문에 그냥 폼에 놨을 때는 기존과 동일하게 보이죠. 그래서 기능이 추가된 것을 바로 알아채기 어렵습니다. (이 새 버튼 두 개를 보이게 하면 오른쪽 그림처럼 오른쪽 끝에 나타납니다)
문제는, 이 두 개의 추가 버튼들이 보이지 않는 디폴트 상태에서도, 단지 보이지만 않을 뿐 항상 ApplyUpdate 동작을 체크한다는 것입니다. 레코드에서 커서가 이동할 때마다 ApplyUpdate 관련 체크를 하는데, 버튼이 보이지도 않으니 당연히 전혀 불필요한 동작이죠. 이 동작 때문에 TDBNavigator가 연결된 ClientDataSet은 극단적으로 느려지게 됩니다.
가장 간단하게 이 문제를 피해가려면, TClientDataSet의 ReadOnly 속성을 true로 설정하면 됩니다. 읽기 전용이니 수정이 일어날 수 없고, 수정될 내용이 없으니 변경 사항을 반영할 일도 없으니까요. 실제로 TDBNavigator의 소스 코드에서도 TClientDataSet이 ReadOnly인 경우 ApplyUpdate 체크를 하지 않게 되어 있습니다. 그런데 애플리케이션의 동작상 이렇게 해서는 안되는 경우가 종종 있습니다. ReadOnly 속성을 true로 설정하면 당연히 데이터가 전혀 수정되지 않으니까요.
근본적으로 따지자면, 도대체 ApplyUpdate 관련 체크를 하면 왜 그렇게 느려지는 것일까가 문제입니다.
TClientDataSet은 IDataSetCommandSupport 인터페이스를 상속받습니다. 이 인터페이스의 메소드 GetCommandStates 함수가 ApplyUpdate 가능 여부 확인을 하는 역할을 하는데요. TClientDataSet에서 이 함수를 구현한 코드를 보면, ChangeCount라는 속성을 호출해서 변경된 레코드의 갯수를 알아내고 그 값이 0보다 크면 ApplyUpdate가 가능한 상태라고 리턴합니다. 이 ChangeCount는 Midas.dll을 호출해서 알아오는데, Midas.dll의 소스를 확인해보면 for 루프를 돌면서 변경된 레코드 건수를 세고 있습니다. 허덕!
결론적으로 말하자면, TClientDataSet에 TDBNavigator가 연결되어 있는 경우, 데이터를 불러올 때와 레코드 커서를 이동할 때마다 매번 레코드 전체 루프를 도는 것입니다. 당연히 레코드가 많을 수록 급격히 느려질 수밖에 없는 거죠.
다시 말해서, 심지어 TDBNavigator와 연결하지 않은 경우라도 TClientDataSet는 기본적으로 ApplyUpdate 관련 체크 때문에 동작이 느려집니다. 다만 TDBNavigator가 연결되었을 때는 그리드에서 아래위 스크롤을 하는 등의 간단한 작업을 할 때마다 매번 발생하기 때문에 성능 저하를 사용자가 극단적으로 체감하게 되는 것이구요. 따라서 작성중인 애플리케이션에서 ApplyUpdate 동작은 필요하지 않다면 역시 위의 방법을 이용해서 ApplyUpdate를 꺼버리는 것이 속도 개선에 큰 도움이 됩니다.
자, 그럼 TClientDataSet에서 이 ApplyUpdate 동작을 어떻게 하면 끌 수 있는지가 관건입니다. 이것은 사실 앞서 설명드린 내용에 힌트가 있습니다.
ApplyUpdate 가능 여부 확인을 계속 반복함으로써 속도를 떨어뜨리는 원흉은 TClientDataSet의 GetCommandStates 함수입니다. 다행히 이 함수는 virtual로 선언되어 있어서 오버라이드가 가능합니다. 따라서 이 함수를 오버라이드해서 아무 동작도 하지 않게 불구(?)를 만들어버리면 되죠. 먼저, 해당 TClientDataSet을 사용한 폼의 interface 섹션, 폼 클래스 선언보다 앞에, 다음과 같이 새로운 TClientDataSet 선언을 추가합니다.
1 2 3 4 |
TClientDataSet = class (Datasnap.DBClient.TClientDataSet) protected function GetCommandStates(const ACommand: string): TDataSetCommandStates; override; end; |
그리고 이 클래스의 함수 GetCommandStates의 구현부는 아래와 같이 추가하면 됩니다. (보시다시피 빈 값을 리턴하는 것 외에 아무런 동작을 하지 않습니다)
1 2 3 4 |
function TClientDataSet.GetCommandStates(const ACommand: string): TDataSetCommandStates; begin result := []; end; |
이것이면 충분합니다. 이제 TClientDataSet에 TDBNavigator가 연결되어 있어도, 오픈할 때나 레코드 커서를 이동할 때 전혀 느려지지 않을 것입니다.
만약 폼이 아주 많고 그래서 여러 곳에 이런 코드를 추가해야 한다면, 따로 유닛을 만들어서 빼놓고 uses 하면 편리하겠지요. ^^