[Java] String, StringBuffer, StringBuilder 총정리

반응형

오늘은 Java의 String에 대해서 이야기해보겠습니다. 

Java에는 문자열을 표현하기 위한 3가지 클래스가 있습니다. 

 

  • String
  • StringBuffer
  • StringBuilder

 

3개 모두 String을 표현하기 위한 클래스이지만 여러분들이 짚어야할 것이 있는데요. 이 3가지는 모두 문자열을 표기하기 위한 것이지만 각자 구현되는 방법이 전혀 다른 클래스들입니다.

 

 

 

See Java Reference

자바 레퍼런스 문서에 나와 있는 세 가지의 클래스는 다음과 같이 설명되어 있습니다

 

  • String
    Strings are constant; their values cannot be changed after they are created.
    String은 상수이며, 값을 만든 뒤에는 변경할 수 없다 (Immutable)

  • StringBuffer
    A thread-safe, mutable sequence of characters. A stringbuffer is like a String, but can be modified.
    StringBuffer는 String과 유사하지만 변경할 수 있으며 thread-safe하다. (Mutable)

  • StringBuilder
    A mutable sequence of characters. This class is designed for use as a drop-in replacement for StringBuffer in places where the string buffer was being used by a single thread (as is generally the case).

    StringBuilder는 StringBuffer와 호환되지만 단일 스레드(Single thread)에서의 StringBuffer 대체품이다 

 

Java 레퍼런스에서 볼 때는 String, StringBuffer, StringBuilder 모두 문자열을 표현하기 위한 클래스들이었습니다. 다만 String은 불변이고, StringBuffer와 StringBuilder는 가변이며 이 두 가지에서도 멀티 스레드에서도 잘 동작하는 클래스와 그렇지 못한 클래스로 구분이 됩니다.

 

 

 

String is immutable ?

Java API 문서에서 보셨다시피 String은 불변(Immutable)입니다. 따라서 한 번 String 클래스로 정의된 변수는 수정이 불가능합니다. 그런데...

 

위 코드를 작성하고 실행하면 오류가 나타나지 않고, 정상적으로 실행됩니다.

아니, String 클래스는 불변이라고 했는데, 그러면 str의 값은 바뀔 수 없어야 하는 것이 아닌가요?

 

좀 더 구체적으로 살펴보기 위해 String 클래스의 코드를 가져와봤습니다. String은 value라는 멤버 변수에 값을 담는데, 이 값은 final로 선언되어 있는데다 private으로 되어 있어, 외부에서는 접근도, 수정도 전혀 안되는 상황입니다.

 

따라서 불변(Immutable) 객체인 것입니다. 그런데, 어떻게 값이 바뀔 수 있는 것일까요?

 

System.identityHashCode 함수를 이용해 객체의 변화를 체크해봤습니다. str 변수에 K.I.D를 추가했을 때 객체의 주소값이 달라지는 것을 알 수 있는데요. 

 

String 클래스는 이처럼 변수에 변화가 생기면 새로운 String 인스턴스를 생성하고, 교체하는 방식으로 문자열의 변화를 주게 됩니다. 따라서 기존의 객체를 변화하는 것이 아닌 새로운 값을 하나 만들게 되는 것이죠.

 

Java는 클래스를 정의하고 인스턴스를 생성하는 시간이 비싸기 때문에 이런 현상이 자주 반복되면 성능에 좋지 않은 영향을 미치게 됩니다.

 

 

StringBuffer / StringBuilder

String이 불변 객체였다면, 하나의 객체에서 여러번 문자열을 지웠다 썼다 할 수 있는 녀석이 바로 StringBuffer입니다. 그와 비슷한 기능을 하는 클래스에는 StringBuilder가 있습니다.

 

하지만 두 클래스 모두 String 인스턴스를 직접 만드는 클래스는 아닙니다. 먼저 Buffer, Builder에서 문자열 작업을 이룬 다음, 최종적으로 toString() 메서드를 통해 String 클래스를 반환하는 형식으로 String 인스턴스를 만들게 됩니다.

 

StringBuffer 클래스의 sb 변수를 만들고 append를 이용해서 memory에 값을 append 하는 형식으로 문자열을 뒤에 끼워 붙일 수 있습니다. System.identityHashcode 함수로 확인해보면 sb는 새로운 객체로 변경되지 않고, 원래 객체에서 메모리의 값만 변경하는 방식으로 이뤄지는 것을 볼 수 있습니다.

 

같은 코드로 StringBuffer 클래스를 StringBuilder 클래스로만 변경하면 동일하게 동작하는 것을 알 수 있습니다. StringBuffer와 StringBuilder는 모두 AbstractStringBuilder 클래스를 부모 클래스로 가지고 있으며 capacity도 기본값이 16으로 동일합니다.

 

그럼에도 불구하고, 두 클래스에는 차이가 있는데, 바로 syncronization의 적용 유무입니다. StringBuffer에는 반환 메서드에 syncronized가 적용되어 있지만 StringBuilder에는 적용되어 있지 않습니다.

 

반환하는 클래스에 따라서 구분할 수 있으며 StringBuffer에만 syncronized가 적용된 모습입니다. 따라서 두 값을 스레드를 사용해 비교해보면 아래와 같습니다.

 

3개의 스레드를 생성하여 각각의 스레드가 StringBuffer 변수와 StringBuilder 변수에 접근하도록 했습니다. 

 

​여러번 실행해보면 StringBuffer 변수의 길이는 380으로 동일하지만 StringBuilder 변수는 늘 변하는 것을 볼 수 있습니다. 이는 StringBuffer가 좀 더 thread-safe하게 동작한다는 것입니다. StringBuffer에서는 두 개 이상의 스레드가 동시에 해당 자원을 동시에 점유할 수 없도록 하기 때문이죠.

 

따라서 자원 동시 점유 이슈를 바라보는 서버 프로그래밍의 경우는 StringBuffer가 더 안전합니다.

 

 

 

String is Primitive type ?

앞서 String은 클래스라고 설명한 바 있습니다. 그런데, 코드에서는 String 클래스의 변수를 생성할 때 Primitive type처럼  Literal(리터럴, 소스 코드에 표기하는 상숫값) 형태로 선언한 것을 볼 수 있습니다.

 

사실 이렇게 표시한 코드는 new String("Neon")처럼 new String 연산자를 생략한 것이지, Primitive type은 아닙니다. Java에서 String은 Literal처럼 사용할 수 있는 유일한 클래스입니다. 

 

그 근거로 아래의 코드를 볼 수 있습니다.

 

우리가 Primitive type을 비교할 때는 연산자 "==" 를 사용합니다. 하지만 클래스에서는 "=="를 사용해 비교하면 객체의 값을 비교하기 때문에 false로 표시합니다. 따라서 이럴 때는 Object 클래스가 제공하는 equals 메서드를 이용해야 합니다. 

 

equals 메서드를 사용했을 때 true를 표시하므로 String은 Primitive type이 아니라는 것을 알 수 있습니다.

그런데....

 

Primitive type처럼 String 클래스를 Literal로 선언하고 비교하니 equals랑 똑같이 비교가 잘되는데요?

 

 

 

String Pool

분명, new String 연산자를 통해서는 "==" 연산자를 통해 두 문자열 값을 비교하지 못하고, 객체의 주소값을 비교해 false가 나온 것을 알 수 있었는데요. 

 

Literal로 선언해서 비교했을 때는 "==" 연산자를 통해서도 두 문자열 값을 잘 비교했는데, 이것이 가능한 이유는 바로 new String 연산자를 사용했을 때와 Literal로 선언했을 때 문자열 생성 방법이 다르기 때문입니다.

 

이번엔 3개의 변수를 가지고 보겠습니다. 첫 번째, 두 번째 변수는 Literal을 사용했고, 세 번째 변수는 new String 연산자를 사용한 모습입니다. 각각 비교했을 때 Literal 끼리는 서로 연산자로 비교되고, new String은 연산자로는 비교가 안되는 모습입니다.

 

Java에서 String을 효율적으로 사용하기 위해 String Pool(String Constant Pool)이라는 영역을 Heap 영역에 별도로 예약하여 사용합니다. String Pool은 기본적으로 HashMap 형태를 지니고 있으며 미리 선언해둔 상수 문자열이 풀에 존재하는 경우 stack 영역에 생성된 변수가 이를 접근할 때 해당 객체의 참조값 리턴해 메모리를 절약하는 용도입니다.

 

따라서 fir 변수와 sec 변수는 String Pool에서 같은 Heap 영역을 참조하고 있기 때문에 "==" 연산자를 통해서도 true가 나오는 것입니다.

 

String 클래스에서는 intern() 메서드가 있습니다. new String 연산자를 사용했더라도 String Pool에서 그 값을 참조하도록 하고 싶다면 intern() 메서드를 사용하면 됩니다. 만약, String Constant Pool에 해당 문자열이 없다면 풀에 그 문자열이 생성됩니다.

 

 

 

마치며...

Java의 String에 대해 좀 더 심오하게 알아봤습니다. String은 우리가 프로그래밍 언어를 쉽게 접할 수 있는 매개체라고 생각하지만 어떻게 쓰냐에 따라서 성능을 좌지우지 할 수 있는 부분 또한 존재합니다.

 

특히 Java의 경우는 무심코 인터프리터 언어처럼 String을 사용하는 경우, 성능 차이에 많은 영향을 주게 됩니다. 이전 Java, String Constant Pool이 없던 Java에서는 + 연산자처럼 String 불변 객체에 계속 새로운 문자열을 추가해주게 되는 경우 Heap 영역에 새로운 객체가 생성되어 불필요한 메모리 공간을 차지하며 GC가 자주 발생하는 성능 저하를 발생시킵니다.

 

불변성이라는 장점을 누리면서 성능에 부담을 덜준 것이 바로 String Constant Pool이며 이를 잘 숙지하여 String 클래스를 이용하시면 좋겠습니다.

 

 

반응형

Tistory Comments 0