주요글: 도커 시작하기
반응형
String과 StringBuffer의 성능상의 차이를 살펴본다.

String 클래스와 StringBuffer 클래스의 특징

대체로 초보자는 String 클래스만을 사용하고 있으며, 실력이 향상되어 StringBuffer 클래스를 알게 되면 성능 향상이라는 이유로 StringBuffer 클래스를 자주 사용하는 경향이 있다. 사실 두개의 클래스는 똑같이 문자열 처리를 위한 클래스이며 메모리상의 처리 방법에서 차이를 보여주고 있을 뿐이다. 이러한 처리 방법의 차이가 또한 성능의 차이를 보여주고 있다. 실제로 많은 경우 String 클래스보다 StringBuffer 클래스의 성능이 훨씬 좋다고 많은 사람들이 생각하고 있으며, 실제로 많은 경우에 성능 차이를 보이고 있다.

그렇다면, StringBuffer 클래스가 String 클래스보다 항상 더 나은 성능을 가지고 있을까? 그렇다면 왜, 자바를 설계한 사람들은 String 클래스를 기본 문자열처리 클래스로 정했을까? 여기에 대한 답을 알아보기로 하자.

String 클래스는 변경이 불가능한 immutable 클래스이다.

String 클래스에서 substring(), toLowerCase(), concat(), trim() 등의 메소드를 생각하면 String 클래스는 변경 가능한 클래스처럼 보인다. 그러나, 실제로는 이러한 메소드들은 원래 객체와 다른 새로운 String 객체를 만들어 반환한다. 또 하나의 String 객체가 생성되는 것이다. 따라서 원래 String 객체는 가지고 있는 문자열이 변경되지 않으며 여전히 사용가능한 채로 남는다.

즉, 기존의 String 객체에 substring()과 같은 문자열에 변경을 가하는 메소드를 실행하면 또 하나의 String 객체가 생성되어 서로 다른 두개의 String 객체가 존재하게 된다.

이런 이유로 String 클래스의 변경은 객체를 생성하기 위하여 시스템 자원(시간, 메모리 등)을 낭비한다고 생각되는 경향이 있다. 그렇다면 왜 immutable(변경불가) 클래스인가?

왜 immutable(변경불가) 클래스인가?

immutable 클래스는 몇 가지 조건과 특징을 가지고 있다.

첫번째는, 클래스가 가지고 있는 값(즉, String 클래스에서는 문자열)은 오직 생성자에서만 설정될 수 있으며, 그 값을 변경할 수 있는 어떠한 메소드도 가지고 있지 않아야 한다. 만약 변경을 원한다면, 원하는 값을 가진 새로운 객체를 생성한다.

이런 immutable 클래스의 가장 큰 장점은 안전하게 공유될 수 있다는 점이다. 즉, 변경은 적고, 읽기(즉, 문자열의 참조)만 많은 경우, 또는, 여러 쓰레드나 객체에서 공유하는 경우, synchronization(동기화)와 같은 특별한 안전장치 없이도 안전하게 공유될 수 있다.

대부분의 문자열이 복잡한 문자열 처리과정보다는 한번 설정된 문자열들을 여러 곳에서 공유하는 경우가 많으므로, 자바에서 기본 문자열을 처리하는 클래스로 String 클래스를 immutable 패턴으로 설정하였다.

StringBuffer 클래스는 변경이 가능한 mutable 클래스이다.

StringBuffer 클래스는 가지고 있는 문자열의 내용을 변경 가능하도록 만든 클래스이다. 즉, append(), insert(), delete() 등의 메소드를 통하여 StringBuffer 객체가 가지고 있는 문자열을 변경할 수 있으며, 이 때, String 클래스처럼 새로운 객체를 생성하지 않고, 기존의 문자열을 변경한다. 이 경우 객체 생성을 하지 않으므로, String 클래스보다 효율적이라고 생각하기 쉽지만, 동기화(synchronization)를 보장해야 하기 때문에 단순한 참조에서는 상대적으로 String 보다 나쁜 성능을 보인다. 따라서, 단순 참조가 많은 경우 StringBuffer 클래스보다 String 클래스가 유리하다. 물론, StringBuffer 클래스는 동기화되어 있으므로, 멀티 쓰레드에 대하여 안전하다.

또한, StringBuffer 객체는 문자열을 다루는 다른 메소드에서 사용되기 위하여, toString() 메소드를 통하여 String 객체를 생성하게 된다. 이때, 일반적으로 String 객체의 생성과 함께, 가지고 있는 문자열에 대한 복사가 이루어진다. 물론, 자바 규약은 성능 향상을 위하여 String 객체 생성 후에 문자열을 복사하지 않고, StringBuffer 객체와 문자열을 공유하여 참조하는 프록시 패턴을 적용하는 것을 허용하고 있다. 그러나, 이것은 반드시 그런 것은 아니며, 프록시 패턴의 특성상 StringBuffer 객체에 변경이 가해지면, 프록시는 바로 해제되며, 그 시점에서 문자열의 복사가 이루어진다. (프록시 패턴의 적용은 필수 요건이 아니며, 자바 가상 머신 구현체에 따라 다를 수 있으며, 사용상의 차이는 전혀 없고 성능 상의 차이만을 보일 뿐이다.)

성능 차이의 실제적인 비교

다음과 같은 질문을 생각해보자.

  • StringBuffer 객체는 내용을 변경할 때 String 객체보다 효율적인가?
  • String 객체는 가지고 있는 문자열을 변경할 때 어느 정도 StringBuffer 객체에 비해 성능 저하를 보이는가?
  • StringBuffer 및 String 클래스는 모두 문자열 처리에서 가장 많이 쓰이는 substring() 메소드에 대하여 String 객체를 생성한다. 그렇다면 성능상의 차이가 있는가?
  • StringBuffer 객체는 toString() 메소드를 통하여 String 객체를 생성하여야만 다른 객체에 문자열을 전달할 수 있다. toString() 메소드를 통한 String 객체 생성의 자원 소모는 어느 정도 인가?
  • String 객체 및 StringBuffer 객체의 생성은 어느 정도의 자원 소모를 필요로 하는가?
여기에 대한 답을 얻기 위하여 간단한 테스트 프로그램을 작성하여 결과를 구해보았다. 다음과 같은 8가지 경우에 대하여 각각 64만번의 반복을 통하여 소요된 시간과 자유 메모리의 변화를 그래프로 보면 다음과 같다.

비교를 위해 테스트에 사용된 8가지 메소드

  • String.concat() - String 클래스에 문자열 추가
  • StringBuffer.append() - StringBuffer 클래스에 문자열 추가
  • Stirng.substring() - String 클래스에서 문자열 일부 추출
  • StirngBuffer.substring() - StringBuffer 클래스에서 문자열 일부 추출
  • Stirng.toString() - String 클래스의 toString() 메소드 호출 (실제로는 자기자신을 돌린다)
  • StirngBuffer.toString() - StringBuffer 클래스의 toString() 메소드 호출 (즉, String 객체로 변환)
  • new String() - String 객체 생성
  • new StringBuffer() - StringBuffer 객체 생성


그림 1. 64만번 반복 동안 감소되어가는 자유 메모리의 양 (단위: MB)



그림 2. 64만번 반복 동안 소요된 시간 (단위: 밀리초)
위의 그래프를 통해 알 수 있는 몇가지 중요한 사실을 정리하면 다음과 같다.

  1. 객체를 생성하지 않는 String.toString() 메소드와 StringBuffer.append() 메소드는 메모리 자원을 거의 소모하지 않는다.
  2. StringBuffer 객체의 생성이 시간과 메모리 자원을 가장 많이 필요로 한다.
  3. StringBuffer의 toString() 메소드 등과 같이 String 객체를 생성하는 메소드들은 일정한 시간과 일정한 메모리 자원을 소모한다.
성능 향상에 대한 결론

문자열을 추가하기 위하여 append()와 같은 메소드를 사용할 때 StringBuffer 클래스는 String 클래스와 비교하여 아주 뛰어난 성능을 보인다. 그러나. StringBuffer 객체의 생성 및 toString() 메소드를 통한 String 객체의 생성을 반드시 필요로 하므로 더 많은 시간 및 메모리 자원의 낭비를 초래한다.

그에 비하여, String 클래스는 StringBuffer 클래스와 비교하여 인스턴스화를 통하여 객체를 생성할 때 상대적으로 적은 자원을 소모하며, toString() 메소드를 통하여 String 객체로 바꿀 필요가 없다.

따라서, StringBuffer 클래스는 하나의 문자열에 대하여 다른 문자나 문자열의 추가가 여러 번 이루어지는 경우 유리하며, 단 한번의 문자열 추가에 대하여 StringBuffer 클래스를 사용하는 것은 오히려 시간 및 메모리 자원 낭비를 초래하게 된다.

64만번이란 반복 횟수가 많은 것처럼 보일지도 모르지만 실제로 대부분의 웹 사이트와 같은 곳에서 서블릿/JSP 기술을 사용할 경우 동시 접속자 수에 따른 문자열 처리가 쉽게 수십만번까지 이루어질 수 있다.

따라서, 자신의 프로그래밍 코드에 따라 String 클래스와 StringBuffer 클래스 중 어떤 클래스가 적합한지 스스로 잘 선택하여야 한다. 어떤 메소드를 사용하고 객체의 생성이 얼마나 존재하는지 고려하는게 중요하다.

아무런 자체 평가없이 StringBuffer 클래스가 더 빠르다고 생각하는 것은 오히려 성능 저하를 불러올 수가 있다. 참고로 테스트에 사용된 코드는 다음과 같다.

  
  public class StringTest {
  
     private final static String HELLO  = "안녕하세요!";
  
     private int count = 1000;
     private int size  = 640;
     private long[] timeStamp  = new long[count+1];
     private long[] freeMem = new long[count+1];
     String s  = new String(HELLO);
     String s2 = new String(HELLO);
     StringBuffer sb = new StringBuffer(HELLO);
     StringBuffer sb2 = new StringBuffer(HELLO);
  
     public StringTest() {
     }
  
     public void printResult() {
        for (int i = 1; i <= count; i++) {
           System.out.println(i + "\t" + 
            (timeStamp[i] - timeStamp[0]) + "\t" + freeMem[i]);
        }
        System.gc();
     }
     
     public void test1() {
        for (int i = 0; i <= count; i++) {
           for (int j=0; j < size; j++) {
              s2 = s.concat(HELLO);
           }
           freeMem[i] = Runtime.getRuntime().freeMemory();
           timeStamp[i] = System.currentTimeMillis();
        }
     }
  
     public void test2() {
        for (int i = 0; i <= count; i++) {
           for (int j=0; j < size; j++) {
              sb = sb.append(HELLO);
              sb.setLength(6);
           }
           freeMem[i] = Runtime.getRuntime().freeMemory();
           timeStamp[i] = System.currentTimeMillis();
        }
     }
  
     public void test3() {
        for (int i = 0; i <= count; i++) {
           for (int j=0; j < size; j++) {
              s2 = s.substring(0,2);
           }
           freeMem[i] = Runtime.getRuntime().freeMemory();
           timeStamp[i] = System.currentTimeMillis();
        }
     }
  
     public void test4() {
        for (int i = 0; i <= count; i++) {
           for (int j=0; j < size; j++) {
              s2 = sb.substring(0,2);
           }
           freeMem[i] = Runtime.getRuntime().freeMemory();
           timeStamp[i] = System.currentTimeMillis();
        }
     }
  
     public void noop(String st) {
     }
     
     public void test5() {
        for (int i = 0; i <= count; i++) {
           for (int j=0; j < size; j++) {
              noop(s.toString());
           }
           freeMem[i] = Runtime.getRuntime().freeMemory();
           timeStamp[i] = System.currentTimeMillis();
        }
     }
  
     public void test6() {
        for (int i = 0; i <= count; i++) {
           for (int j=0; j < size; j++) {
              noop(sb.toString());
           }
           freeMem[i] = Runtime.getRuntime().freeMemory();
           timeStamp[i] = System.currentTimeMillis();
        }
     }
  
     public void test7() {
        for (int i = 0; i <= count; i++) {
           for (int j=0; j < size; j++) {
              s = new String(HELLO);
           }
           freeMem[i] = Runtime.getRuntime().freeMemory();
           timeStamp[i] = System.currentTimeMillis();
        }
     }
  
     public void test8() {
        for (int i = 0; i <= count; i++) {
           for (int j=0; j < size; j++) {
              sb = new StringBuffer(HELLO);
           }
           freeMem[i] = Runtime.getRuntime().freeMemory();
           timeStamp[i] = System.currentTimeMillis();
        }
     }
  
     public static void main(String[] args) {
        StringTest test = new StringTest();
        System.gc();
        test.test1();   // String에 문자열 추가
        test.printResult();
        test.test2();   // StringBuffer에 문자열 추가
        test.printResult();
        test.test3();   // String에서 substring() 호출
        test.printResult();
        test.test4();   // StringBuffer에서 substring() 호출
        test.printResult();
        test.test5();   // String에서 toString() 호출
        test.printResult();
        test.test6();   // StringBuffer에서 toString() 호출
        test.printResult();
        test.test7();   // String 객체 생성
        test.printResult();
        test.test8();   // StringBuffer 객체 생성
        test.printResult();
     }
  }

결론

String 클래스와 StringBuffer 클래스 중에 어떤 클래스가 어떤 경우에 적합한지 알아보기 위하여 성능 테스트를 실시해보았다. 객체의 생성과 문자열의 추가, 다른 메소드에 대한 참조를 위한 변경 등을 비교하여 전체적인 성능 평가의 기준을 보였다. 결과적으로, 프로그래밍의 성능 향상을 위하여 도움이 될 수 있는 비교 자료를 제시하고 있으며, 많은 프로그래머들에게 성능 향상에 도움이 될 것이라고 생각한다.



본 글의 저작권은 이동훈에 있으며 저작권자의 허락없이 온라인/오프라인으로 본 글을 유보/복사하는 것을 금합니다.

+ Recent posts