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를 호출하지 않은 결과와 같습니다. (설령 호출했다고 해도 같습니다.) 참고로, 비관리 메모리를 직접 사용하지 않은 경우라면 소멸자를 정의할 필요가 없습니다.
정성태

1  2  3  4  5  6  7  8  9  10  11  [12]  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
13324정성태4/17/20234255.NET Framework: 2108. C# - Octave의 "save -binary ..."로 생성한 바이너리 파일 분석파일 다운로드1
13323정성태4/16/20234150개발 환경 구성: 677. Octave에서 Excel read/write를 위한 io 패키지 설치
13322정성태4/15/20234947VS.NET IDE: 182. Visual Studio - 32비트로만 빌드된 ActiveX와 작업해야 한다면?
13321정성태4/14/20233745개발 환경 구성: 676. WSL/Linux Octave - Python 스크립트 연동
13320정성태4/13/20233746개발 환경 구성: 675. Windows Octave 8.1.0 - Python 스크립트 연동
13319정성태4/12/20234203개발 환경 구성: 674. WSL 2 환경에서 GNU Octave 설치
13318정성태4/11/20234016개발 환경 구성: 673. JetBrains IDE에서 "Squash Commits..." 메뉴가 비활성화된 경우
13317정성태4/11/20234169오류 유형: 855. WSL 2 Ubuntu 20.04 - error: cannot communicate with server: Post http://localhost/v2/snaps/...
13316정성태4/10/20233495오류 유형: 854. docker-compose 시 "json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)" 오류 발생
13315정성태4/10/20233674Windows: 245. Win32 - 시간 만료를 갖는 컨텍스트 메뉴와 윈도우 메시지의 영역별 정의파일 다운로드1
13314정성태4/9/20233749개발 환경 구성: 672. DosBox를 이용한 Turbo C, Windows 3.1 설치
13313정성태4/9/20233844개발 환경 구성: 671. Hyper-V VM에 Turbo C 2.0 설치 [2]
13312정성태4/8/20233827Windows: 244. Win32 - 시간 만료를 갖는 MessageBox 대화창 구현 (개선된 버전)파일 다운로드1
13311정성태4/7/20234325C/C++: 163. Visual Studio 2022 - DirectShow 예제 컴파일(WAV Dest)
13310정성태4/6/20233889C/C++: 162. Visual Studio - /NODEFAULTLIB 옵션 설정 후 수동으로 추가해야 할 library
13309정성태4/5/20234057.NET Framework: 2107. .NET 6+ FileStream의 구조 변화
13308정성태4/4/20233948스크립트: 47. 파이썬의 time.time() 실숫값을 GoLang / C#에서 사용하는 방법
13307정성태4/4/20233733.NET Framework: 2106. C# - .NET Core/5+ 환경의 Windows Forms 응용 프로그램에서 HINSTANCE 구하는 방법
13306정성태4/3/20233571Windows: 243. Win32 - 윈도우(cbWndExtra) 및 윈도우 클래스(cbClsExtra) 저장소 사용 방법
13305정성태4/1/20233917Windows: 242. Win32 - 시간 만료를 갖는 MessageBox 대화창 구현 (쉬운 버전)파일 다운로드1
13304정성태3/31/20234280VS.NET IDE: 181. Visual Studio - C/C++ 프로젝트에 application manifest 적용하는 방법
13303정성태3/30/20233592Windows: 241. 환경 변수 %PATH%에 DLL을 찾는 규칙
13302정성태3/30/20234222Windows: 240. RDP 환경에서 바뀌는 %TEMP% 디렉터리 경로
13301정성태3/29/20234335Windows: 239. C/C++ - Windows 10 Version 1607부터 지원하는 /DEPENDENTLOADFLAG 옵션파일 다운로드1
13300정성태3/28/20233961Windows: 238. Win32 - Modal UI 창에 올바른 Owner(HWND)를 설정해야 하는 이유
13299정성태3/27/20233737Windows: 237. Win32 - 모든 메시지 루프를 탈출하는 WM_QUIT 메시지
1  2  3  4  5  6  7  8  9  10  11  [12]  13  14  15  ...