2016년 2월 17일 수요일

String, StringBuffer, StringBuilder 차이점

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


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

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

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


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

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

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


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


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

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

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

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

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


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

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

5) 성능 차이의 실제적인 비교


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

- 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 객체를 생성하는 메소드들은 일정한 시간과 일정한 메모리 자원을 소모한다.

6) StringBuffer 클래스와 StringBuilder 클래스


StringBuilder 클래스는 JDK 5.0 에서 새로 추가된 클래스이다.
StringBuffer 클래스나 StringBuilder 클래스에서 제공하는 메소드는 동일하다.

StringBuffer 클래스
: 스레드에 안전하게 설계 (ThreadSafe)
여러개의 스레드에서 하나의  StringBuffer 객체를 처리해도 전혀 문제가 되지 않는다.

StringBuilder 클래스
: 단일 스레드에서의 안전성망을 보장
여러개의 스레드에서 하나의 StringBuilder 객체를 처리하면 문제가 발생한다.

####### 사용 ########

StringBuffer sb = new StringBuffer();

// 이렇게 사용해도 되고
sb.append(" ABCDEF ");
sb.append(" GFEFEF ");
sb.append(" WEFWEFWE ");

// 이렇게 사용해도 된다.
sb.append(" ABCDEF ")
    .append( "  GFEFEF ")
    .append( "  WEFWEFWE ");

// 이렇게 만은 제발 사용하지 말자
 sb.append( "ABCDE" + " = " + "FEFEF ");
sb.insert( 3, "1234");


// insert() 메소드는 지정된 위치 이후에 넘어온 값을 덧붙이는 작업을 수행한다.
//insert() 메소드를 수행할 때 지정한 위치까지 값이 할당되어 있지 않으면 StringIndexOutOfBoundsException 이 발생

** append() 메소드를 사용할 때 append() 메소드 내에서 + 를 이용해 문자열을 더하면  StringBuffer 를 사용하는 효과가 
전혀 없게 된다.

7) String, StringBuffer, StringBuilder 선택 기준


1. String 객체는 불변이기 때문에 변하지 않는 문자열은 String을 사용한다.
2. StringBuilder는 비동기방식이기 때문에 Single Thread 환경하에서, 변화되는 문자열의 사용한다.
3. StringBuffer 동기방식으로 저장되기 때문에 멀티쓰레드로 접근하거나 문자열이 변경될 경우에 사용한다.


8) JDK 버전에 따른 차이  


JDK 5.0  이상의 WAS 를 사용한다면 결과가 약간 달라진다. 

다음과 같은 소스를  JDK 1.4 일 경우에는 아래와 동일하게 컴파일 되지만, 

String s = "Here " + "is " + "samples";

JDK 1.5 일 경우 컴파일인 경우에는 다음과 같이 컴파일 된다.

String s = (new StringBuilder ("Here is")).append("samples").toString();

 결론적으로...

 String, StringBuffer, StringBuilder  이 세가지 클래스 중에서 가장 메모리를 많이 차지하고
응답시간에 많은 영향을 주는 것은 String  클래스 이다. 
만약 여러분의 WAS  나 시스템이  JDK 5.0 이상을 사용한다면,
컴파일러에서 자동으로 StringBuilder  로 변환하여 준다.
하지만 반복 루프를 사용해서 문자열을 더할때에는 객체를 계속 추가해야 한다는 사실에는 변함이 없다.
그러므로  String 클래스를 쓰는대신, 스레드와 관련이 있으면  StringBuffer  를,
스레드 안전여부와 상관이 없으면  StringBuilder  를 사용하는 것을 권장한다.

9) 성능 향상에 대한 결론


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

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

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

*reference




  • http://javacan.tistory.com/entry/39
  • http://egloos.zum.com/top2blue/v/5148222



Share:

댓글 2개: