Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
(연관된 글이 1개 있습니다.)

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를 해주는 것과 그렇지 않은 경우의 성능 차이는 분명히 발생한다는 것!

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




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

[연관 글]






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

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

비밀번호

댓글 작성자
 



2011-10-07 04시24분
[Pinekiss] 좋은 글 감사합니다. 저도 궁금했던 부분이었는데 확인못해보고 항상 지나쳤었던 게으름을 반성해봅니다. ㅜㅜ
[guest]
2018-04-03 09시28분
[heygongc] 궁금해서 검색하던 중 우연히 보게 되었습니다.
감사합니다!
[guest]
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);
    }
}
[guest]
2019-11-06 01시29분
말씀하신 것처럼, 위와 같은 경우에는 Dispose를 호출하지 않은 결과와 같습니다. (설령 호출했다고 해도 같습니다.) 참고로, 비관리 메모리를 직접 사용하지 않은 경우라면 소멸자를 정의할 필요가 없습니다.
정성태

... 181  182  183  184  185  186  187  188  189  190  191  192  193  194  [195]  ...
NoWriterDateCnt.TitleFile(s)
68정성태10/31/200421969.NET Framework: 17. Win32_NTLogEvent를 c#에서 wmi 쿼리할 때..에러..
67정성태10/22/200419170COM 개체 관련: 12. Microsoft.XMLHTTP 개체에서 Microsoft.XMLDOM 개체를 전송할 때 charset 지정 문제?
66정성태10/16/200420362.NET Framework: 16. [닷넷 리모팅] 프록시가 죽은 것을 원격 개체가 알 수 있는 방법은?
65정성태10/16/200419347VS.NET IDE: 8. Windows 가상 메모리 사용 해제
64정성태10/3/200423029.NET Framework: 15. ASP.NET에서 .NET COM+ 개체 등록 시 "Local System"이어야 하는 이유.
63정성태10/3/200423140.NET Framework: 14. Response.Cookies.Clear는 기존 설정된 Cookie 헤더를 삭제하는 것이 아닙니다.
62정성태10/3/200422129기타: 7. DB 트랜잭션에서 Lock이 걸릴 수 있는 전형적인 예.
61정성태10/3/200421684.NET Framework: 13. Main 메서드에 붙은 STAThread 의미
60정성태10/3/200420356.NET Framework: 12. IDispatch::GetIDsOfNames 역변환 메서드 작성 힌트 ( DISPID 로 메서드 이름 알아내는 것 )
58정성태10/3/200423461.NET Framework: 11. HttpContext의 간략이해
56정성태10/3/200419946.NET Framework: 10. [.NET 리모팅] 원격개체를 호출한 클라이언트의 연결이 유효한지 판단하는 방법.
55정성태10/3/200420695COM 개체 관련: 11. 내가 생각해 보는 COM의 현재 위치
54정성태8/30/200426357VC++: 10. 내가 생각해 보는 MFC OCX와 ATL DLL에 선택 기준
53정성태11/20/200525707VC++: 9. CFtpFileFind/FtpFileFind가 일부 Unix FTP 서버에서 목록을 제대로 못 가져오는 문제
184정성태11/23/200519363    답변글 VC++: 9.1. FTP 관련 토픽파일 다운로드1
51정성태6/24/200424285VC++: 8. BSTR 메모리 할당 및 해제(MSDN Library 발췌) [1]
50정성태6/16/200417640기타: 6. 1차 데스크톱 컴퓨터 사양
49정성태6/16/200418138기타: 5. 53만 원대 Second PC 마련. ^^
48정성태3/2/200419995.NET Framework: 9. 윈도우즈 발전사를 모아 둔 사이트. [1]
47정성태11/14/200518501VS.NET IDE: 7. 한글 OS에서 Internet Explorer 6.0 with SP1의 UI 언어 바꾸는 방법
45정성태1/26/200417856기타: 4. MCAD 시험
44정성태1/26/200418625VS.NET IDE: 6. 터미널 서비스 포트 변경 ( 서버 및 클라이언트 )
46정성태1/26/200423710    답변글 VS.NET IDE: 6.1. Windows 2003 터미널 서비스 라이센스 서버 없이 접속
114정성태11/14/200515140    답변글 VS.NET IDE: 6.2. [터미널 서버 라이센스] : 활성화 시 오류
43정성태12/23/200318343기타: 3. XP/2003 개인 방화벽 설정파일 다운로드1
40정성태7/23/200321814COM 개체 관련: 10. IE BHO 개체를 개발할 때, 인터넷 익스플로러가 아닌 탐색기에서 활성화 되는 문제 해결 [1]
... 181  182  183  184  185  186  187  188  189  190  191  192  193  194  [195]  ...