자바 개발을 하다 보면 문자열을 다루는 작업이 빈번하게 발생한다. 보통은 특별히 고려하지 않고 String 클래스를 사용하는 경우가 많다. 하지만 자바에서는 String 클래스 외에도 StringBuffer와 StringBuilder라는 클래스도 제공되며, 상황에 따라 이러한 클래스가 더 효율적일 수 있다. 이 글에서는 StringBuffer, StringBuilder, String의 차이를 명확히 이해하고, 각각의 메모리 사용과 응답 시간을 측정한 결과를 공유한다. 이를 통해 어떤 상황에서 어떤 클래스를 사용하는 것이 가장 적합한지 정리해 보고자 한다.
String: 불변(immutable) 객체이다. 한 번 생성된 문자열은 변경할 수 없으며, 수정 작업이 필요할 경우 새로운 String 객체가 생성된다. 문자열을 자주 변경해야 하는 상황에서는 비효율적일 수 있다.StringBuilder: 가변(mutable) 객체로, 문자열을 수정할 수 있다. 동기화(synchronization를 지원하지 않기 때문에 단일 스레드 환경에서 사용하면 빠르고 효율적이다.StringBuffer: StringBuilder와 마찬가지로 가변 객체지만, 동기화를 지원한다. 멀티스레드 환경에서 안전하게 사용할 수 있도록 설계되었지만, 동기화로 인해 성능이 다소 떨어질 수 있다.| 응답 시간 (ms) | 메모리 사용량 (kb) | |
|---|---|---|
| String | 10회 평균 약 1944 | 10회 평균 약 173926 |
| StringBuffer | 10회 평균 약 2.041 | 10회 평균 약 1155 |
| StringBuilder | 10회 평균 약 0.774 | 10회 평균 약 1189 |
public class PerformanceTest {
public static void main(String[] args) {
int iterations = 100000; // 문자열 조작을 반복할 횟수 설정
int numTests = 10; // 테스트를 10회 반복
// 평균값을 저장할 변수
double totalTimeStringBuilder = 0;
double totalMemoryStringBuilder = 0;
double totalTimeStringBuffer = 0;
double totalMemoryStringBuffer = 0;
double totalTimeString = 0;
double totalMemoryString = 0;
for (int i = 0; i < numTests; i++) { // 10회 반복
System.out.println("테스트 " + (i + 1) + "회차:");
double[] result;
// StringBuilder 성능 측정
result = measurePerformance(new StringBuilder(), iterations, "StringBuilder");
totalTimeStringBuilder += result[0];
totalMemoryStringBuilder += result[1];
// StringBuffer 성능 측정
result = measurePerformance(new StringBuffer(), iterations, "StringBuffer");
totalTimeStringBuffer += result[0];
totalMemoryStringBuffer += result[1];
// String 성능 측정
result = measurePerformance(new String(), iterations, "String");
totalTimeString += result[0];
totalMemoryString += result[1];
System.out.println();
}
// 평균값 계산 및 출력
System.out.println("=== 평균값 ===");
System.out.println("StringBuilder 평균 시간: " + (totalTimeStringBuilder / numTests) + " ms");
System.out.println("StringBuilder 평균 메모리 사용: " + (totalMemoryStringBuilder / numTests) + " KB");
System.out.println("StringBuffer 평균 시간: " + (totalTimeStringBuffer / numTests) + " ms");
System.out.println("StringBuffer 평균 메모리 사용: " + (totalMemoryStringBuffer / numTests) + " KB");
System.out.println("String 평균 시간: " + (totalTimeString / numTests) + " ms");
System.out.println("String 평균 메모리 사용: " + (totalMemoryString / numTests) + " KB");
}
public static double[] measurePerformance(Object obj, int iterations, String type) {
// 가비지 컬렉션 강제 호출 (테스트 전 메모리 정리)
System.gc();
Runtime runtime = Runtime.getRuntime();
long startMemory = runtime.totalMemory() - runtime.freeMemory(); // 시작 메모리 측정
long startTime = System.nanoTime(); // 시작 시간 측정
// 문자열 조작 수행
if (obj instanceof StringBuilder) {
StringBuilder sb = (StringBuilder) obj;
for (int i = 0; i < iterations; i++) {
sb.append("test");
}
} else if (obj instanceof StringBuffer) {
StringBuffer sb = (StringBuffer) obj;
for (int i = 0; i < iterations; i++) {
sb.append("test");
}
} else if (obj instanceof String) {
String str = (String) obj;
for (int i = 0; i < iterations; i++) {
str += "test";
}
}
long endTime = System.nanoTime(); // 끝난 시간 측정
long endMemory = runtime.totalMemory() - runtime.freeMemory(); // 끝 메모리 측정
// 결과 출력
double time = (endTime - startTime) / 1e6; // 시간을 밀리초로 변환
double memory = (endMemory - startMemory) / 1024.0; // 메모리를 KB로 변환
System.out.println(type + " 시간: " + time + " ms");
System.out.println(type + " 메모리 사용: " + memory + " KB");
// 시간과 메모리 사용량 반환
return new double[]{time, memory};
}
}
String의 성능StringBuilder의 성능StringBuffer의 성능String: 문자열이 불변이어야 하거나, 문자열 수정 작업이 거의 없는 경우에만 사용하는 것이 좋다.StringBuilder: 단일 스레드 환경에서 문자열을 자주 수정해야 할 때 가장 효율적이다.StringBuffer: 멀티스레드 환경에서 안전한 문자열 조작이 필요할 때 선택한다.문자열을 다루는 상황에 따라 적절한 클래스를 선택하는 것이 성능 최적화에 중요하다는 걸 알게 되었다. 앞으로 코드를 작성할 때 이 점을 꼭 고려해야겠다.