자바에서는 대표적으로 문자열을 다루는 자료형 클래스로 3가지가 있습니다.

 

위 3가지 클래스 자료형은 모두 문자열을 다루는데 있어 공통적으로 사용되지만, 사용 목적에 따라 쓰임새가 많이 달라지게 됩니다.

 

오늘은 이 3가지의 차이와 어느 상황에서 어느 자료형을 사용하는 것이 이상적이고 성능적으로 어느것이 더 좋은지 글을 작성해 봤습니다.

 

 

 

String 과 SringBuffer & StringBuilder

String은 불변입니다. 기본적으로 객체 값을 변경할 수 없습니다.

 

 

이는 한번 할당된 공간이 변하지 않는다고 해서 불변 자료형이라고 부릅니다. 그래서 초기공간과 다른 값에 대한 연산에서 많은 시간과 자원을 사용하게 되는 특징이 있습니다.

 

 

실제로 String 객체의 내부 구성 요소를 보면 다음과 같습니다.

 

 

인스턴스 생성 시 생성자의 매개변수로 입력받는 문자열은 이 value 라는 인스턴스 변수에 문자형 배열로 저장이 되는데 이 value라는 변수는 상수형(final)으로 값을 바꾸지 못합니다.

public final class String implements java.io.Serializable, Comparable {
	private final byte[] value;
}
더보기

jdk 8 까지는 String 객체의 값은 char[] 배열로 구성되어져 있지만, jdk 9부터 기존 char[]에서 byte[]을 사용하여 String Compacting을 통한 성능 및 heap 공간 효율(2byte -> 1byte)을 높이도록 수정되었다.

 

 

아래 예제 코드를 보면 변수 a 가 참조하는 메모리의 "Hello"라는 값과  "World" 라는 문자열을 더해서 String 객체의 자체 값을 업데이트 시킨 것으로 볼 수 있습니다.

 

 

하지만 실제로는 메모리에 새로 "Hello World" 값을 저장한 영역을 따로 만들고 변수 a를 다시 참조하는 식으로 작동합니다.

String str = new String("Hello");

str = str + "world"

이외에도 문자열을 다루는데 있어 가장 많이 사용하는 trim이나 toUpperCase 메소드 사용 형태를 보면, 문자열이 변경되는 것 처럼 생각 될 수 있지만 해당 메소드 수행 시 또 다른 String 객체를 생성하여 리턴합니다.

 

 

그렇다면 자바에서는 왜 String을 불변으로 설정을 했을까?

 

 

String 객체를 불변하게 설계한 이유는 캐싱, 보안, 동기화, 성능측면 이점을 얻기 위해서 입니다.

 

  1. 캐싱 : String을 불변하게 함으로써 String pool에 각 리터럴 문자열의 하나만 저장하며 다시 사용하거나 캐싱에 이용가능하며 이로 인해 힙 공간을 절약할 수 있다는 장점이 있다.
  2. 보안 : 예를 들어 데이터베이스 사용자 이름, 암호는 데이터베이스 연결을 수신하기 위해 문자열로 전달되는데, 만일 번지수의 문자열 값이 변경이 가능하다면 해커가 참조 값을 변경하여 애플리케이션에 보안 문제를 일으킬 수 있다.
  3. 동기화 : 불변함으로써 동시에 실행되는 여러 스레드에서 안정적이게 공유가 가능하다.

 

 

StringBuffer나 StringBuilder의 경우 문자열 데이터를 다룬다는 점에서 String 객체와 같지만, 객체의 공간이 부족해지는 경우 버퍼의 크기를 유연하게 늘려주어 가변(mutable)적이라는 차이점이 있습니다.

 

 

두 클래스는 내부 Buffer(데이터를 임시로 저장하는 메모리)에 문자열을 저장해두고 그 안에서 추가, 수정, 삭제 작업을 할 수 있도록 설계되어 있습니다.

 

 

String 객체는 한번 생성되면 불변적인 특징 때문에 값을 업데이트하면, 매 연산 시마다 새로운 문자열을 가진 String 인스턴스가 생성되어 메모리공간을 차지하게 되지만, StringBuffer / StringBuilder 는 가변성 가지기 때문에 동일 객체내에서 문자열 크기를 변경하는 것이 가능합니다.

 

 

따라서 값이 변경될 때마다 새롭게 객체를 만드는 String 보다 훨씬 빠르기 때문에, 문자열의 추가, 수정, 삭제가 빈번하게 발생할 경우라면 String 클래스가 아닌 StringBuffer / StringBuilder를 사용하는 것이 이상적이라 말할 수 있습니다.

 

 

 

String 객체일 경우 매번 별 문자열이 업데이트 될때마다 계속해서 메모리 블럭이 추가되게 되고, 일회용으로 사용된 이 메모리들은 후에 Garbage Collector(GC)의 제거 대상이 되어 빈번하게 Minor GC를 일으켜 Full GC(Major Gc)를 일으킬수 있는 원인이 됩니다. 

 

 

반면 StringBuffer는 위 사진 처럼 자체 메모리 블럭에서 늘이고 줄이고를 할수 있기 때문에 훨씬더 효율적으로 문자열 데이터를 다룰 수 있다는 것을 볼 수 있다.

 

 

 

 

StringBuffer && StringBuilder

문자열 연산을 추가하거나 변경 할 때 주로 사용하는 자료형입니다.

 

 

물론 String 자료형만으로도,  '+' 연산이나 concat( ) 메소드로 문자열을 이어붙일수 있지만 '+' 연산자를 이용해 String 인스턴스 문자열을 결합하면, 내용이 합쳐진 새로운 String 인스턴스를 생성하게 되어, 따라서 문자열을 많이 결합하면 결합할수록 공간의 낭비뿐만 아니라 속도 또한 매우 느려지는 단점이 있습니다.

 

 

그래서 자바에서는 이러한 문자열 연산을 전용으로 하는 자료형을 제공하는데 StringBuffer 클래스는 내부적으로 버퍼 & buffer 라고 하는 독립적인 공간을 가지게 되어, 문자열을 바로 추가할 수 있는 공간의 낭비도 없으며 문자열 연산 속도가 매우 빠르다는 특징이 있습니다.

 

 

그리고 기본적으로 StringBuffer의 버퍼 크기의 기본값은 16개의 문자를 저장할 수 있는 크기이며, 생성자를 통해 그 크기를 별도로 설정할 수 있습니다. 

 

 

StringBuffer 내장 메소드

String str = "abcdefg";
StringBuffer sb = new StringBuffer(str); // String -> StringBuffer

System.out.println("처음 상태 : " + sb); // 처음상태 : abcdefg

System.out.println("문자열 String 변환 : " + sb.toString()); // StringBuffer를 String으로 변환하기

System.out.println("문자열 추출 : " + sb.substring(2,4)); // 문자열 추출하기

System.out.println("문자열 추가 : " + sb.insert(2,"추가")); // 문자열 추가하기

System.out.println("문자열 삭제 : " + sb.delete(2,4)); // 문자열 삭제하기

System.out.println("문자열 연결 : " + sb.append("hijk")); // 문자열 붙이기

System.out.println("문자열의 길이 : " + sb.length()); // 문자열의 길이구하기

System.out.println("용량의 크기 : " + sb.capacity()); // 용량의 크기 구하기

System.out.println("문자열 역순 변경 : " + sb.reverse()); // 문자열 뒤집기

System.out.println("마지막 상태 : " + sb); // 마지막상태 : kjihgfedcba

 

그렇다면 StringBuffer / StringBuilder를 사용하는 것이 좋은거구나 ?!

생각할 수 있습니다. 그러나 그건 상황에 따라 다릅니다.

StringBuffer나 StringBuilder을 생성할 경우 buffer의 크기를 초기에 설정해줘야하는데 이러한 동작으로 인해 무거운 편에 속합니다.

 

 

그리고 StringBuffer나 StringBuilder에서 문자열 수정을 할 경우에도 마찬가지로 버퍼의 크기를 늘리고 줄이고 명칭을 변경해야하는 내부적인 연산이 필요하므로 많은 양의 문자열 수정이 아니라면 String 객체를 사용하는것이 오히려 나을 수 있습니다.

String 클래스는 크기가 고정되어 있으므로 단순하게 읽는 조회 연산에서는 StringBuffer나 StringBuilder 클래스보다 빠르게 읽을 수 있습니다.

 

 

따라서 정리하자면, 문자열 추가나 변경등의 작업이 많을 경우에는 StringBuffer를, 문자열 변경 작업이 거의 없는 경우에는 그냥 String을 사용하는 것만 기억하면 됩니다.

 

 

StringBuffer 과 비슷한 자료형으로는 StringBuilder 자료형이 있습니다. 사용법도 둘이 같습니다.

 

 

그렇다면 이 둘의 차이는 무엇일까요?

 

 

StringBuilder는 StringBuffer는 멀티 스레드 완경에서 안전하다는 장점이 있고, StringBuilder는 문자열 파싱 성능이 가장 우수하다는 장점이 있습니다.

 

 

 

StringBuffer vs StringBuilder 차이점

StringBuffer와 StringBuilder 클래스는 둘 다 크기가 유연하게 변하는 가변적인 특성을 가지고 있으며, 제공하는 메서드도 똑같고 사용하는 방법도 동일합니다.

 

 

그럼 이 둘의 차이점은 무엇일까? 어렵게 생각할 필요없이, 사실 둘의 차이는 딱 한가지입니다.
바로 멀티 쓰레드(Thread)에서 안전(safe)하냐 아니냐 이 차이 뿐입니다.

 

 

 

  • StringBuffer 클래스는 쓰레드에서 안전하다. (thread safe)
  • StringBuilder 클래스는 쓰레드에서 안전하지 않다.(thread unsafe) 

 

 

두 클래스는 문법이나 배열구성도 모두 같지만, 동기화(Synchronization)에서의 지원의 유무가 다릅니다. 

StringBuilder는 동기화를 지원하지 않는 반면, StringBuffer는 동기화를 지원하여 멀티 스레드 환경에서도 안전하게 동작할 수 있습니다.

 

그 이유는 StringBuffer는 메서드에서 synchronized(동기화)키워드를 사용하기 때문입니다.

더보기

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

 

 

글로만 보면 이게 정확히 어떤 현상인지 와닿지 않습니다. 직접 StringBuilder와 StringBuffer를 코드로 테스트 결과값을 보겠습니다.

 

 

아래의 예제는 StringBuilder와 StringBuffer 객체를 선언하고, 두개의 멀티 쓰레드를 돌려 StringBuilder와 StringBuffer 객체에 각각 1 요소를 1만번 추가하는(append) 로직을 수행한 코드입니다.

 

 

두개의 쓰레드가 있고 한개의 쓰레드에서 배열요소를 1만번 추가하니 문자열 배열의 길이는 20000이 된다고 유추할 수 있습니다.

import java.util.*;

public class Main extends Thread{
  public static void main(String[] args) {
    StringBuffer stringBuffer = new StringBuffer();
    StringBuilder stringBuilder = new StringBuilder();

    new Thread(() -> {
        for(int i=0; i<10000; i++) {
            stringBuffer.append(1);
            stringBuilder.append(1);
        }
    }).start();

    new Thread(() -> {
        for(int i=0; i<10000; i++) {
            stringBuffer.append(1);
            stringBuilder.append(1);
        }
    }).start();

    new Thread(() -> {
        try {
            Thread.sleep(2000);

            System.out.println("StringBuffer.length: "+ stringBuffer.length()); // thread safe 함
            System.out.println("StringBuilder.length: "+ stringBuilder.length()); // thread unsafe 함
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }).start();
  }
}
StringBuffer.length: 20000
StringBuilder.length: 19628

 

하지만 위의 결과를 보면 다른 값이 나온 것을 볼 수 있습니다.

 

 

StringBuilder의 값이 더 작은 것을 볼 수 있는데, 이는 쓰레드들이 동시에 StringBuilder 클래스에 접근하여 동시에 append() 를 수행하는데 몇번 씹혀서 제대로 수행이 안되어 일어난 결과라고 보면 됩니다.

StringBuilder는 Thread safe 하지 않아서 각기 쓰레드가 객체에 접근해서 변경을 하면 기다려주지 않기 때문에 이러한 현상이 발생한 것이다.

 

 

이와 달리 StringBuffer는 멀티 쓰레드(multi thread)환경에서, 한 쓰레드가 append() 를 수행하고 있을경우 다른 쓰레드가 append() 를 수행을 동시에 하지 못하도록 잠시 대기를 시켜주고 순차적으로 실행하게 합니다. 이처럼 동시에 접근해 다른 값을 변경하지 못하도록 하므로 Thread Safe로서 정상적으로 2만개의 배열요소가 추가된 것입니다.

 

 

그래서 web이나 소켓환경과 같이 비동기로 동작하는 경우가 많을 때는 StringBuffer를 사용하는 것이 안전하다는 것을 알수가 있습니다.

 

 

그렇다면 순수하게 이 두 클래스의 성능은 누가 더 우월할까요?

 

 

문자열 연산 수행 시간 결과를 보면, 기본 성능은 StringBuilder 클래스가 우월하다는 것을 알 수 있습니다. 

 

 

StringBuffer와 StringBuilder 의 차이는 쓰레드 안정성에 있다고 학습했는데, 아무래도 쓰레드 안전성을 버린 StringBuilder가 좀더 덜 따지고 연산을 하니 당연히 좀 더 빠를 수 밖에 없다고 생각합니다.

 

 

 

위 그래프를 보면 10만번 이상의 연산시 String 객체는 수행시간이 기하급수적으로 늘어나지만, StringBuilder와 StringBuffer는 1000만번까지 버티며, 그 이상은 StringBuilder가 우월하다는 것을 볼 수 있습니다.

 

 

그래서 만약 싱글 쓰레드 환경에서나 비동기를 사용할 일이 없으면, StringBuilder를 쓰는 것이 이상적이라 할 수 있습니다.

 

 

하지만 현업에서는 자바 어플리케이션을 대부분 멀티 스레드 이상의 환경에서 돌아가기 때문에 왠만하면 안정적인 StringBuffer로 통일하여 코딩하는것이 좋습니다. (솔직히 StringBuffer 와 StringBuilder 속도 차이는 거의 미미하다고 합니다.)

 

 

 

정리하자면

  String StringBuffer StringBuilder
가변여부 불변 가변 가변
스레드 세이프 O O X
연산 속도 느림 빠름 아주 빠름
사용 시점 문자열 추가 연산이 적고,
스레드 세이프 환경
문자열 추가 연산이 많고,
스레드 세이프 환경
문자열 추가 연산이 많고,
빠른 연산이 필요한 경우
단일 스레드 환경

 

 

String 

  • 불변성
  • 문자열 연산이 적고 변하지 않는 문자열을 자주 사용할 경우
  • 멀티쓰레드 환경일 경우

StringBuilder

  • StringBuilder는 가변성
  • 문자열의 추가, 수정, 삭제 등이 빈번히 발생하는 경우
  • 동기화를 지원하지 않아, 단일 쓰레드이거나 동기화를 고려하지 않아도 되는 경우
  • 속도면에선 StringBuffer 보다 성능이 좋습니다.

StringBuffer

  • StringBuffer는 가변성
  • 문자열의 추가, 수정, 삭제 등이 빈번히 발생하는 경우
  • 동기화를 지원하여, 멀티 스레드 환경에서도 안전하게 동작

 

 

 

참고자료