본문 바로가기
■Development■/《Java》

[Java] StringBuffer vs StringBuilder

by 은스타 2019. 9. 26.
반응형

StringBuffer vs StringBuilder 완벽 비교

안녕하세요.
이번 포스팅은 Java에서 문자열을 효율적으로 처리하기 위한 두 클래스인 StringBuffer와 StringBuilder의 차이점에 대해 자세히 알아보겠습니다. 간단해 보이지만 프로젝트의 성능에 큰 영향을 미칠 수 있는 이 두 클래스의 특징과 사용법, 그리고 언제 어떤 클래스를 선택해야 하는지 명확하게 이해할 수 있도록 안내해 드리겠습니다.


목차

  1. StringBuffer와 StringBuilder 개요
  2. 주요 차이점
  3. 성능 비교
  4. 메서드와 사용법
  5. 코드 예제로 알아보기
  6. 멀티스레드 환경 테스트
  7. String, StringBuffer, StringBuilder 비교
  8. 적합한 상황별 선택 가이드
  9. 자주 묻는 질문
  10. 결론

#1. StringBuffer와 StringBuilder 개요

Java에서 문자열을 다룰 때 가장 기본이 되는 String 클래스는 불변(immutable)입니다. 이는 한 번 생성된 문자열은 변경할 수 없다는 의미로, 문자열 연산이 많은 경우 성능 저하의 원인이 됩니다. 이러한 한계를 극복하기 위해 Java는 가변(mutable)한 문자열을 처리할 수 있는 StringBufferStringBuilder 클래스를 제공합니다.

StringBuffer

StringBuffer는 Java 1.0부터 제공된 클래스로, 가변적인 문자열을 처리할 수 있습니다. 내부적으로 버퍼(buffer)를 사용하여 문자열을 저장하고, 필요에 따라 크기를 조절합니다.

StringBuffer buffer = new StringBuffer("Hello");
buffer.append(" World"); // "Hello World"가 됨

StringBuilder

StringBuilder는 Java 5(JDK 1.5)에서 추가된 클래스로, StringBuffer와 동일한 API를 가지지만 동기화(synchronization)를 지원하지 않아 더 나은 성능을 제공합니다.

StringBuilder builder = new StringBuilder("Hello");
builder.append(" World"); // "Hello World"가 됨

 

#2. 주요 차이점

StringBuffer와 StringBuilder의 핵심 차이점은 다음과 같습니다:

1. 스레드 안전성 (Thread Safety)

  • StringBuffer: 동기화되어 있어 멀티스레드 환경에서 안전합니다.
  • StringBuilder: 동기화되어 있지 않아 멀티스레드 환경에서 안전하지 않습니다.

2. 성능

  • StringBuffer: 동기화 오버헤드로 인해 상대적으로 느립니다.
  • StringBuilder: 동기화가 없어 단일 스레드 환경에서 StringBuffer보다 빠릅니다.

3. 도입 시기

  • StringBuffer: Java 1.0부터 존재했습니다.
  • StringBuilder: Java 5(JDK 1.5)에서 도입되었습니다.

다음 표는 StringBuffer와 StringBuilder의 주요 차이점을 요약합니다:

No 특성 StirngBuffer StingBuilder
1 스레드 안전성 O (Thread-safe) X (Not Thread-safe)
2 동기화 O (Synchronized) X (Not Synchronized)
3 성능 상대적으로 느림 상대적으로 빠름
4 도입 버전 Java 1.0 Java 5 (JDK 1.5)
5 용도 멀티스레드 환경 단일스레드 환경
6 메서드 append(), insert(), replace(), delete(), reverse() 등 StringBuffer와 동일

 

#3. 성능 비교

StringBuffer와 StringBuilder의 성능 차이를 확인하기 위해 간단한 벤치마크 테스트를 수행해 보겠습니다. 다음 코드는 각각 문자열 연결 작업을 수행하는 데 걸리는 시간을 측정합니다.

public class StringPerformanceTest {
    public static void main(String[] args) {
        int iterations = 1_000_000;

        // String 성능 측정
        long startTime = System.currentTimeMillis();
        String str = "";
        for (int i = 0; i < iterations; i++) {
            str += "a";
        }
        long endTime = System.currentTimeMillis();
        System.out.println("String 연결 시간: " + (endTime - startTime) + "ms");

        // StringBuffer 성능 측정
        startTime = System.currentTimeMillis();
        StringBuffer buffer = new StringBuffer();
        for (int i = 0; i < iterations; i++) {
            buffer.append("a");
        }
        endTime = System.currentTimeMillis();
        System.out.println("StringBuffer 연결 시간: " + (endTime - startTime) + "ms");

        // StringBuilder 성능 측정
        startTime = System.currentTimeMillis();
        StringBuilder builder = new StringBuilder();
        for (int i = 0; i < iterations; i++) {
            builder.append("a");
        }
        endTime = System.currentTimeMillis();
        System.out.println("StringBuilder 연결 시간: " + (endTime - startTime) + "ms");
    }
}

벤치마크 결과

일반적인 실행 결과는 다음과 같습니다 (시스템 사양에 따라 다를 수 있음):

String 연결 시간: 47253ms
StringBuffer 연결 시간: 11ms
StringBuilder 연결 시간: 7ms

이 결과에서 알 수 있듯이:

  1. String을 사용한 연결은 매우 비효율적입니다. (각 연결마다 새 객체 생성)
  2. StringBufferString보다 훨씬 빠르지만, StringBuilder보다는 느립니다.
  3. StringBuilder는 세 가지 방법 중 가장 빠릅니다.

 

#4. 메서드와 사용법

StringBuffer와 StringBuilder는 동일한 메서드를 제공합니다. 주요 메서드는 다음과 같습니다:

주요 메서드

  1. append(): 문자열 끝에 데이터 추가
  2. StringBuilder sb = new StringBuilder("Hello"); sb.append(" World"); // "Hello World"
  3. insert(): 지정한 위치에 데이터 삽입
  4. StringBuilder sb = new StringBuilder("Hello World"); sb.insert(6, "Java "); // "Hello Java World"
  5. delete() / deleteCharAt(): 문자열의 일부 삭제
  6. StringBuilder sb = new StringBuilder("Hello World"); sb.delete(5, 11); // "Hello" sb.deleteCharAt(0); // "ello"
  7. replace(): 문자열의 일부를 다른 문자열로 교체
  8. StringBuilder sb = new StringBuilder("Hello World"); sb.replace(6, 11, "Java"); // "Hello Java"
  9. reverse(): 문자열을 반전
  10. StringBuilder sb = new StringBuilder("Hello"); sb.reverse(); // "olleH"
  11. capacity() / ensureCapacity(): 버퍼 용량 관리
  12. StringBuilder sb = new StringBuilder(); System.out.println(sb.capacity()); // 기본 16 sb.ensureCapacity(100); // 최소 용량을 100으로 설정

 

#5. 코드 예제로 알아보기

실제 개발에서 StringBuffer와 StringBuilder를 어떻게 활용하는지 몇 가지 예제를 통해 살펴보겠습니다.

예제 1: 단순 문자열 연결

public class SimpleStringConcatenation {
    public static void main(String[] args) {
        // 문자열이 적을 때
        String result1 = "Hello" + " " + "World" + "!";
        System.out.println(result1); // Hello World!

        // 문자열이 많을 때 (StringBuilder 사용 권장)
        StringBuilder sb = new StringBuilder();
        sb.append("Hello");
        sb.append(" ");
        sb.append("World");
        sb.append("!");
        String result2 = sb.toString();
        System.out.println(result2); // Hello World!
    }
}

예제 2: 반복문에서의 문자열 처리

public class LoopStringProcessing {
    public static void main(String[] args) {
        String[] words = {"Java", "is", "awesome", "and", "powerful"};

        // 잘못된 방법 (String 사용)
        String badResult = "";
        for (String word : words) {
            badResult += word + " ";
        }
        System.out.println(badResult.trim());

        // 좋은 방법 (StringBuilder 사용)
        StringBuilder sb = new StringBuilder();
        for (String word : words) {
            sb.append(word).append(" ");
        }
        String goodResult = sb.toString().trim();
        System.out.println(goodResult);
    }
}

예제 3: CSV 파일 생성

public class CSVGenerator {
    public static String generateCSV(String[] headers, String[][] data) {
        StringBuilder csv = new StringBuilder();

        // 헤더 작성
        for (int i = 0; i < headers.length; i++) {
            csv.append(headers[i]);
            if (i < headers.length - 1) {
                csv.append(",");
            }
        }
        csv.append("\n");

        // 데이터 작성
        for (String[] row : data) {
            for (int i = 0; i < row.length; i++) {
                csv.append(row[i]);
                if (i < row.length - 1) {
                    csv.append(",");
                }
            }
            csv.append("\n");
        }

        return csv.toString();
    }

    public static void main(String[] args) {
        String[] headers = {"이름", "나이", "이메일"};
        String[][] data = {
            {"홍길동", "30", "hong@example.com"},
            {"김철수", "25", "kim@example.com"},
            {"이영희", "28", "lee@example.com"}
        };

        String csvContent = generateCSV(headers, data);
        System.out.println(csvContent);
    }
}

 

#6. 멀티스레드 환경 테스트

StringBuffer와 StringBuilder의 가장 큰 차이점인 스레드 안전성을 테스트해 보겠습니다.

public class ThreadSafetyTest {
    public static void main(String[] args) throws InterruptedException {
        // StringBuffer 테스트 (Thread-safe)
        StringBuffer buffer = new StringBuffer();
        Runnable bufferTask = () -> {
            for (int i = 0; i < 1000; i++) {
                buffer.append("a");
            }
        };

        Thread t1 = new Thread(bufferTask);
        Thread t2 = new Thread(bufferTask);

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        System.out.println("StringBuffer 길이: " + buffer.length()); // 항상 2000

        // StringBuilder 테스트 (Not Thread-safe)
        StringBuilder builder = new StringBuilder();
        Runnable builderTask = () -> {
            for (int i = 0; i < 1000; i++) {
                builder.append("a");
            }
        };

        Thread t3 = new Thread(builderTask);
        Thread t4 = new Thread(builderTask);

        t3.start();
        t4.start();

        t3.join();
        t4.join();

        System.out.println("StringBuilder 길이: " + builder.length()); // 2000이 아닐 수 있음
    }
}

이 테스트를 실행하면 StringBuffer는 항상 예상된 길이(2000)를 가지지만, StringBuilder는 경쟁 상태(race condition)로 인해 예상보다 짧은 길이를 가질 수 있습니다.

 

#7. String, StringBuffer, StringBuilder 비교

세 가지 주요 문자열 처리 클래스의 특성을 비교해 보겠습니다:

No 가변성 불변(Immutable) 가변(Mutable) 가변(Mutable)
1 가변성 불변(Immutable) 가변(Mutable) 가변(Mutable)
2 스레드 안전성 O O X
3 성능 연결 연산에 취약 중간 가장 빠름
4 메모리 효율성 새 객체 생성으로 비효율적 효율적 효율적
5 사용 시나리오 문자열 변경이 적은 경우 멀티스레드 환경 단일스레드 환경

 

#8. 적합한 상황별 선택 가이드

어떤 상황에서 어떤 클래스를 선택해야 할지 가이드라인을 제시합니다:

String 사용이 적합한 경우

  • 문자열이 한 번 생성된 후 변경되지 않는 경우
  • 짧은 문자열을 적은 횟수로 연결하는 경우
  • 문자열 리터럴을 사용할 때 (String str = "Hello";)

StringBuffer 사용이 적합한 경우

  • 멀티스레드 환경에서 문자열을 처리할 때
  • 여러 스레드가 동일한 문자열 객체에 접근하는 경우
  • 스레드 안전성이 필요한 경우

StringBuilder 사용이 적합한 경우

  • 단일 스레드 환경에서 문자열을 처리할 때
  • 문자열 연결이 많이 발생하는 경우
  • 반복문 내에서 문자열을 구성할 때
  • 최대한의 성능이 필요한 경우

 

#9. 자주 묻는 질문

Q1: StringBuffer와 StringBuilder 중 무엇을 사용해야 할까요?

A: 멀티스레드 환경에서는 StringBuffer를, 단일 스레드 환경에서는 StringBuilder를 사용하는 것이 좋습니다. 일반적인 애플리케이션의 대부분의 상황에서는 StringBuilder가 성능상 유리합니다.

Q2: String 대신 StringBuilder를 사용해야 하는 기준은 무엇인가요?

A: 문자열 연결 작업이 많거나 반복문 안에서 문자열을 구성하는 경우 StringBuilder를 사용하는 것이 좋습니다. 단순히 몇 개의 문자열만 연결하는 경우는 컴파일러가 자동으로 최적화하므로 String을 사용해도 무방합니다.

Q3: StringBuffer와 StringBuilder의 내부 구현은 어떻게 다른가요?

A: 내부 구현은 거의 동일하지만, StringBuffer의 메서드들은 synchronized 키워드가 추가되어 있어 스레드 안전성을 보장합니다. 이로 인해 약간의 성능 차이가 발생합니다.

Q4: StringBuilder의 초기 용량은 얼마인가요?

A: 기본 생성자로 생성 시 초기 용량은 16자입니다. 필요에 따라 자동으로 용량이 증가하지만, 대용량 문자열을 처리할 때는 적절한 초기 용량을 지정하는 것이 좋습니다.

Q5: String concatenation(+)과 StringBuilder 중 어느 것이 효율적인가요?

A: 간단한 문자열 연결(2-3개)은 컴파일러가 내부적으로 StringBuilder로 최적화하기 때문에 차이가 없습니다. 하지만 반복문 안에서의 문자열 연결은 명시적으로 StringBuilder를 사용하는 것이 훨씬 효율적입니다.

 

#10. 결론

StringBuffer와 StringBuilder는 Java에서 가변 문자열을 처리하는 데 중요한 클래스입니다. 두 클래스는 동일한 API를 제공하지만 스레드 안전성에 차이가 있습니다.

  • StringBuilder는 단일 스레드 환경에서 최고의 성능을 제공하므로, 일반적인 상황에서는 StringBuilder를 사용하는 것이 좋습니다.
  • StringBuffer는 멀티스레드 환경에서 안전하게 사용할 수 있지만, 동기화 오버헤드로 인해 성능이 약간 떨어집니다.

일반적인 권장 사항은 특별한 이유가 없다면 StringBuilder를 사용하고, 멀티스레드 환경에서 동일한 문자열 객체에 여러 스레드가 접근해야 하는 경우에만 StringBuffer를 사용하는 것입니다.

여러분의 프로젝트에서는 어떤 클래스를 주로 사용하시나요? 특별한 문자열 처리 패턴이나 최적화 기법이 있다면 댓글로 공유해 주세요!


마무리

긴 글 읽어주셔서 감사합니다.끝.

 

 

참고 자료

 

 

반응형