본문 바로가기
Language/java

Java: String과 StringBuffer, String Builder

by S2채닝S2 2025. 4. 7.

String

  • 불변 객체
  • 한 번 생성되면 할당된 메모리 공간이 불변이다.
  • +연산자 또는 concat 메서드를 통해 문자열을 합치면, 기존에 생성된 String 클래스에 문자열을 붙이는 것이 아니라, 새로운 String 객체에 두 문자열을 합쳐서 리턴한다.
  • 새로운 객체를 생성하기 때문에, 문자열 연산이 많은 경우 성능이 좋지 않다.
  • 하지만, 간단하게 사용 가능하고 변경될 가능성이 거의 없는 경우, 동기화 문제를 신경쓰지 않아도 된다.(Thread-safe, 자원 공유 가능)

🤔Java에서 왜 String을 불변으로 설정했을까?
❗캐싱, 동기화, 보안 등에 이점이 있다.

  1. 캐싱: String Constant(상수) Pool에 각 리터럴 문자열의 하나만 저장, 캐싱하여 재사용 가능하며, 힙 공간을 절약할 수 있다.
    - 해시코드 캐싱: hashCode()구현 시 최초 1번만 계산 로직을 수행하고, 이후에는 이전에 계산한 값을 return 한다. String이 불변이기 때문에 Cashing을 활용할 수 있다.
  2. 동기화: 불변이므로 값이 바뀔 일이 없어, 멀티스레드 환경에서 Thread-safe하다.

보안: 사용자 이름, 암호, 네트워크 연결 등과 같은 민감한 정보를 저장할 때, 불변 객체이므로 조작으로부터 안전하다. (SQL injection, 키 변경 등 불가)


StringBuffer, StringBuilder

  • 가변 객체
  • 내부 Buffer(데이터를 임시로 저장하는 메모리)에 문자열을 저장해두고 그 안에서 추가, 수정, 삭제 작업을 수행
  • 문자열 연산 등으로 메모리 공간이 부족할 경우, 버퍼의 크기를 유연하게 늘릴 수 있다
  • 동일 객체 내에서 문자열 크기를 변경할 수 있다.
  • StringBuilder는 동기화를 지원하지 않는 반면, StringBuffer는 동기화를 지원한다. (StringBuffer는 메서드에서 synchronized 키워드를 사용)
  • StringBuilder가 동기화 관련 로직을 수행하지 않으므로, 성능 측면에서 이점이 있다.
  • 그러나, Thread-safe하지 않으므로, web, socket환경 등의 비동기 환경일 경우, StringBuffer를 사용하는 것이 안전하다.

멀티스레드 환경에서의 동작 차이

두 개의 스레드에서 StringBuffer, StringBuilder에 각각 1을 1만번 더하는 연산을 수행해보자. 우리는 두 문자열의 길이가 2만번일 것으로 예상하지만(두 개의 스레드에서 각각 만 번씩 추가하므로), StringBuffer, StringBuilder의 길이가 다르다는 것을 볼 수 있다.
StringBuffer는 동기화를 지원하여 Thread-safe하므로 문자열의 길이가 2만이지만, StringBuffer는 동기화를 지원하지 않아 문자열의 길이가 2만보다 작다.

import java.util.*;

public class Main extends Thread{
    public static void main(String[] args){
        StringBuffer stringBuffer = new StringBuffer();
        StringBuilder stringBuilder = new StringBuilder();

        new Thread(() -> {
            for(int i=0; i<10000; i++){
                stringBuffer.append(1);
                stringBuilder.append(1);
            }
        }).start();

        new Thread(() -> {
            for(int i=0; i<10000; i++){
                stringBuffer.append(1);
                stringBuilder.append(1);
            }
        }).start();

        new Thread(() -> {
            try{
                Thread.sleep(2000);

                System.out.println("StringBuffer.length: "+ stringBuffer.length()); //20000, 동기화 지원
                System.out.println("StringBuilder.length: "+ stringBuilder.length()); //16343, 동기화 미지원

            }catch(InterruptedException e){
                e.printStackTrace();
            }
        }).start();

    }
}

성능 비교

String의 +연산, StringBuffer, StringBuilder의 append()연산의 성능을 비교해보자. 각각의 문자열에 "*"을 5만번 더한다.
연산 시간은 String > StringBuffer > StringBuilder 순으로, StringBuilder가 가장 빠른 것을 볼 수 있다. String 객체는 매번 새로운 객체를 생성해야하므로 수행시간이 기하급수적으로 늘어나지만(보통 10만번 까지 버틴다), StringBuilder, StringBuffer는 1000만번까지는 버티며 그 점점 StringBuilder와 성능차이가 나는 것을 볼 수 있다.

    public static void main(String[] args) {
        final int N = 50000;

        String str = "";
        long start1 = System.currentTimeMillis(); // 시작 시간
        for (int i=0; i < N; i++){
            str += "*"; //concat이 더 빠름
        }
        long end1 = System.currentTimeMillis(); // 종료 시간

        StringBuffer stringBuffer = new StringBuffer();
        long start2 = System.currentTimeMillis(); // 시작 시간
        for (int i=0; i < N; i++){
            stringBuffer.append("*");
        }
        long end2 = System.currentTimeMillis(); // 연산에 걸린 시간

        StringBuilder stringBuilder = new StringBuilder();
        long start3 = System.currentTimeMillis(); // 시작 시간
        for (int i=0; i < N; i++){
            stringBuilder.append("*");
        }
        long end3 = System.currentTimeMillis(); // 연산에 걸린 시간

        System.out.println("String 연산: " + (end1 - start1)); 
        System.out.println("StringBuffer 연산: " + (end2 - start2));
        System.out.println("StringBuilder 연산: " + (end3 - start3));
    }

 

최근댓글

최근글

skin by © 2024 ttuttak