[Java] StringBuilder와 StringBuffer의 차이


Development note/Java  2019. 6. 22. 09:00

안녕하세요. 명월입니다.


이 글은 StringBuffer와 StringBuilder의 차이에 대해 소개하겠습니다.


우리가 프로그램을 작성하면 String과 StringBuffer, StringBuilder를 많이 사용합니다.

많은 사람들이 String과 StringBuffer 혹은 StringBuilder 의 차이는 알고 있으나 StringBuffer와 StringBuilder의 차이를 헷갈려 하는 분이 많습니다.


필자인 저도 가끔 헷갈립니다.

먼저 String과 StringBuffer 혹은 StringBuilder 의 차이에 대해 간략하게 설명하겠습니다.


우리가 문자열 변수 타입으로써 String을 많이 사용합니다만 사실 String은 클래스 타입입니다. 즉 char의 배열로 이루어진 클래스 타입이죠.

그래서 우리가 String을 한번 선언하면 내부 객체는 변하지 않습니다. 연결 리스트의 타입처럼 "abc" + "def" 를 하면 "abc" 위에 자연스럽게 추가 되는 건 아닙니다.

char[] data1 = new char[] { 'a', 'b', 'c' };
char[] data2 = new char[] { 'd', 'e', 'f' };

// + 연산자를 사용하면 아래처럼의 연상이 이루어 지지 않을까?
int length = data1.length + data2.length;
char[] ret = new char[length];
for (int i = 0; i < data1.length; i++) {
  ret[i] = data1[i];
}
for (int i = 0; i < data2.length; i++) {
  ret[i + data1.length] = data2[i];
}

// 결과는 ret이고 data1과 data2는 해제

위와 같은 방식으로 생성이 됩니다. 즉, "abc"의 크기와 "def"의 크기를 합친 크기의 char 배열을 생성해서 생성한 char 배열에 차례로의 배열로 데이터를 옮긴 후에 최종적으로 "abc"와 "def"의 메모리를 해제하는 것입니다.

모르긴 몰라도 이렇게 흘러가는 게 맞습니다. 예전에 java 1.6이후 부터는 바뀌었다고 하는데 잘 모르겠네요.


그렇다면 연결리스트의 패턴처럼 정적 array 배열이 아닌 동적으로 add로 추가 시킬 수 없을까 해서 나온 것이 StringBuffer와 StringBuilder입니다.

List<Character> data = new LinkedList<>();
data.add('a');
data.add('b');
data.add('c');

// append 함수를 하면 그대로 추가한다.
data.add('d');
data.add('e');
data.add('f');

// toString을 실행하면 char[]로 변환
char[] ret = new char[data.size()];
for (int i = 0; i < data.size(); i++) {
  ret[i] = data.get(i);
}

단순히 문자열 2개로 해서 별 차이 없어 보이지만 "abc" + "def" + "ghi" + "jkl" 이렇게 하면 엄청나게 달라질 것입니다.

먼저 "abc" + "def"를 해서 char[6]을 만든 후에 다시 "ghi"를 더해서 char[9]를 만들고 "jkl"를 더해서 char[12]로 만드는 것입니다.


단순 계산만 해도 new char[]만 9번 이루어 지네요.


그럼 StringBuffer와 StringBuilder의 차이는 무엇일까요?

그런 동기화의 차이입니다. StringBuffer의 경우는 동기화를 처리해 주지만 StringBuilder의 경우는 동기화를 처리해 주지 않습니다.

private static void sleep(int time) {
  try {
    Thread.sleep(time);
  } catch (Throwable e) {

  }
}
public static void main(String[] test) {

  StringBuilder builder = new StringBuilder();
  StringBuffer buffer = new StringBuffer();
  // 쓰레드를 두개 만들어서 0.1초의 간격으로 데이터를 넣습니다.
  // 이 쓰레드는 0를 넣습니다.
  Executors.newSingleThreadExecutor().execute(() -> {
    for (int i = 0; i < 10; i++) {
      builder.append(0);
      buffer.append(0);
      sleep(100);
    }
  });
  // 이 쓰레드는 1를 넣습니다.
  Executors.newSingleThreadExecutor().execute(() -> {
    for (int i = 0; i < 10; i++) {
      builder.append(1);
      buffer.append(1);
      sleep(100);
    }
  });

  // 결과가 끝낳때까지 10초 기다린다.
  sleep(10000);
  System.out.println(builder.toString());
  System.out.println(buffer.toString());
}

첫번째 스레드와 두번째 스레드에서 각각 10개의 문자를 넣었기 때문에 20개의 문자를 예상했습니다만, StringBuilder의 경우는 몇 개의 문자가 빠져있습니다. 그러나 StringBuffer의 경우는 동기화 처리가 있기 때문에 제대로 20개 되었있네요.


여기까지 보면 StringBuilder를 버리고 StringBuffer로만 사용하는게 맞다고 생각할 수 있습니다만, 그러나 꼭 그렇지는 않습니다.

private static void sleep(int time) {
  try {
    Thread.sleep(time);
  } catch (Throwable e) {

  }
}

public static void main(String[] test) {

  StringBuffer buffer1 = new StringBuffer();
  StringBuffer buffer2 = new StringBuffer();

  Executors.newSingleThreadExecutor().execute(() -> {
    for (int i = 0; i < 10; i++) {
      // 데드락에 걸려 버렸다.
      synchronized (buffer1) {
        buffer1.append(0);
        buffer2.append(0);
      }
      sleep(100);
    }
  });
  Executors.newSingleThreadExecutor().execute(() -> {
    for (int i = 0; i < 10; i++) {
      // 데드락에 걸려 버렸다.
      synchronized (buffer2) {
        buffer1.append(1);
        buffer2.append(2);
      }
      sleep(100);
    }
  });

  // 결과가 끝낳때까지 10초 기다린다.
  sleep(2000);
  System.out.println(buffer1.toString());
  System.out.println(buffer2.toString());
}

그렇죠.. StringBuffer는 클래스 안에 lock 기능이 존재하기 때문에 deadlock에 걸릴 위험이 큼니다. 여기서 StringBuffer 대신 StringBuilder를 쓰면 데드락에 걸릴 일이 없습니다.


이렇게만 보면 역시 StringBuilder를 사용해서 필요할 때 lock으로 제어하는 게 낫다고 생각할 지도 모릅니다.


솔직히 위 두 예제처럼 바보처럼 프로그램을 작성하는 사람은 없을 것입니다. String 데이터를 여러 쓰레드에 공유를 해서 사용하지는 않을 것입니다..... 정말 그럴까요?? ㅎㅎㅎ


StringBuilder와 StringBuffer의 특성은 확실히 lock이 있고 없고의 차이입니다. 어느 것이 더 낫다고 표현하기도 그렇네요..

딱 상황에 맞게 사용하는게 맞을 듯싶네요.


보통은 StringBuffer를 많이 사용하겠네요.. 왠지 synchronized 키워드가 소스에 많이 깔리면 두렵습니다. 그러나 멀티 환경에서 사용하는 String이라면 StringBuilder의 용법도 꼭 염두를 해야 하겠습니다.


여기까지 StringBuilder와 StringBuffer의 차이에 대한 설명이었습니다.


궁금한 점이나 잘못된 점이 있으면 댓글 부탁드립니다.