StringBuilder는 어떻게 구현 되어있을까? (2)

마수리·2024년 7월 30일
0

StringBuilder

목록 보기
2/5
post-thumbnail

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

오늘은 StringBuidler에 대한 이야기 2번째 시간입니다.

오늘부터 차차 실제 구현에 대해서 알아볼 생각인데요.
이 시리즈를 천천히 읽어보시면 분명 StringBuilder에 대해서 많은 것을 알게 되실 것이고 면접에서도 string 관련 질문이 나왔을 때 막힘없이 대답하실 수 있을 것이라고 생각합니다.

자 그럼 StringBuilder의 내부 구현에 대해서 알아보도록 하겠습니다.

모든 코드를 하나하나 볼 순 없고 가장 많이 쓰는 것부터 몇 차례 나누어서 설명 드려볼까합니다.

  1. StringBuilder의 생성자들
  2. AppendLine()
  3. Append()
  4. ExpandByABlock() // 버퍼가 찼을 때 행동하는 함수
  5. ToString()

1. 생성자

기본 생성자를 포함한 몇 개의 생성자를 가져와 보겠습니다.

internal const int DefaultCapacity = 16;

// 기본 생성자 16개의 문자를 받을 수 있도록 초기화 한다.
public StringBuilder()
    : this(DefaultCapacity) {
}

// 초기에 수용량을 초기화 할 수 있는 생성자 (이 생성자를 쓰도록 하자 🤞)
public StringBuilder(int capacity)
    : this(String.Empty, capacity) {
}

public StringBuilder(String value)
    : this(value, DefaultCapacity) {
}

// 최종적으로 불리는 생성자
// 주요 부분만 코드를 가져옴
public StringBuilder(String value, int startIndex, int length, int capacity) { 
	if (capacity == 0) {
        capacity = DefaultCapacity;
    }
    
    // 이것이 StringBuilder가 가지고 있는 버퍼
    // 버퍼의 크기를 생성자에서 받은 capacity로 초기화 하고 있다.
    m_ChunkChars = new char[capacity];
    m_ChunkLength = length;
}

기본적으로 가장 많이 사용할 것으로 예상 되는 기본 생성자 StringBuilder()는 16개의 문자를 받을 수 있게 버퍼를 초기화 하고 있는 것을 확인할 수 있다. 추후에 보게 되겠지만 이 버퍼는 더 많은 양의 문자를 받아야 하는 상황이 되면 계속 새로운 버퍼를 만들게 되어있다 이 말은 코드를 보면 m_ChunkCharsnew char[]를 한다는 말인데 결국 heap에 연속된 메모리를 할당하고 최종적으론 GC를 통해 지우는 부담을 늘릴 것이다.

웬만하면 기본 생성자를 사용하지말고 StringBuilder(int) 등을 사용해 메모리 할당을 최소화 하자

2. AppendLine()

다음으로 알아볼 내용은 AppendLine()이다.

public StringBuilder AppendLine() {    
    return Append(Environment.NewLine);
}

public StringBuilder AppendLine(string value) {    
    Append(value);
    return Append(Environment.NewLine);
}

보신 것과 같이 AppendLine()Append()함수를 사용해 구현되었다. Append()를 살펴보자.

3. Append()

위에서 보았던 AppendLine()Append()를 감싸 만든 함수였을 뿐이였다. StringBuilder의 문자열 추가 함수의 핵심 역할은 Append()가 하고 있었다. Append()의 코드를 살펴보자. 복잡해 보이는 최적화 등을 제외하고 변수명을 이해하기 쉽게 한글로 바꾼 후 핵심만 볼 수 있도록 수정 한 코드를 봐보도록 합시다.

public StringBuilder Append(String value) 
{
    Contract.Ensures(Contract.Result<StringBuilder>() != null);
	    
    if (value != null) {           	
        int newCurrentIndex = 현재버퍼에_들어있는_문자열.Length + 새로운_문자열.Length
        
        // 현재버퍼에 새로운문자를 추가해도 버퍼의 길이를 초과하지 않니?
        if (newCurrentIndex < 현재버퍼.Length)
        {
        	// 현재버퍼에 파라미터로 받은 value를 복사한다.            
            unsafe {
                fixed (char* valuePtr = value)
                fixed (char* destPtr = &chunkChars[chunkLength])
                    string.wstrcpy(destPtr, valuePtr, valueLen);
            }
            // 현재버퍼에 들어있는 문자열의 길이를 새로운 문자열이 들어간 길이로 업데이트한다.
            m_ChunkLength = newCurrentIndex;
        }
        else
        {
        	// 여기 들어왔다는 소리는 현재 버퍼에 새로운 문자열을 입력할 공간이 부족한단 소리이다.
            // AppendHelper() 안에서는 아마 버퍼를 늘려주는 행동을 할 것이다.
            AppendHelper(value);
        }
    }
    return this;
}

모든 코드를 하나하나 이해할 필요는 없고 주석에 달린 글이라도 읽고 핵심만 파악해보아도 된다.

핵심은 아주 간단하다. 새로운 문자열이 들어갈 공간이 충분하면 현재 버퍼에 문자열을 복사하고 그렇지 않으면 AppendHelper()를 호출한다.

AppendHelper()를 살펴보면 버퍼가 부족할 때 StringBuilder에서 어떤 동작을 하는지 알 수 있을 것이다.

다음 시간에는 AppendHelper() 함수를 이용해서 StringBuilder가 버퍼를 늘리는 방법을 살펴보도록 하겠다.

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

0개의 댓글