본문 바로가기
Language/Java

[Java API] StringBuilder 클래스

by 클레어몬트 2024. 8. 22.

불변인 String 클래스에는 단점이 존재한다.

 

"A" + "B"
String("A") + String("B") // 문자는 String 타입이다
String("A").concat(String("B")) // 문자의 더하기는 concat을 사용한다
new String("AB") // String은 불변이므로 새로운 객체를 생성한다

 

두 문자를 더하는 경우 다음과 같이 동작한다.

더 많은 문자를 더하는 경우에는

String str = "A" + "B" + "C" + "D";
/*
String str = String("A") + String("B") + String("C") + String("D");
String str = new String("AB") + String("C") + String("D");
String str = new String("ABC") + String("D");
String str = new String("ABCD");
*/

이 경우 총 3개의 String 클래스가 추가로 생성된다.
그런데 문제는 중간에 만들어진 new String("AB"), new String("ABC")는 사용되지 않는다. 최종적으로 만들어진 new String("ABCD")만 사용된다. 결과적으로 중간에 만들어진 new String("AB"), new String("ABC")는 제대로 사용되지도 않고, 이후 GC의 대상이 된다.

 

불변인 String 클래스의 단점은 문자를 더하거나 변경할 때마다 계속해서 새로운 객체를 생성해야 한다는 점이다. 문자를 자주 더하거나 변경해야 하는 상황이라면 더 많은 String 객체를 만들고, GC(Garbage Collection)해야 한다. 결과적으로 컴퓨터의 CPU, 메모리 자원을 더 많이 사용하게 된다. 그리고 문자열의 크기가 클수록, 문자열을 더 자주 변경할수록 시스템의 자원을 더 많이 소모하게 된다.

 

이 문제를 해결하는 방법은 단순하다. 바로 불변이 아닌 가변 String을 사용하면 된다. 가변은 내부의 값을 바로 변경하면 되기 때문에 새로운 객체를 생성할 필요가 없다. 따라서 성능과 메모리 사용면에서 불변보다 더 효율적이다. "하지만 사이드 이펙트를 주의해야 한다"

ㅇStringBuilder 클래스: 불변인 String을 보완하기 위해 나온 가변 String

StringBuilder는 내부에 final이 아닌 변경할 수 있는 byte[]을 가지고 있다

public final class StringBuilder {
    char[] value;// 자바 9 이전 
    byte[] value;// 자바 9 이후
    
    //여러 메서드
    public StringBuilder append(String str) {...}
    public int length() {...}
	...
}

 

 

[StringBuilder 메서드]

- append(String str): 문자열 추가

 

- insert(int idx, String str): 해당 인덱스에 문자열 삽입

 

- setCharAt(int idx, char ch): 해당 인덱스에 문자 교체

 

- delete(int start, int end): 특정 범위의 문자열 삭제

 

- reverse(): 문자열 뒤집기

 

package lang.string.builder;

public class StringBuilderMain1_1 {
    public static void main(String[] args) {
        StringBuilder sb = new StringBuilder();
        sb.append("A");
        sb.append("B");
        sb.append("C");
        sb.append("D");
        System.out.println("sb = " + sb);

        // 불변이 아닌 가변이기 때문에 따로 객체를 안받아도 바로 적용이 된다
        sb.insert(4, "Java");
        System.out.println("insert = " + sb);

        sb.setCharAt(2, 'V');
        System.out.println("setCharAt = " + sb);

        sb.delete(4, 8);
        System.out.println("delete = " + sb);

        sb.reverse();
        System.out.println("reverse = " + sb);

        // StringBuilder -> String
        String str = sb.toString();
        System.out.println("String = " + str);
    }
}

 

 

 

(정리) 가변(Mutable) vs 불변(Immutable)

String은 불변하다. , 한 번 생성되면 그 내용을 변경할 수 없다. 따라서 문자열에 변화를 주려고 할 때마다 새로운 String 객체가 생성되고, 기존 객체는 버려진다. 이 과정에서 메모리와 처리 시간을 더 많이 소모한다. 반면에, StringBuilder는 가변적이다. 하나의 StringBuilder 객체 안에서 문자열을 추가, 삭제, 수정할 수 있으며, 이때마다 새로운 객체를 생성하지 않는다. 이로 인해 메모리 사용을 줄이고 성능을 향상시킬 수 있다. "단 사이드 이펙트를 주의해야 한다."

StringBuilder는 보통 문자열을 변경하는 동안만 사용하다가 문자열 변경이 끝나면 안전한(불변) String으로 변환하는 것이 좋다.

 

 

 

(중요!)

Java는 문자열을 다룰 때 Java 자체 내부에서 StringBuilder를 이용하여 스스로 최적화를 한다. (최적화 방식은 Java 버전에 따라 상이하다. e.g. Java 9부터는 StringConcatFactory를 사용해서 최적화를 수행) 따라서 웬만해서는, 직접 StringBuilder를 사용하지 않고 문자열 더하기 연산(+)을 사용해도 충분하다. 하지만, StringBuilder를 직접 사용하는 것이 훨씬 훨씬 더 좋은 케이스들이 있다.

[StringBuilder를 직접 사용하는 것이 훨씬 훨씬 더 좋은 케이스]

- 반복문에서 반복해서 문자를 연결할 때

- 조건문을 통해 동적으로 문자열을 조합할 때

- 복잡한 문자열의 특정 부분을 변경해야 할 때

- 매우 긴 대용량 문자열을 다룰 때

 

<예시>

반복문의 범위가 100000일 때, StringBuilder를 사용하지 않으면 2341ms가 걸린다

package lang.string.builder;

public class LoopStringMain {
    public static void main(String[] args) {
        long startTime = System.currentTimeMillis();
        String res = "";
        for (int i = 0; i < 100000; i++) {
            res += "Hello Java ";
        }
        long endTime = System.currentTimeMillis();

        // 2341ms - 어마어마하게 오래 걸린다
        System.out.println("result = " + res);
        System.out.println("time = " + (endTime - startTime) + "ms");
    }
}

 

 

 

하지만 StringBuilder를 사용하면 3ms가 걸린다

package lang.string.builder;

public class LoopStringBuilderMain {
    public static void main(String[] args) {
        long startTime = System.currentTimeMillis();
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < 100000; i++) {
            sb.append("Hello Java ");
        }
        long endTime = System.currentTimeMillis();

        // 3ms - 어마어마하게 짧게 걸린다
        String res = sb.toString();
        System.out.println("result = " + res);
        System.out.println("time = " + (endTime - startTime) + "ms");
    }
}

 

차이가 아주 어마어마한 것을 알 수가 있다. 무조건 써야 한다!

 

 

 

(참고) StringBuilder vs StringBuffer

StringBuilder와 똑같은 기능을 수행하는 StringBuffer 클래스도 있다. 이 둘의 차이점을 알아두자.

- StringBuilder: 멀티 쓰레드 상황에 안전하지 않지만 동기화 오버헤드가 없어 속도가 빠르다.

- StringBuffer: 내부에 동기화가 되어 있어서 멀티 스레드 상황에 안전하지만, 동기화 오버헤드로 인해 성능이 느리다.

 

 

 

 

 

 

 

참고 및 출처: 김영한의 실전 자바 - 중급 1편

https://www.inflearn.com/course/%EA%B9%80%EC%98%81%ED%95%9C%EC%9D%98-%EC%8B%A4%EC%A0%84-%EC%9E%90%EB%B0%94-%EC%A4%91%EA%B8%89-1