무지성으로 String +이나 concat을 휘갈겨 본적 없으신가요? (전 있어요.)
StringBuilder를 왜 활용하는걸까요? StringBuffer는 또 무엇인가요?
이런 의문들 오늘 해결해봅시다.
Java를 사용하면 문자열 클래스를 밥 먹듯이 쓰게 됩니다.
그러면 String만 쓰면 되지 왜 StringBuffer와 StringBuilder와 같은 문자열 클래스도 사용하는 걸까요?
그 역할을 설명하기 앞서 이 아이들의 탄생기를 살펴봅시다.
String은 JDK 1.0에 탄생했습니다. StringBuilder는 JDK 1.5에 탄생했죠.
JDK 1.5 이전에는 Immutable 문자열을 위한 String과 Mutable 문자열을 위한 StringBuffer가 있었습니다.
StringBuffer는 동기화를 수행하기 때문에 단일 스레드에서는 불필요한 성능 저하를 초래했고,이러한 단일 스레드에서 가변 문자열을 효과적으로 사용하기 위하여 StringBuilder가 탄생했습니다.
Strings are constant; their values cannot be changed after they are created. String buffers support mutable strings. Because String objects are immutable they can be shared.
ref : https://docs.oracle.com/javase/8/docs/api/java/lang/String.html
String은 상수입니다. 한번 생성하여 할당된 메모리 공간은 변하지 않는다는 것입니다.
String 클래스의 객체는 Heap 메모리 영역(GC의 대상 영역)에 생성됩니다. 그래서 문자열 내용을 변경시키면 새로운 메모리를 할당하여 참조하고 원래의 객체는 GC의 대상이 됩니다.
즉,+나 concat 메서드를 통해서 기존에 생성된 String 객체 문자열에 다른 문자열을 붙인다는 것은 기존 문자열을 변경하는 것이 아니라, 새로운 String 클래스를 만든 후, 새 String 객체에 문자열을 저장하고, 그 객체를 참조하도록 합니다.
String은 내부 동작이 Immutable하게 설계되어 있기에 동기화에 대해 신경쓰지 않아도 됩니다(Thread-safe). 그러므로 내부 데이터를 자유롭게 공유할 수 있습니다.
String str1 = "123";
String str2 = "abc";
String str3 = str1 + str2; // 새로운 인스턴스 할당!!
위 코드는 간단하게 str1과 str2 내용을 + 연산자를 활용해 합쳐서 str3에게 할당하고 있습니다. 이 과정에서 불필요하게 3개의 인스턴스가 생성됩니다.
public String statement() {
String result = "";
for(int i = 0; i < numItems(); i++) {
result += lineForItem(i); // 문자열 연결 -> 성능 저하!!!
}
return result;
}
이러한 동작이 단순하고 작을 때에는 괜찮지만 무수하게 반복하고 거대해진다면 성능 저하가 따를 수 밖에 없습니다.
Effective Java에 따르면, 문자열 연결 연산자로 문자열 n개를 잇는 시간은 n^2에 비례한다고 합니다.
JDK 1.5 이후에는 컴파일 단계에서 String 객체를 사용하더라도 StringBuilder로 컴파일 되도록 변경되었다고👀. 자동 최적화되어 성능상 차이가 없다는데요?
String str = "1" + "2" + "3";
위 코드처럼 + 연산을 한줄로 선언하는 경우 아래처럼 변환되어 성능 최적화가 이루어진다고 합니다.
String str = "123";
어이어이 그러면 StringBuilder를 굳이 안써도 되는거 아니요?
아래 코드를 봅시다.
String str = "1";
str += "2";
str += "3";
위 코드는 아래처럼 수행됩니다.
String str = "";
str = (new StringBuilder()).append(str).append("0").toString();
str = (new StringBuilder()).append(str).append("1").toString();
아까의 예시처럼 반복문에서 문자열을 더한다면 결국 객체가 매번 생성되니 성능 저하를 일으키게 됩니다. 따라서 이런 경우에는 다른 방법을 활용해야겠죠.
또한 concat 메서드를 사용해서 문자열을 연산한다면 StringBuilder로 변환되지 않는다고 합니다.
StringBuffer와 StringBuilder는 앞서 언급했 듯 String과 내부 동작이 살짝 다릅니다.
주로 문자열을 동적으로 변경하거나 조작하는 용도로 쓰이며 Mutable하게 설계되어 있죠.
연산을 하며 기존 객체의 공간이 부족하게 된다면, 내부적으로 가진 바이트 버퍼 공간을 확장하여 유연하게 동작합니다. 그래서 새로운 인스턴스를 만들지 않아도 문자열을 변경할 수 있습니다.
StringBuffer와 StringBuilder는 제공하는 메서드가 서로 동일합니다.
각 클래스를 뜯어보면 바로 차이를 확인할 수 있는데요.
/** StringBuffer **/
@Override
@IntrinsicCandidate
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
/** StringBuilder **/
@Override
@IntrinsicCandidate
public StringBuilder append(String str) {
super.append(str);
return this;
}
차이를 찾으셨나요?
StringBuffer의 메서드에는 **synchronized** 키워드가 존재하여 멀티 스레드 환경에서도 동기화를 지원하는 반면, StringBuilder는 동기화를 보장해주지 않습니다.
그러한 측면에서 StringBuffer는 동기화를 수행하기 때문에 단일 스레드에서는 불필요한 성능 저하를 초래했고, 이러한 단일 스레드에서 가변 문자열을 효과적으로 사용하기 위하여 StringBuilder가 탄생했다고 한 것이죠.
짧고 간단한 문자열을 연산해야 하는 경우 개선된 String이 효과적일 수 있습니다. 하지만 여전히 반복문에서 연산이 사용되는 경우 StringBuffer나 StringBuilder 활용이 더 성능적으로 우수합니다.
멀티 스레드 환경이라면 동기화가 보장이 되어야 하므로 StringBuffer를 사용해 Thread-Safe한 프로그램을 만들어야 합니다. 반면 상관이 없다면(단일 스레드 등) StringBuilder를 사용하는 것이 성능상 이점이 있겠죠.
ref