알고리즘 문제를 풀다가 반복문을 이용해 문자열을 합치는 부분이 있었다. 간단한 문제여서 String으로 문자열을 합쳤었는데 다른 사람들의 풀이를 보니 StringBuilder로 문제를 풀었다. 해당 문제를 String과 StringBuilder로 풀어봤는데 속도 차이가 많이 나서 어떤 차이 때문에 그런지 확인해보려고 한다. 

String으로 풀었을 때
StringBuilder로 풀었을 때


String

  • String은 불변(immutable)한 객체이며, 한 번 생성되면 내용을 변경할 수 없으며 길이가 정해져 있다. 이러한 특성 때문에 멀티스레드 환경에서 사용하기 적절하다. 
    public class TEST {
        public static void main(String[] args){
            String str = "ABC";
            System.out.println("ABC : " + str.hashCode());
    
            str = "AAA";
            System.out.println("AAA : " + str.hashCode());
        }
    }​


    위 코드는 동일한 str 변수이지만, ABC와 AAA를 가리키는 참조값이 다른 것을 확인할 수 있다.
  • 위 결과에서 알 수 있는 것처럼, 문자열 조작(추가, 자르기 등) 시 새로운 객체를 생성해 조작된 문자열을 할당하고 기존 String 객체는 GC(Garbage Collection)에 의해 수거된다. 
    public class TEST {
        public static void main(String[] args){
            String str = "ABC";
            System.out.println("ABC : " + str.hashCode());
    
            str += "AAA";
            System.out.println("ABCAAA : " + str.hashCode());
        }
    }
    단, JDK 1.5 이상부터는 위와 같이 + 로 문자열을 합칠 경우 컴파일러 단에서 자동으로 StringBuilder로 변경해준다고 한다. 

  • String 객체는 Comparable 인터페이스를 상속하고 있기 때문에 equals 메서드로 문장이 동일한지 확인 가능하지만 StringBuilder는 Comparable 인터페이스를 상속하지 않기 때문에 equals 메서드가 없으며 비교 메서드가 따로 존재하지 않는다. 
  • 새로운 String 객체를 만들고 값을 할당할 때 JVM(Java Virtual Machine)은 내부적으로 가지고 있는 string pool에서 같은 값이 있는지 확인한다. 같은 값이 있을 경우, pool에서 해당 객체의 참조값을 반환하며 값이 없을 경우엔 pool에 값을 생성한 후 참조값을 반환한다. 그래서 각 다른 String 객체에 같은 값을 할당하면 가리키는 값이 동일하며 똑같은 값을 두 번 할당하지 않기 때문에 메모리 또한 절약할 수 있다.
    public class TEST {
        public static void main(String[] args){
            String str = "ABC";
            String str2 = "ABC";
            System.out.println("str : " + str.hashCode());
            System.out.println("str2 : " + str2.hashCode());
        }
    }​

StringBuilder/StringBuffer

  • StringBuilder는 가변(mutable)한 객체이며 내용의 변경이 가능하며, 내용을 변경할 경우 내부에서 내용의 길이를 변경한다. 이러한 특성 때문에 멀티스레드에서 사용하기에는 적절하지 않다. 
    public class TEST {
        public static void main(String[] args){
            StringBuilder sb = new StringBuilder("ABC");
            System.out.println(sb.toString() + " : " + sb.hashCode());
    
            sb.delete(0, sb.length());
            sb.append("AAA");
            System.out.println(sb.toString() + " : " + sb.hashCode());
        }
    }​

    동일한 sb 변수에 ABC값을 할당한 후 hashCode값과 sb 변수에 할당되어있는 값을 지우고 append() 메서드로 AAA 값을 할당한 결과 동일한 참조값을 가지고 있음을 볼 수 있다.

  • StringBuilder는 문자열 조작 시 새로운 객체를 할당하지 않으며 단순히 내부에서 문자열의 길이를 변경해 사용한다. 
    public class TEST {
        public static void main(String[] args){
            StringBuilder sb = new StringBuilder("ABC");
            System.out.println(sb.toString() + " : " + sb.hashCode());
    
            sb.append("AAA");
            System.out.println(sb.toString() + " : " + sb.hashCode());
        }
    }​

  • String에서 언급했지만 StringBuilder는 Comparable 인터페이스를 상속하지 않기 때문에 equals 메서드를 이용한 비교가 불가능하다. 따라서 아래와 같이 String 형식으로 변환 후 비교를 진행해야 한다.
    public class TEST {
        public static void main(String[] args){
            StringBuilder sb = new StringBuilder("ABC");
            StringBuilder sb2 = new StringBuilder("ABC");
    
            if (sb.toString().equals(sb2.toString())){
                System.out.println("sb, sb2 is equal");
            }
            else{
                System.out.println("sb, sb2 is not equal");
            }
        }
    }​
  • String과 다르게 StringBuilder는 객체에 동일한 값을 할당하더라도 참조값이 서로 다름을 확인할 수 있다.
    public class TEST {
        public static void main(String[] args){
            StringBuilder sb = new StringBuilder("ABC");
            System.out.println("sb : " + sb.hashCode());
    
            StringBuilder sb2 = new StringBuilder("ABC");
            System.out.println("sb2 : " + sb2.hashCode());
        }
    }​

     
  • StringBuffer는 StringBuilder와 사용하는 메서드가 같은데, 차이점이라면 synchronized 키워드가 존재하여 멀티스레드 환경에서 안전하다고 한다. 

String vs StringBuilder/StringBuffer 성능 비교

동일한 문자를 100만 번 반복했을 때의 속도를 비교해보자.

public class TEST {

    private static final int REPEAT_COUNT = 1000000;

    public static void main(String[] args){
        long start, end;

        start = System.currentTimeMillis();
        StringAppend();
        end = System.currentTimeMillis();
        System.out.println("StringAppend : " + (end - start) + "ms");

        start = System.currentTimeMillis();
        StringBuilderAppend();
        end = System.currentTimeMillis();
        System.out.println("StringBuilderAppend : " + (end - start) + "ms");

        start = System.currentTimeMillis();
        StringBufferAppend();
        end = System.currentTimeMillis();
        System.out.println("StringBufferAppend : " + (end - start) + "ms");
    }

    public static void StringAppend(){
        String str = "";
        for(int i=0; i<REPEAT_COUNT; i++){
            str += '!';
        }
    }

    public static void StringBuilderAppend(){
        StringBuilder sb = new StringBuilder();
        for(int i=0; i<REPEAT_COUNT; i++){
            sb.append("!");
        }
    }

    public static void StringBufferAppend(){
        StringBuffer sb = new StringBuffer();
        for(int i=0; i<REPEAT_COUNT; i++){
            sb.append("!");
        }
    }
}

결과에서 확실하게 알 수 있듯이 String을 + 문자로 계속 추가하게 되면 객체의 생성, 삭제가 계속해서 발생하게 되며 GC가 개입하기 때문에 단순히 문자열을 연결할 경우 속도면에서 매우 불리하다. 그리고 StringBuffer를 이용한 값이 StringBuilder보다 조금 더 오래 걸리는데, 그 이유가 StringBuffer는 내부적으로 thread-safe 를 유지하기 위해 별도의 작업을 진행하기 때문에 시간차이가 난다고 한다.

 

https://coderanch.com/t/666742/java/understand-StringBuffer-thread-safe-StringBuilder

 

How to understand StringBuffer is thread safe and StringBuilder is not by example output? (Beginning Java forum at Coderanch)

 

coderanch.com

위 주소에는 StringBuilder와 StringBuffer를 Thread 환경에서 사용했을 때 어떻게 다른지 알 수 있는 예제코드가 댓글에 달려있다. 

 


결론

1. 간단한 문자열 조작은 단순히 String 객체를 이용해 +로 조작하는 것이 가독성에 좋다.

2. 빈번한 문자열 조작이 필요할 경우 StringBuilder나 StringBuffer를 이용하는 것이 성능상에 유리하다.

3. 멀티스레드 환경에서 thread-safe 해야한다면 StringBuffer를, 그렇지 않다면 StringBuilder를 이용하는 것이 성능에 유리하다.

 

'0x10. 프로그래밍 > 0x11. Java' 카테고리의 다른 글

[JAVA] 정규표현식  (0) 2021.07.11

+ Recent posts