Java - String vs StringBuilder vs StringBuffer 성능 비교

박민수·2024년 11월 19일
post-thumbnail

개요

자바 개발을 하다 보면 문자열을 다루는 작업이 빈번하게 발생한다. 보통은 특별히 고려하지 않고 String 클래스를 사용하는 경우가 많다. 하지만 자바에서는 String 클래스 외에도 StringBuffer와 StringBuilder라는 클래스도 제공되며, 상황에 따라 이러한 클래스가 더 효율적일 수 있다. 이 글에서는 StringBuffer, StringBuilder, String의 차이를 명확히 이해하고, 각각의 메모리 사용과 응답 시간을 측정한 결과를 공유한다. 이를 통해 어떤 상황에서 어떤 클래스를 사용하는 것이 가장 적합한지 정리해 보고자 한다.

String, StringBuilder, StringBuffer 비교

  • String: 불변(immutable) 객체이다. 한 번 생성된 문자열은 변경할 수 없으며, 수정 작업이 필요할 경우 새로운 String 객체가 생성된다. 문자열을 자주 변경해야 하는 상황에서는 비효율적일 수 있다.
  • StringBuilder: 가변(mutable) 객체로, 문자열을 수정할 수 있다. 동기화(synchronization를 지원하지 않기 때문에 단일 스레드 환경에서 사용하면 빠르고 효율적이다.
  • StringBuffer: StringBuilder와 마찬가지로 가변 객체지만, 동기화를 지원한다. 멀티스레드 환경에서 안전하게 사용할 수 있도록 설계되었지만, 동기화로 인해 성능이 다소 떨어질 수 있다.

메모리와 성능 비교 테스트

테스트 설정

  • 목적: 각 클래스의 응답 시간과 메모리 사용량을 비교하여 효율성을 분석한다.
  • 테스트 조건:
  • 문자열 추가 작업을 100,000회 반복
  • 응답 시간(밀리초)과 메모리 사용량(KB)을 측정
  • 각각 10회 테스트를 반복하여 평균값을 도출

테스트 결과

응답 시간 (ms)메모리 사용량 (kb)
String10회 평균 약 194410회 평균 약 173926
StringBuffer10회 평균 약 2.04110회 평균 약 1155
StringBuilder10회 평균 약 0.77410회 평균 약 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};
    }
}

결과 분석

  1. String의 성능
  • 응답 시간: String은 불변 객체이기 때문에 문자열을 수정할 때마다 새로운 객체를 생성한다. 100,000번의 문자열 수정 작업을 수행한 결과, 평균 시간이 1944.35 ms로 측정되었다. 이는 매우 비효율적인 결과이다.
  • 메모리 사용량: 새로운 객체가 계속 생성되면서 메모리 사용량이 급격히 증가했다. 평균 메모리 사용량은 173,926.75 KB로, 다른 클래스에 비해 압도적으로 높았다.
  • 결론: 문자열을 자주 변경해야 하는 작업에서는 String을 사용하면 성능과 메모리 관리 측면에서 매우 비효율적이다.
  1. StringBuilder의 성능
  • 응답 시간: StringBuilder는 가변 객체로, 문자열을 효율적으로 수정할 수 있다. 동기화를 지원하지 않기 때문에 평균 시간이 0.774 ms로, 세 클래스 중 가장 빠른 성능을 보였다.
  • 메모리 사용량: 메모리를 효율적으로 관리하여 평균 메모리 사용량이 1189.34 KB로 매우 안정적이었다.
  • 결론: 단일 스레드 환경에서 문자열을 자주 조작할 때 가장 적합한 선택이다.
  1. StringBuffer의 성능
  • 응답 시간: StringBuffer는 동기화를 지원하기 때문에 StringBuilder보다 다소 느리다. 평균 응답 시간이 2.041 ms로 측정되었다.
  • 메모리 사용량: 메모리 사용량은 1155.69 KB로, StringBuilder와 비슷한 수준이다.
  • 결론: 멀티스레드 환경에서 안전하게 문자열을 수정해야 할 때 사용한다. 성능은 StringBuilder에 비해 다소 떨어지지만, 스레드 안전성을 제공한다.

결론

  • String: 문자열이 불변이어야 하거나, 문자열 수정 작업이 거의 없는 경우에만 사용하는 것이 좋다.
  • StringBuilder: 단일 스레드 환경에서 문자열을 자주 수정해야 할 때 가장 효율적이다.
  • StringBuffer: 멀티스레드 환경에서 안전한 문자열 조작이 필요할 때 선택한다.

문자열을 다루는 상황에 따라 적절한 클래스를 선택하는 것이 성능 최적화에 중요하다는 걸 알게 되었다. 앞으로 코드를 작성할 때 이 점을 꼭 고려해야겠다.

profile
안녕하세요 백엔드 개발자입니다.

0개의 댓글