통계 위젯 (화이트)

2548
244
2206298

저작권

모든 내용은 허락없이 상업적으로 사용하실 수 없습니다. - 오광섭 -

클릭몬 (와이드)





.NET의 StringBuilder 클래스.. 너무해.. ▣ 컴터야그 ▣

원래 오늘의 일기는 종무식에 대한 이야기를 쓰려고 했는데 C#의 StringBuilder 덕분에 종무식에는 참석도 못하고 사이트에 나가 디버깅을 하고 있어야 했다.. 더구나 내가 작성한 코드도 아닌, 내가 수행한 프로젝트도 아닌데 나가서 이러고 있자면 과히 즐겁지는 않지만, 그래도 어떻하리.. 어차피 해야할 일이라면 그런 생각 안할 수록 도움이 된다..

.NET에서 string 클래스는 일단 만들어진 후에는 내용을 변경할 수 없는 객체이다.. 따라서 문자열 재할당을 하게 되면 생성과 파괴가 반복되게 된다.. 그래서 string 클래스와 StringBuilder 클래스를 비교하여 설명하는데 보면 빠지지 않고 등장하는 문구가 있다..

성능상의 이유로 대용량의 연결이나 관련된 다른 문자열 조작 작업은 아래의 코드 예제에서처럼 StringBuilder 클래스를 사용하여 수행해야 합니다.

좋다.. 그래서 StringBuilder를 사용을 했는데, 그 코드에서 문제가 생긴 것이다.. 일단 문제는 OutOfMemoryException이 발생하는 것 이었다.. 도대체 이런 예외상황이 왜 발생할까? 도무지 이해할 수가 없었다.. 옛날 도스시절도 아니고, 메모리가 제한적인 임베디드 시스템도 아니고, 1기가 짜리 동영상을 다루는 프로그램도 아닌데 이런 메시지를 접해야 한다니..

문제의 핵심은 base64 인코딩, 디코딩 같은 아주 길게 이어지는 문자열 처리에 StringBuilder 혹은 string 클래스를 어떻게 사용하는 것이 효율적이냐 하는 것이다.. 문제가 생겼던 코드는 바이너리 파일을 헥사문자열로 덤프시켜주는 코드였는데 StringBuilder를 암 생각 없이 사용했더니 문제가 생긴 것이다.. 클래스란 이땐 이렇게 저땐 저렇게 사용해야 하는 것이 아니라 암 생각없이 사용할 수 있도록 만들어줘야 한다고 생각하기 때문에 이건 아니라고 본다..

디버깅을 통해 문제의 코드를 추적해보니 아래와 같은 코드였다.. 그런데 아무리 쳐다봐도 왜 메모리 부족현상이 일어나는지 도저히 이해를 할 수가 없었다.. 도대체가 뭐가 잘못된 코드란 말인가? 이 코드를 작성한 사람이 누군진 모르겠지만, 내가 봐도 별달리 하자가 될만한 부분은 없어보였다.. 이건 잘 돌아가야 정상인건데.. 우선 프로그램을 사용하는데 문제가 없도록 만들어주고, 원천적인 해결은 해야하므로 본사로 들어와 차분히 생각해보기로 하고 철수..

이 코드는 바이너리 파일을 넘겨받아 문자열 헥사덤프로 변환하고, 그 결과를 이전 결과물 문자열 (StringBuilder인 sb)에 추가해서 넘기는 형태다.. 이 함수를 호출하는 바깥 함수에서 StringBuilder의 사이즈를 미리 잡아 초기화를 시켜놓고 호출을 하는데도 실행시간이 오래걸리고 메모리 부족 현상이 발생.. 분명 뭔간 잘 못하고 있는건데, 거 참 이해가 안된다..
    private string StreamToHexString
        (System.IO.Stream stream, System.Text.StringBuilder sb)
    {
        int iData = -1;
        stream.Position = 0;

        while (-1 != (iData = stream.ReadByte()))
            sb.Append(iData.ToString("x2"));

        return sb.ToString();
    }
팀원에게 해결책 점검을 부탁하고 돌아오는 내내 생각을 해봤지만 아무리 짱구를 굴려도 이해가 안간다.. 왜 StringBuilder가 이렇게 멍청하게 동작을 하는 것일까? 속도가 절라 느려지는 것은 이해가 간다고 치자.. 하지만, 왜 메모리 부족 예외상황이 발생하는 것일까? 도대체 안에서 무슨 일이 일어나고 있길래? 정말 이해가 안되는군..

사무실에 돌아와보니 두 팀원이 문제를 해결해 놓고 기다리고 있었다.. 속도, 성능은 예전 코드보다 훨씬 만족스럽게 잘 동작하고 있었는데, 왜 이런 차이가 나는지에 대해서는 의견이 분분해졌다.. 결론은 StringBuilder라도 append 메쏘드가 무지하게 호출되면 그 성능이 엄청나게 저하되면서 제대로 동작을 안한다는 것이었다.. 쩝, 너무 믿었나.. 예상밖임..
    private string StreamToHexString(System.IO.Stream stream)
    {
        StringBuilder sb = new StringBuilder();
 
        sb.Capacity = (int)(stream.Length * 2);
        sb.Append(' ', (int)(stream.Length * 2));
 
        stream.Position = 0;
 
        byte[] baData = new byte[stream.Length];
        stream.Read(baData, 0, (int)stream.Length);
 
        for (int i = 0; i < baData.Length; ++i)
        {
            string sHex = baData[i].ToString("X2");
            sb[i * 2] = sHex[0];
            sb[i * 2 + 1] = sHex[1];
        }

        return sb.ToString();
    }
결국 찾은 방법이 1번의 append 호출로 공간확보를 해두고, 일련의 변환과정에서는 확보된 공간에 변환된 값을 채우는 방법이었다.. 우아해 보이지 않는 해결책이지만, 동작은 활실히 하니 좋다.. 하지만, 그럼 이런 상황에 써먹을 수 있는 방법이 무엇인가 하는 부분이다.. 꼭 이런 방법을 써야한단 말인가? 용량이 적으면 append를 상식적으로 사용하면 되고, 용량이 커지면 위와 같이 이상하게 사용해야 한다.. 좀 이상하잖아.. 정말 이런 방법밖에 없을까?

나 : 아니 그럼 base64 인코딩 같은 루틴도 이딴식으로 해야한단 말인가?
팀원 : 건 Microsoft 애들이 짜뒀으니 걱정할 필요 없어요~
나 : .NET Framework에 이미 구현되어 있으면 걔들은 C#으로 작성했을거 아녀..
팀원 : .NET core 부분은 모두 C++로 작성하지 않았을까요?
나 : 에이, 정말 필요한 몇개를 제외하고는 .NET 라이브러리는 모두 C#으로 작성했을거야.. 자바는 라이브러리 모두 소스 공개되어 있는데 .NET은 소스공개 안되어 있나?
팀원2 : 공개 안되어 있어도 까보면 되죠. 함 까보죠.

내부에서는 무슨 일이? 그래~ 함 살펴보자.. 팀원2는 재빨리 내부를 살펴볼 수 있는 리플렉트 프로그램을 구해 .NET 내부 소스를 열어보았다.. 이런 함수와 가장 유사한 것이 base64 인코딩/디코딩 함수니 그걸 함 찾아봐라.. 찾아보니 unsafe, fixed 같은 단어들이 자주 등장하며, 포인터들이 마구 등장한다.. 음.. 이거 좀 실망인데.. 고작 base64 같은 간단한 함수를 작성하는데 .NET에서 사용하지 않기로 한 포인터 같은 개념이 마구 등장해야 하는건가?

자바도 그럴까? 소스코드를 좀 찾아봤다.. StringBuffer 라는 클래스를 사용하는데, 일반적으로 사용하는 경우와 별다른 코드가 보이지 않는다.. 즉 상황에 따라 다르게 사용해야 하는 것이 아니라 문자열을 사용하는데 있어 string 보다 효율적으로 사용하고 싶은 경우에 사용하면 된다.. 같은 역할을 하는 클래스가 .NET에서는 StringBuilder인데, 이 넘은 그렇지가 못한 것 같다.. base64 같은 코드에서는 string 클래스를 가지고 fixed, 포인터 등의 개념을 가져다 코딩을 하는 것을 보면 말이다.. 이땐 이렇게, 저땐 저렇게라면 클래스들의 책임과 권한이 너무 불명확한 것이 아닌가 하는 생각이 드는 것이다.. 정말로 그렇다면 설계에서 밀린다는 이야기가 된다.. 플랫폼의 한계인지.. C# 언어적인 구조상의 문제인지..

물론 위의 내용은 .NET과 C#에 대해 잘 모르면서 리플랙터 프로그램으로 디스어셈블된 코드만 보고서 섣부른 판단을 했는지도 모른다.. StringBuilder 클래스를 제대로 사용하는 법을 아시는 분, 좋은 의견 줬으면 좋겠다.. 만약 위의 내용이 현실이면 좀 우울하다.. 10년된 자바와 4년된 .NET의 공력차이라 봐야 하는건가?



핑백

  • 미친병아리가 삐약삐약 : .NET Reflector 프로그램.. 2011-07-03 05:47:03 #

    ... 어제 StringBuilder 클래스가 속을 썩이는 상황에서 활용한 프로그램.. 이 프로그램을 찾은 팀원 이야기로는 리플렉터로 검색을 하면 유사한 프로그램들이 꽤 많이 있다고 한다.. 쓸만한 프로그램 여러가지 ... more

덧글

  • 백읾ㅗㅇ 2006/12/31 00:37 # 삭제 답글

    string이 이뮤터블 한 놈이라니 잼있네요.
  • 미친병아리 2006/12/31 00:56 # 답글

    백읾ㅗㅇ님 : 거기까지는 그렇게 하는게 좋겠다는 생각도 듭니다..
  • 쌍부라 2006/12/31 04:28 # 답글

    stringbuilder도 내부 버퍼에서는 string을 쓸 겁니다. (용빼는 재주가 없는 한 ^^;)
    아무래도 기반 클래스 라이브러리니까 뭐 string에 좀 더 접근할 수 있을지는 모르겠는데.. 아무튼 append를 하다보면 버퍼가 꽉 찰 때마다 그 두배 크기의 새 버퍼를 할당하고, 기존 버퍼를 복사하고 Dispose 처리를 하면 GC가 와서 그걸 가져가고 하는 식일텐데, 그보다는 아예 필요한 만큼 충분히 큰 버퍼를 할당해서 쓰시는건 어떨까요? (생성자중에 있습니다만)
  • codian 2006/12/31 13:02 # 삭제 답글

    첫번째 소스 루프전에

    sb.Remove(0, sb.Length);

    를 넣어도 그럴까요?

    StringBuilder라면 문자열을 다루는 거의 모든 프로그램에서 사용되는 클래스일텐데 그런 클래스에 문제가 있을까요?
  • wafe 2007/01/01 11:11 # 삭제 답글

    쌍부라님 말씀대로 두 배 크기의 새 메모리를 잡고 원래 메모리 내용을 복사해 준 후 원래 메모리를 반환하는 식이니... 원래 사용하고 있던 메모리가 크다면 문제가 생길수도 있지 않을까요? 메모리를 자주 새로 할당해야 하는 경우라면 String을 쓰는 것과 차이가 없게 되겠죠. 작업을 하는데 필요한 메모리 크기를 예상할 수 있다면 Capacity를 충분히 크게 잡고 시작하는게 정석이라고 알고 있습니다.

    그리고 Capacity를 지정한 뒤에 굳이 Append를 호출해서 문자열로 채워놓을 필요는 없을 것 같은데요...
  • 하늘이 2007/01/01 12:21 # 삭제 답글

    StringBuilder 같은 경우는 컴파일 되면서, 컴파일러가 저렇게 바꿔버린 것은 아닐까요? ^^; (라는 추측?)
  • 미친병아리 2007/01/02 00:14 # 답글

    쌍부라님 : 바깥에 호출하는 함수에서 StringBuilder 객체를 사이즈 할당해 만들어 놓고 넘기는데도 영 신통치 않네요..

    codian님 : 바깥에서 생성해서 넘기는 넘이라 이 함수 안에서는 그런 호출 하기가.. 하지만, 바깥에서 새로 생성하고 사이즈 정해서 생성하는지라 비슷한 효과가 있었던것 같은데 소용없더군요..

    wafe님 : 음.. 문자열을 할당하는데 append로 항상 사용하면 될 줄 알았는데, 그게 아닌가 보군요..

    하늘이님 : 음.. 글쎄요.. 컴파일러가 뭘 알아서 해주는건 아닌 것 같습니다..
  • codian 2007/01/02 10:14 # 삭제 답글

    밖에서 넘겨진 StringBuilder 객체가 초기화되지 않고 반복적으로 넘겨져서 데이터가 누적되는게 아닌가 싶었는데 그건 아니가 보군요.(그런 실수를 하실 분이 아니시죠 :)
    정말 말씀데로라면 앞으로 StringBuilder를 사용할 수 없을 정도의 큰 문제군요.
  • 김명신 2007/01/02 11:59 # 답글

    새해 복많이 받으십시오. StringBuilder 자체는 내부적으로 string을 container로 사용하고 lazy allocation(필요시 할당하는)방식을 사용하고 있습니다. allocation단위는 capacity size * 호출횟수 정도로 할당하는 것으로 보입니다. 만일 stream에 포함된 data가 큰 경우라면, 상당한 횟수의 allocation이 발생할 것 같은데, GC의 type에 따라서 수행중에 GC가 수행되지 못하면, 새로 allocation발생시 이전에 할당된 메모리 공간은 그대로 메모리 공간을 점유하게 됩니다. 만일 StringBuider가 다루는 공간이 85000 bytes를 넘어서는 경우 GC는 LOH(Large Object Heap)의 공간을 사용하게 되는데 이 공간은 memory compaction이 일어나지 않으므로, 반복적인 allocation호출시 Out of Memory가 발생할 가능성이 있습니다. 뭐 dump를 안봐서 확실하지는 않구요. 참고하십시오.
  • 미친병아리 2007/01/14 20:41 # 답글

    codian님 : 작은 크기로 반복되는 문자열 연산에는 사용하면 안될 것 같습니다..

    김명신님 : 아무리 그래도 넘 심하네요.. 하지만, MS 개발자들도 Base64 같은 코드 작성시 사용을 한하는 것으로 봐서는 이런 상황에서 사용할만한 클래스가 아닌 것 같습니다..
댓글 입력 영역