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

... 46  47  48  49  50  51  52  [53]  54  55  56  57  58  59  60  ...
NoWriterDateCnt.TitleFile(s)
12300정성태8/29/20209772.NET Framework: 935. C# - ETW 관련 Win32 API 사용 예제 코드 (4) CLR ETW Consumer파일 다운로드1
12299정성태8/27/202010701.NET Framework: 934. C# - ETW 관련 Win32 API 사용 예제 코드 (3) ETW Consumer 구현파일 다운로드1
12298정성태8/27/202010408오류 유형: 640. livekd - Could not resolve symbols for ntoskrnl.exe: MmPfnDatabase
12297정성태8/25/20209608개발 환경 구성: 503. SHA256 테스트 인증서 생성 방법
12296정성태8/24/202010071.NET Framework: 933. C# - ETW 관련 Win32 API 사용 예제 코드 (2) NT Kernel Logger파일 다운로드1
12295정성태8/24/20209454오류 유형: 639. Bitvise - Address is already in use; bind() in ListeningSocket::StartListening() failed: Windows error 10013: An attempt was made to access a socket ,,,
12293정성태8/24/202010779Windows: 171. "Administered port exclusions" 설명
12292정성태8/20/202012042.NET Framework: 932. C# - ETW 관련 Win32 API 사용 예제 코드 (1)파일 다운로드2
12291정성태8/15/202010975오류 유형: 638. error 1297: Device driver does not install on any devices, use primitive driver if this is intended.
12290정성태8/11/202011694.NET Framework: 931. C# - IP 주소에 따른 국가별 위치 확인 [8]파일 다운로드1
12289정성태8/6/20209174개발 환경 구성: 502. Portainer에 윈도우 컨테이너를 등록하는 방법
12288정성태8/5/20209146오류 유형: 637. WCF - The protocol 'net.tcp' does not have an implementation of HostedTransportConfiguration type registered.
12287정성태8/5/20209649오류 유형: 636. C# - libdl.so를 DllImport로 연결 시 docker container 내에서 System.DllNotFoundException 예외 발생
12286정성태8/5/202010511개발 환경 구성: 501. .NET Core 용 container 이미지 만들 때 unzip이 필요한 경우
12285정성태8/4/202010866오류 유형: 635. 윈도우 10 업데이트 - 0xc1900209 [2]
12284정성태8/4/202010242디버깅 기술: 169. Hyper-V의 VM에 대한 메모리 덤프를 뜨는 방법
12283정성태8/3/202010738디버깅 기술: 168. windbg - 필터 드라이버 확인하는 확장 명령어(!fltkd) [2]
12282정성태8/2/20209498디버깅 기술: 167. windbg 디버깅 사례: AppDomain 간의 static 변수 사용으로 인한 crash (2)
12281정성태8/2/202011989개발 환경 구성: 500. (PDB 연결이 없는) DLL의 소스 코드 디버깅을 dotPeek 도구로 해결하는 방법
12280정성태8/2/202011155오류 유형: 634. 오라클 (평생) 무료 클라우드 VM 생성 후 SSH 접속 시 키 오류 발생 [2]
12279정성태7/29/202011992개발 환경 구성: 499. 닷넷에서 접근해보는 InterSystems의 Cache 데이터베이스파일 다운로드1
12278정성태7/23/20209359VS.NET IDE: 149. ("Binary was not built with debug information" 상태로) 소스 코드 디버깅이 안되는 경우
12277정성태7/23/202010766개발 환경 구성: 498. DEVPATH 환경 변수의 사용 예 - .NET Reflector의 (PDB 연결이 없는) DLL의 소스 코드 디버깅
12276정성태7/23/20209997.NET Framework: 930. 개발자를 위한 닷넷 어셈블리 바인딩 - DEVPATH 환경 변수
12275정성태7/22/202012561개발 환경 구성: 497. 닷넷에서 접근해보는 InterSystems의 IRIS Data Platform 데이터베이스파일 다운로드1
12274정성태7/21/202011922개발 환경 구성: 496. Azure - Blob Storage Account의 Location 이전 방법 [1]파일 다운로드1
... 46  47  48  49  50  51  52  [53]  54  55  56  57  58  59  60  ...