Java의 String은 불변 객체입니다. String a와 String b가 있을 때, a = a+b; 이 문장은 어떻게 동작할까요? 결론부터 말하자면, 비효율적으로 동작합니다. 어디서 오버헤드가 많이 발생하는지 천천히 분석해 보도록 합시다.

 

 


 디버깅을 해 볼 프로그램은 아래와 같습니다.

 

 

 String 객체 str에다가 "05"라는 String을 계속 +하고 있습니다. String a와 b가 있을 때, a+b의 결과값은, String a 뒤에 b를 이어 붙인 String 객체입니다. 예를 들어서, a가 "chogahui"이고 b가 "05"라면, a+b는 "chogahui05"입니다. 9번째 줄에 break point를 걸어두고 어떻게 함수를 호출하는지 간략하게 보도록 하겠습니다.

 

 

 일단, 뜬금없이 String 클래스 안에 있는 valueOf 함수를 호출합니다. str을 넘겼을 건데요. 내부에 있는 toString 함수는, String 객체인 경우, 자기 자신을 리턴하게 되어 있습니다.

 

 

 9번째 줄이 1번 실행되고 난 후, str은 "chogahui05"입니다. 여기서 "05"를 뒤에 concat 할 때 어떤 일이 일어나는지 봅시다. str은 "chogahui05" 객체를 가리킵니다. 문자열의 길이는 10입니다. 따라서 112번째 줄의 부모를 호출하는 생성자에는, 10 + 16의 값인 28이 넘어가게 됩니다.

 

 

 112번째 줄 안으로 들어가 봅시다.

 

 

 그냥 capacity만큼 새로운 char 배열을 할당합니다.

 

 

 이 함수는 현재 String의 길이 + 16만큼 배열을 할당하기 때문에, 공간 복잡도와 시간 복잡도는 현재 String의 길이에 비례합니다. 그 다음에 append 함수를 호출합니다.

 

 


 처음에 count는 0, len은 10인 상태였습니다. 이 때에는 어떻게 동작할까요? 448번째 줄은 건너 뛰고 449번째 줄에 있는 getChars 안으로 들어가 봅시다.

 

 

 getChars는 아래 코드와 같습니다.

 

 

 srcBegin이 0, srcEnd가 10입니다. 안의 value는 String 객체 안에 있는 "chogahui05"를 의미합니다. 이것을 dst에 복사하는데요. StringBuilder의 부모 객체인, value를 의미합니다. 쉽게 말하면, StringBuilder의 value라고 생각하시면 좋습니다.

 

 826번째 줄은 보라색 부분을 초록색 부분에 복사하라는 의미입니다. mem 복사 함수가 빠르다고 해도, 시간 복잡도는 String의 길이에 비례합니다. 벌써부터 비효율적인 소리가 들립니다.

 

 

 새로 생성된, StringBuilder의 value에는 "chogahui05"라는 값이 저장되어 있습니다. 이제 + 연산을 수행해 볼 거에요.

 

 


 StringBuilder의 append 매서드를 호출합니다.

 

 

 어떤 값이 넘어왔을까요? Variable 탭을 보면 알 수 있습니다.

 

 

 "05"라는 값이 넘어 왔어요. 현재 StringBuilder 객체에는 "chogahui05"라는 값이 저장이 되어 있습니다.

 

 

 append에 넘어간 String Object 또한 "05"일 겁니다.

 

 

 여기서, getChars 메서드를 호출해 봅시다.

 

 

 역시 arraycopy 메서드를 수행하네요. 여기서 value랑 dst를 구분하면 쉽게 어떠한 일을 하는지 알 수 있는데요. value는 String 객체의 value를 의미합니다. 그리고 dst는 StringBuilder의 value를 의미합니다. 즉, 보라색 부분을 초록색 부분으로 복사한다는 의미입니다.

 

 

 이 때에는 concat를 할 문자열의 길이에 비례하게 걸리겠네요.

 

 


 이것으로 끝이 날까요? 아닙니다. StringBuilder를 String으로 변환해야 합니다.

 

 

 toString 매서드의 시간 복잡도와 공간 복잡도는 새로운 String의 길이에 비례할 겁니다. 정리하면, String의 + 연산자로 concat 하는 경우에는, 기존 문자열의 길이 + 새로운 문자열의 길이만큼의 공간이 필요합니다. old_s를 + 연산을 수행하기 전에 String이라 하고, new_s를 + 연산을 수행한 후의 String이라고 해 봅시다. 1 단위 연산을 하나의 문자를 다른 공간에 복사하는 연산이라고 해 봅시다. 또, 한 개의 문자를 저장하기 위한 공간을 할당하는 연산이라 해 봅시다. 빈 문자열을 concat 하지 않는다면, |old_s| + |new_s| 단위만큼 걸릴 겁니다.

 

 따라서, 문자열을 여러번 concat 하려는 경우, String + 연산 대신에 StringBuilder나 StringBuffer의 append 메서드를 이용하는 것이 적합합니다.