세 클래스의 공통점은 모두 문자열(String)을 저장하고 관리하는 클래스라는 것입니다.

String

String클래스는 immutable(불변)하다는 특성이 있습니다. String클래스의 문자열을 저장하는 char[]을 보면 final로 선언되어 있다는 것을 확인할 수 있습니다.

public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
...
}

그 때문에, 한번 할당한 문자열을 변경하는 것은 불가능하며, 더하기 연산을 하여 붙일 시 새로운 객체가 생성되어 재할당 됩니다.

String s = "hello";
System.out.println(s.hashCode()); // 99162322
s += " delf!";
System.out.println(s.hashCode()); // 1776255224

반복적으로 문자열을 이어 붙이면 Heap 영역에 참조를 잃은 문자열 객체가 계속해서 쌓이게 됩니다. 물론 나중에 GC에 의해 수거가 되지만, 메모리 관리 측면에서 이러한 코드는 결코 좋다고 할 수 없습니다. 또한 계속해서 객체를 생성하기 때문에 연산 속도적인 측면에서도 뒤떨어집니다.

이러한 String의 성능 이슈를 개선하기 위해 JDK 1.5 이상에서는 컴파일 단계에서 내부적으로 StringBuilder로 변경되어 동작됩니다.

StringBuilder

반면 StringBuilder클래스는 mutable(가변) 합니다. 상속 받고있는 AbstractStringBuilder 클래스의 내부를 보면 변경 가능하도록 선언되어 있습니다.

abstract class AbstractStringBuilder implements Appendable, CharSequence {
/**
* The value is used for character storage.
*/
char[] value;
...
}

append() 메소드를 호출하면, char[] 배열의 길이를 늘리고 같은 객체에 문자열을 더합니다. 아래의 코드를 보면 append() 호출 이후에도 StringBuilder 객체에 변함이 없음을 확인할 수 있습니다.

StringBuilder s = new StringBuilder("hello");
System.out.println(s.hashCode()); // 859417998
s.append(" delf!");
System.out.println(s.hashCode()); // 859417998

String vs StringBuilder

위에서도 언급했듯이, String의 성능 이슈를 개선하기 위해 JDK 1.5 이상에서는 컴파일 단계에서 내부적으로 StringBuilder로 변경되어 동작됩니다.

예를들어 아래와 같이 + 연산자를 이용한 코드는

String outside = str1 + str2 ... + strN;

내부적으로 다음과 같이 동작하게 됩니다.

String inside = new StringBuilder(String.valueOf(str1)).append(str2).[...].apend(strN).toString();

+연산과 append()메소드의 실질적인 성능 차이는 거의 없습니다만, 아래와 같은 코드는 매 루프마다 StringBuilder객체가 생성되므로 기존(JDK 1.5 이하)의 String+연산과 같이 마찬가지로 불필요한 객체가 쌓이게 되고 성능에 악영향을 미칩니다.

for (int i = 0; i < 100000; i++) {
string += value;
}

때문에 어떠한 경우라도 반복문에서는 String+ 연산은 피하는게 좋습니다.

반복문에서의 StringStringBuilder의 속도차이 실험 결과 약 만배의 속도 차이를 확인할 수 있었습니다. - code

StringBuffer

StringBuffersynchronized가 적용되어 멀티스레드 환경에서 Thread-safe하게 동작할 수 있습니다. 한마디로 동기화를 지원하는 StringBuilder라고 이해하시면 편합니다.

// StringBuffer 클래스의 append 메소드
@Override
public synchronized StringBuffer append(CharSequence s) {
toStringCache = null;
super.append(s);
return this;
}

StringBuilder vs StringBuffer

하나의 StringBuilder객체와 StringBuffer객체에 10,000개의 스레드가 접근해서 append()를 수행히는 실험을 해봤습니다.

public class AppendTest {
private static StringBuilder builder = new StringBuilder("");
private static StringBuffer buffer = new StringBuffer("");

public static void main(String[] args) throws InterruptedException {

/* 1. StringBuilder에 문자열 붙이는 스레드 10000개 생성 */
Appenders[] appenders = new Appenders[10];
for (int i = 0; i < appenders.length; i++) {
appenders[i] = new Appenders(builder,10000, "a");
}
for (Appenders a : appenders) { // 스레드 시작
a.start(); //10000개의 스레드가 하나의 StringBuilder에 append
}
Thread.sleep(3000); // 3초 대기
System.out.println(builder.length());

/* 2. StringBuffer에 문자열 붙이는 스레드 10000개 생성 */
appenders = new Appenders[10];
for (int i = 0; i < appenders.length; i++) { // 스레드 10000개 생성
appenders[i] = new Appenders(buffer,10000, "a");
}
for (Appenders a : appenders) { // 스레드 시작
a.start(); // 10000개의 스레드가 하나의 StringBuffer에 append
}
Thread.sleep(3000); // 3초 대기
System.out.println(buffer.length());
}
}

class Appenders extends Thread {
private Appendable ap;
private int cnt;
private String s;

public Appenders(Appendable ap, int cnt, String s) {
this.cnt = cnt;
this.ap = ap;
this.s = s;
}

@Override
public void run() {
while (cnt-- > 0) {
try {
ap.append(s);
} catch (IOException e) {}
}
}
}

실행 결과

93221 // 매 실행 마다 결과가 다름.
100000

Thread-safe하지 않은 StringBuilder에 여러 스레드가 동시에 append()할 경우에, 기존 문자열 뒤 인덱스 값이 동기화되지 않아 중복된 장소에 여러 번 값을 덮어 쓰는 경우가 발생하고, 목표 횟수만큼 문자열이 추가되지 않게됩니다.

심지어 char[]의 버퍼를 늘리기 전에 문자 할당을 시도해서 ArrayIndexOutOfBoundsException이 발생하기도 하지요.

반면, synchronized 키워드를 지원하는 StringBuffer는 깔끔하게 목표 횟수만큼 문자열이 추가되었습니다.

정리

String

  • 단순히 문자열을 참조하거나 탐색 및 검색이 잦을 때 좋습니다.

StringBuilder

  • 런타임 때, 반복적인 문자열 추가 연산이 많을 때 좋습니다.
  • 단일 스레드 환경이라면 StringBuffer보다 성능이 좋을 수 있습니다.

StringBuffer

  • 멀티 스레드 환경에서 반복적인 추가 문자 추가 연산이 많을 때 좋습니다.

Reference