Java에서 문자열을 핵심적으로 다루는 클래스는 String, StringBuilder, StringBuffer입니다.
String만 사용해도 문자열을 만드는데 지장은 없을 수 있습니다.
하지만, 이번 시간에 이것들이 대체 무엇인지, 왜 필요한지 알아보겠습니다.
String은 문자열 중 가장 많이 사용됩니다.
어떤 특징들 때문에 사용되는 걸까요?
String의 특징
- 불변(Immutable) 객체
- 문자열 수정 시 새로운 객체가 생성됨
- 성능보다 안정성이 중요한 경우에 적합
불변이란 한 번 생성된 객체의 값이 변경되지 않는 것을 의미합니다.
String 객체는 내부적으로 변경할 수 없는 char[] 배열을 가지고 있으며, 한 번 생성되면 그 값을 바꿀 수 없습니다.
public class StringTest {
public static void main(String[] args) {
String str = "Hello";
str = str + " World";
System.out.println(str); // "Hello World"
}
}
간단한 예시 코드를 가져왔습니다.
이 코드에서 str이라는 변수에 "Hello"라는 데이터를 넣은 상태입니다.
그리고 str 변수에 " World"라는 데이터를 집어넣어서
str을 프린트했을 때 "Hello World"가 되었습니다.
보기엔 값이 변한 것처럼 보이지만 사실 새로운 str이라는 문자열 객체를 만든 것입니다.
원시타입(Primitive Type)은 소문자로 시작합니다.
예) int, double, long, char 등등
하지만 문자열은 항상 'String' 대문자로 시작합니다.
이유는 String은 참조타입이기 때문입니다.
참조타입은 Heap 영역에 데이터를 저장합니다.
Stack 영역에서 주소값을 들고, Heap 영역에서 주소값과 일치하는 데이터 가져와 사용하는 방식입니다.

한발 더 나아가 알아보자면 String 객체는 특별히 String constant pool에서 따로 관리가 됩니다.
🔹 String Constant Pool이란?
String 객체는 일반적인 객체처럼 Heap 메모리에 생성될 수도 있지만, "String Constant Pool"이라는 별도의 공간에서 관리됩니다.
이는 메모리 사용을 최적화하고, 불필요한 중복을 줄이기 위해서 입니다.
다시 Heap으로 돌아가자면, 0x11(그림) 주소값에 있는 데이터는 stack에서 바라보는 존재가 없기 때문에 GC에서 제거해줍니다.
String의 추가 or 변경 발생하는 일
=> 새로운 객체 생성, GC에서 이전 주소값의 데이터 제거
이러한 이유 때문에 변경에는 성능적으로 좋지 않습니다.
그럼 어떤 장점때문에 많이 사용되는 걸까요?
String str1 = "Java";
String str2 = "Java";
System.out.println(str1 == str2); // true (같은 객체를 참조)
위에서 기본이라고 할 수 있는 String에 대해서 알아봤습니다.
그럼 StringBuilder와 StringBuffer는 어떠한지 알아보겠습니다.
StringBuffer와 StringBuilder 클래스가 가진 큰 특징은 String과는 다르게 가변(mutable) 문자열을 나타냅니다.
위에서 얘기한 것처럼 String은 char[]이라는 배열에 있습니다.
이 StringBuffer와 StringBuilder도 char[] 배열로 이루어져있습니다.
그런데 여기서 의문이 생깁니다.
똑같은 배열이고, 자바에서 배열은 크기를 지정해줍니다.
그런데 어떻게 효과적으로 배열의 크기를 늘릴 수 있었을까요?
ArrayList와 굉장히 유사합니다.
거의 문자열 특화로된 ArrayList라고 할 수 있다고 생각합니다.
유사한 특징들을 나열해보겠습니다.
String은 수정 혹은 추가할 때, 매번 새로운 객체 만들었습니다.
하지만 StringBuilder와 StringBuffer는 크기가 부족할 때만, 새로운 객체를 만듭니다.
이러한 이유로 문자열이 변동이 심한 곳에는 StringBuilder와 Buffer를 쓰는게 효율적입니다.
여지껏 보기엔 둘은 다를게 없어보이는데 왜 두개를 따로 만들었을까요?
StringBuffer와 StringBuilder의 주요 차이점은 동기화(synchronization) 지원 여부때문입니다.
동기화(synchronization)는 여러 스레드가 동시에 접근할 때 발생할 수 있는 문제를 해결하기 위한 방법입니다. 여러 스레드가 동시에 공유 자원에 접근하여 데이터를 수정하거나 읽을 때, 데이터의 일관성이 깨질 수 있는 문제가 생깁니다. 동기화는 이러한 문제를 방지하고, 정상적인 순서대로 실행되도록 보장하는 기술입니다.
| 특성 | StringBuilder | StringBuffer |
|---|---|---|
| 동기화 | ❌ | 🟢 |
| 멀티쓰레드 안정성 | ❌ | 🟢 |
| 성능 | 상대적으로 빠름 | 상대적으로 느림 |
| 사용처 | 싱글 쓰레드 환경 | 멀티쓰레드 환경 |
위에 표를 보만 봐도, 만약 프로그램을 만들었을 때 어떤 쓰레드 환경인지 알아야겠다는 생각을 할 수 있습니다.
제대로 파악하지 못한다면 분명 여러 트러블이 생길 것은 자명합니다.
지금까지 String, StringBuffer, StringBuilder 클래스에 대해 알아보았습니다.
각자의 특성이 있고, 상황에 따라 좋을 수도 나쁠 수도 있다는 것을 알았습니다.
자신의 프로젝트의 환경을 이해하고, 상황에 따라 적절히 사용하여 좋은 서비스를 만들기 바랍니다.
F-Lab 자바에서의 문자열 처리: String, StringBuffer, StringBuilder의 이해와 활용