목표
- String, StringBuilder, StringBuffer의 차이점을 안다.
- 멀티 스레드 환경에서 StringBuffer을 이용해야 하는 이유를 안다.
개요
문자열과 관련된 프로그래밍을 위해서라면 String을 사용해야 한다. 아마 Java나 Kotlin으로 프로그래밍 하는 사람이라면 누구나 String을 사용해 보았을 것이다.
val kotlinWorld: String = "Kotlin" + "World"
하지만, String을 여러번 바뀌는 문자열을 위해 사용하는 것은 좋지 않다. 이유는 String은 내부에 value값이 final로 선언되어 있는 불변 객체고 값을 바꾸기 위해서는 새로운 객체에 할당을 해주어야 해서 비용이 크기 때문이다.
이에 따라서 우리는 String을 만들기 위해서 StringBuilder나 StringBuffer을 사용한다.
val kotlinWorld = StringBuilder("Kotlin")
kotlinWorld.append(" World")
val string = kotlinWorld.toString()
val kotlinWorld = StringBuffer("Kotlin")
kotlinWorld.append(" World")
val string = kotlinWorld.toString()
이 두개는 같은 결과값을 내어 놓는다. 그렇다면 왜 StringBuilder와 StringBuffer로 클래스가 나눠져 있을까?
바로, 스레드 안정성 때문이다. StringBuilder, StringBuffer 내부에서 append가 가능하다는 것은 가변 객체라는 뜻이다. 멀티스레드 환경에서 가변 객체는 동시접근이 가능하여 안전하지 않다. 멀티 스레드 환경에서 가변 객체의 동시 접근을 허용하면, 여러 스레드가 같은 객체를 동시에 변경 가능하여 서로 다른 데이터로 변환이 되어 데이터 일관성이 깨지게 된다.
Java에서는 이를 해결하기 위해서 메서드 제어자에 synchronized를 붙일 수 있다. synchronized 제어자는 멀티스레드 환경에서 메서드에 한번에 하나의 스레드만 접근을 허용하는 제어자이다. 하나의 스레드가 메서드 내부의 연산을 돌리고 있을 경우 lock을 잡아 다른 스레드가 접근을 불가능하게 만든다.
자 이제 StringBuilder와 StringBuffer의 append 연산을 살펴보자
@Override
public StringBuilder append(String str) {
super.append(str);
return this;
}
코드1. StringBuilder의 append
@Override
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
코드2. StringBuffer의 append
둘의 차이를 알겠는가? 바로 위에서 언급한 synchronized 제어자의 차이이다. 기본적으로 StringBuffer의 모든 메서드에는 Synchronized가 붙어있다. 즉, StringBuffer은 멀티스레드 환경에서 안전한 연산을 수행할 수 있는 클래스이며, StringBuilder 클래스는 멀티스레드 환경에서 안전하지 않은 클래스이다.
정리
- 변하지 않는 값은 String을 사용하는 것이 좋다. 재할당 하는 연산을 하지 않는 이상 불변객체라서 멀티 스레드 환경에서 안전하다.
- 변하는 값에 대해서는 동시 접근이 가능한 환경(멀티 스레드 환경)에서는 StringBuffer을 사용하는 것이 좋다.
- 동시 접근이 가능하지 않은 환경에서는 StringBuilder을 사용하는 것이 좋다. StringBuffer 사용 시 lock을 잡아 비용이 들기 때문이다.
시간 비교
String : 685ms
StringBuilder의 2750배, StringBuffer의 1275배 정도의 시간이 걸린다.
val duration: TimedValue<Unit> = measureTimedValue {
var string = "a"
for(i in 0 until 10000){
string += "kotlinWorld"
}
}
println("elapsedTime : ${duration}")
//elapsedTime : TimedValue(value=kotlin.Unit, duration=685ms)
StringBuilder : 249us
StringBuffer의 1/2정도의 시간만 걸린다.
val timeStringBuilder: TimedValue<Unit> = measureTimedValue {
var stringBuilder = StringBuilder("a")
for(i in 0 until 10000){
stringBuilder.append("kotlinWorld")
}
}
println("stringBuilderTime : ${timeStringBuilder}")
//stringBuilderTime : TimedValue(value=kotlin.Unit, duration=249us)
StringBuffer : 537us
StringBuilder의 2배 정도의 시간이 걸린다.
val timeStringBuffer: TimedValue<Unit> = measureTimedValue {
var stringBuilder = StringBuffer("a")
for(i in 0 until 10000){
stringBuilder.append("kotlinWorld")
}
}
println("stringBufferTime : ${timeStringBuffer}")
//stringBufferTime : TimedValue(value=kotlin.Unit, duration=537us)
해석
String은 불변 객체이기 때문에 새로운 String을 매번 생성해주어야 해서 가장 시간이 오래 걸린다. StringBuilder은 동기화를 지원하지 않아 lock이 잡히고 풀리는 과정이 없기 때문에 가장 적은 시간이 걸리며, StringBuffer은 동기화를 지원해서 lock이 잡히고 풀리는 과정이 있기 때문에 StringBuilder보다 많은 시간이 걸린다.
즉, StringBuffer은 lock이 잡히고 풀리는 과정이 있어 Multi Thread 환경에서 안전한 대신 시간이 오래 걸린다.