바쁜 분은 이것만
- 한 줄에서 몇 개 더하는 건
+로 충분 — 반복문 안에서 계속 붙일 때만StringBuilder - 쉼표로 이어 붙이기는
String.join(",", list)가 가장 간단 - StringBuilder끼리 비교는
sb1.toString().equals(sb2.toString())(또는compareTo)
자바에서 문자열을 붙일 때 "반복문에서는 + 대신 StringBuilder를 써라"는 말을 많이 듣습니다. 얼마나 차이 나는지 직접 재 봤어요. macOS(Apple M4 Pro), OpenJDK 27에서 실행한 결과입니다.
1. 반복문에서 += 와 StringBuilder 비교
static long plus(int n) {
long t = System.nanoTime();
String s = "";
for (int i = 0; i < n; i++) s += i + ",";
return (System.nanoTime() - t) / 1_000; // 마이크로초
}
static long sb(int n) {
long t = System.nanoTime();
StringBuilder b = new StringBuilder();
for (int i = 0; i < n; i++) b.append(i).append(',');
String s = b.toString();
return (System.nanoTime() - t) / 1_000;
}
숫자 0부터 n-1까지를 쉼표로 이어 붙였고, 측정 전에 한 번씩 미리 돌려 놓았습니다. 3번 실행한 결과(단위 µs = 100만분의 1초):
| 반복 횟수 | += (3회) | StringBuilder (3회) |
|---|---|---|
| 1,000 | 940 / 946 / 1,064 | 109 / 205 / 199 |
| 10,000 | 9,560 / 10,062 / 11,001 | 264 / 260 / 283 |
| 50,000 | 341,611 / 268,729 / 246,711 | 470 / 509 / 484 |
| 100,000 | 624,355 / 619,070 / 626,945 | 503 / 480 / 480 |
10만 번이면 +=는 약 0.62초, StringBuilder는 약 0.0005초로 1,000배 넘게 차이 났습니다. 1,000번 정도에서는 둘 다 1밀리초(0.001초) 안팎이라 체감할 일이 없어요.
2. 왜 이렇게 차이 날까
자바의 String은 한번 만들면 바꿀 수 없습니다(불변). 그래서 s += "x"는 기존 s 뒤에 붙이는 게 아니라, 기존 내용 + "x"를 담은 새 문자열을 매번 새로 만들고 앞의 것을 버립니다. 문자열이 길어질수록 매번 복사하는 양도 늘어나서, 횟수가 늘면 시간이 훨씬 빠르게 커져요(1만 → 10만, 10배 늘었는데 시간은 60배 넘게).
StringBuilder는 안에 여유 공간을 두고 그 뒤에 계속 이어 쓰다가, 마지막에 toString()으로 한 번만 String을 만듭니다.
3. 그럼 + 는 쓰면 안 되나? 아니다
String x = "a" + "b" + 1 + 2;
String y = 1 + 2 + "a" + "b";
"a"+"b"+1+2 = ab12 / 1+2+"a"+"b" = 3ab
한 줄에서 몇 개 붙이는 건 +로 써도 됩니다. 코드가 훨씬 읽기 쉽고, 위 표처럼 문제가 되는 건 반복문 안에서 수천 번 이상 계속 붙일 때예요.
다만 위 결과처럼 계산 순서는 조심해야 합니다. 왼쪽부터 계산해서, 문자열이 먼저 나오면 뒤의 숫자는 글자로 붙고(ab12), 숫자가 먼저면 먼저 더해집니다(3ab).
4. StringBuilder 자주 쓰는 메서드
StringBuilder b = new StringBuilder("hello");
b.append(" world").insert(0, ">> ").reverse();
System.out.println(b); // dlrow olleh >>
b.reverse();
b.setLength(b.length() - 6); // 뒤에서 6글자 잘라내기
System.out.println(b); // >> hello
append, insert, reverse는 자기 자신을 돌려줘서 이어 쓸 수 있습니다.
5. 마지막 쉼표 지우기 vs String.join
StringBuilder csv = new StringBuilder();
for (String s : List.of("사과", "배", "감")) csv.append(s).append(",");
System.out.println(csv); // 사과,배,감,
System.out.println(csv.deleteCharAt(csv.length() - 1)); // 사과,배,감
String.join(",", List.of("사과", "배", "감")) // 사과,배,감
Stream.of("사과", "배", "감").collect(Collectors.joining(", ", "[", "]")) // [사과, 배, 감]
구분자로 이어 붙이는 거라면 String.join이 한 줄로 끝나고 마지막 쉼표 처리도 필요 없습니다. 앞뒤에 괄호까지 붙이려면 Collectors.joining(구분자, 앞, 뒤)나 StringJoiner를 씁니다.
6. 함정: StringBuilder.equals는 내용 비교가 아니다
StringBuilder s1 = new StringBuilder("abc"), s2 = new StringBuilder("abc");
s1.equals(s2) // false
s1.toString().equals(s2.toString()) // true
s1.compareTo(s2) // 0 (같음)
String과 달리 StringBuilder는 equals()가 "같은 객체인지"만 봅니다. 내용 비교는 toString()으로 바꾼 뒤 하거나 compareTo()가 0인지 보세요.
덤: String 메서드는 결과를 받아야 한다
String str = "abc";
str.concat("def");
System.out.println(str); // abc
str = str.concat("def");
System.out.println(str); // abcdef
String은 불변이라 concat, replace, toUpperCase 모두 새 문자열을 돌려줄 뿐 원래 변수는 그대로입니다. StringBuilder의 append는 자기 자신을 바꾸는 것과 반대예요.
정리
| 상황 | 쓸 것 |
|---|---|
| 한 줄에서 몇 개 붙이기 | + |
| 반복문에서 계속 붙이기 | StringBuilder.append |
| 구분자로 이어 붙이기 | String.join / Collectors.joining |
| StringBuilder 내용 비교 | toString().equals() |
참고: Oracle Java SE API 문서 — String, StringBuilder, String.join, StringJoiner, Collectors.joining
