StringBuilder는 최종적으로 어떻게 string객체를 만들어낼까 (4)

마수리·2024년 8월 13일
0

StringBuilder

목록 보기
4/5
post-thumbnail

안녕하세요 마수리입니다!

오늘은 StringBuilder 시리즈의 마지막 시간으로 StringBuilder.ToString()은 어떻게 최종적으로 1개의 string객체를 뽑아내는지 살펴볼 예정입니다.

사실 이전 시리즈를 계속 보셨다면 쉽게 유추가 되실 것으로 판단됩니다. 왜냐하면 제가 계속 이야기를 드렸기 때문이죠 😁

시리즈의 마지막 글이기 때문에 간단하게 StringBuilder에 대해서 정리해보고 들어갈까요?

  1. StringBuilder는 버퍼를 갖는다.
  2. 생성된 StringBuilder에 계속 문자열을 추가하다보면 언젠가 버퍼가 가득 차는 상황이 온다.
  3. 그러면 StringBuilder는 새로운 버퍼를 생성한다.
  4. 새로운 버퍼가 생성되면 기존에 버퍼와 Linked List 형태로 이어서 데이터를 저장한다.
  5. 새로운 버퍼가 생기면 통상적으로(?) 기존의 총 버퍼의 크기는 2배가 된다.
  6. 최종적으로 .ToString() 함수가 호출되면 Linked List로 이어진 모든 버퍼를 방문하면서 최종 1개의 string 객체를 만든다.

오늘은 이 과정에서 6번 과정에 대해서 이야기 해볼까합니다.

과연.... 정말로... 이 글에서 이야기 하는대로 StringBuilder는 동작하고 있을까요? 우리는 직접! .ToString() 함수를 들여다 보기로 했습니다..

// 실제 코드는 더 복잡합니다.
public override String ToString() {

    // 총 버퍼의 크기만큼 메모리를 미리 할당
    string ret = string.FastAllocateString(Length);
    // 현재 버퍼부터 방문 시작
    StringBuilder chunk = this;
    
    unsafe {
        fixed (char* destinationPtr = ret)
        {
            do
            {
                if (chunk.m_ChunkLength > 0)
                {
                    // Check that we will not overrun our boundaries. 
                    if ((uint)(chunkLength + chunkOffset) <= ret.Length && (uint)chunkLength <= (uint)sourceArray.Length)
                    {
                        fixed (char* sourcePtr = sourceArray)
                        {
                            // chunkOffset, sourcePtr, chunkLength을 이용해서 뒤에서부터 버퍼의 문자열을 새로운 문자열에 복사한다.
                            string.wstrcpy(destinationPtr + chunkOffset, sourcePtr, chunkLength);
                        }
                    }
                    else
                    {
                        throw new ArgumentOutOfRangeException("chunkLength", Environment.GetResourceString("ArgumentOutOfRange_Index"));
                    }
                }
                // 현재 버퍼부터 역순으로 모든 버퍼를 방문한다.
                chunk = chunk.m_ChunkPrevious;
            } while (chunk != null);
        }
    }
    // 최종적으로 모두 복사된 1개의 String을 반환
    return ret;
}

제가 가장 처음 이 코드를 보면서 이해가 되지 않았던 포인트는 반복문을 돌면서 chunk = chunk.m_ChunkPrevious; 이 구문을 통해 역순으로 버퍼를 방문한다는 것이였습니다. 역순으로 방문하면.. 그걸 다 합쳤을 때 최종적으로 만들어지는 문자는 이상하게 나오지 않을까..?? 예를들어 오늘 날씨는 아주 맑네요.라는 문자를 쓰다 한번 버퍼가 갱신되어 오늘 날씨는 아주 맑네요. 2개의 버퍼가 생겼을 때 역순으로 방문해서 복사를 시작한다면 2번째 버퍼의 내용이 먼저 오므로 최종적으로는 네요. 오늘 날씨는 아주 맑가 되지 않을까요..?? 저만 그렇게 생각하는건가요?? 🤣

그런데.. 그게 아니였습니다.

그게 아닌 이유는 복사 함수에 있었는데요. 제가 잘 모르는 함수라 자세히 보기전까지 어떻게 동작하는지 잘 몰랐었는데요 자세히 보니 문자를 앞에서부터 복사하는 것이 아니였습니다.

// dmem : destination memory를 의미하며 이 위치에서부터 복사된 문자열이 쓰여집니다. (여기가 포인트!! 🌟)
// smem : smem은 source memory를 의미하며 이 포인터가 가리키는 메모리 위치에서 데이터를 읽어와 dmem이 가리키는 위치로 복사합니다.
// charCount : 몇 개의 문자를 복사할지를 지정합니다. 
internal static unsafe void wstrcpy(char *dmem, char *smem, int charCount)

자! 이제 실제 호출 되는 함수를 보도록 하겠습니다.

// destinationPtr의 시작 주소부터 복사하는 것이 아니라 chunkOffset만큼 이동한 위치에서 복사하네?
string.wstrcpy(destinationPtr + chunkOffset, sourcePtr, chunkLength);
  • 이 함수는 destinationPtr에서 chunkOffset만큼 이동한 자리부터 버퍼를 버퍼의 길이만큼 복사합니다.
  • 무조건 앞에서부터 문자를 복사하지 않습니다. 😜

결론적으로 최종적으로 리턴할 문자열의 앞에서부터 복사하는 것이 아니라 현재 방문하고 있는 버퍼가 모든 버퍼를 다 합했을 때 앞에서부터 얼마나 떨어져 있는지 알고 거기서부터 현재 버퍼의 내용을 쓰는 것입니다.

한번 번호를 매겨서 예를 들어 설명해보겠습니다.

  1. [안][녕][하][세][요]라는 문자열이 2개의 버퍼로 저장되어 [안][녕][하], [세][요]로 저장 됐고 가정해 보겠습니다.
  2. .ToString()에선 총 버퍼의 크기인 5의 크기로 최종적으로 모든 버퍼의 문자열이 저장될string 메모리를 미리 잡는다.
  3. .ToString()을 호출하면 버퍼를 역순으로 방문하기 때문에 [세][요], [안][녕][하] 순으로 버퍼를 방문한다.
  4. string.wstrcpy()를 호출하면 다음과 같이 복사된다.
    4-1. 첫 번째 자리부터 chunkOffset 만큼 이동한 자리부터 현재 버퍼의 내용을 복사한다.
    4-2. [][][][세][요]
    4-3. [안][녕][하][세][요].
  5. 최종적으로 [안][녕][하][세][요]를 반환한다.

혹시 이해가 잘 되시나요?

마무리

자..!! 드디어 최종 마무리의 시간이 다가왔습니다. 여러분들이 StringBuilder라는 단어를 들었을 때 지금 저와 같은 자신감이 있는 상태가 되셨길 희망합니다. 혹시 제 글에서 이해가 잘 안가시는 부분이 있으시면 언제든지 댓글 주셔도 됩니다. 😍
그리고 .NET4.0 이전 버전과 이후의 StringBuilder의 내용이 아주 많이 다른걸로 알고 있습니다. .NET4.0이 전 버전에선 버퍼를 생성할 때마다 이전 버퍼에 있던 모든 문자를 새로운 버퍼에 복사하는 과정이 있었다고 합니다. 저희가 살펴보았던 .NET4.0 이후 버전에선 새로운 버퍼가 생겼을 때 어떻게 하나요? 따로 복사하지 않고 이전 버퍼의 내용을 그냥 Linked List로 가지고 있고 마지막에 .ToString()을 호출 했을 때 이 데이터를 모두 방문해서 리턴하는 것으로 바뀌었습니다. 불필요한 복사 연산을 많이 줄인 노력이 보이시나요?
StringBuilder에 대해서 더 궁금하신 점이 있으신 분들은 .NET4.0 이전 버전과 이후 버전을 비교해보시는 것도 재미있을 것 같네요. 그럼 이만 이 시리즈를 마무리 하도록 하겠습니다.

감사합니다. 🤞

profile
.NET 개발자 마수리입니다 🖐

0개의 댓글