Microsoft MVP성태의 닷넷 이야기
글쓴 사람
홈페이지
첨부 파일

DataTable 에 대해서 Dispose 메서드를 호출할 필요가 있을까?


재미있는 질문 글이 하나 눈에 띄더군요. ^^

DataTable DataSet의 경우 Dispose 해주지 않으면 메모리 Leak이 나는가요? 
; http://www.devpia.com/MAEUL/Contents/Detail.aspx?BoardID=17&MAEULNO=8&no=140314&ref=140314

이런 류의 질문은 직접 테스트 해보는 경우 닷넷 프레임워크에 대한 내부적인 이해도를 증가시키기 때문에, 성질상 그냥 지나칠 수가 없더군요. ^^

일단, 제 처음 예상은 위의 댓글(techshare)에서도 달려있지만 다음과 같았습니다.

문제는, 다름 아닌 DataTable 이 상속받은 MarshalByValueComponent 가 Finalizer 를 구현했다는 것입니다.

즉, Dispose를 명시적으로 호출하지 않으면 Finalizer 가 구현되었다는 것으로 인해 해당 개체는 첫번째 GC 사이클에서 살아남아 1세대 힙으로 넘어가게 됩니다. 이후 1세대 GC 가 구동이 될 때에야 비로써 힙에서 제거될 수 있는 것입니다.

반면, DataTable의 Dispose를 명시적으로 해줄 경우, Dispose 메서드 내에 포함된 GC.SuppressFinalize 호출로 인해 Finalizer 큐의 관리에서 제외되므로 일반적인 개체처럼 첫번째 GC 사이클에서 힙이 해제될 수 있습니다.


즉, 가능하다면 명시적으로 DataTable.Dispose를 해줄 것을 권장한다는 내용의 댓글을 달은 것입니다.

자, 그럼 정말 그런지 한번 테스트 해볼까요? ^^




우선, 다음과 같이 DataTable의 내용을 채워주는 코드를 만들고,

DataTable NewDataTable()
{
    DataTable dt = new DataTable("test");
    DataColumn dc1 = dt.Columns.Add();
    DataColumn dc2 = dt.Columns.Add();
    DataColumn dc3 = dt.Columns.Add();
    for (int i = 0; i < 10000; i++)
    {
        DataRow dtRow = dt.Rows.Add();
        dtRow.SetField(dc1, Guid.NewGuid().ToString());
        dtRow.SetField(dc2, Guid.NewGuid().ToString());
        dtRow.SetField(dc3, Guid.NewGuid().ToString());
    }

    return dt;
}

버튼 1, 버튼 2를 각각 두어 다음과 같이 테스트 코드를 만들어 줍니다.

private void button1_Click(object sender, EventArgs e)
{
    for (int i = 0; i < 1000; i++)
    {
        DataTable dt = NewDataTable();
    }
}

private void button2_Click(object sender, EventArgs e)
{
    for (int i = 0; i < 1000; i++)
    {
        DataTable dt = NewDataTable();
        dt.Dispose();
    }
}

테스트를 들어가기 전에, GC 카운트를 확인할 수 있도록 별도의 쓰레드를 만들어 다음과 같이 출력해 주는 코드를 만들어 주면,

Thread thread = new Thread(ThreadFunc);
thread.IsBackground = true;
thread.Start();

void ThreadFunc(object state)
{
    while (true)
    {
        string txt = string.Format("{0,6}, {1,6}, {2,6}, {3,10}", GC.CollectionCount(0),
            GC.CollectionCount(1), GC.CollectionCount(2), GC.GetTotalMemory(false));
        System.Diagnostics.Trace.WriteLine(txt);

        Thread.Sleep(1000 * 2);
    }
}

준비는 이걸로 모두 끝입니다. 이제 응용 프로그램을 실행하고, "버튼 1"을 눌러 "DebugView" 화면에서 GC 호출 횟수를 확인하고, 다시 응용 프로그램을 재시작한 후 "버튼 2"를 눌러 그 결과를 비교하면 다음과 같습니다.

===== DataTable - Dispose 호출하지 않은 경우 =====           ===== DataTable - Dispose 호출한 경우 =====  
[2376]      0,      0,      0,     324132                   [9372]      0,      0,      0,     332324
[2376]     20,     15,      7,   11297372                   [9372]     30,     23,     11,    9738848
[2376]     65,     51,     25,   16362628                   [9372]     78,     61,     30,    7734852
[2376]    112,     88,     44,   12551840                   [9372]    125,     99,     49,   12852724
[2376]    160,    127,     63,   10160820                   [9372]    173,    137,     68,   11636900
[2376]    203,    161,     80,   12678136                   [9372]    220,    175,     87,   16065456
[2376]   1120,    883,    441,   12516788                   [9372]   1127,    888,    444,   10145972
...[중간 생략]...                                            ...[중간 생략]...
[2376]   1171,    921,    460,   14585640                   [9372]   1176,    926,    463,   10166208
[2376]   1219,    959,    479,    7840512                   [9372]   1224,    965,    482,    8799928
[2376]   1264,    995,    497,    9269460                   [9372]   1272,   1003,    501,    9937680
[2376]   1304,   1027,    513,    7965312                   [9372]   1319,   1041,    520,   15611404
[2376]   1349,   1063,    531,   13537072                   [9372]   1368,   1080,    540,   14742832
[2376]   1382,   1090,    545,    8501220                   [9372]   1380,   1090,    545,    8507300

이럴 수가... ^^; Dispose 호출 유무에 상관없이 GC #0, GC #1, GC #2 에 대한 호출 횟수가 거의 유사합니다. 어떻게 이런 결과가 나온 걸까요?




혹시나 싶어서, DataTable이 아닌 그와 유사하게 메모리를 소비하는 클래스를 별도로 만들어서 테스트를 해보았습니다.

public class MemData : IDisposable
{
    byte[] bytes;

    public MemData()
    {
        bytes = new byte[1024];
    }

    ~MemData()
    {
    }

    public void Dispose()
    {
        GC.SuppressFinalize(this);
    }
}

버튼 1, 2에 대해 각각 다음과 같은 코드로 루프를 돌아 실행했고,

private void button1_Click(object sender, EventArgs e)
{
    for (int i = 0; i < 10000000; i++)
    {
        MemData md = new MemData();
    }
}

private void button2_Click(object sender, EventArgs e)
{
    for (int i = 0; i < 10000000; i++)
    {
        MemData md = new MemData();
        md.Dispose();
    }
}

다시 DebugView에 출력된 결과를 비교하면 다음과 같습니다.

===== MemData - Dispose 호출하지 않은 경우 =====           
[4592]      0,      0,      0,     324132                 
[4592]    227,    226,      3,    4609632 
[4592]    725,    724,      8,    7622192 
[4592]   1210,   1209,     14,    4507120 
[4592]   1717,   1716,     21,    4567520 
[4592]   2199,   2198,     30,    5552640 
[4592]   2498,   2497,     33,    5340032 

===== MemData - Dispose 호출한 경우 =====  
[7308]      0,      0,      0,     332324
[7308]   1570,      1,      0,    2282728
[7308]   2498,      1,      0,    1308048

오호~~~ 이번에는 예상했던 결과가 나왔습니다. GC #0 의 횟수는 같지만 GC #1, #2 단계에서 확실히 비교가 되는 차이를 보여주었으며 GC Heap 메모리도 Dispose를 호출한 경우에 안정적으로 유지가 되었습니다. 결국 GC 호출로 인한 오버헤드가 줄어듦으로 인해 실행시간도 빨라져서 Dispose를 호출하지 않은 경우 약 14초의 실행시간을 보인 반면 Dispose를 호출한 경우에는 6초 정도에 테스트가 마무리되었습니다.




임의로 작성한 MemData 에서는 예상되는 결과를 보였지만, DataTable 에서는 전혀 의도치 않은 결과가 나온 것을 어떻게 해석할 수 있을까요?

DataTable 의 테스트 결과에 대해 고민한 끝에, 이것은 분명 내부적으로 DataTable 측에서 이미 GC.SuppressFinalize 호출을 했을 거라는 판단이 들었습니다. 그래서, .NET Reflector를 이용하여 DataTable의 생성자를 확인해 보았는데! ^^ 아니나 다를까, 반갑게 다음과 같은 코드가 포함되어 있었습니다.

public DataTable()
{
    this.tableName = "";
    ...[생략]...
    GC.SuppressFinalize(this);
    ...[생략]...
    this.rowBuilder = new DataRowBuilder(this, -1);
}

자, 그럼 마음 편하게 ^^ 결론을 내려볼까요? DataTable 은 Dispose를 명시적으로 호출해 주지 않아도 성능적인 면에서 아무런 영향도 발생하지 않습니다.

그리고 위의 테스트 결과에 따라, Finalizer를 구현한 클래스의 경우 Dispose를 해주는 것과 그렇지 않은 경우의 성능 차이는 분명히 발생한다는 것!

첨부된 파일은 위의 코드를 포함한 예제 프로젝트입니다.




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]

[연관 글]





[최초 등록일: ]
[최종 수정일: 10/7/2011 ]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer@outlook.com

비밀번호

댓글 쓴 사람
 



2011-10-07 04시24분
[Pinekiss] 좋은 글 감사합니다. 저도 궁금했던 부분이었는데 확인못해보고 항상 지나쳤었던 게으름을 반성해봅니다. ㅜㅜ
[손님]
2018-04-03 09시28분
[heygongc] 궁금해서 검색하던 중 우연히 보게 되었습니다.
감사합니다!
[손님]
2019-11-05 08시38분
[디] 궁금한것이 있는데요!!!
메모리관련하여 보고있는데 "소멸자도 만들지 않고", GC.SuppressFinalize()도 불러주지 않으면 결과가
dispose() 안부른 결과와 같을까요???


public class MemData : IDisposable
{
    byte[] bytes;

    public MemData()
    {
        bytes = new byte[1024];
    }

   // ~MemData()
   // {
   // }

    public void Dispose()
    {
        // GC.SuppressFinalize(this);
    }
}


[손님]
2019-11-06 01시29분
Dispose 안 부른 결과와 같지만 그런 경우에는 Dispose 호출 여부에 상관이 없습니다. Dispose를 호출했다고 해서 bytes 메모리가 해제되는 것은 아니기 때문에.
정성태

1  2  3  4  5  6  7  8  9  10  11  12  13  [14]  15  ...
NoWriterDateCnt.TitleFile(s)
11796정성태12/19/20181210개발 환경 구성: 424. vcpkg - "File does not have expected hash" 오류를 무시하는 방법
11795정성태12/19/20181443Windows: 154. PowerShell - Zone 별로 DNS 레코드 유형 정보 조회 [1]
11794정성태12/17/20181171오류 유형: 508. Get-AzureWebsite : Request to a downlevel service failed.
11793정성태12/16/20181341개발 환경 구성: 423. NuGet 패키지 제작 - Native와 Managed DLL을 분리하는 방법
11792정성태12/11/20181587Graphics: 34. .NET으로 구현하는 OpenGL (11) - Per-Pixel Lighting파일 다운로드1
11791정성태12/11/20181611VS.NET IDE: 130. C/C++ 프로젝트의 시작 프로그램으로 .NET Core EXE를 지정하는 경우 닷넷 디버깅이 안 되는 문제 [1]
11790정성태12/11/20181432오류 유형: 507. Could not save daemon configuration to C:\ProgramData\Docker\config\daemon.json: Access to the path 'C:\ProgramData\Docker\config' is denied.
11789정성태12/10/20182586Windows: 153. C# - USB 장치의 연결 및 해제 알림을 위한 WM_DEVICECHANGE 메시지 처리파일 다운로드1
11788정성태12/4/20181158오류 유형: 506. SqlClient - Value was either too large or too small for an Int32.Couldn't store <2151292191> in ... Column
11787정성태11/29/20182004Graphics: 33. .NET으로 구현하는 OpenGL (9), (10) - OBJ File Format, Loading 3D Models파일 다운로드1
11786정성태11/29/20181211오류 유형: 505. OpenGL.NET 예제 실행 시 "Managed Debugging Assistant 'CallbackOnCollectedDelegate'" 예외 발생
11785정성태12/23/20191867디버깅 기술: 120. windbg 분석 사례 - ODP.NET 사용 시 Finalizer에서 System.AccessViolationException 예외 발생으로 인한 비정상 종료
11784정성태11/18/20181628Graphics: 32. .NET으로 구현하는 OpenGL (7), (8) - Matrices and Uniform Variables, Model, View & Projection Matrices파일 다운로드1
11783정성태11/18/20181550오류 유형: 504. 윈도우 환경에서 docker가 설치된 컴퓨터 간의 ping IP 주소 풀이 오류
11782정성태2/20/20191444Windows: 152. 윈도우 10에서 사라진 "Adapters and Bindings" 네트워크 우선순위 조정 기능 - 두 번째 이야기
11781정성태11/17/20182058개발 환경 구성: 422. SFML.NET 라이브러리 설정 방법파일 다운로드1
11780정성태11/17/20181956오류 유형: 503. vcpkg install bzip2 빌드 에러 - "Error: Building package bzip2:x86-windows failed with: BUILD_FAILED"
11779정성태11/17/20182073개발 환경 구성: 421. vcpkg 업데이트 [1]
11778정성태11/14/20181405.NET Framework: 803. UWP 앱에서 한 컴퓨터(localhost, 127.0.0.1) 내에서의 소켓 연결
11777정성태11/13/20181963오류 유형: 502. Your project does not reference "..." framework. Add a reference to "..." in the "TargetFrameworks" property of your project file and then re-run NuGet restore.
11776정성태11/13/20181175.NET Framework: 802. Windows에 로그인한 계정이 마이크로소프트의 계정인지, 로컬 계정인지 알아내는 방법
11775정성태11/18/20182471Graphics: 31. .NET으로 구현하는 OpenGL (6) - Texturing파일 다운로드1
11774정성태11/13/20181626Graphics: 30. .NET으로 구현하는 OpenGL (4), (5) - Shader파일 다운로드1
11773정성태11/7/20181434Graphics: 29. .NET으로 구현하는 OpenGL (3) - Index Buffer파일 다운로드1
11772정성태11/6/20181866Graphics: 28. .NET으로 구현하는 OpenGL (2) - VAO, VBO파일 다운로드1
11771정성태11/5/20181482사물인터넷: 56. Audio Jack 커넥터의 IR 적외선 송신기 - 두 번째 이야기 [1]
1  2  3  4  5  6  7  8  9  10  11  12  13  [14]  15  ...