String 불변성과 StringBuilder
선수학습(1개)
요약
Java의 String은 한 번 만들면 내용을 바꿀 수 없는 불변(immutable) 객체입니다. 반복문에서 + 연산으로 문자열을 누적하면 매번 새 객체가 생기는 성능 함정을, 가변 버퍼인 StringBuilder의 append()로 어떻게 해결하는지 정리합니다.
핵심 정리
| 개념 | 설명 |
|---|---|
String | 불변(immutable) 객체. 연결 시 새 객체가 생성됨 |
StringBuilder | 가변(mutable) 문자열 버퍼. 같은 객체에 누적 |
| 반복 + 연결 | String은 매 반복마다 새 객체, StringBuilder는 객체 1개 유지 |
| 주요 메서드 | append, insert, delete, reverse, toString |
여기서 객체는 '메모리에 만들어진 데이터 덩어리'라고 생각하면 됩니다. 자세한 의미는 객체와 클래스에서 다룹니다.
String 불변성 심화
Java의 String은 불변(immutable) 객체입니다. 한 번 만들어진 문자열 객체의 내용은 절대 바뀌지 않습니다.
변수 s는 객체 그 자체가 아니라 '객체가 저장된 위치(주소)'를 들고 있는 종이쪽지입니다. 종이에 적힌 주소가 바뀔 수는 있지만, 그 주소가 가리키는 객체의 내용물 자체는 바뀌지 않습니다. 이 차이가 '불변'의 핵심입니다. str = str + "A" 처럼 보이는 코드도 실제로는 기존 객체를 수정하는 것이 아니라 새 문자열 객체를 만들어 참조만 바꿉니다. 변수 s가 가리키는 객체가 새 객체로 갈아끼워질 뿐, 옛 객체 자체의 내용이 바뀐 적은 없습니다. 이걸 '객체는 불변'이라고 합니다.
| 단계 | 메모리 상태 | 참조 |
|---|---|---|
String s = "Hello"; | "Hello" 객체 1개 | s → "Hello" |
s = s + " World"; | "Hello", "Hello World" 객체 2개 | s → "Hello World" |

원래의 "Hello" 객체는 그대로 남아 있지만, 더 이상 가리키는 참조가 없으면 Garbage Collector1가 정리합니다. 정확한 시점은 정해져 있지 않고, JVM이 메모리가 필요해질 때 알아서 회수합니다.
반복 연결의 비용
문자열을 반복해서 이어 붙이면, 매 반복마다 새로운 String 객체가 하나씩 생깁니다. 1000번 반복하면 약 1000개의 String 객체가 만들어지고, 매번 기존 문자열을 새 객체로 복사하므로 반복 횟수가 커질수록 처리 시간이 급격히 늘어납니다.
| 반복 | 내부 동작 |
|---|---|
| 1회차 | "" + "x" → "x" 객체 생성 |
| 2회차 | "x" + "x" → "xx" 객체 생성 (기존 "x"는 버려짐) |
| 3회차 | "xx" + "x" → "xxx" 객체 생성 |
| ... | 반복마다 새 객체 누적 |

StringBuilder — 가변 문자열 심화
StringBuilder 는 내용을 수정할 수 있는(가변) 문자열 버퍼입니다. append()로 문자를 덧붙여도 객체가 새로 만들어지지 않고 같은 객체의 내부 배열2에 덧붙습니다.

String vs StringBuilder 비교
| 구분 | String | StringBuilder |
|---|---|---|
| 가변성 | 불변 (immutable) | 가변 (mutable) |
| 반복 연결 객체 수 | 반복마다 새 객체 | 같은 객체 유지 |
| 주요 메서드 | +, concat() | append(), insert(), delete(), reverse() |
| 속도 (반복 연결) | 느림 | 빠름 |
| 용도 | 문자열이 거의 바뀌지 않을 때 | 반복 수정이 필요할 때 |
참조 동일성 차이
StringBuilder는 객체가 바뀌지 않으므로 append() 전후의 참조가 동일합니다. 반면 String은 매 연결마다 새 객체이므로 참조가 바뀝니다.
==는 두 변수가 같은 주소를 가리키는지 비교합니다. 내용이 같아도 다른 주소면 false가 됩니다. 내용 비교는 equals()로 따로 해야 합니다.

StringBuilder 주요 메서드
시작값 "Hello"를 기준으로 각 메서드를 단독 호출했을 때의 결과입니다.
| 메서드 | 동작 | 예시 | 결과 |
|---|---|---|---|
append(x) | 끝에 덧붙임 | sb.append("W") | "HelloW" |
insert(i, x) | i번 자리에 삽입 | sb.insert(0, "A") | "AHello" |
delete(a, b) | a ~ b-1 범위 삭제 | sb.delete(1, 3) | "Hlo" |
reverse() | 문자열 뒤집기 | sb.reverse() | "olleH" |
toString() | String으로 변환 | sb.toString() | "Hello" |
length() | 현재 길이 | sb.length() | 5 |
insert와 delete는 인덱스를 어떻게 다루는지 그림으로 보면 이해가 쉽습니다.
