티스토리 뷰
과거 레거시 시스템을 리팩터링 할 때, 문자열 변경 작업이 빈번하게 발생하면서 성능 문제가 발생했던 적이 있습니다. 당시에는 String 클래스가 사용되고 있었는데, 이 클래스는 불변 객체이기 때문에 문자열이 변경될 때마다 새로운 객체를 생성하는 특성으로 인해 메모리 사용량이 증가하고 처리 속도가 느려지는 문제가 있었습니다.
이를 해결하기 위해 StringBuffer 클래스를 도입하여 기존 객체를 수정하는 방식으로 작업을 처리했고, 이를 통해 성능 문제를 효과적으로 해결할 수 있었습니다. 당시에는 속도 개선이라는 결과에만 초점을 맞췄지만, 지금은 그 원리와 동작 방식에 대해 더 깊이 이해하고자 합니다.
문자열 클래스의 특성과 동작 원리
1. String
불변 객체로, 문자열 변경 시 새로운 객체가 생성되어 기존 데이터를 복사합니다. 자주 변경되는 문자열 처리에서 메모리와 성능이 저하 발생합니다.
2. StringBuffer
가변 객체로, 기존 객체를 수정하며 동기화를 지원해 스레드 안전 (Thread-safe) 합니다. 자주 변경되는 문자열 처리에 적합하고 기존 객체를 수정하므로 성능이 향상됩니다.
3. StringBuilder
StringBuffer와 동일한 가변 객체지만, 동기화를 지원하지 않아 단일 스레드 환경에서 더 빠릅니다.
※ 가변 객체
생성된 이후에도 내부 상태를 변경할 수 있는 객체를 말합니다. 예를 들어, StringBuilder나 StringBuffer는 문자열을 추가하거나 수정할 때 새로운 객체를 생성하지 않고 기존 객체의 상태를 변경합니다.
※ 불변객체
생성된 이후에는 내부 상태를 변경할 수 없는 객체입니다. String이 그 대표적인 예로, 문자열을 수정하거나 추가하려고 하면 항상 새로운 객체를 생성하고 기존 객체는 변경되지 않습니다.
StringBuffer와 StringBuilder의 차이점
모두 가변 객체로, 문자열 변경 작업 시 기존 객체를 수정하여 메모리와 성능 효율성을 제공합니다. 주요 차이점은 동기화 여부입니다.
예제 코드
package Test;
public class StringBufferVsStringBuilderMultiThreadTest {
private static final int ITERATIONS = 10_000;
public static void main(String[] args) throws InterruptedException {
StringBuffer stringBuffer = new StringBuffer("X");
runMultiThreadTest(stringBuffer, "StringBuffer");
StringBuilder stringBuilder = new StringBuilder("X");
runMultiThreadTest(stringBuilder, "StringBuilder");
}
private static void runMultiThreadTest(Object stringObject, String objectType) throws InterruptedException {
int numberOfThreads = 10; // Number of threads
Thread[] threads = new Thread[numberOfThreads];
Runnable task = () -> {
for (int i = 0; i < ITERATIONS; i++) {
if (stringObject instanceof StringBuffer) {
((StringBuffer) stringObject).append("X");
} else if (stringObject instanceof StringBuilder) {
((StringBuilder) stringObject).append("X");
}
}
};
for (int i = 0; i < numberOfThreads; i++) {
threads[i] = new Thread(task);
threads[i].start();
}
for (Thread thread : threads) {
thread.join();
}
System.out.println(objectType + " final length: " + getObjectLength(stringObject));
}
private static int getObjectLength(Object stringObject) {
if (stringObject instanceof StringBuffer) {
return ((StringBuffer) stringObject).length();
} else if (stringObject instanceof StringBuilder) {
return ((StringBuilder) stringObject).length();
}
return -1;
}
}
StringBuffer final length: 100001
StringBuilder final length: 57920

실행할 때마다 StringBuilder의 결괏값이 달라지고, 멀티 스레드 환경에서는 동기화를 지원하지 않기 때문에 데이터 충돌 문제도 발생했습니다.
단일 스레드 환경에서는 StringBuilder가 성능 면에서 더 빠르고 효율적이지만, 현업에서는 멀티 스레드 환경에서 안정성 또한 중요한 요소입니다. 멀티 스레드 환경에서는 여러 스레드가 동시에 문자열을 수정할 수 있기 때문에 데이터 충돌을 방지할 수 있는 동기화 기능을 제공하는 StringBuffer를 사용하는 것이 더 적합합니다.
StringBuffer는 String보다 빠른 성능을 제공하는데, 그 이유는 String이 불변 객체여서 매번 새로운 객체를 생성하는 반면, StringBuffer는 가변 객체로 기존 객체를 수정하기 때문에 성능이 더 낫습니다.
※ 단일 스레드
하나의 스레드만 사용하여 작업을 처리하는 방식입니다. 모든 작업이 순차적으로 진행되며, 한 번에 하나의 작업만 수행할 수 있습니다.
※ 멀티 스레드
여러 개의 스레드가 동시에 실행되어 작업을 병렬로 처리하는 방식입니다. 여러 스레드가 독립적으로 작업을 수행하기 때문에 긴 작업도 다른 작업을 동시에 처리할 수 있습니다.
※ 동기화
여러 스레드가 동시에 공유 자원(데이터나 객체 등)에 접근할 때 발생할 수 있는 데이터 충돌이나 불일치를 방지하기 위해 작업의 순서를 제어하는 방법입니다.
String, StringBuffer, StringBuilder 속도 비교
예제 코드
package Test;
public class test_StringVsStringBufferVsStringBuilderTest {
public static void main(String[] args) {
int iterations = 100000;
long startTime = System.currentTimeMillis();
testString(iterations);
long endTime = System.currentTimeMillis();
System.out.println("String execution time: " + (endTime - startTime) + " ms");
startTime = System.currentTimeMillis();
testStringBuffer(iterations);
endTime = System.currentTimeMillis();
System.out.println("StringBuffer execution time: " + (endTime - startTime) + " ms");
startTime = System.currentTimeMillis();
testStringBuilder(iterations);
endTime = System.currentTimeMillis();
System.out.println("StringBuilder execution time: " + (endTime - startTime) + " ms");
}
private static void testString(int iterations) {
String str = "";
for (int i = 0; i < iterations; i++) {
str += "X";
}
}
private static void testStringBuffer(int iterations) {
StringBuffer stringBuffer = new StringBuffer();
for (int i = 0; i < iterations; i++) {
stringBuffer.append("X");
}
}
private static void testStringBuilder(int iterations) {
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < iterations; i++) {
stringBuilder.append("X");
}
}
}
String execution time: 465 ms
StringBuffer execution time: 4 ms
StringBuilder execution time: 3 ms
문자열 연산 수행 결과를 보면 StringBuilder가 가장 빠르다. 성능만 본다면 StringBuilder를 사용하는 것이 가장 적합하다.
결론
문자열 처리에서 성능 최적화를 고려할 때, String은 불변 객체로, 문자열을 수정할 때마다 새로운 객체를 생성하는 특성 때문에 자주 변경되는 문자열에 대해서는 성능에 문제가 발생합니다. StringBuffer와 StringBuilder는 가변 객체로, 문자열을 변경할 때 기존 객체를 수정하므로 성능이 String보다 효율적입니다. StringBuilder는 단일 스레드 환경에서 가장 빠른 성능을 제공하지만 안정성이 떨어지며, StringBuffer는 동기화를 제공하여 멀티 스레드 환경에서 안정성을 보장하므로 약간의 성능 차이를 감수하고도 안정적인 성능을 제공합니다.
따라서 상황에 맞는 문자열 클래스를 사용하되, 빈번한 수정이 필요한 경우 StringBuffer나 StringBuilder를 사용하고, 안정성까지 보장하려면 StringBuffer를 사용하는 것이 좋을 거 같습니다.
참고 자료
☕ 자바 String / StringBuffer / StringBuilder 차이점 & 성능 비교
자바에서는 대표적으로 문자열을 다루는 자료형 클래스로 String, StringBuffer, StringBuilder 라는 3가지 자료형을 지원한다. 위 3가지 클래스 자료형은 모두 문자열을 다루는데 있어 공통적으로 사용되
inpa.tistory.com
끝.
'Java' 카테고리의 다른 글
ExecutorService와 ForkJoinPool (0) | 2025.03.31 |
---|---|
ConcurrentLinkedQueue와 LinkedBlockingQueue (0) | 2025.03.31 |
우선순위 큐(Priority Queue) with 백준 1715 (0) | 2025.01.03 |
[Algorithms] 탐욕법 (Greedy) with 프로그래머스 (0) | 2024.12.30 |
[프로그래머스] 뒤에 있는 큰 수 찾기 - Java (0) | 2024.12.06 |
- Total
- Today
- Yesterday
- isalpha
- zip
- combinations
- for
- function
- Python
- Built-in Functions
- permutations
- counter
- Lower
- Method
- index
- find
- bool
- If
- Upper
- operators
- Lambda
- isdigit
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |