본문 바로가기

Develop/Java

Java String, StringBuilder, StringBuffer

728x90

문자열

자바에는 문자열을 다룰 수 있는 데이터형에는 3가지 종류가 존재합니다.

 

  1. String 개체
  2. StringBuilder 클래스
  3. StringBuffer 클래스

String 개체

먼저 String 개체는 자바 코드를 작성해보신 경험이 있다면 누구나 잘 아실텐데요.

String의 메모리 동작 방식에 대해 간략하게 소개 해보겠습니다.

 

String str = "text";
String str2 = "text";

String str3 = new String("text");

 

먼저 위와 같은 코드 Snippet이 런타임 메모리에 올라간다고 가정하겠습니다.

런타임 메모리

  1. 위의 사진과 같이 첫 줄인 `String str = "text";` 에서 리터럴인 `text` 문자열이 Heap 영역 안의 String constant pool에 등록이 됩니다.
  2. 두번째 줄인 `String str2 = "text";`에서는 이미 `text`라는 문자열이 String constant pool에 등록이 되어 있기 때문에 메모리 주소를 그대로 참조하여 매핑합니다.
  3. 세 번째 줄은 new 예약어를 통해 새로운 문자열 개체를 Heap 영역에 생성합니다.

결론적으로 str과 str2는 동일한 String constant pool의 리터럴 문자열 `text`의 참조를 가리키며

str3는 String constant pool의 `text`가 아닌 Heap 영역에 새로 할당된 `text`라는 문자열의 주소를 참조하는 것 입니다.

 

추가적으로 `str3.intern()` 메서드 호출을 통해 Heap 영역의 `text` 문자열을 String constant pool 영역으로 이동시킬 수 있습니다. 하지만 String constant pool의 영역에는 이미 `text` 리터럴이 존재하기 때문에

Heap 영역의 `text` 문자열은 사라지고 String constant pool의 `text`만 남게 됩니다.

 

마지막으로 위의 예시를 통해 String은 한번 값이 할당되면 그 공간 안의 값은 변하지 않습니다.

이러한 할당된 공간이 변하지 않는 특성을 불변(Immutable)성이라 하며 String은 Immutable한 데이터 타입입니다.

 

String s1 = "Hello";
s1.concat("Java"); // 혹은 + "Java"를 사용해도 결과와 메모리 내의 작동은 동일함
System.out.println(s1); // Hello Java

 

만약 위와 같은 코드를 작성하게 된다면 String constant pool에는 `Hello`와 `Java` 두 문자열 개체가 생성되며

Heap 영역에 새로운 문자열인 `Hello Java`가 할당되고 추후에 가비지 컬렉터(GC)에서 참조가 없어진

String constant pool 영역의 `Hello`와 `Java`를 정리하게 되는 것 입니다.

 

String의 concat 메서드 또는 + 연산자를 사용할 경우의 메모리 변화

 

이러한 문자열 연산이 위의 코드처럼 한 두개면 성능 상의 미세한 차이밖에 없지만

많은 반복을 수행하는 반복문 안에서 저러한 연산을 사용하게 되면 기하 급수적으로 메모리 낭비가 발생하기 시작합니다.

따라서 빈번한 문자열 수정이 필요한 경우에는 StringBuilder 혹은 StringBuffer를 사용하는 것입니다.


StringBuilder와 StringBuffer

StringBuilder와 StringBuffer는 String과 다르게 데이터 가변성을 갖습니다.

 

AbstractStringBuilder의 자식 클래스들

StringBuffer와 StringBuilder는 모두 AbstractStringBuilder를 상속 받습니다.

StringBuilder와 StringBuffer 클래스의 문자열을 수정하고 싶으면 append() 메서드를 사용하게 되는데

`append()` 메서드는 AbstractStringBuilder 에 다음과 같이 구현되어 있습니다.

 

public AbstractStringBuilder append(String str) {
    if (str == null) {
        return appendNull();
    }
    int len = str.length();
    ensureCapacityInternal(count + len);
    putStringAt(count, str);
    count += len;
    return this;
}

다음과 같이 StringBuilder, StringBuffer에 문자열을 추가하게 되면 추가할 문자열의 크기만큼

현재의 문자열을 저장하는 배열의 공간을 늘려주고, 늘려준 공간에 추가할 문자열을 넣어주는 방식으로 되어있습니다.

위에서 살펴본 코드 스니펫의 내부 동작을 통해 값이 변경되더라도 같은 주소 공간을 참조하게 되며

값이 변경되는 가변성을 띄게 되는 것입니다.

 

상황에 따른 StringBuilder와 StringBuffer의 적절한 선택

그러면 이제 StringBuilder와 StringBuffer의 차이점에 대해 알아보겠습니다.

 

StringBuffer와 StringBuilder의 차이

StringBuffer의 경우 StringBuilder와 다르게 synchronized 예약어가 사용되었습니다.

특정한 스레드에서 메서드가 호출된 상황에서 다른 스레드에서 이 메서드에 진입할 수 없도록 보장해 주기 때문에 멀티 스레드 환경에서 안정적인 사용과 예상된 동작을 기대할 수 있습니다.

 

예시로 멀티스레드 환경에서 A 스레드와 B스레드 모두 같은 StringBuffer 클래스 객체의

append() 메서드를 사용하려고 하면 다음과 같은 절차를 수행하게 됩니다.

 

  • A 스레드 : StringBuffer의 append() 동기화 블록에 접근 및 실행
  • B 스레드 : A 스레드 StringBuffer의 append() 동기화 블록에 들어가지 못하고 block 상태가 됨
  • A 스레드 : StringBuffer의 append() 동기화 블록에서 탈출
  • B 스레드 : block 에서 running 상태가 되며 sb 의 append() 동기화 블록에 접근 및 실행

추가적으로 StringBuilder 클래스 주석에서 동기화가 필요할 경우 StringBuffer을 추천한다는 문구를 확인할 수 있습니다.


최종적인 정리

String의 문자열을 많이 변동시킬 일이 없을 때는 String을 사용하고

문자열 데이터를 자주 변경해야 하는 상황에서는 StringBuilder를 사용하고

Thread Safe가 필요한 상황에서는 StringBuffer를 사용하면 됩니다.