StringBuffer와 StringBuilder의 차이점은 무엇이며, 어떤 상황에서 각각을 사용해아 하는지 설명해주세요.
기본적으로 자바에서는 String 객체의 값을 변경할 수 없다.
이는 한번 할당된 공간이 변하지 않는다고 해서 불변(immutable)
자료형이라고 불린다. 그래서 초기공간과 다른 값에 대한 연산에서 많은 시간과 자원을 사용하게 된다는 특징이 있다.
실제로 String 클래스의 내부를 살펴보면
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence,
Constable, ConstantDesc {
@Stable
private final byte[] value;
}
인스턴스 생성 시 생성자의 매개변수로 입력받는 문자열은 이 value라는 인스턴스 변수에 문자형 배열로 저장하게 된다. 이 value는 상수(final)
이므로 바꿀 수 없다.
String str = "hello";
str = str + "world";
위 예제를 보면 hello
라는 값에 world
라는 문자열을 더해서 String 객체 자체를 업데이트한거처럼 보인다.
하지만 실제로는 메모리에 새로 "helloworld" 값을 저장한 영역을 따로 만들고 변수 str
을 다시 참조하는 식으로 작동한다.
StringBuffer나 StringBuilder의 경우 문자열 데이터를 다룬다는 점에서 String 객체와 동일하지만, 객체의 공간이 부족해지는 경우 버퍼의 크기를 유연하게 늘려주어 가변(mutable)적
이라는 차이점이 있다.
두 클래스는 내부 Buffer(데이터를 임시적으로 저장하는 메모리)에 문자열을 저장해두고 그 안에 추가, 수정, 삭제를 할 수 있도록 설계되었다.
String
객체는 한번 생성되면 불변적인 특징 때문에 업데이트하면, 매 연산 시마다 새로운 문자열을 가진 String 인스턴스가 생성되어 메모리 공간을 차지하게 되지만,
StringBuffer / StringBuilder
객체는 가변성을 가지기에 .append() .delete()
메서드를 이용하여 동일 객체내에서 문자열의 크기 변경이 가능하다.
따라서 값이 변경될때마다 새로운 객체를 만드는 String보다 훨씬 빠르기 때문에, 문자열의 추가, 수정, 삭제가 빈번하게 발생할 경우라면 String 클래스가 아닌 StringBuffer / StringBuilder를 사용하는게 이상적이다.
public class Main {
public static void main(String argsp[]) {
String str1 = "hello";
String str2 = "hello";
System.out.println(str1 == str2);
System.out.println(str1.equals(str2));
StringBuffer sb1 = new StringBuffer("hello");
StringBuffer sb2 = new StringBuffer("hello");
System.out.println(sb1 == sb2);
System.out.println(sb1.equals(sb2));
String sb_str1 = sb1.toString();
String sb_str2 = sb2.toString();
System.out.println(sb_str1 == sb_str2);
System.out.println(sb_str1.equals(sb_str2));
}
}
StringBuffer
와 StringBuilder
클래스는 둘 다 크기가 유연하게 변하는 가변적인 특성을 가지고 있으며, 제공하는 메서드도 똑같고 사용법도 같다.
StringBuffer 클래스는 멀티 쓰레드에서 안전하고 동기화를 지원한다. (thread safe)
synchronized 키워드를 사용한다.
StringBuilder 클래스는 멀티 쓰레드에서 안전하지 않고 동기화를 지원하지 않는다.(thread unsafe)
StringBuilder가 동기화를 지원하지 않는 이유는 그 자체로 thread-safe하지 않기 때문이다.
다중 스레드 환경에서 StringBuilder에 접근할 경우 예상치 못한 결과가 나타날 수 있다.
StringBuilder는 주로 문자열의 결합 연산을 효율적으로 실행하기 위해 사용되고, 이러한 연산은 대부분 단일 스레드 환경에서 실행된다. 따라서 StringBuilder 인스턴스를 여러 개의 스레드가 사용하게 된다면 동기화 문제로 인한 데이터 일관성이 깨질 수 있다.
멀티 쓰레드 환경에서 일반적으로 변수, 함수, 객체 등에 여러 스레드로부터 동시에 접근이 이루어져도 프로그램의 실행이 문제가 없음을 의미한다.
thread safe
에는 Re-entrancy, Thread-local storage, Mutual exclusion, Atomic operations, Immutable Object 등이 있다.
public class Main extends Thread{
public static void main(String args[]) {
StringBuffer stringBuffer = new StringBuffer();
StringBuilder stringBuilder = new StringBuilder();
new Thread(() -> {
for (int i = 0; i < 10000; i++) {
stringBuffer.append("*");
stringBuilder.append("@");
}
}).start();
new Thread(() -> {
for (int i = 0; i < 10000; i++) {
stringBuffer.append("*");
stringBuilder.append("@");
}
}).start();
new Thread(() -> {
try {
Thread.sleep(2000);
System.out.println("StringBuffer.length: " + stringBuffer.length());
System.out.println("StringBuilder.length: " + stringBuilder.length());
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
StringBuilder
의 값이 더 작은 것을 볼 수 있는데, 이는 쓰레드들이 동시에 StringBuilder
클래스에 접근하여 동시에 append() 를 수행하다 몇번 씹혀서 제대로 수행이 안되어 일어난 결과라고 보면 된다.
StringBuilder
는 Thread safe
하지 않아서 각기 쓰레드가 객체에 접근해서 변경을 하면 기다려주지 않기 때문에 이러한 현상이 발생한 것이다.
초기 용량 설정
: 기본적으로 StringBuffer와 StringBuilder는 16개의 문자를 저장할 수 있는 공간이 생깁니다. 이를 초과하면 내부적으로 배열 크기를 늘리는 작업이 발생한다. 이러한 작업은 비용이 들기 때문에 대략적으로 문자열의 크기를 알고 있다면 그 크기에 맞춰 초기 용량을 설정해주는 것이 좋다.
setLength(0)
: StringBuffer와 StringBuilder를 재사용해야 하는 경우, 새로운 인스턴스를 생성하는 대신에 setLength(0) 메서드를 통해 내용을 지우고 길이를 0으로 만들 수 있다. 이는 메모리 할당과 가비지 컬렉션에 대한 오버헤드를 줄일 수 있다.
ensureCapacity(int minimumCapacity)
: StringBuffer와 StringBuilder의 내부 버퍼 크기를 늘릴 수 있는 메서드이다. 예상되는 문자열의 크기가 큰 경우 적절한 크기를 설정하여, 내부적으로 버퍼의 크기를 늘리는데 드는 비용을 줄일 수 있다.
trimToSize()
: StringBuffer와 StringBuilder 내부 버퍼의 크기를 문자열의 길이에 맞게 줄인다. 이는 불필요한 메모리를 사용하는걸 방지해준다.
불필요한 메모리 사용
: 이 메서드를 사용하면 더 큰 내부 버퍼를 위한 메모리 공간이 할당됩니다. 따라서 예상치 못한 큰 값으로 설정하면, 실제로 사용되지 않는 메모리 공간이 낭비될 수 있다.
성능 저하
: ensureCapacity(int minimumCapacity) 메서드를 호출하면, 내부적으로 배열 복사 작업이 발생할 수 있다. 이는 상당한 비용이 들 수 있으므로, 과도하게 호출되지 않도록 주의해야 한다.
trimToSize() 메서드는 StringBuilder의 내부 버퍼 크기를 현재 문자열 크기에 맞게 줄이는 데 사용됩니다. 이 메서드를 호출하는 상황은 주로 다음과 같다.
메모리 최적화
: 큰 용량의 StringBuffer와 StringBuilder를 사용하고 나서, 더 이상 그 객체를 확장할 필요가 없는 경우, trimToSize() 메서드를 호출하여 불필요한 메모리 사용을 줄일 수 있습니다.
긴 생명주기의 객체
: StringBuffer와 StringBuilder 객체가 애플리케이션에서 오랫동안 사용되는 경우, trimToSize() 메서드를 호출하여 메모리를 효율적으로 관리할 수 있습니다.
이러한 방법을 통해 StringBuffer와 StringBuilder의 메모리 사용을 최적화하고 성능을 개선할 수 있습니다.
public class Main3 {
public static void main(String args[]) {
final int lengths = 50000;
long st1 = System.currentTimeMillis();
String str = "";
for (int i = 0; i < lengths; i++) {
str += "*";
}
long et1 = System.currentTimeMillis();
long st2 = System.currentTimeMillis();
StringBuffer stringBuffer = new StringBuffer();
for (int i = 0; i < lengths; i++) {
stringBuffer.append("*");
}
long et2 = System.currentTimeMillis();
long st3 = System.currentTimeMillis();
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < lengths; i++) {
stringBuilder.append("*");
}
long et3 = System.currentTimeMillis();
long duration1 = et1 - st1;
long duration2 = et2 - st2;
long duration3 = et3 - st3;
System.out.println("duration1 = " + duration1);
System.out.println("duration2 = " + duration2);
System.out.println("duration3 = " + duration3);
}
}
String 클래스의 문자열에 대한 변경 속도가 제일 느린 것을 확인할 수 있다.