9.4 StringBuilder와 문자열 처리 고급
1. String의 불변성(Immutability)과 성능 문제
String은 한 번 만들어지면 절대 변하지 않는(Immutable) 객체입니다. 문자열에 + 연산을 하면 기존 문자열이 변하는 게 아니라 새로운 String 객체가 매번 생성됩니다.
// 이 코드는 겉으로는 멀쩡해 보이지만...
String result = "";
for (int i = 0; i < 10000; i++) {
result += i; // ← 루프 1만 번마다 새 String 객체 생성! 매우 비효율적
}
왜 문제인가? + 연산 한 번에 내부적으로 새 String 객체가 생성되고, 이전 객체는 GC 대상이 됩니다. 10,000번 반복이면 10,000개의 객체가 생성·소멸되어 메모리와 CPU를 낭비합니다.
2. StringBuilder: 가변(Mutable) 문자열
StringBuilder는 내부에 수정 가능한 버퍼(char[]) 를 갖고 있어서 문자열 추가·삽입·삭제 시 새 객체를 만들지 않습니다.
StringBuilder sb = new StringBuilder(); // 빈 버퍼 (초기 용량 16)
sb.append("Hello"); // Hello
sb.append(", "); // Hello,
sb.append("Java"); // Hello, Java
sb.append("!"); // Hello, Java!
sb.insert(5, " World"); // Hello World, Java! (인덱스 5에 삽입)
sb.delete(5, 11); // Hello, Java!
sb.reverse(); // !avaJ ,olleH
sb.replace(0, 5, "Bye"); // Bye, Java! (아직 reverse 상태이므로 주의)
System.out.println(sb.toString()); // 최종 결과 String으로 변환
System.out.println(sb.length()); // 현재 문자열 길이
System.out.println(sb.charAt(0)); // 특정 위치 문자
실전 예제: 루프에서 올바른 문자열 조합
// ✅ StringBuilder 사용 (권장)
StringBuilder sb = new StringBuilder();
for (int i = 1; i <= 100; i++) {
sb.append(i);
if (i < 100) sb.append(", ");
}
String csv = sb.toString();
System.out.println(csv); // 1, 2, 3, ..., 100
// ✅ String.join으로도 가능 (Java 8+)
List<Integer> numbers = IntStream.rangeClosed(1, 100)
.boxed()
.collect(Collectors.toList());
String result = numbers.stream()
.map(String::valueOf)
.collect(Collectors.joining(", "));
메서드 체이닝
StringBuilder의 대부분의 메서드는 this를 반환하므로 체이닝이 가능합니다.
String result = new StringBuilder()
.append("이름: ")
.append("홍길동")
.append(", 나이: ")
.append(30)
.append("세")
.toString();
System.out.println(result); // 이름: 홍길동, 나이: 30세
3. StringBuilder vs StringBuffer
| 구분 | StringBuilder | StringBuffer |
|---|---|---|
| 스레드 안전 | ❌ 안전하지 않음 | ✅ synchronized (스레드 안전) |
| 성능 | ✅ 빠름 | 느림 (동기화 오버헤드) |
| 사용 시기 | 단일 스레드 (일반적) | 멀티 스레드 환경 |
// 일반적인 상황 (단일 스레드) → StringBuilder
StringBuilder sb = new StringBuilder("Hello");
// 여러 스레드가 동시에 접근하는 경우 → StringBuffer
StringBuffer safeSb = new StringBuffer("Hello");
실무 팁: 현대 자바에서는 멀티스레드 환경의 공유 문자열 처리에는
StringBuffer보다 별도 동기화 전략을 사용하는 경우가 많습니다. 대부분의 상황에서StringBuilder를 사용하세요.
4. 유용한 String 메서드 총정리
String s = " Hello, Java World! ";
// 공백 처리
System.out.println(s.trim()); // "Hello, Java World!" (앞뒤 공백 제거)
System.out.println(s.strip()); // "Hello, Java World!" (유니코드 공백까지 처리, Java 11+)
System.out.println(s.stripLeading()); // "Hello, Java World! " (앞 공백만)
System.out.println(s.stripTrailing()); // " Hello, Java World!" (뒤 공백만)
System.out.println(s.isBlank()); // false (공백만 있으면 true, Java 11+)
// 대소문자
String str = "Hello Java";
System.out.println(str.toUpperCase()); // HELLO JAVA
System.out.println(str.toLowerCase()); // hello java
// 검색
System.out.println(str.contains("Java")); // true
System.out.println(str.startsWith("Hello")); // true
System.out.println(str.endsWith("Java")); // true
System.out.println(str.indexOf("a")); // 7 (첫 번째 'a' 위치)
System.out.println(str.lastIndexOf("a")); // 9 (마지막 'a' 위치)
// 추출
System.out.println(str.substring(6)); // "Java"
System.out.println(str.substring(0, 5)); // "Hello"
System.out.println(str.charAt(0)); // 'H'
// 변환
System.out.println(str.replace("Java", "World")); // "Hello World"
System.out.println(str.replaceAll("\\s+", "_")); // "Hello_Java" (정규식)
System.out.println("a,b,c".split(",").length); // 3
// 비교
System.out.println("hello".equals("HELLO")); // false
System.out.println("hello".equalsIgnoreCase("HELLO")); // true
System.out.println("abc".compareTo("abd")); // -1 (사전 순서 비교)
// 변환 (Java 11+)
System.out.println("Java\n".repeat(3)); // "Java\n" 3번 반복
System.out.println("hello".indent(4)); // 4칸 들여쓰기
// 포맷 (Java 15+)
String name = "홍길동";
int age = 30;
String formatted = "이름: %s, 나이: %d".formatted(name, age);
System.out.println(formatted); // 이름: 홍길동, 나이: 30
5. String.format vs formatted vs 문자열 템플릿
String name = "홍길동";
double gpa = 3.75;
// 방법 1: String.format (전통적)
String s1 = String.format("이름: %s, GPA: %.2f", name, gpa);
// 방법 2: formatted() 인스턴스 메서드 (Java 15+, 더 읽기 쉬움)
String s2 = "이름: %s, GPA: %.2f".formatted(name, gpa);
// 방법 3: Text Block + formatted (여러 줄 포맷)
String json = """
{
"name": "%s",
"gpa": %.2f
}
""".formatted(name, gpa);
System.out.println(json);
고수 팁
JVM의 String + 최적화: Java 컴파일러는 "a" + "b" + "c" 같은 상수 연결은 컴파일 타임에 "abc"로 합쳐 버립니다. 또한 Java 9+에서는 루프 밖의 단순 + 연산을 StringBuilder로 자동 변환하지만, 루프 내의 +=는 자동 최적화가 안 됩니다. 루프에서는 반드시 StringBuilder를 직접 사용하세요.
// 컴파일러가 자동으로 StringBuilder로 최적화
String s = "Hello" + ", " + "World"; // → "Hello, World" (상수 폴딩)
// 루프 내 += 는 최적화 안 됨 → StringBuilder 사용 필수
for (...) {
s += something; // ❌ 매번 새 객체 생성
}