이 글은 오래된 전에 작성된 글입니다. 따라서 최신 버전의 기술에 알맞지 않거나 오류를 유발할 수 있습니다. 저자는 이 글에 대한 질문을 받지 않을 것입니다. 하지만 이 글이 리뉴얼 되면 이 글에 대한 질문을 하거나 토론을 할 수도 있습니다.
문자열 이야기 두 번째 입니다. 이번 글도 마찬가지로 딴지 스타일로 적어봤습니다. 머 완전한 딴지 스타일은 아니고... 세미 딴지 정도? 약간 얌전하게... 역시 기술 자료를 딱딱하지 않게 쓴다는 것이 힘들군요. 쫌더 딴지 스타일에 가깝게 쓰면 몰라도 세미 딴지 스타일은 좀 힘들다는...

Story about StringBuilder - (II)

앞서 문자열 이야기 포스트에서 StringBuilder에 대해 간략한 설명을 해보았다. 이번엔 좀더 꽁꼬 깊숙히 StringBuilder의 내부를 살펴보도록 하자.

Inside StringBuilder

StringBuilder가 생성되면 디폴트로 16문자를 담을 내부 문자열 버퍼를 생성한다. 요것이 오늘의 중요한 뽀인또가 되것다. 연속되는 Append 호출이 발생하여 문자열이 16문자를 초과하게 되면 2배인 32 문자를, 그 후에 또 버퍼가 초과되면 64, 128, 256, 512, ... 이런식으로 계속 새로운 버퍼를 할당한다. 새로운 버퍼를 할당하는 것 외에도 좆지(험험) 못한 것은 기존 버퍼의 내용을 새 버퍼로 복사해야 한다는 것이다. 그리고 나서 기존 문자열 버퍼는 어떻게 하냐고? 그걸 나한테 물으면 어떡하나? GC(Gargabe Collection)에게 물어봐야지.

오늘의 또 한가지 뽀인또는 StringBuilder.ToString() 메쏘드다. StringBuilder를 통해 기껏 문자열을 만들어 대면 뭐하나? 정작 필요한 것은 문자열, 즉 System.String 타입인걸... 그래서 항상 우린 StringBuilder.ToString()을 호출하여 문자열을 받아 낸다. StringBuilder.ToString 메쏘드는 새로운 문자열을 할당하고 내부 문자열 버퍼의 내용을 이 새로운 문자열에 복사하여 반환한다. StringBuilder.ToString이 새로운 문자열을 할당하는 것을 주목할 필요가 있겠다. 만약 새로운 문자열을 만들어 반환하지 않고 StringBuilder의 내부 문자열을 그대로 반환한다면, 하나의 문자열에 대해 2개 이상의 참조(reference)가 존재하게 된다. 만약 StringBuilder 가 재사용되어 변경이 가해지면 문제가 발생하게 될 것이다. ToString() 이 호출된 후, StringBuilder는 버퍼를 초기화 하여 StringBuilder가 재사용될 수 있도록 만드는 것 역시 알아두면 피가되고 살이되는 지식이다. 이쯤 되면 펜을 들고 메모할 필요를 느끼지 않는가?

StringBuilder의 내부를 까보면 생각보다 간단하지 않다는 것을 알 수 있다. 크기가 자동으로 증가되어야 하므로 StringBuilder는 내부 문자열 버퍼가 넘치는 것을 막기 위해 할당된 내부 문자열 버퍼의 크기를 유지하며 또한 이 내부 버퍼에 기록된 문자의 개수 역시 유지해야 하는 등의 오버헤드를 갖고 있다. StringBuilder는 빠르지 않다. StringBuilder에 대한 다양한 성능 테스트(난중에 성능 테스트 자료를 올리겠다. 지금 성능 테스트 자료까지 올리면 블로그 하나가 너무 빡세지기 때문에...)를 살펴보아도 StringBuilder의 오버헤드가 적지 않다는 것을 알 수 있다.

StringBuilder Usage

StringBuilder를 쓰지 말라고 ? 전혀 쓰지 말라는 얘기가 아니다. StringBuilder를 쓸때와 그렇지 않을 때를 명확히 구분하는 것이 좋다는 얘기이다. 이쯤 되면 독자제위들은 어떤 때 StringBuilder를 쓰지 말아야 하는가를 말할 것이라고 예상할 것이다. 반대로 StringBuilder를 사용해야 하는 때는 필자 생각에는 그다지 많지 않다. 대부분의 경우 String.Concat 이나 C#, VB.NET의 + 연산자를 쓰는 것이 더 효율적이며, 반복적으로 다수(수십 ~ 수백회) 문자열을 연결하는 경우에나 StringBuilder를 사용하는 것이 좋다. StringBuilder를 사용하지 않을 때 사용할 문자열 연산 방법은 쪼금 있다 설명하기로 하고 여기서는 StringBuilder의 올바른 용법에 대해서 살펴보자.

StringBuilder를 쓸 때는 가급적 버퍼 크기를 명시하는 것이 좋다. 즉 달랑 디폴트 생성자로 StringBuilder를 생성하지 말고 생성자에 버퍼 크기를 주라는 것이다. 버퍼 크기는 대략적으로 예상되는 크기를 적어주면 되겠다. 그렇다고 무조건 겐또로 찍는 것도 곤란하지만 대략적으로 크기를 알 수 있을 것이다. 전혀 모르겠으면 넉넉하게 크게 잡아줘라. 잘모르겠다고 ? 다음 코드를 보자.

// 좋지 못한 StringBuilder 사용법
StringBuilder sb = new StringBuilder();    // 16 문자 버퍼(문자열)를 생성
sb.Append("1234567890");
sb.Append("1234567890");    // 버퍼가 부족하므로 32 문자를 담을 새로운 버퍼를 생성하고 기존 버퍼 내용을 복사한다.
sb.Append("1234567890");
sb.Append("1234567890");    // 버퍼가 또 부족하므로 64 문자를 담을 새로운 버퍼를 생성하고 기존 버퍼 내용을 복사한다.
string s = sb.ToString();   // 새로운 문자열을 만들어서 내부 버퍼의 내용을 복사하고 반환한다.

위 코드는 4개의 문자열이 StringBuilder와 관련되어 생성되고 사용되며 이중 2개는 아주 짧은 시간 동안만 사용되고 버려진다. 반면 다음 코드는 2개의 문자열만이 사용된다.

// 그나마 나은 StringBuilder 사용법
StringBuilder sb = new StringBuilder(64);    // 64 문자 버퍼(문자열)를 생성
sb.Append("1234567890");
sb.Append("1234567890");
sb.Append("1234567890");
sb.Append("1234567890");    // 버퍼가 충분하므로 새로운 내부 버퍼를 요구하지 않는다.
string s = sb.ToString();   // 새로운 문자열을 만들어서 내부 버퍼의 내용을 복사하고 반환한다.

이 예제에서는 최종적인 문자열 크기를 알기 때문에 64라는 capacity 값을 줄 수 있었지만 최종적인 크기를 전혀 알 수 없다면 넉넉하게 capacity 값을 주는 것이 좋다. 항상 이런 말을 하면 걱정되는 것이 무식하게 딥따 큰 capacity를 주는 인간들이다. 이런 단순 무식은 명랑한 프로그래밍 문화에 아무런 도움이 못 된다. 적당히 넉넉히 주면 StringBuilder가 알아서 그 크기를 2배씩 키워 줄 것이다. 초기 값이 작으면 작을 수록 반복적으로 임시 문자열이 만들어졌다가 사라지므로 이것을 방지해 보자는 것이다.

잠시 후 알게 되겠지만 위의 두 예제 코드는 모두 삽질이 되겠다.... -_- 위와 같이 간단한 문자열 연결(concatenate)은 StringBuilder를 쓰는 것이 더 비효율적일 뿐더러, 위 예제와 같이 문자열 상수(유식하게는 문자열 리터럴 이라구도 한다)를 연결할 때는 더더욱 그렇다. StringBuilder는 커다란 문자열 버퍼에 반복적으로 수십, 수백 회의 문자열 연결하는 때가 아니라면 사용할 일이 별로 없다고 보문 되긋다. 특히 for, while, foreach 류의 반복문 안에서는 StringBuilder를 쓰는 것이 좋다. 그런 때가 언제냐고? ASP.NET에서 HTML을 말 그대로 '만들어'낼 때는 반복적으로 문자열은 연결해야 하는 경우가 마니 생긴다. 안 해봤다고? 그럼 지금이라도 메뉴 웹 컨트롤을 만들어 보라...

Comments (read-only)
#re: 문자열 이야기 (2) - StringBuilder에 대한 진실 혹은 거짓말 (II) / 아 이해 할 수가 없다. / 2007-10-09 오전 4:59:00
StringBuilder sb = new StringBuilder(); 
StringBuilder sb2 = new StringBuilder(); 

sb2 = sb; 
sb.Append("1234567890"); 
Console.WriteLine("{0}", object.ReferenceEquals(sb, sb2)); 

sb.Append("1234567890"); 
Console.WriteLine("{0}", object.ReferenceEquals(sb, sb2)); 

sb.Append("1234567890"); 
Console.WriteLine("{0}", object.ReferenceEquals(sb, sb2)); 

위와 같이 코딩을 해봤습니다. 
두번째와 이후는 16글짜가 넘었기 때문에 새로운 메모리를 생성하여 서로 다른 주소가 나올것이라고 생각했습니다. 
헌데 True가 나옵니다... 
실행시 오브젝트의 주소를 알수 있는방법이 있나요? 그것을 몰라서 object.ReferenceEquals를 이용하여 object의 주소를 비교해 보았습니다.
#re: 문자열 이야기 (2) - StringBuilder에 대한 진실 혹은 거짓말 (II) / 블로그쥔장 / 2007-10-09 오후 2:15:00
-_-; StringBuilder는 참조형 타입(reference type)으로써 객체 입니다. 
sb2 = sb; 
이 코드가 수행됨으로써 sb 와 sb2는 동일한 StringBuilder 객체를 참조하게 됩니다. 
따라서 sb에 벼라별 생 SHOW를 해도 sb와 sb2는 동일한 StringBuilder 객체를 가르키고 
항상 ReferenceEquals가 같다는 것이지요. 
참조형 변수에서 할당 연산자(assignment operator)는 참조값(객체 주소)을 복사하는 것이지 
객체 자체를 복사하는 것이 아님을 이해하셔야 합니다.
#re: 문자열 이야기 (2) - StringBuilder에 대한 진실 혹은 거짓말 (II) / 크레이그진 / 2007-11-23 오후 10:38:00
좋은글 잘 읽었습니다. 그런데 내용중 다음 부분에서 잘 이해가 되지 않습니다. 

ToString() 이 호출된 후, StringBuilder는 버퍼를 초기화 하여 StringBuilder가 재사용될 수 있도록 만드는 것 역시 알아두면 피가되고 살이되는 지식이다. 이쯤 되면 펜을 들고 메모할 필요를 느끼지 않는가? 

ToString()가 최초 호출된후 StringBuilder 버퍼가 재사용될수있도록 만드는것 이것이 무슨말씀이신지 이해가 안됩니다. StringBuilder의 내부버퍼를 초기화한다는 말씀이신지요. 
#re: 문자열 이야기 (2) - StringBuilder에 대한 진실 혹은 거짓말 (II) / 블로그쥔장 / 2007-11-25 오후 12:30:00
ToString() 호출 이후 내부적으로 Null 문자를 추가하고 쓰레드 관련 내부 필드를 초기화하는 등의 
작업을 수행합니다. 특별히 버퍼를 지워버린다거나 하는 작업은 아니므로 
그렇게 크게 신경써야할 부분은 아닙니다만... 
보다 중요한 것은 ToString() 호출이 새로운 문자열을 만들어 StringBuilder 내의 문자열을 
복사한다는 점입니다.
#re: 문자열 이야기 (2) - StringBuilder에 대한 진실 혹은 거짓말 (II) / 크레이그진 / 2007-11-27 오후 2:18:00
아 감사합니다. 요즘 닷넷의 세계에 빠지려는 중생인데 많이 배우고갑니다. 
항상 Under Hood 한 곳을 알아갈때의 재미는 정말 쏠쏠하지요. 닷넷계의 Matt Pietrek 이 되시길~ ^^ 
그리고 닷넷에서 리버싱도 조회가 깊으신것 같은데 다 보여주진 못하셔도 약간의 흰트글들을 올려주시면 정말 감사하겠습니다. 
바라는것만 많아서 정말 죄송하구요. 좋은글들 적으시는데 다시한번 감사드립니다. ^^
#re: 문자열 이야기 (2) - StringBuilder에 대한 진실 혹은 거짓말 (II) / 조승태 / 2008-02-22 오후 5:28:00
앞글에서 저는 그냥 string만 쓴다고 했는데,,, 
무식한게 덕이 되고 말았군요. ^^; 

#re: 문자열 이야기 (2) - StringBuilder에 대한 진실 혹은 거짓말 (II) / 이시택 / 2008-06-11 오전 4:59:00
뉴욕에서 인턴쉽 하고 잇는 학생인데요 

비베 닷넷 개발 팀이거든요 ~ 호호호 많은 더욱 되었네용 ㅎ 

#re: 문자열 이야기 (2) - StringBuilder에 대한 진실 혹은 거짓말 (II) / 이시택 / 2008-06-11 오전 4:59:00
뉴욕에서 인턴쉽 하고 잇는 학생인데요 

비베 닷넷 개발 팀이거든요 ~ 호호호 많은 더욱 되었네용 ㅎ 

#re: 문자열 이야기 (2) - StringBuilder에 대한 진실 혹은 거짓말 (II) / 강윤태 / 2009-07-05 오후 10:35:00
안녕하세요 자바를 공부하다가 코드에서 StringBuilder 부분이 나와서 찾아보던중 들렀는데요. 
ToString을 하면 StringBuilder의 버퍼에 있는 스트링을 복사해준다는 것이 맞나요? 

그런다음 초기화하라는 말씀은.. 버퍼를 지우는게 아니라 NULL문자만 추가한다는게.. 초기화란 말이랑 잘 이해가 안갑니다 ㅠㅠ


출처 - http://www.simpleisbest.net/archive/2005/05/17/149.aspx


===================================================================================


Q. 문자열을 합치는 방법인 StringBuffer, String + String, concat 의 성능을 비교한다.



※ 소스


String + String

for( int j = 0; j < 10; j++ )

{

String str = "";

start = System.nanoTime();

for( int i = 0; i < 10000; i++ )

str += String.valueOf( i );

end = System.nanoTime();

System.out.println( ( end - start ) + "(ns)" );

}

StringBuffer

for( int j = 0; j < 10; j++ )

{

StringBuffer strBuf = new StringBuffer();

start = System.nanoTime();

for( int i = 0; i < 10000; i++ )

strBuf.append( String.valueOf( i ) );

end = System.nanoTime();

System.out.println( ( end - start ) + "(ns)" );

}

concat

for( int j = 0; j < 10; j++ )

{

String str = "";

start = System.nanoTime();

for( int i = 0; i < 10000; i++ )

str = str.concat( String.valueOf( i ) );

end = System.nanoTime();

System.out.println( ( end - start ) + "(ns)" );

}




※ 결론


String + String

StringBuffer

concat 

219381486(ns)

211511302(ns)

207293854(ns)

210680966(ns)

208196763(ns)

208814199(ns)

208915404(ns)

203914398(ns) -> BAD

205495126(ns)

206599593(ns)

3971662(ns)

1469033(ns)

1302910(ns)

1094546(ns)

1116375(ns)

2104329(ns)

832036(ns) -> BEST

843092(ns)

840541(ns)

865488(ns)

110126748(ns)

103956922(ns)

102395471(ns)

103247069(ns)

102242388(ns)

101415170(ns) -> BAD

105201432(ns)

103828218(ns)

104164435(ns)

103972796(ns)



음.. 결과적으로는 StringBuffer, concat, String + String 순으로 빨랐다.


예상외로 StringBuffer 가 빠르다.



+ 추가(2012.05.08)


아래와 같은 String + String 의 경우 


System.out.println("x:"+x+" y:"+y);


컴파일러는 위 부분을 StringBuilder() 라는 객체 변환하여 실행한다.


System.out.println((new StringBuilder()).append("x:").append(x).append(" y:").append(y).toString());





String 은 Char[] 을 가지고 표현된다.


String 은 특성상 주소를 참조하지 않고 값을 복사하여 가지고 있는다.

(String 의 주소를 참조하여 값을 바꾸지 못하게 하기 위함)


String + String 을 하게 되면,


첫번째, 두번째 String 은 복사가 될 또다른 new String 으로 생성하여 복사하게 된다.


그러므로 String + String 복사시마다 새로운 String 객체가 생성된다고 보면 된다.





그에 비해 StringBuffer 는 최초 생성된 StringBuffer 에 계속되어 append 가 되기 때문에,


객체 생성이 줄어 속도와 리소스 면에서 우위에 있다.




참조 : http://ralf79.tistory.com/89

참조 : http://kaioa.com/node/59


출처 - http://blog.naver.com/PostView.nhn?blogId=kiho0530&logNo=150135830348


===================================================================================


String 은 Charecter Line을 나타낸다. Character Line 객체는 변형되지 않기 때문에 공통으로 사용할 수 있다.

그 값을 바꾸기 위해서는 필요에 따라 대입을 시켜줘야 한다.

StringBuffer는 Thread를 사용할 수 있는 변형이 가능한 캐릭터라인이다. 
예를 들어,
 StringBuffer z = "start";
라고 한 경우, 
z.append("le")라고 하면 z의 내용은 "startle"가 되며, z.insert(4,"le")라고 하면 z의 내용은 starlet가 된다.

두 클래스 다 직렬화(Serializable)를 지원하는구나. ㅡㅅ-);;

참고 : http://hongsgo.egloos.com/2033998 요 글을 보면, String < StringBuffer < StringBuilder 속도 차이가 있다. 흠... String은 적게 쓰는게 좋군요. ㅡㅅ-);;

참고 : http://cacky.tistory.com/36

  • String : 변경되지 않는 Character 문자열 객체
    문자열이 변경되지 않을 경우에는 String 사용
  • StringBuffer : 값이 변경 가능 // 동기화 가능 : 다중 스레드 일 경우에 사용
    문자열이 변경되고 다중 스레드에서 사용될 경우 사용
  • StringBuilder : 값이 변경 가능 // 동기화 되지 않음 : 단일 스레드일 경우에 사용
    문자열이 변경되고, 단일 스레드에서 사용될 경우 사용


출처 - http://java.ihoney.pe.kr/75


===================================================================================


자바 문자열 객체(String,StringBuffer,StringBuilder) 정규표현식..

내가 만든 코드를 튜닝(?)해 나가면서 가장 신경쓰이는 부분이
자주 쓰는 문자열과 관련된 객체의 자원이다.
문자열에 대해서 변경이 잦다면 String이 아니라 StringBuffer나 StringBuilder를 써야 할 것이다.
보통 StringBuffer는 알지만 StringBuilder는 .NET에만 있는 객체라고 잘못 알고 있는 사람이 많다.
하지만, 엄연히 자바에도 StringBuilder 객체가 있으며 문자열을 다루는 이 세가지 객체의 차이는 
크게 연산속도와 메모리 공간으로 볼 수 있다.
과연 어떤 특징이 있는지, 동일한 연산을 반복하고, 최종적으로 String 객체로 리턴 해서 비교해보자.

연산속도 : String 95초 , StringBuffer 0.24초 StringBuilder 0.17초
메모리 용량 : String 약 95GB , StringBuffer 약 28MB,9.5MB(String으로변환), StringBuilder 약 28MB,9.5MB(String으로변환)

정리 응답시간은 String보다 StringBuffer가 약 367배 빠르며, StringBuilder가 약 512배 빠르다. 메모리 용량은 StringBuffer와 StringBuilder보다 String이 약 3,390배 더 사용한다.

*여기서 잠깐.. 이글루스는 표만드는 기능도 없냐(내가 만들고 있는 사이트에는 있는데.. 음화화화)

여튼,

그럼 어떨때 String,StringBuffer,StringBuilder를 사용해야 할까?

 

  • String은 짧은 문자열을 더할 경우 사용한다.
  • StringBuffer는 스레드에 안전한 프로그램이 필요할 때나, 개발중인 시스템의 부분이 스레드에 안전한지 모를 경우 사용하면 좋다.
  • StringBuilder는 스레드에 안전한지 여부가 전혀 관계없는 프로그램을 개발 할 때 사용하면 좋다.

(와 문단 형식 다르게 잡았는데 실제론 안나타난다. 이글루스 개발 포기했나 이거뭥미 ㅜ.ㅜ)

* 버젼에 따라 다른 차이

개발자가 String객체를 사용하더라도 WAS가 JDK 5.0 이상의 버젼을 사용한다면 결과가 약간 달라진다.
왜냐하면 코드상에서 String객체를 사용하여 연산하였더라도 WAS에 탑재된 JDK가 컴파일시 StringBuilder로 변환하여 컴파일 하기 때문이다.

정규표현식과 문자열 객체,
퍼포먼스 측면이나 메모리 관리 측면에서 String 객체를 많이 안만들고 싶었으나
현실적으로 String객체에서 제공해주는 메서드가 막강하기 때문에 자주 쓰게 된다.
따라서, 어떻게하면 보다 더 적은 String객체를 사용할 수 있게 비즈니스 로직을 설계 하는것이 중요한 요소라고 생각한다.
그 해답 가운데 한가지가 정규표현식이라고 생각한다. 자바 String객체에 replaceAll이라는 메서드에 일반적인 문자열이 아닌
정규표현식을 사용하여 한번에 원하는 문자열을 탐색하거나 지우는것이 좋은 해답이라고 생각한다.


출처 - http://hongsgo.egloos.com/2033998


===================================================================================




















Posted by linuxism

댓글을 달아 주세요