c# 떠블 버퍼로 DataGridView 빠르게 출력하기

DataGridView 성능의 한계

DataGridView는 느리다. 모니터 한 화면 정도 채우는 셀들을 출력하는데도 한꺼번에 뜨질 않고 줄줄이 뜬다. 델파이에 비해 .네트의 일반적인 연산 능력이 떨어지는 건 모르겠지만 비주얼 처리는 주의를 기울이지 않아도 느껴질 정도로 느리다. 윈도우즈 폼즈도 느리지만 wpf는 더 심하다. 웹페이지 정도의 정적인 애플리케이션은 wpf로 예쁘게 만들 수 있지만 주식 차트처럼 빠르게 리프레쉬해야 하는 건 사실상 구현이 가능하지 않다. 이는 벡터 그래픽의 일반적인 특성으로 일렉트론으로 만든 바이낸스 애플리캐이션을 봐도 벡터 그래픽이 이용되는지라 느리고 무겁다.

떠블 버퍼링

DataGridView 출력 속도를 빠르게 하기 위해 검색을 해 보면 SendMessage를 이용하거나 PropertyInfo 클래스를 이용하는 방법들을 쉽게 찾을 수 있다. 이 방법들로 되기는 하지만 복잡하고 본질적인 접근 방법은 아니다. 비주얼 처리가 껌뻑거리거나 느릴 땐 DoubleBuffered 프라퍼티를 먼저 살피는 게 정석이다. 떠블 버퍼는 말 그대로 버퍼를 두 개 쓴다는 말이다. 버퍼는 메모리의 공간이다. 모니터를 통해 보는 비주얼 출력에 필요한 데이터는 메모리에 올려진다. 모니터 한 가득 출력해야 하는 상황에서 연산과 출력을 동시에 하여 조금 계산하고 조금 출력하고를 반복하면 줄줄이 그려져서 보기가 불편하다. 출력할 걸 한꺼번에 계산하면서 일정 공간의 버퍼를 쓰고 계산이 끝나면 이 버퍼를 모니터 출력을 담당하는 버퍼에 통째로 복사한다. 이러면 모니터의 모든 픽셀들이 거의 동시에 리프레쉬된다. 이러한 두 개의 과정들은 메모리 안에서 이루어지므로 무척 빠르다. 떠블 버퍼를 쓰면 느리기 때문에 DataGridView에 DoubleBuffered 프라퍼티가 없다는 주장은 틀리다. 떠블 버퍼의 단점은 속도가 아니라 메모리 공간을 많이 차지한다는 거다. 일반적으로 메모리 공간은 넉넉하고 속도는 빠를수록 좋으므로 떠블 버퍼링은 가급적 하는 게 낫다.

DoubleBuffered는 윈도우즈의 비주얼 컨트롤들에 기본적으로 적용이 가능한 프라퍼티다. 근데 모든 비주얼 컨트롤들이 이 프라퍼티를 가지고 있는 건 아니다. DataGridView도 없다. 레퍼런스를 보면 DataGridView는 Control이라는 객체를 상속한다. 이건 DoubleBuffered 프라퍼티를 갖고 있다. 그런데 이 프라퍼티의 모디파이어접근자는 protected다. 다루기 까다로운 모디파이어지만 이용 방법은 아래와 같이 간단하다.

DataTable DataTable1 = new();

private void Form1_Load(object sender, EventArgs e)
{
    for (int i = 0; i < 10; i++)
    {
        DataTable1.Columns.Add();
    }

    for (int i = 0; i < 100; i++)
    {
        DataTable1.Rows.Add(i, i, i, i, i, i, i, i, i, i);
    }
}

class DerivedDataGridView : DataGridView
{
    public DerivedDataGridView()
    {
        DoubleBuffered = true;
    }
}

private void Form1_Click(object sender, EventArgs e)
{
    DerivedDataGridView derivedDataGridView = new();

    derivedDataGridView.Location = new System.Drawing.Point(0, 0);
    derivedDataGridView.Size = new System.Drawing.Size(2000, 1700);

    Controls.Add(derivedDataGridView);

    derivedDataGridView.DataSource = DataTable1;
}

protected 모디파이어

protected로 선언된 멤버는 이것이 선언된 클래스와 이 클래스를 상속한 클래스의 내부within에서만 접근할 수 있다. 상속은 ‘물려받는다’는 뜻이며 그 주체는 자식이다. 부모가 자식에게 재산을 물려주면 자식이 상속인이고 부모는 피상속인이다. 많은 사람들은 거꾸로 알고 있는데 심지어 뉴스나 신문에도 틀리게 나오는 경우들이 흔하다. 영어로는 부모가 deprive하고 자식이 inherit한다. deprived class는 자식 클래스이고 inherited class는 부모 클래스이며 base class라고도 한다. Control은 부모 클래스이고 DataGridView는 자식 클래스이며 위 예제에서 DerivedDataGridView는 DataGridView의 자식 클래스이자 Control의 손자 클래스이다. 원칙적으로는 DataGridView 안에서 접근할 수 있지만 이건 이미 만들어져 있어서 우리가 건드릴 수 없으므로 복잡하지만 DerivedDataGridView를 하나 더 만들어 이용하면서 그 안에서 DoubleBuffered 프라퍼티를 설정한 거다.

public은 밖에서 자유롭게 접근할 수 있고 private는 접근할 수 없으며 protected는 이들의 중간이다. DataGridView가 Control을 상속했으니 Control.DoubleBuffered처럼 DataGridView.DoubleBuffered = true로 설정할 수 있으면 좋겠지만 이러면 public이랑 같은 셈이다. protected 멤버에 대한 접근은 상속하는 클래스의 선언부를 벗어나면 안 된다. 위 예제에서는 인스턴스가 만들어질 때 바로 true 설정이 되도록 컨스트럭터에 넣은 거다. this.DoubleBuffered에서 this는 생략이 가능하므로 그냥 DoubleBuffered라고 했다. 여기에서 this는 DerivedDataGridView이다.

이렇게 만든 DerivedDataGridView는 윈도우즈 컨트롤이 아니고 사용자가 직접 만든 거라서 디자이너 모드에서는 제어할 수 없다. 위와 같이 모든 걸 코드로 설정해 줘야 한다. DataGridView를 만들어서 설정을 한 뒤 객체의 이름만 DerivedDataGridView로 바꾸고 DataGridView는 없애 버리면 간단하다. 프라퍼티들을 설정만 하는 거로는 나타나지 않고 Controls.Add로 폼에 포함시켜야 비로소 나타나는 거에 유의한다. Controls는 위의 this.DoubleBuffered와 같이 this.Controls에서 this가 생략된 것이며 이때 this는 form을 가리킨다.