| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | ||||
| 4 | 5 | 6 | 7 | 8 | 9 | 10 |
| 11 | 12 | 13 | 14 | 15 | 16 | 17 |
| 18 | 19 | 20 | 21 | 22 | 23 | 24 |
| 25 | 26 | 27 | 28 | 29 | 30 | 31 |
- JVM
- 개발공부
- 개발자취업
- 멀티스레드
- 백준
- 자바기초
- 알고리즘
- 자바프로그래밍
- 클린코드
- 개발자팁
- 코딩테스트팁
- 프로그래밍기초
- 코딩테스트
- 코딩인터뷰
- 코딩테스트준비
- 객체지향
- 자바
- 코딩공부
- 자바공부
- 예외처리
- 알고리즘공부
- 가비지컬렉션
- 자바개발
- HashMap
- 파이썬
- 자료구조
- 메모리관리
- 정렬
- 프로그래머스
- Java
- Today
- Total
코드 한 줄의 기록
Java StringBuilder vs StringBuffer - 성능과 스레드 안전성 완전 정복 가이드 본문
개발을 하다 보면 문자열 처리는 피할 수 없는 작업입니다. 특히 Java에서는 String, StringBuilder, StringBuffer 등 다양한 문자열 처리 클래스를 제공하는데요, 오늘은 이 중에서도 가장 많이 헷갈리는 StringBuilder와 StringBuffer의 차이점을 성능과 스레드 안전성 관점에서 자세히 살펴보겠습니다.
왜 StringBuilder와 StringBuffer가 필요한가?
우선 이 두 클래스가 왜 생겨났는지부터 이해해보겠습니다. Java의 String은 불변(immutable) 객체입니다.
즉, 한 번 생성된 문자열은 변경할 수 없어요.
String str = "Hello";
str = str + " World"; // 새로운 String 객체 생성!
위 코드에서 str + " World"가 실행되면 기존 "Hello" 문자열에 " World"를 추가하는 게 아니라, 새로운 "Hello World" 문자열 객체를 만들고 기존 "Hello" 객체는 가비지 컬렉션 대상이 됩니다.
문자열 연산이 많아질수록 이런 객체 생성/삭제가 반복되어 메모리 낭비와 성능 저하가 발생하죠.
이런 문제를 해결하기 위해 나온 것이 StringBuilder와 StringBuffer입니다.
StringBuilder vs StringBuffer - 핵심 차이점
두 클래스 모두 가변(mutable) 문자열 클래스로 동일한 API를 제공합니다. 하지만 가장 중요한 차이점이 하나 있어요.
스레드 안전성의 차이
StringBuffer는 모든 메서드에 synchronized 키워드가 붙어 있습니다.
// StringBuffer의 append 메서드
public synchronized StringBuffer append(String str) {
super.append(str);
return this;
}
반면 StringBuilder는 동기화 처리가 되어 있지 않습니다.
// StringBuilder의 append 메서드
public StringBuilder append(String str) {
super.append(str);
return this;
}
- StringBuffer: 멀티스레드 환경에서 안전(Thread-safe)
- StringBuilder: 단일 스레드 환경에서 더 빠른 성능
성능 비교 실험
실제로 성능 차이가 얼마나 날까요? 여러 개발자들이 진행한 벤치마크 테스트 결과를 보면 흥미로운 패턴을 발견할 수 있습니다.
단일 스레드 환경에서의 성능
일반적으로 StringBuilder > StringBuffer >>> String 순서로 성능이 좋습니다.
10,000번의 문자열 추가 작업을 했을 때
- StringBuilder: 약 3ms
- StringBuffer: 약 4ms
- String: 약 465ms
StringBuilder가 StringBuffer보다 빠른 이유는 동기화 오버헤드가 없기 때문입니다. synchronized 키워드는 락(lock)을 걸고 푸는 과정에서 추가적인 연산이 필요하거든요.
멀티스레드 환경에서의 안전성
다음 코드로 멀티스레드 환경에서의 차이를 확인해볼 수 있습니다.
public class ThreadTest implements Runnable {
StringBuffer sb; // 또는 StringBuilder
public ThreadTest() {
sb = new StringBuffer(); // 또는 StringBuilder
}
public void run() {
for (int i = 0; i < 10000; i++) {
sb.append("1").append("0");
}
}
}
- StringBuffer: 정확히 40,000개의 문자가 추가됨
- StringBuilder: 예상보다 적은 수의 문자만 추가됨 (race condition 발생)
내부 동작 원리
두 클래스 모두 AbstractStringBuilder를 상속받아 내부적으로 char 배열을 사용합니다.
기본 구조
abstract class AbstractStringBuilder {
char[] value; // 문자열 저장 배열
int count; // 실제 문자열 길이
// ...
}
동적 크기 조정
초기 capacity는 16개 문자를 저장할 수 있는 크기로 설정됩니다. 문자열이 추가되어 배열 크기가 부족하면 기존 배열 크기의 2배로 확장하면서 기존 내용을 복사합니다.
StringBuilder sb = new StringBuilder(); // 기본 capacity: 16
sb.append("Hello World Hello World Hello World"); // capacity 부족 시 자동 확장
이런 동적 확장 방식 덕분에 String처럼 매번 새 객체를 생성하지 않고도 유연하게 문자열을 처리할 수 있어요.
언제 무엇을 사용할까?
String 사용 시기
- 문자열 변경이 거의 없는 경우
- 멀티스레드 환경에서 불변성이 필요한 경우
StringBuilder 사용 시기
- 단일 스레드 환경에서 문자열 연산이 빈번한 경우
- 성능이 중요한 상황
- 대부분의 일반적인 상황
StringBuffer 사용 시기
- 멀티스레드 환경에서 여러 스레드가 동일한 문자열 객체에 접근하는 경우
- 스레드 안전성이 성능보다 중요한 경우
컴파일러 최적화의 진실
흥미롭게도 현대 Java 컴파일러는 문자열 연결을 자동으로 최적화합니다.
단순 문자열 연결
// 컴파일 전
String result = "Hello" + " " + "World";
// 컴파일 후 (자동 최적화)
String result = "Hello World";
변수를 포함한 연결 (Java 8)
// 작성한 코드
String result = str1 + str2;
// 컴파일러가 변환한 코드
String result = new StringBuilder().append(str1).append(str2).toString();
Java 9 이후의 개선
Java 9부터는 StringConcatFactory를 사용한 더 효율적인 최적화가 적용됩니다. invokedynamic을 활용해 런타임에 최적화된 문자열 연결 전략을 선택할 수 있게 되었어요.
최적화의 한계
하지만 반복문 내에서의 문자열 연결은 여전히 최적화되지 않습니다.
String result = "";
for (int i = 0; i < 100000; i++) {
result += "Hello Java "; // 매번 새로운 StringBuilder 생성!
}
이런 경우에는 직접 StringBuilder를 사용하는 것이 훨씬 효율적입니다.
실무에서의 선택 기준
제가 프로젝트를 진행하면서 적용하고 있는 실무 기준을 공유해드릴게요.
기본 원칙
- 99%의 상황에서 StringBuilder 사용
- 명확한 멀티스레드 공유가 필요한 경우에만 StringBuffer 선택
성능 고려사항
- 문자열 연산이 1000회 이상 예상되면 반드시 StringBuilder/StringBuffer 사용
- 초기 capacity 설정으로 배열 재할당 최소화
StringBuilder sb = new StringBuilder(예상크기); // 성능 향상
코드 가독성
- 간단한 문자열 연결은 '+' 연산자 사용 (컴파일러가 최적화)
- 복잡한 로직은 StringBuilder의 메서드 체이닝 활용
return new StringBuilder() .append("User: ").append(name) .append(", Age: ").append(age) .toString();
메모리 효율성 팁
적절한 초기 크기 설정
기본 capacity(16)보다 큰 문자열을 다룰 때는 미리 크기를 지정하세요.
// 비효율적
StringBuilder sb1 = new StringBuilder(); // capacity: 16
sb1.append(매우긴문자열); // 여러 번 배열 재할당 발생
// 효율적
StringBuilder sb2 = new StringBuilder(예상크기); // 재할당 최소화
StringBuilder 재사용
같은 패턴의 문자열을 반복 생성할 때는 StringBuilder를 재사용할 수 있어요.
StringBuilder reusableSb = new StringBuilder(100);
for (Data data : dataList) {
reusableSb.setLength(0); // 내용 초기화
String result = reusableSb.append(data.getName())
.append(": ")
.append(data.getValue())
.toString();
// result 사용
}
주의사항과 함정
StringBuffer의 과도한 사용
대부분의 경우 StringBuffer보다 StringBuilder가 적절합니다. StringBuffer는 정말로 멀티스레드에서 동일한 객체를 공유할 때만 사용하세요.
메서드별 동기화의 한계
StringBuffer가 Thread-safe하다고 해서 모든 상황에서 안전한 건 아닙니다.
StringBuffer sb = new StringBuffer();
// 두 스레드에서 실행
if (sb.length() < 10) { // Thread A가 확인
sb.append("new string"); // Thread B가 먼저 추가할 수 있음
}
이런 경우에는 별도의 동기화 처리가 필요해요.
StringBuilder와 StringBuffer는 모두 문자열 처리 성능을 크게 향상시켜주는 도구입니다. 성능이 중요한 단일 스레드 환경에서는 StringBuilder를, 스레드 안전성이 중요한 멀티스레드 환경에서는 StringBuffer를 선택하시면 됩니다. 하지만 현실적으로는 StringBuilder를 기본으로 선택하고, 정말 필요한 경우에만 StringBuffer를 고려하는 것이 좋겠어요.
개발하면서 "이 정도 문자열 처리면 최적화가 필요할까?"라는 의문이 들 때는 간단히 테스트해보세요. 성능 차이가 체감될 정도라면 주저하지 말고 StringBuilder를 도입하는 것이 좋겠습니다. 결국 사용자 경험을 개선하는 것이 우리 개발자의 목표니까요!
Java String 불변성과 메모리 최적화: 리터럴 풀과 equals 비교 완벽 가이드
Java를 배우면서 가장 자주 사용하게 되는 데이터 타입이 바로 String입니다. 하지만 String의 내부 동작 원리를 제대로 이해하고 있는 개발자는 그리 많지 않습니다. 오늘은 Java String의 불변성(Immutab
byteandbit.tistory.com
'JAVA' 카테고리의 다른 글
| Java 클래스와 객체 완벽 정리 - 초보자도 이해하는 객체지향 프로그래밍 핵심 개념 (0) | 2025.10.07 |
|---|---|
| Java 날짜·시간 API 완벽 가이드: java.time으로 손쉽게 포맷·파싱하기 (0) | 2025.10.06 |
| Java String 불변성과 메모리 최적화: 리터럴 풀과 equals 비교 완벽 가이드 (0) | 2025.10.04 |
| Java 정규표현식(java.util.regex) 기초 완벽 가이드: 초보 개발자를 위한 이해하기 쉬운 학습 노트 (0) | 2025.10.03 |
| Java 문자열 포맷팅 완벽 가이드: String.format과 Formatter 사용법 소개 (0) | 2025.10.02 |