[Java] String, StringBuilder, StringBuffer 총 정리 (feat. String 의 Stringbuilder 변환)

2022. 1. 14. 10:11Backend/☕️ Java

String vs StringBuffer/StringBuilder

자바에서 문자열을 다룰 때 사용하는 대표적인 클래스로 String, StringBuffer, StringBuilder 가 있습니다. 

문자열을 다루는데 서로 다른 3개의 클래스가 존재하는 이유가 무엇일까요?

String 의 불변성


String 과 StringBuilder/Buffer 클래스의 가장 큰 차이점은 불변성에 있습니다. 불변성은 이름 그대로 변하지 않는 성질을 의미합니다. 그럼 String 클래스가 불변성을 띈다는 의미는 무엇일까요?

String str = "java";
str = str + "chip";

>>> str
>>> "javachip"


String 클래스는 불변성을 띄기 때문에 변하지 않는다고 했습니다. 그런데 string1 객체는 "java" 가 아닌 "java language" 라는 값으로 변경됐네요. 전혀 불변적이지 않다고 생각할 수 있습니다. 

그림으로 왜 String이 불변의 객체인지 알아보겠습니다.

"java" 문자열을 저장한 str 변수에, "chip" 이 더해지면서 "javachip" 이라는 문자열을 지닌 것 처럼 보입니다.

하지만 실제로는 기존의 "java" 를 저장한 메모리 영역은 str 과의 참조가 끊어지면서 GC 의 수거대상이 됩니다. "java" 를 저장한 메모리 영역이 "javachip" 으로 변경되는 것이 아닙니다.

즉 매번 문자열에 변경이 생길 때마다 기존의 문자열을 저장한 메모리 영역이 버려져 메모리 공간만 차지하게 되는 (메모리 부족) 상황을 유발하게 됩니다.

 

StringBuffer, StringBuilder

String 의 immutable 한 속성으로 인해, 문자열을 자주 수정해야하는 상황에서 메모리 낭비 문제가 발생합니다. 이를 해결하기 위해 StringBuffer 와 StringBuilder 가 등장하였습니다. 이 둘은 String 과 달리 mutable(가변적) 속성을 가지기 때문에 .append() 등의 API 를 이용하여 동일한 객체의 문자열을 변경하는 것이 가능합니다. 즉 가변적으로 크기가 변할 수 있는 것이죠. 문자열이 변경됐다고 하여 기존 객체를 버리는 String 과 상반된 모습입니다. 

정리하자면 문자열의 수정(추가/삭제 등) 이 빈번히 발생하는 경우 라면 String 이 아닌 StringBuilder 나 StringBuffer 를 사용하여 메모리 낭비를 방지할 수 있습니다.

 

StringBuffer  StringBuilder 차이점

가장 큰 차이점은 Thread safe 의 유무입니다. StringBuffer 는 Thread safe 한 반면, StringBuilder 는 그렇지 않습니다. (Buffer가 붙었으니 safe하겠구나, 그래서 thread safe ~ 이런식으로 외웠습니다.)

StringBuffer 와 StringBuilder 는 내부적으로 제공하는 메서드도 같고, 사용하는 방법도 동일합니다. 그런데 어떻게 StringBuffer 는 thread safe 한 특성을 가지게 됐을까요? 바로 내부 메서드 별로 synchronized 키워드가 선언되기 때문입니다. 

StringBuilder, StringBuffer 클래스 내부 메서드 선언 부분. 동일한 로직이지만 synchronized 의 유무 차이가 있다.

synchronized 키워드가 선언된 StringBuffer 는 멀티스레드 환경에서 안전하게 사용할 수 있습니다. 하지만 동기화 작업을 거쳐야하기 때문에 StringBuilder 에 비해 속도 성능은 떨어집니다.

StringBuilder 는 동기화를 지원하지 않기 때문에 단일 스레드 환경에서 사용이 적합하며, 해당 환경에서 StringBuffer 보다 빠른 성능을 낼 수 있는 장점이 있습니다. 

즉 개발 스레드 환경에 따라 필요한 클래스를 적절히 사용할 줄 알아야 합니다. 

 

String 이 Thread Safe 하다는 점이 의아할 수 있습니다. String 은 final 로 선언된  클래스로 값이 바뀔 일이 없기 때문에 멀티스레드 환경에서도 안전하게 공유될 수 있습니다. 스레드가 만약 String 객체의 값을 변경한다면, 동일한 문자열을 수정하는 것이 아닌 String pool 에 새로운 문자열이 생성하므로 thread safe 를 지키게 됩니다. 

 

String 을 사용해야 하는 경우

StringBuffer , StringBuilder 를 생성할 경우 buffer 의 크기를 초기에 설정해야 하는데, 개발자가 명시적으로 설정하지 않는다면 자동으로 16 크기로 설정됩니다. 만약 저장되는 문자열이 이보다 길 때는 16보다 큰 값이 지정됩니다. 

바로 이러한 초기 크기 설정 작업으로 인해 String 객체보다 생성속도가 떨어질 수 있습니다. StringBuffer/Builder 의 mutable 속성 덕분에 메모리 측면에서는 String 에 비해 이점이 있을 수 있지만, 많은 양의 문자열 수정이 필요한 경우가 아니라면 내부적으로 많은 연산(버퍼 조절)을 피하기 위해 String 을 사용하는 것이 나을 수 있습니다. 

 

String 이 StringBuilder 로 자동 컴파일 된다?

StringBuilder 는 String 의 immutable 속성의 문제점 해결을 위해 도입됐습니다. 

JDK 1.5 의 StringBuilder 도입으로 String 의  + 연산시 StringBuilder 로 자동 변환되며 성능 최적화를 이룹니다.

그렇다고 해서 항상 String 이 StringBuilder 로 변환되는 것도 아닙니다. 아래 stackoverflow 의 설명을 참고해보면, 

 

즉, 한 줄(Single Statement) 로 문자열을 + 연산하면, 컴파일러가 자동으로 String 을 StringBuilder 로 변환하기 때문에 성능 최적화가 이뤄집니다.

String a = "b" + "c" + "d";

 

반면 SingleStatement 가 아닌 for 문 같은 반복문의 경우엔 최적화 하지 못하기 때문에 StringBuilder 를 직접 사용하는것이 좋다는 내용입니다.

정리하자면 SingleStatement 에서는 compiler 에 의해 String 이 StringBuilder 로 자동 변환되지만, 그 외의 경우에는 자동 변환이 이뤄지지 않기 때문에 개발자가 직접 StringBuilder 를 선언하여 사용하는 것이 최적화에 도움이 됩니다. 하지만 StringBuilder 역시 새로 생성되는 인스턴스이기 때문에 성능을 고려하며 사용할 것을 권장합니다.