문자열 이야기 (2) - StringBuilder에 대한 진실 혹은 거짓말 (II)
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 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을 말 그대로 '만들어'낼 때는 반복적으로 문자열은 연결해야 하는 경우가 마니 생긴다. 안 해봤다고? 그럼 지금이라도 메뉴 웹 컨트롤을 만들어 보라...
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의 주소를 비교해 보았습니다.
sb2 = sb;
이 코드가 수행됨으로써 sb 와 sb2는 동일한 StringBuilder 객체를 참조하게 됩니다.
따라서 sb에 벼라별 생 SHOW를 해도 sb와 sb2는 동일한 StringBuilder 객체를 가르키고
항상 ReferenceEquals가 같다는 것이지요.
참조형 변수에서 할당 연산자(assignment operator)는 참조값(객체 주소)을 복사하는 것이지
객체 자체를 복사하는 것이 아님을 이해하셔야 합니다.
ToString() 이 호출된 후, StringBuilder는 버퍼를 초기화 하여 StringBuilder가 재사용될 수 있도록 만드는 것 역시 알아두면 피가되고 살이되는 지식이다. 이쯤 되면 펜을 들고 메모할 필요를 느끼지 않는가?
ToString()가 최초 호출된후 StringBuilder 버퍼가 재사용될수있도록 만드는것 이것이 무슨말씀이신지 이해가 안됩니다. StringBuilder의 내부버퍼를 초기화한다는 말씀이신지요.
작업을 수행합니다. 특별히 버퍼를 지워버린다거나 하는 작업은 아니므로
그렇게 크게 신경써야할 부분은 아닙니다만...
보다 중요한 것은 ToString() 호출이 새로운 문자열을 만들어 StringBuilder 내의 문자열을
복사한다는 점입니다.
항상 Under Hood 한 곳을 알아갈때의 재미는 정말 쏠쏠하지요. 닷넷계의 Matt Pietrek 이 되시길~ ^^
그리고 닷넷에서 리버싱도 조회가 깊으신것 같은데 다 보여주진 못하셔도 약간의 흰트글들을 올려주시면 정말 감사하겠습니다.
바라는것만 많아서 정말 죄송하구요. 좋은글들 적으시는데 다시한번 감사드립니다. ^^
무식한게 덕이 되고 말았군요. ^^;
비베 닷넷 개발 팀이거든요 ~ 호호호 많은 더욱 되었네용 ㅎ
비베 닷넷 개발 팀이거든요 ~ 호호호 많은 더욱 되었네용 ㅎ
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://blog.naver.com/PostView.nhn?blogId=kiho0530&logNo=150135830348
===================================================================================
String 은 Charecter Line을 나타낸다. Character Line 객체는 변형되지 않기 때문에 공통으로 사용할 수 있다.
참고 : 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
===================================================================================
- 2008/08/26 10:40
- hongsgo.egloos.com/2033998
- 덧글수 : 3
'Development > Java' 카테고리의 다른 글
java - HashMap vs LinkedHashMap (0) | 2012.08.29 |
---|---|
java - Vector 와 ArrayList , Linked List 의 차이점 (0) | 2012.08.29 |
java - 윈도우, 리눅스 특정 환경 변수의 값 구하기 (0) | 2012.08.22 |
java - 리플렉션(reflection) (0) | 2012.08.22 |
java - Enumeration VS Iterator (0) | 2012.08.22 |