[JAVA] - String, StringBuilder, StringBuffer의 차이

Gates·2022년 7월 5일
0

JAVA 이론

목록 보기
2/4
post-thumbnail

우리는 이전 글에서 String이 Immutable(불변) 하다는 것을 알게 되었습니다.

만약에 아래 소스코드처럼 String 객체에 빈번하게 값을 변경해줘야 하는 코드에서는 어떻게 해야할까요?

String temp = "abc";
temp = temp + "def";
temp = temp + "ghi";
temp = temp + "jkl";
temp = temp + "123";
temp = temp + "456";
temp = temp + "789";

위 소스코드가 실행되면 매번 새로운 객체를 만들어서 할당해 줄 것입니다. 왜냐하면 String은 Immutable하기때문에 매번 새로운 객체를 만들어서 String contant pool에 넣어주기 때문입니다. 매번 새로운 값을 만들어서 할당해주면 그만큼 unreachable한 데이터만 쌓이고 가비지 컬렉터가 그만큼 자주 실행되게 됩니다. (가비지 컬렉터의 작동원리에 대해서는 나중에 더 자세히 알아보겠습니다. ) 가비지 컬렉터가 자주 실행되면 성능 이슈가 발생할 수밖에 없습니다.

이런 성능 이슈를 방지하기 위해 StringBuilder, StringBuffer를 사용합니다.
우선 StringBuilder와 StringBuffer의 클래스 내부를 보겠습니다.

public final class StringBuilder extends AbstractStringBuilder
    implements java.io.Serializable, Comparable<StringBuilder>, CharSequence
 public final class StringBuffer extends AbstractStringBuilder
    implements java.io.Serializable, Comparable<StringBuffer>, CharSequence

StringBuilder와 StringBuffer는 AbstractStringBuilder 라는 추상클래스를 상속받습니다. 그럼 AbstractStringBuilder의 멤버변수를 보도록 하겠습니다.

/**
 * The value is used for character storage.
 * 문자를 저장하기 위해 사용되는 값
 */
byte[] value;
    
/**
 * The id of the encoding used to encode the bytes in {@code value}.
 * @Code 값의 바이트를 인코딩 하는 데 사용되는 인코딩(타입)의 ID
 */
byte coder;
/**
 * Constructs a string buffer with no characters in it and an initial capacity of 16 characters.
 * 문자가 없는 string buffer를 생성하고 최초 16개의 크기를 초기화한다. 
 */
int count;

우리는 여기서 value와 count에 집중하기 위해 append() 메소드를 살펴보겠습니다.

// StringBuilder 클래스의 append 메소드
@Override
@HotSpotIntrinsicCandidate
public StringBuilder append(String str) {
    super.append(str);
    return this;
}
// AbstractStringBuilder클래스의 append 메소드
public AbstractStringBuilder append(String str) {
    if (str == null) {
        return appendNull();
    }
    int len = str.length();
    ensureCapacityInternal(count + len);
    putStringAt(count, str);
    count += len;
    return this;
}
private final void putStringAt(int index, String str) {
    if (getCoder() != str.coder()) {
        inflate();
    }
    str.getBytes(value, index, coder);
}
void getBytes(byte dst[], int dstBegin, byte coder) {
    if (coder() == coder) {
        System.arraycopy(value, 0, dst, dstBegin << coder, value.length);
    } else {    // this.coder == LATIN && coder == UTF16
        StringLatin1.inflate(value, 0, dst, dstBegin, value.length);
    }
}

위 코드들을 보면 알 수 있듯이, StringBuilder의 append 메소드는 AbstractStringBuilder의 append 메소드를 오버라이딩 한 형태이며, AbstractStringBuilder 클래스의 append 메소드를 호출하여 자기 자신을 반환합니다. 그래서 아래와 같은 코드를 작성 가능합니다.

StringBuilder builder = new StringBuilder("123")
		.append("456")
        .append("789");
builder.append("123")
		.append("456")
        .append("789");

AbstractStringBuilder의 append() 메소드에 String을 매개변수로 넣으면 String의 길이만큼 AbstractStringBuilder의 value 배열의 길이를 늘려주고, 늘려준 공간에 문자열을 추가하기 위해 putStringAt 메소드를 호출합니다. 이때, 최종적으로 getBytes 메소드를 통해 System 클래스의 arraycopy 메소드를 호출하는데요. 이 메소드는 오라클 문서에 따르면 배열을 필요한 부분만 복사하는 시스템 메소드입니다.

StringBuffer는 뭐가 다를까요? StringBuffer도 StringBuilder와 마찬가지로 AbstractStringBuilder를 상속받아 사용합니다. 그럼 append 메소드도 있겠죠? 클래스의 append 메소드를 살펴보겠습니다.

@Override
@HotSpotIntrinsicCandidate
public synchronized StringBuffer append(String str) {
    toStringCache = null;
    super.append(str);
    return this;
}

StringBuffer도 똑같이 AbstractStringBuilder의 append 메소드를 오버라이딩 하여 사용합니다. 다만, StringBuilder와는 다른점이 있습니다. synchronized 지시자와 toStringCache입니다.
synchronized 먼저 보자면, synchronized는 여러 개의 스레드가 한 개의 자원을 사용하고자 할 때, 현재 사용중인 데이터를 lock을 걸어 자신을 제외한 다른 스레드가 현재 사용중인 데이터에 접근하는 것을 방지하는 지시자입니다. 즉, 자바 내부적으로 메소드나 변수를 동기화하기 위한 지시자입니다. 이를 Thread-Safe 하다고 하며, 결과적으로 StringBuilder와 StringBuffer의 차이는 동기화의 차이입니다.
그렇다면 StringBuffer는 Thread-Safe 하다는 장점이 있으니까 단점은 무엇이 있을까요?
앞에서 사용중인 데이터를 lock을 건다고 했습니다. 데이터를 사용할때 lock을 걸었다면 데이터를 더이상 사용하지 않을 때 unlock을 처리해야 합니다. 이런 lock-unlock 처리하게 되는데 이로 인해 연산이 StringBuilder에 비해 더 필요하므로, StringBuffer가 StringBuilder보다 조금이나마 느립니다.

profile
어제보다 성장한 개발자의 DEBUG 로그

0개의 댓글