String 클래스는 잘 사용하면 상관이 없지만 잘못 사용하면 메모리에 많은 영향을 준다.
왜 그럴까?
String은 불변(Immutable) 속성을 가진다. 즉, 문자열을 수정하는 것처럼 보여도 실제로는 수정되는 것이 아니라 매번 새로운 객체를 생성해서 참조를 바꾸는 방식이다. 반면 아래 소개할 두 클래스는 가변(Mutable) 속성을 지녀 내부 버퍼를 직접 수정하므로 메모리 낭비가 적다.
문자열을 만드는 클래스는 String, StringBuffer, StringBuilder 가 많이 사용된다. (모두 CharSequence 인터페이스의 구현체이다.)
StringBuffer 와 StringBuilder 는 사용하는 메소드는 동일하다. 둘의 차이는 스레드에 안전한지의 차이이다.
StringBuffer 는 스레드에 안전하게 설계되어있어 여러개의 스레드에서 하나의 StringBuffer 객체를 처리해도 문제가 발생하지 않는다. 하지만 StringBuilder 는 단일 스레드에서만 안정성을 보장한다.
이 세가지의 실제 성능을 코드로 비교해보겠다. (jdk 17)
public static void main(String[] args) {
int n = 100_000;
long t0 = System.nanoTime();
String s = "";
for (int i = 0; i < n; i++) {
s += i;
}
long t1 = System.nanoTime();
StringBuilder sb = new StringBuilder(n * 2);
for (int i = 0; i < n; i++) {
sb.append(i);
}
long t2 = System.nanoTime();
StringBuffer sbf = new StringBuffer(n * 2);
for (int i = 0; i < n; i++) {
sbf.append(i);
}
long t3 = System.nanoTime();
System.out.printf("String : %4d ms (len=%d)%n", (t1 - t0) / 1_000_000, s.length());
System.out.printf("StringBuilder: %4d ms (len=%d)%n", (t2 - t1) / 1_000_000, sb.length());
System.out.printf("StringBuffer : %4d ms (len=%d)%n", (t3 - t2) / 1_000_000, sbf.length());
}
위 코드는 같은 작업(숫자를 문자열로 이어붙이기)을 10만 번 수행하며 System.nanoTime()으로 String/Builder/Buffer의 시간을 측정하는 코드이다.
String : 781 ms (len=488890)
StringBuilder: 2 ms (len=488890)
StringBuffer : 2 ms (len=488890)
| 특성 | String | StringBuilder | StringBuffer |
|---|---|---|---|
| 가변성 | 불변 (Immutable) | 가변 (Mutable) | 가변 (Mutable) |
| 스레드 안전 | 안전 (불변이므로) | 불안전 (Not Synchronized) | 안전 (Synchronized) |
| 주요 용도 | 단순 참조 및 조회 | 단일 스레드 루프 연산 | 멀티 스레드 환경 |
| 성능 | 연산 시 느림 | 가장 빠름 | 빠름 (동기화 오버헤드 있음) |
반복적인 문자열 결합은 단일 스레드는 StringBuilder, 멀티 스레드는 StringBuffer를 사용하는게 좋다.
for 루프 안 +=를 빌더로 바꾸는 것만으로도 수십~수백 배 개선된다.
new StringBuilder(예상길이)처럼 초기 용량을 잡아두면 리사이즈 비용을 줄여 추가로 빨라진다.
습관적으로 빌더 패턴을 쓰면 불필요한 객체 생성과 GC 부담을 크게 줄일 수 있다.