String 파헤치기

Updated: Categories:

String의 동작원리와 StringBuffer, StringBuilder에 대해 알아보자

String

  • 우선 String을 생성하는 방법에는 두가지가 있다
    • literal : ""를 사용하여 생성하기
    • Object : new를 사용하여 생성하기

리터럴 방식은 평소에 자연스럽게 사용하는 방식이고, 다른 방식으로 선언할 생각을 안해봤다..
String도 참조타입이니까 Object 방식으로 선언할 수도 있는데 말이다

우선 리터럴 방식의 동작원리부터 살펴보자

리터럴 방식으로 초기화

String str1 = "a";
String str2 = "a";

System.out.println(str1 == str2);  // -> true

동일한 문자열 변수의 주소값을 비교했을 때 true가 나온다. (??)
str1과 str2는 다른 객체니까 주소값이 달라야 하는게 아닌가?
이는 리터럴 방식으로 생성한 String이 불변(Immutable) 객체라는 것과 String Constant Pool의 이해가 필요하다.

위의 예제는 JVM 메모리 영역에 아래와 같이 저장된다.

string

분명 String은 참조 타입으로 객체이지만, 동일한 문자열을 선언할때 메모리를 새롭게 할당받지 않고 기존 주소를 가리킨다.
리터럴 방식으로 생성했을 경우, 이렇게 String Constant Pool 메모리 영역에 문자열이 저장되게 된다.

그럼 new를 사용하여 인스턴스를 생성하게 되면 어떻게 동작할까?

new 방식으로 초기화

String str3 = new String("a");
String str4 = new String("a");

System.out.println(str3 == str4);  // -> false

리터럴 방식과 달리 두 변수가 서로 다른 주소값을 가지는 것을 알 수 있다.

JVM 메모리 영역에서는 아래와 같이 저장된다.

분명 String Constant Pool에 동일한 문자열이 있었음에도 pool을 사용하지 않고 메모리를 새롭게 할당받는다.
또한 동일한 인스턴스를 생성해도 주소값을 공유하지 않고 계속 새롭게 할당되는 것을 알 수 있다.
결론적으로 String을 리터럴 방식으로 초기화하여 사용하는것이 효율적인 것이다.

리터럴 방식으로 초기화할 땐 String이 불변객체로 동작하는데, 이는 문자열의 변경과정을 살펴보면 더 쉽게 이해할 수 있다.

리터럴 문자열 변경

String str1 = "a";
String str2 = "a";

str2 = str2 + "b";

위와 같이 문자열 변수에 “b”를 덧붙여 주었다.

이 동작을 메모리 영역에서 살펴보면 아래와 같다.

단순히 “b”를 추가하는데도 “b”가 String Constant Pool에 추가된다.
String Constant Pool에 있는 문자열들은 불변객체로 동작하기 때문에 이런 일이 발생하는 것이다.

따라서 String에서의 무분별한 추가, 삭제 연산은 메모리 낭비를 발생시킨다는 것을 알 수 있다.
StringBufferStringBuilder와 같은 컬렉션을 사용한다면 이런 메모리 누수를 방지할 수 있다.


StringBuffer

  • 내부적으로 구현된 buffer가 존재해 문자열 변경을 효율적으로 도와주는 클래스

내부는 어떻게 생겼을까?

public final class StringBuffer implements java.io.Serializable{
    private char[] value;
        ...

    // 1. 버퍼 크기 지정할 수 있는 생성자
    public StringBuffer(int length){
        value = new char[length];
        shared = false;
    }
    // 2. Default 생성자
    public StringBuffer(){
        this(16);
    }
    // 3. 문자열로 생성
    public StringBuffer(String str){
        this(str.length() + 16);
        append(str);
    }
}

StringBuffer는 내부적으로 버퍼라고 부르는 char 가변 배열을 가진다.
입력값 없이 Default로 객체 생성시 버퍼의 기본크기는 16이다
문자열을 입력해 생성시 버퍼의 크기는 해당 문자열 길이 + 여유분 16으로 생성된다

StringBuffer로 문자열 변경하기

StringBuffer sb = new StringBuffer("abc");
sb.append("123");
StringBuffer sb2 = sb.append("ZZ");

이 동작을 메모리 영역에서 살펴보면 아래와 같다.

String과 달리 StringBuffer는 내부 버퍼가 있기에, 불변객체를 따로 생성할 필요가 없다.
문자열 변경시 내부 버퍼만 수정해주면 되기 때문이다.

그럼 StringBuffer와 비슷한 StringBuilder는 어떤 차이점을 가지고 있을까?


StringBuilder

  • 기본적인 기능은 StringBuffer와 같다.
  • StringBuffer는 멀티스레드 환경에서 thread safe 하도록 동기화되어 있는 반면, StringBuilder는 비동기적으로 동작한다.
  • 따라서 멀티스레드 환경이 아닌 경우, StringBuilder의 성능이 더 뛰어나다.
// StringBuffer와 선언이랑 사용법까지 다 똑같음
StringBuilder sb = new StringBuilder("");