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)
12129정성태1/26/2020525.NET Framework: 882. C# - 키움 Open API+ 사용 시 Registry 등록 없이 KHOpenAPI.ocx 사용하는 방법
12128정성태1/26/2020195오류 유형: 591. The code execution cannot proceed because mfc100.dll was not found. Reinstalling the program may fix this problem.
12127정성태1/28/2020263.NET Framework: 881. C# DLL에서 제공하는 Win32 export 함수의 내부 동작 방식(VT Fix up Table)파일 다운로드1
12126정성태1/25/2020229.NET Framework: 880. C# - PE 파일로부터 IMAGE_COR20_HEADER 및 VTableFixups 테이블 분석파일 다운로드1
12125정성태1/24/2020136VS.NET IDE: 141. IDE0019 - Use pattern matching
12124정성태1/24/2020407VS.NET IDE: 140. IDE1006 - Naming rule violation: These words must begin with upper case characters: ...
12123정성태1/23/2020210웹: 39. Google Analytics - gtag 함수를 이용해 페이지 URL 수정 및 별도의 이벤트 생성 방법
12122정성태1/22/2020224.NET Framework: 879. C/C++의 UNREFERENCED_PARAMETER 매크로를 C#에서 우회하는 방법(IDE0060 - Remove unused parameter '...')파일 다운로드1
12121정성태1/24/2020168VS.NET IDE: 139. Visual Studio - Error List: "Could not find schema information for the ..."파일 다운로드1
12120정성태1/20/2020261.NET Framework: 878. C# DLL에서 Win32 C/C++처럼 dllexport 함수를 제공하는 방법 - 네 번째 이야기(IL 코드로 직접 구현)파일 다운로드1
12119정성태1/17/2020287디버깅 기술: 160. Windbg 확장 DLL 만들기 (3) - C#으로 만드는 방법
12118정성태1/17/2020275개발 환경 구성: 466. C# DLL에서 Win32 C/C++처럼 dllexport 함수를 제공하는 방법 - 세 번째 이야기
12117정성태1/15/2020246디버깅 기술: 159. C# - 디버깅 중인 프로세스를 강제로 다른 디버거에서 연결하는 방법파일 다운로드1
12116정성태1/15/2020212디버깅 기술: 158. Visual Studio로 디버깅 시 sos.dll 확장 명령어를 (비롯한 windbg의 다양한 기능을) 수행하는 방법
12115정성태1/14/2020166디버깅 기술: 157. C# - PEB.ProcessHeap을 이용해 디버깅 중인지 확인하는 방법파일 다운로드1
12114정성태1/13/2020367디버깅 기술: 156. C# - PDB 파일로부터 심벌(Symbol) 및 타입(Type) 정보 열거 [1]파일 다운로드3
12113정성태1/12/2020334오류 유형: 590. Visual C++ 빌드 오류 - fatal error LNK1104: cannot open file 'atls.lib'
12112정성태1/12/2020144오류 유형: 589. PowerShell - 원격 Invoke-Command 실행 시 "WinRM cannot complete the operation" 오류 발생
12111정성태3/23/2020524디버깅 기술: 155. C# - KernelMemoryIO 드라이버를 이용해 실행 프로그램을 숨기는 방법(DKOM: Direct Kernel Object Modification) [1]
12110정성태1/12/2020262디버깅 기술: 154. Patch Guard로 인해 블루 스크린(BSOD)가 발생하는 사례파일 다운로드1
12109정성태1/10/2020187오류 유형: 588. Driver 프로젝트 빌드 오류 - Inf2Cat error -2: "Inf2Cat, signability test failed."
12108정성태1/10/2020144오류 유형: 587. Kernel Driver 시작 시 127(The specified procedure could not be found.) 오류 메시지 발생
12107정성태1/10/2020261.NET Framework: 877. C# - 프로세스의 모든 핸들을 열람 - 두 번째 이야기
12106정성태1/8/2020295VC++: 136. C++ - OSR Driver Loader와 같은 Legacy 커널 드라이버 설치 프로그램 제작 [1]
12105정성태1/8/2020213디버깅 기술: 153. C# - PEB를 조작해 로드된 DLL을 숨기는 방법
12104정성태1/9/2020423DDK: 9. 커널 메모리를 읽고 쓰는 NT Legacy driver와 C# 클라이언트 프로그램 [2]
1  2  3  [4]  5  6  7  8  9  10  11  12  13  14  15  ...