반응형

바쁜 분은 이것만

  1. 한 줄에서 몇 개 더하는 건 +로 충분 — 반복문 안에서 계속 붙일 때만 StringBuilder
  2. 쉼표로 이어 붙이기는 String.join(",", list)가 가장 간단
  3. 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

반응형

+ Recent posts