코딩테스트나 업무에서 개발을 하다보면 자주 접하는 StringBuilder 클래스.
문자열을 다룰 때 주로 쓰는데, 대충 메모리 효율에 좋다는 정도만 알고있다.
사실 예전에 궁금해서 자세히 찾아봤지만 정리해두지 않아 잊어버렸고, 이번엔 다시 찾아보며 포스팅으로 남겨본다.
"굳이 StringBuilder를 써야 할까? 그냥 +로 이어붙이면 되지 않나?" 에 대한 답변이 될 것이다.
StringBuilder는 가변(Mutable)한 문자열을 다루기 위한 클래스이다.
즉, 한 번 만든 문자열을 바로 수정하고 덧붙일 수 있는 객체다.
코딩테스트나 개발을 하다 보면 문자열을 이어붙이거나 수정할 일이 정말 많다.
그럴 때 자바에서는 항상 등장하는 클래스가 있다. 바로 StringBuilder.
| 메서드 | 설명 |
|---|---|
append() | 문자열을 뒤에 덧붙인다. |
insert(int index, String str) | 지정한 위치에 문자열을 삽입한다. |
delete(int start, int end) | 지정한 구간의 문자열을 삭제한다. |
reverse() | 문자열을 뒤집는다. |
toString() | StringBuilder를 일반 String으로 변환한다. |
StringBuilder sb = new StringBuilder("Hello");
sb.append(" World"); // → "Hello World"
sb.insert(5, ","); // → "Hello, World"
sb.delete(0, 1); // → "ello, World"
sb.reverse(); // → "dlroW ,olle"
System.out.println(sb); // 출력: dlroW ,olle
처음엔 "문자열 더 쉽게 붙이는 도구겠지" 하고 쓰지만,
이 녀석의 진짜 가치는 "문자열의 효율적 관리"에 있다.
설명은 간단하다.
1. String은 한번 포장되면 풀 수 없어서 계속 새로운 박스를 추가해야한다.
2. StringBuilder는 임시상자다. 한 상자 안에서 물건을 넣었다 뺐다, 순서를 바꾸고 다시 정리할 수 있다.
3. 즉, 매번 새로운 박스를 만들지 않고 하나의 임시 상자에서 문자열을 다루는 도구다. 그래서 문자열 연산이 많을수록 훨씬 효율적이다.
String은 불변(Immutable)한 문자열이다.
StringBuilder는 가변(Mutable)한 문자열이다.
한 번 생성된 문자열은 절대 바뀌지 않으며, 문자열을 더하거나 수정하면 항상 새로운 객체가 생성된다.
예시를 보자.
String result = "";
for (int i = 0; i < 10000; i++) {
result += i;
}
이 코드는 10,000번의 + 연산마다 새로운 String 객체가 만들어지고, 기존 문자열은 버려진다.
즉, 10,000개의 문자열 객체가 힙(Heap)에 쌓였다가 가비지 컬렉터가 정리해주는 구조다.
끔찍하다. 불필요한 메모리 낭비와 연산 비용이 너무 크다. (메모리 관련 포스팅은 👉 여기)
이럴 때 StringBuilder는 내부 버퍼(Buffer) 에 문자열을 저장해
하나의 객체에서 계속 수정한다.
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++) {
sb.append(i);
}
String result = sb.toString();
이제는 문자열 결합이 아무리 많아도 객체는 단 하나.
💡 GC 부담은 줄고, 속도는 압도적으로 빨라진다.
진짜 빨라지는지 확인해보자.
public class Compare {
public static void main(String[] args) {
long start, end;
// String 테스트
start = System.currentTimeMillis();
String s = "";
for (int i = 0; i < 100000; i++) {
s += i;
}
end = System.currentTimeMillis();
System.out.println("String 시간: " + (end - start) + "ms");
// StringBuilder 테스트
start = System.currentTimeMillis();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 100000; i++) {
sb.append(i);
}
end = System.currentTimeMillis();
System.out.println("StringBuilder 시간: " + (end - start) + "ms");
}
}
String 시간: 850ms
StringBuilder 시간: 6ms
단순히 문자열 10만 번만 붙였는데도 140배 이상 차이가 나는 경우가 흔하다.
이게 바로 StringBuilder를 써야 하는 이유다.
StringBuilder는 내부적으로 char 배열(buffer) 을 사용한다.
이 배열이 꽉 차면 JVM이 더 큰 배열을 만들어 복사하고, 기존 데이터를 옮겨서 이어붙인다.
즉, StringBuilder는 “문자열 전용 ArrayList”처럼 동작한다.
StringBuilder는 실제로 다음과 같은 형태를 가진 클래스이다.
public final class StringBuilder extends AbstractStringBuilder implements java.io.Serializable {
public StringBuilder() {
super(16); // 기본 버퍼 크기 16
}
}
AbstractStringBuilder는 StringBuilder와 StringBuffer가 공통으로 상속하는 부모 클래스다.
이 내부에는 다음과 같은 필드가 있다.
char[] value; // 문자를 저장하는 버퍼 배열
int count; // 현재 문자열의 길이
value: 실제 문자를 저장하는 버퍼
count: 현재 문자열의 길이
StringBuilder는 문자열을 직접 저장하는 것이 아니라 문자들을 담고 있는 char 배열(value[])을 관리한다.
처음 StringBuilder를 생성하면 기본 용량(capacity) 은 16이다.
문자열을 붙일 때마다 count가 증가하고, 버퍼가 부족해지면 JVM은 자동으로 더 큰 배열을 만들고 기존 내용을 복사한다.
이 구조 덕분에 매번 새로운 객체를 만들 필요 없이 버퍼 내부에서만 문자열을 수정할 수 있다.
이를통해 문자열이 많아져도 효율적으로 확장된다.
이외 내부적인 append 로직 같은 것은 생략하겠다.
StringBuilder와 StringBuffer는 거의 동일한 기능을 가진다.
둘 다 내부적으로 문자열을 담는 가변 버퍼(char[] 배열) 을 사용하며, append(), insert(), delete(), reverse() 등 메서드도 동일하다.
하지만 하나의 아주 큰 차이가 있다.
바로 멀티스레드 환경에서의 동기화(synchronization).
구조적 차이
StringBuffer는 모든 주요 메서드에 synchronized 키워드가 붙어 있다.
public synchronized StringBuffer append(String str) {
super.append(str);
return this;
}
반면 StringBuilder에는 synchronized가 없다.
public StringBuilder append(String str) {
super.append(str);
return this;
}
이 차이 하나가 성능과 안전성을 완전히 갈라놓는다.
| 구분 | StringBuilder | StringBuffer |
|---|---|---|
| 스레드 안전성 | 비동기 (Thread Unsafe) | 동기화 (Thread Safe) |
| 동기화 방식 | 없음 | synchronized 키워드 사용 |
| 속도 | 빠름 (락 오버헤드 없음) | 느림 (락 획득 비용 존재) |
| 사용 환경 | 단일 스레드 (코테, 일반 로직) | 멀티스레드 (서버, 병렬 처리) |
쉽게 말하면,
StringBuffer의 모든 주요 메서드(append, insert, delete 등)에는 synchronized 키워드가 붙어 있어서, 한 번에 한 스레드만 접근할 수 있다.
그래서 여러 스레드가 동시에 하나의 StringBuffer를 사용해도 데이터가 깨지지 않는다.
웹 서버 개발(우리 회사 같은 경우)에는 대부분 StringBuilder를 사용한다.
왜냐하면 요청 단위로 스레드가 분리되어, 한 요청 내에서는 안전하기 때문이다.
다만, 여러 스레드가 하나의 문자열 객체를 동시에 조작해야 하는 경우에는
여전히 StringBuffer 또는 synchronized 블록을 사용해야 한다.
결론
요즘은 Python으로 코테를 풀던 시절의 문제들을 Java로 다시 푸는 중이다.
한창 코테를 풀 때는 파이썬이 훨씬 편했는데 업무에서 Java를 많이 쓰다 보니 코테도 Java가 더 편하게 느껴지기 시작했다.
그렇지만... 코테는 역시 파이썬이 갑오브갑...
프로그래머스에 남아 있던 예전 파이썬 풀이들을 지우고 자바로 다시 풀고 있는데, 파이썬으로는 10줄 이내에 끝났던 문제도 많다.
그래도 자바 코테 화이팅..!!