ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • (java)String vs StringBuilder 수행 시간 차이
    Java & Kotlin 2021. 3. 28. 18:14
    반응형

    1. 서론

    일반적으로 문자열을 저장할 때 String 클래스를 사용합니다.

     

    하지만 이미 저장된 String 문자열에 반복적으로 추가 저장할 때도 좋은 방법일까요?

     

    2. String - Immutable Object

    String은 불변(immutable) 객체입니다. 불변하다는 것을 어떤 의미일까요? 

     

    불변하다는 것은 만들어진 상태 그대로 상수로서 존재합니다. 즉 객체로서 생성된 후 상태를 변경하지 못합니다. 

     

    그렇다면 이미 만들어진 String 객체에 추가적인 데이터를 저장할 때는 어떻게 될까요? 이때는 메모리에서 추가 공간을 할당해 새 객체를 만듭니다. 즉 추가적이 메모리 주소의 할당이 발생합니다. 같은 참조 변수를 사용하더라도 메모리를 추가로 할당해야 합니다. 

     

    예를 들어 String hello1 = new String("hello world"), String hello2 = new String("hello world")가 같은 String 객체를 사용하고 있지만, 당연하게도 다른 참조 주소를 가지고 있습니다. 즉 같은 String, 같은 "hello world"를 가지고 있더라도 각각의 String 객체의 불변성을 보장합니다. 

     

    불변 객체에 데이터의 추가가 빈번하지 않다면 오히려 성능상 이점이 있습니다.

     

    자바 공식 API 문서에서 불변 객체는 새로 호출을 할 때 원본의 객체를 그대로 참조시켜 준다고 합니다. 즉 할당됐다가 해제되는 객체가 없으니, GC의 오버헤드(overhead)를 줄일 수 있다는 이점이 있습니다. 

     

    JVM에는 String을 위한 String pool이 있습니다. 즉 큰 Pool을 만들어 두고, 같은 문자열이 호출된다면 참조만 복사해 반환합니다. 즉 참조만 복사하는 것이 객체 전체를 복사 후 할당하는 것보다는 비용 절감할 수 있습니다. 

     

    여기서 조금 이상합니다. 어차피 같은 pool을 사용하는데 StringBuilder에 비해 왜 메모리 측면에서 불리할까요? 이것은 빈번한 데이터 추가가 발생할 때 입니다. 즉 새로운 데이터가 추가적으로 발생하면, 기존의 객체 전체를 복사 후 새롭게 할당해야 합니다. 왜냐하면 '불변 객체'이기 때문에 새로운 문자열을 추가하려면 그냥 변경하는 것이 아니라, 복사 후 붙이고 할당해야 합니다. 만약 이를 수백 번 반복하게 된다면 비용 측면에서 불리합니다. 

     

    3. StringBuilder - Mutable Object

    StringBuilder 클래스는 가변(Mutable) 객체입니다. 

     

    빈번한 데이터 추가가 있을 때 StringBuilder는 성능상 이점이 있습니다. 왜냐하면 StringBuilder는 16의 inital capacity를 가집니다. 즉 16보다 적은 데이터가 온다면, 추가적인 데이터가 발생해도 메모리를 추가 할당할 필요가 없습니다. 또한 추가 데이터가 발생하면 전체 길이의 2배 길이로 복사 후 할당합니다. String과 마찬가지로 복사 후 할당이 발생하지만, String에 비해 그 횟수가 현저히 적습니다. 

     

    4. 수행 시간 측정

    아래는 String, StringBuilder 각각을 사용해서 100,000개의 데이터를 추가했을 때입니다. 

     

     

    각각의 방법으로 3회 측정 시 수행 시간입니다.

     

    String 

    • 5429.7ms

     

    StringBuilder 

    • 69.7ms

     

    StringBuilder.setLength (initial capacity 100)

    • 53.7ms 

     

    실제로 StringBuilder가 String에 비해 훨씬 빠르고, 적당한 capacity를 줬을 때 더 빠른 모습을 보입니다. 

     

    하지만 capacity를 지나치게 많이 주게 되면, 큰 capacity의 복사가 발생해서 수행 시간이 증가하는 모습을 보여줬습니다.

     

    4. 실제 프로젝트 적용

     

    위는 웹 사이트에서 비밀번호 찾기 했을 때 새 임시 비밀번호를 생성 후 전송하는 코드입니다. 이때 기존의 String 배열에 새 비밀번호를 추가 생성하는 것에서 StringBuilder로 바꿨습니다.

     

    기존의 6ms --> 0ms로 수행 시간 단축이 있었습니다.

     

    5. 추가 내용 

    이 글을 작성한 뒤 알게 됐습니다.

     

    JDK9부터는 invokeDynamic을 활용한 String Concatenation으로 성능 최적화를 했다고 합니다. 

     

    JDK 9 이전

     

    아래의 컴파일러 구조로 바뀌었습니다.

    JDK 9

     

    기존

    • String hello ="hello", String world="world" 
    • 각각의 String 객체를 만들고, StringBuilder.append() 반복합니다.

     

    JDK 9 이후 

    • String = "Hello" +  "World"
    • 같은 String 객체를 사용하여 주소가 변하지 않고, 같은 주소 공간을 사용합니다.
    • 그리고 LString에서 int, String 값이 모두 합쳐집니다. 

     

    저는 JDK 11을 사용하고 있었으므로, 제가 테스트한 것처럼 수백 번씩 추가를 하는 것이 아니라면, String으로 써도 컴파일 성능에 큰 차이가 없어야 합니다. 위의 컴파일러 차이에서 보듯, 기존에는 컴파일 시에 StringBuilder.append 연산을 계속해서 했습니다.

     

    하지만 JDK 9의 컴파일러에서는 구조가 변경되어 invokeDynamic 바이트코드를 이용합니다. 이는 String이 들어올 때마다 append 하면 StringBuilder를 계속해서 호출해야 했지만, String이 들어오는 것을 계속해서 쌓다가 한 번에 구현합니다. 이것을 openJDK에서는 'lazy linkage'라고 표현을 하고 있습니다. 

     

    이때 입력되는 String의 concatenation을 담당하는 것은 StringConcatFactory 클래스입니다. 한 번에 쌓아둔 String은 StringConcatFactory.makeConcatWithConstants()의 single call로 인해서 String concatenation으로 return 됩니다. 

     

    다만, 실제 수행 시간이 차이 나는 이유는 for loop에서의 특수한 상황으로 "추측"하고 있습니다.

     

     

    6. 참고 문헌

    https://docs.oracle.com/javase/9/docs/api/java/lang/invoke/StringConcatFactory.html

    ko.wikipedia.org/wiki/불변객체

    docs.oracle.com/javase/tutorial/java/data/buffers.html

    velog.io/@kyle/불변-객체란-Java-Immutable-Object

    반응형

    댓글

Designed by Tistory.