[JAVA] 비싼 객체인 String을 보완할 수 있는 StringBuffer 와 StringBuilder

Mando·2023년 4월 13일
1

JAVA

목록 보기
8/10

String을 생성할 수 있는 방법

  1. String
  2. StringBuffer
  3. StringBuilder

String vs StringBuffer / StringBuilder

  • String은 불변
  • StringBuffer와 StringBuilder은 가변이다.

String

이전의 글을 보면 알 수 있지만
String은 value 멤버 변수에 값을 담는데, 이 값은 final로 선언되어 있고
private 접근 제어자를 가지고 있어 외부에서는 접근도 수정도 안 된다.

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence,
               Constable, ConstantDesc {
    @Stable
    private final byte[] value;

따라서 String 객체는 한 번 생성하면 할당된 메모리 공간이 변하지 않는다.

  • 연산자를 통해 기존에 생성된 String 객체 문자열에 다른 문자열을 붙여도 기존 문자열에 새로운 문자열을 붙이는 것이 아니라,

새로운 String 객체를 만든 후, 새 String 객체에 문자열을 저장하고, 그 객체를 참조하도록 한다.

즉, String 객체는 한 번 생성된 객체의 내부 내용을 변화시킬 수 없다.

이러한 경우로 String 객체는 문자열 연산이 많은 경우 성능이 좋지 않다.

public class StringTest {
  @DisplayName("String은 불변 객체이다.")
  @Test
  void immutableTest(){
    String str1="안녕";
    String str2="하세요";

    Assertions.assertNotSame(str1,str2);
  }
}

StringBuilder와 StringBuffer

StringBuilder와 StringBuffer 모두 AbstractStringBuilder 추상 클래스를 상속받아 구현합니다.

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

AbstractStringBuilder 추상 클래스는

  • value : 문자열 값을 저장하는 byte형 배열
  • count : 문자열의 크기 값을 가지는 int형 변수

StringBuffer와 StringBuilder는 문자열을 추가하고 싶으면 append()메서드를 이용하는데, 이때 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;
    }

따라서 값이 변경되어도 같은 주소 공간을 참조하게 되므로, 값이 변경되는 가변성을 띄게 된다.

가변이다

  @DisplayName("StringBuilder 는 가변객체이다.")
  @Test
  void stringBuilderTest(){
    StringBuilder sb = new StringBuilder();

    sb.append("안녕");
    System.out.println(System.identityHashCode(sb));
    int before = System.identityHashCode(sb);
    sb.append("하세요");
    int after = System.identityHashCode(sb);
    System.out.println(System.identityHashCode(sb));

    Assertions.assertEquals(before,after);
  }

  @DisplayName("StringBuffer는 가변객체이다.")
  @Test
  void stringBufferTest(){
    StringBuffer sb = new StringBuffer();

    sb.append("안녕");
    int before = System.identityHashCode(sb);
    sb.append("하세요");
    int after = System.identityHashCode(sb);

    Assertions.assertEquals(before,after);
  }
}


String과 달리 문자열 연산 등으로 기존 객체의 공간이 부족하게 될 경우 기존의 버퍼 크기를 느리며 유연하게 동작한다.

따라서 String 객체에 + 연산을 했을 때 기존의 String 객체가 GC의 대상이 되었던 것과는 달리 StringBuffer / StringBuilder를 이용하면 버려지는 객체 없이 문자열을 더할 수 있다.(append, insert 메서드 이용)

StringBuffer와 StringBuilder의 차이점은?

동기화 여부

// StringBuffer의 append 메서드 
@Override
public synchronized StringBuffer append(String str) {
    toStringCache = null;
    super.append(str);
    return this;
}

// StringBuilder의 append 메서드
@Override
public StringBuilder append(String str) {
    super.append(str);
    return this;
}

synchronized 키워드
여러 개의 스레드가 한 개의 자원에 접근하려고 할 때, 현재 데이터를 사용하고 있는 스레드를 제외하고, 나머지 스레드들이 데이터에 접근할 수 없도록 막는 역할을 수행

StringBuffer

각 메서드별로 Synchronized Keyword가 존재하여 멀티스레드 환경에서도 동기화 지원
따라서 멀티스레드 환경이라면 동기화 보장을 위해 StringBuffer 사용

하지만 동기화 처리로 인해 StringBuilder에 비해 성능이 좋지 않다.
(Synchronized를 사용한 동기화는 lock을 걸고 푸는 오버헤드가 있어서 속도가 좋지 않다.)

StringBuilder

동기화를 보장하지 않는다.
따라서 단일스레드 환경이라면 StringBuilder를 사용

StringBuffer에만 synchronized가 적용된 예시

두 thread가 StringBuilder, StringBuffer에 값을 추가하는 상황이다.

public class StringTest {
  public static void main(String[] args) {
    StringBuilder sbd = new StringBuilder();
    StringBuffer sbf = new StringBuffer();

      Thread th01 = new Thread(() -> {
          for (int i = 0; i < 100; i++) {
              sbd.append(i);
              sbf.append(i);
          }
      });

      Thread th02 = new Thread(() -> {
          for (int i = 0; i < 100; i++) {
              sbd.append(i);
              sbf.append(i);
          }
      });

      Thread th03 = new Thread(() -> {
          try {
              th01.join();
              th02.join();

              System.out.println(sbf.length());
              System.out.println(sbd.length());
          } catch (InterruptedException e) {
              e.printStackTrace();
          }
      });

      th01.start();
      th02.start();
      th03.start();
  }
}

이때, StringBuffer은 멀티스레드 환경에서 lock을 걸기 때문에 동기화 보장이 되겠지만
StringBuilder은 동기화 보장을 하지 않기에 결과값이 일정하지 않다.

380
370
380
344

String을 자동으로 StringBuilder로 컴파일러가 변경해준다고?

JDK 1.5버전 이전에는 String 연산(+)을 하면 새로운 메모리에 할당하여 참조함으로서 성능상의 이슈가 있었다.

하지만, JDK 1.5버전 이후에는 String 클래스를 사용해도 StringBuilder로 컴파일되도록 변경되었다.

public void stringToBuilder() {
    String temp = "";

    for (int i = 0; i < 100; i++) {
      temp += String.valueOf(i);
    }
  }

컴파일 시 String은 StringBuilder를 통해 문자열을 조합하고 있음을 알 수 있다.

public void stringToBuilder() {
    String temp = "";

    for (int i = 0; i < 100; i++) {
      (new StringBuilder(temp)).append(i).toString();
    }
  }

하지만 컴파일시 자동으로 StringBuilder 객체로 변환되어도 + 연산을 반복한 만큼 새로운 StringBuilder 객체를 생성한다.

이후 + 연산을 하기 위해 생성했던 StringBuilder 객체는 이후에는 사용하지 않으므로 GC의 대상이 된다.

따라서 메모리와 성능에 영향을 미치는 사실은 변함이 없다.

정리

  • String : 문자열 연산이 적고 멀티스레드 환경(불변이기에 멀티스레드 환경에서 안정성을 가진다.)
  • StringBuilder : 문자열 연산이 많고 단일스레드이거나 동기화를 고려하지 않아도 되는 경우
  • StringBuffer : 문자열 연산이 많고 멀티스레드 환경일 경우

0개의 댓글