SimpleIsBest.NET

유경상의 닷넷 블로그
이 글은 오래된 전에 작성된 글입니다. 따라서 최신 버전의 기술에 알맞지 않거나 오류를 유발할 수 있습니다. 저자는 이 글에 대한 질문을 받지 않을 것입니다. 하지만 이 글이 리뉴얼 되면 이 글에 대한 질문을 하거나 토론을 할 수도 있습니다.
이번 포스트는 약간 깁니다. 중간에 도저히 짜를 만한 데가 없어서... 블로그를 쓰는 것도 거의 잡지에 기사 쓰는 것 만큼이나 힘들군요. 그래도 서식이나 형식, 어투 등을 마음대로 할 수 있으니 재미를 붙여볼 만도 한데... 아웅...

Story about StringBuilder - (III)

StringBuilder 만이 문자열 연산의 대수가 아니다. 필자는 과감히 말하고 싶다. StringBuilder를 쓰지 말자. 가능하다면...

Alternative String Operation

StringBuilder를 쓰지 말라고 했으니 다른 방법도 알려줘야 할 것 아닌가... (첨부터 긴 글을 쓰니 대략 귀찮다) 많은 경우, String.Concat 메쏘드와 String.Join 메쏘드는 StringBuilder 보다 효율적이다. 특히 그 크기가 1KB 미만이라면 압도적으로 이들 메쏘드가 StringBuilder에 비해 효율적이며 빠르다. 더욱이 멋진 것은 C#, VB.NET 컴파일러가 문자열의 + 연산자를 String.Concat 메쏘드 호출로 컴파일 해준다는 것이다.

// 원본 코드 (C#)
string s1 = "aaaa";
string s2 = "bbb";
string s3 = s1 + s2;

// C# 컴파일러가 생성한 코드
string s1 = "aaaa";
string s2 = "bbb";
string s3 = String.Concat(s1, s2);

위 코드가 어딜 봐서 StringBuilder 보다 효율적인가는 String.Concat의 내부를 까봐야 한다. 필자가 Reflector를 통해 까본 결과, String.Concat 메쏘드는 매개변수로 주어진 두 문자열의 크기를 계산하여(위의 경우 7) 해당 크기의 문자열을 할당하고 앞서 언급한 FillString 메쏘드를 2회 호출하는 것이였다. StringBuilder를 사용하는 경우, FillString이 2회 호출된다는 것은 동일하지만 StringBuilder는 내부 버퍼를 위한 문자열 할당과 ToString() 호출 시에 또 문자열이 할당되지만 String.Concat은 최종 결과 문자열 한 개만이 할당된다.

String.Concat은 다양한 메쏘드 중복(굳이 한글로 하자면 중복 정도 되겠다. method overload 가 정확한 명칭이다. 대략 한글화는 어렵다. -_-)을 갖고 있다.

      public static string Concat(params object[] args);
      public static string Concat(params string[] values);
      public static string Concat(object arg0);
      public static string Concat(object arg0, object arg1);
      public static string Concat(string str0, string str1);
      public static string Concat(object arg0, object arg1, object arg2);
      public static string Concat(string str0, string str1, string str2);
      public static string Concat(object arg0, object arg1, object arg2, object arg3, __arglist);
      public static string Concat(string str0, string str1, string str2, string str3);

C#, VB.NET 컴파일러는 충분히 영리해서 4개까지의 문자열 + 연산에 대해서는 String.Concat을 호출하도록 만든다는 얘기이다. 즉, s = a + b + c + d 라는 코드가 주어지면 컴파일러는 s = String.Concat(a, b, c, d) 라는 코드를 생성해 낸다. 결국 4개의 문자열을 연결하는데 최종 결과물로 한 개의 문자열만이 생성된다는 얘기가 되겠다. 5개 이상의 문자열에 대해서 + 연산이 수행되면 연산에 참여한 문자열들을 문자열 배열로 만들고 String.Concat(params string[] values) 가 호출된다. 이 메쏘드 역시 최종 결과물로 한 개의 문자열만이 생성된다.

필자가 말하고자 하는 요지는 String.Concat은 충분히 효율적이며 그 성능 또한 빠르다는 것이며 + 연산자를 사용하면 코드의 가독성(readability) 역시 향상된다고 말할 수 있다(Append 호출이 더 읽기 좋다면 말고...). 굳이 StringBuilder를 쓰지 않더라도 원하는 효과를 충분히 낼 수 있다. 3-4개의 문자열을 연결하는 경우에 StringBuilder를 쓰는 것이 오히려 비 효율적인 것이 되어 버린다.

StringBuilder 대신 + 연산자가 좋은 예를 하나 더 들어보자. 다음 코드는 DB를 액세스하는 프로그램들에서 흔히 볼 수 있는 코드로 StringBuilder를 사용해 SQL 문장을 만드는 코드이다. 이 코드가 오늘의 최고의 삽질 코드가 되겠다.

// 오늘의 삽질 코드 ...
StringBuilder sb = new StringBuilder();
sb.Append("SELECT ProductID as '제품아이디', ");
sb.Append("       ProductName as '제품명', ");
sb.Append("       UnitPrice as '단가' ");
sb.Append("FROM Products ");
sb.Append("WHERE UnitPrice IS NOT NULL");
string query = sb.ToString();
// 이하 ADO.NET 코드 (생략)

위 코드가 왜 삽질인가? 비록 StringBuilder에 명시적으로 capacity를 주지 않았다지만 이것이 삽질 정도까지?

그렇다. 삽질이다.

닷넷 컴파일러들(최소한 C#, VB.NET 컴파일러)은 꽤나 영리하다. 문자열 상수가 + 연산자로 연결되면 String.Concat 을 호출조차 하지 않고 컴파일 타임에 문자열을 연결하고 그 결과물에 대해 코드를 생성한다. 말로만 하면 햇갈리니 예제를 보자.

// 원본 소스 (C#) 코드
string query = "SELECT ProductID as '제품아이디', " +
               "       ProductName as '제품명', " +
               "       UnitPrice as '단가' " +
               "FROM Products " +
               "WHERE UnitPrice IS NOT NULL";

위 코드는 앞서 StringBuilder 예제를 + 연산자 버전으로 바꾼 것이다. 위 코드의 특징은 모두 문자열 상수(문자열 리터럴)로만 + 연산이 구성되었다는 점이다. 이때 C# 컴파일러는 String.Concat 호출로 이들 문자열을 연결하는 코드를 생성하는 것이 아니라 다음과 같이 컴파일러가 문자열을 컴파일 타임에 연결하고 연결된 문자열을 가지고 코드를 생성해 낸다는 것이다.

// 컴파일된 결과 코드
string query = "SELECT ProductID as '제품아이디',        ProductName as '제품명',        UnitPrice as '단가' FROM Products WHERE UnitPrice IS NOT NULL";

물론 문자열은 달랑 하나만이 사용된다. 이제 왜 StringBuilder 버전의 코드가 삽질이 되었는가가 이해가 될 것이다. 뜨끔한 독자들이 꽤 있으리라 생각된다. 닷넷 컴파일러들은 문자열 + 연산에서 인접한 문자열이 리터럴이라면 컴파일 타임에 문자열을 연결해 버린다. 물론 + 연산자의 피연산자(operand)가 변수라면 String.Concat 호출이 사용된다.

// 원본 코드 (C#)
string s1 = "aaaa" + "bbbb";
string s2 = s1 + "cccc" + "dddd";

// C# 컴파일러가 생성한 코드
string s1 = "aaaabbbb";
string s2 = s1 + "ccccdddd";

+ 연산자를 사용하여 문자열을 더하는 연산은 많은 경우 StringBuilder보다 효율적이다. 다만 다음과 같이 반복문 안에서 + 연산자가 사용되는 경우는 StringBuilder가 효율적일 수 밖에 없다.

// 원본 코드 (C#)
string s = null;
for(int i = 0; i < 50; i++) {
    s = s + i.ToString() + ",";
}

위 코드는 명백히 50회 이상의 String.Concat이 호출되고 최종적으로 원하는 문자열을 얻기 위해 중간 단계에 49개의 임시 문자열이 사용되었다가 사라진다. 이 경우에는 StringBuilder가 월등히 효율적이라고 할 수 있다. 필자의 경험상 위와 같은 코드가 필요한 경우가 그다지 많지 않았다. 독자들은 어떤가?

Summary

지금까지 이야기(StringBuilder에 대한 앞서의 포스트 첫번째, 두번째 포함)를 요약해 보자. 많은 닷넷 입문서와 온라인 글에서 말하는 StringBuilder를 사용하라는 권고를 맹목적으로 따르는 것은 삽질이 될 수 있다. 그렇다고 이들 글들이 모두 구라를 쳤다는 얘기는 아니다. 책을 정확히 다시 읽어 보면 문자열 + 연산의 문제를 지적하고 이것을 해결하고자 할 때에 StringBuilder를 쓰라고 되어 있을 것이다. 이렇게 얘기 하지 않고 무조건 StringBuilder를 쓰라고 된 문헌이 있다면 대략 난감한 상황이 되겠다. 독자들이 알아서 해라...

문자열 + 연산은 String.Concat 호출로 변환되고 4개 이하의 문자열 + 연산은 압도적으로 + 연산이 효율적이며 빠르다. 다른 블로그를 통해 지적할 것이지만, 작은 문자열 400여 개를 연결하는 작업 역시 String.Concat과 StringBuilder의 성능차이는 그다지 크지 않다. StringBuilder를 사용하면서 유의할 사항들은 다음과 같다.

  • StringBuilder는 배부 문자열 버퍼를 유지하며 그 초기 값은 16이다.
  • StringBuilder는 내부 버퍼가 부족하게 되면 새로운 내부 버퍼를 할당하며 기존 버퍼의 내용을 복사하는 오버헤드를 갖는다.
  • StringBuilder.ToString()은 문자열을 새로이 생성하여 반환하므로 또 다른 오버헤드를 유발할 수도 있다.

또한 닷넷 컴파일러들은 또한 문자열 상수가 + 연산자에 의해 연결되는 경우, 문자열 연결을 컴파일 타임에 수행하므로 문자열 + 연산은 더 이상 기피할 것이 아니라는 것이다.

Epilogue

첫 글(씨리즈)을 이따구로 길게 쓰자니 무쟈게 빡시다. 담부턴 짧게 짧게 여러 번 올려야겠다고 다짐해 본다. 이건 블로그가 아니라... 노가다다...



Comments (read-only)
#re: 문자열 이야기 (3) - StringBuilder에 대한 진실 혹은 거짓말 (III) / 손님 / 2005-08-08 오후 6:15:00
for (int i = 0;i < ds.Tables[0].Rows.Count;i++)
{
tmpHtml += "<tr>"
+" <td>" + ds.Tables[0].Rows[i]["AgreementID"].ToString() + " </td>"
+" <td>" + ds.Tables[0].Rows[i]["GSBNSystem"].ToString() + "</td> "
+" <td>" + ds.Tables[0].Rows[i]["DateCreated"].ToString() + "</td> "
+"</tr>";
}

for (int i = 0;i < ds.Tables[0].Rows.Count;i++)
{
sb.Append("<tr>");
sb.Append(" <td>" + ds.Tables[0].Rows[i]["AgreementID"].ToString() + "</td> ");
sb.Append(" <td>" + ds.Tables[0].Rows[i]["GSBNSystem"].ToString() + "</td> ");
sb.Append(" <td>" + ds.Tables[0].Rows[i]["DateCreated"].ToString() + "</td> ");
sb.Append("</tr>");
}

주인장께서 말씀하신 것이 위의 for문을 아래 for문으로 바꾸는 것이 좋다는 의도인가요?
그런데 아래에 Append하는 것도 리터럴 문자열이 되는 건 아닌지 좀 헷갈리네요.
string변수를 선언하고 for문안에서 대입 한 후 그것을 Append해야 맞는 건가요?
답변 부탁드려요....
#re: 문자열 이야기 (3) - StringBuilder에 대한 진실 혹은 거짓말 (III) / 쥔장 / 2005-08-08 오후 10:31:00
예제로 주신 코드만을 보고 판단하면 아래 코드가 훨씬 더 효율적으로 보입니다.
위쪽 코드는 for 반복문 안에서 매번 새로운 문자열을 만들어(+= 연산자에 의해) 내고
또한 4개 이상의 문자열을 연결(concat) 하기 때문에
배열 객체도 생성됩니다. 하지만 아래쪽 코드 StringBuilder를 이용하므로 이러한 비효율은 없습니다.

아래쪽 코드에서 ds.Tables[0].Rows[i]["xxxx"].ToString() 은 리터럴이 아닙니다.
따라서 런타임에 "<td>", "</td>" 리터럴과 Concat 연산이 수행됩니다. 이런 이유에서 보다 코드를 최적화 시켜본다면

sb.Append("<tr><td>");
sb.Append(ds.Tables[0].Rows[i]["AgreementID"].ToString());
sb.Append("</td><td>")
sb.Append(ds.Tables[0].Rows[i]["GSBNSystem"].ToString());

이런 식으로 하는 것이 불필요한 문자열 생성을 줄이는 것이라 판단됩니다.

마지막으로 StringBuilder에서 버퍼를 늘이는 작업을 최소화 시키기 위해 StringBuilder 생성시
적절한 크기를 주면 더 좋을 듯 싶습니다.

사실 이렇게 코드를 신경써 주면 좋지만 저렇게 까지 신경써 줘야 할 웹 사이트는 그다지 많지 않습니다.
한시간에 수십만명이 오고가는 사이트가 아니라면 아래쪽 코드 정도로 코드를 해줘도 무방할 듯 싶네요.
#re: 문자열 이야기 (3) - StringBuilder에 대한 진실 혹은 거짓말 (III) / 이경구 / 2005-08-09 오전 8:23:00
이윤복씨와 아는 사이로 우연히 들르게 됐는데요. 좋은 내용이 많아서 많은 도움 됩니다.
답변 감사합니다.
#re: StringBuilder 에 대한 질문 입니다. / 정성균 / 2005-12-06 오후 6:44:00
모 프로젝트에 갔더니.... 코딩상의 줄 맞춤을 한다는 명목하에 StringBuilder 사용시 Append 를 사용해도 될 것들을
죄다 AppendFormat 으로 사용하고 있더군요....
StringBuilder 에 관한 포스트들을 읽어본 후 .... StringBuilder 의 AppendFormat 메서드도 내부적으로는 string.format 메서드를 사용할것 같은데여...
이게 맞다면 결론적으로 .... 불필요한 리소스 낭비를 초래하지 않는가 하는 점 입니다. 사실 이런식으로 코딩된게 수두룩한지라....
그나마 긴 문장을 엮는데 있어서 capacity 없이 코딩되어 있어 capacity 도 명시적으로 할당 해주었습니다만....
Append vs AppendFormat
#re: 문자열 이야기 (3) - StringBuilder에 대한 진실 혹은 거짓말 (III) / 블로그쥔장 / 2005-12-07 오전 12:08:00
사실 String.Format 메쏘드 내부에서 StringBuilder를 생성하고 AppendFormat을 호출합니다. -_-;
결국, 불필요한 AppendFormat 은 String.Format을 호출하는 것과 동일하다는 것입니다.
포맷팅이 굳이 필요 없음에도 불구하고 AppendFormat을 사용하는 것은 낭비가 되겠습니다만...

본문에서도 언급했듯이 너무 많은 생각은 개발 생산성을 떨어뜨릴 수 있습니다.
특히 짧은 기간에 대량의 코드(프로그램)를 작성해야 하는 경우에 너무 많은 고려사항은 오히려 해가 될 수
있겠습니다.

그러나... 단순한 코딩상의 줄맞춤을 명목으로 AppendFormat을 사용하는 건 좀 '무식하게' 보이네요... -_-;
프로젝트가 크면 클 수록, 부하가 많으면 많을 수록 서버 측 코딩은 하나의 실수라도 짧은 기간에
많이 축적되기 때문에 가급적 좋은 코딩 습관을 가지는 것이 좋겠지요.
#re: ^^ 역쉬... / 정성균 / 2005-12-07 오후 5:33:00
글치 않아도 내심 걱정이 이만 저만이 아니었는데.... 답이 아주 명확해졌습니다.

요새 강좌가 또 줄줄이 생겨서 아주 기분이 좋습니다..히히 ^^

즐거운 하루되세요
#re: 문자열 이야기 (3) - StringBuilder에 대한 진실 혹은 거짓말 (III) / 블로그쥔장 / 2005-12-07 오후 7:01:00
이렇게 관심을 갖어주셔서 감사합니다... ^^
#re: 문자열 이야기 (3) - StringBuilder에 대한 진실 혹은 거짓말 (III) / 정성균 / 2005-12-08 오후 1:40:00
역시나 실제 테스트를 해보았습니다.
얼마나 차이가 나는지를.... ㅋㅋㅋ
눈에 보이는 테스트를 하고자 동일 결과 문장들에 대해서 방법을 달리해서 10000 번에서 50000 번의 루프를 돌려 테스트....
10000번에서는 거의 2.5배의 속도차이를 보였으며, 50000 에서는 40배 가까이 차이가 나는걸 봤습니다.
결론은 알았지만... 이케 확인해보고나니 게시판에 글남겨놓고 싶더라구여....

실험 1. stringbuilder의 잘못된 사용법 VS stringbuilder 의 잘된 사용법 ? --> 엄청난 차이가 .... 컥...
실험 2. 일반 문자열에 대한 stringbuilder 사용 VS string 사용 --> 결과상에서는 근소한 차이를 보이지만 무시할수 없는것이겠죠....
#re: 문자열 이야기 (3) - StringBuilder에 대한 진실 혹은 거짓말 (III) / 블로그쥔장 / 2005-12-08 오후 2:00:00
저도 테스트를 따로 해서 결과를 별도의 포스트로 쓸까 생각하고 있었는데...
테스트를 해 보셨군요... ^^

실험2의 테스트에서 StringBuilder를 어떻게 사용하셨었나요? 권장되는 방법으로 하셨나요?
"문자열 상수"를 StringBuilder를 이용해 더하는 것과 그냥 + 연산자를 쓰는 것하고는
차이가 많이 날텐데요...
#re: 문자열 이야기 (3) - StringBuilder에 대한 진실 혹은 거짓말 (III) / 정성균 / 2005-12-08 오후 2:33:00

하나는, StringBuilder 클래스로 AppendFormat 을 이용한 것과 Append 메서드를 이용한 문자열 처리 이구요.
(단, 인자가 없는 경우로서 .... 예를 들자면 Email 내용이 되는 html 을 문자열로 더해준거라 보시면 됩니다. 테스트한 문자열의 Length 는 대략 2400 정도
'설마 이렇게 쓰는 사람은 없을꺼야 ' 라는 생각을 해본적도 없었지만, 실제 이런 사례를 경험하고 나니까 심각성을 느낀바... 이렇게 열성적으루다... ㅋㅋ )

두번째는, StringBuilder 클래스로 문자열 처리 [권장되는 방법] 와 string 을 이용한 문자열 처리 [예 : '+' 연산자 없이 strSql = @"......."; ]
==> StringBuilder 와 + 연산자의 비교가 아니라, 적당 ? 한 크기의 문자열을 string 으로 처리할때의 비교였다고 보시면 됩니다. ^^

StringBuilder 에서 공통적으로 테스트된 사항
- capacity 를 부여하면 성능에 도움이 된다. --> 질문 사항 : length 에 따른 capacity 범위 산정 방법이란 ????

이건 많은 테스트를 해 보지 않았지만... 대략 512, 1024, 1300, 2000
---> 큰 차이가 없는듯 하더라구요... 아예 없는 거 [16]와는 차이가 확연하지만요.... 제가 테스트 수치란게 엉망일수도 있겠다 싶은데...... ^^;;;; 뻘줌..
#re: 문자열 이야기 (3) - StringBuilder에 대한 진실 혹은 거짓말 (III) / 블로그쥔장 / 2005-12-08 오후 3:19:00
아래 두 코드의 성능 차이는 상당합니다. 수치적으로는 1초 차이도 안나지만 비율로는
100배가 넘지요. 코드2의 문제는 속도보다는 많은 쓰레기 문자열 객체가 만들어졌다가
사라진다는 것입니다.

코드1)
for(int i=0; i < 1000000; i++) {
res = "1111111111" +
"2222222222" +
"3333333333" +
"4444444444" +
"5555555555" +
"6666666666";
}

코드2)
for(int i=0; i < 1000000; i++) {
StringBuilder sb = new StringBuilder(72);
sb.Append("1111111111");
sb.Append("2222222222");
sb.Append("3333333333");
sb.Append("4444444444");
sb.Append("5555555555");
sb.Append("6666666666");
res = sb.ToString();
}
#re: 문자열 이야기 (3) - StringBuilder에 대한 진실 혹은 거짓말 (III) / 안종윤 / 2006-01-10 오전 1:01:00
SP를 사용하지 않을경우 쿼리 작성을 위해 주로 (+) 연산자를 즐겨 사용했는데 어디 책에서 우연히 stringbuilder를 사용하는것을 보고 또 그게 좋다고 하길래...
실제로 그렇게 사용하지 않아서 내심 마음속으로 guilty를 느끼고 있었는데.. 다행이네요. ^^
#re: 문자열 이야기 (3) - StringBuilder에 대한 진실 혹은 거짓말 (III) / 블로그쥔장 / 2006-02-10 오후 3:05:00
쿼리 작성시 무조건 + 를 사용하는 것 역시 조심해야 합니다. 특히, parameterized query를 사용하지 않는 것에
주의 해야 합니다. 상세한 내용은 다른 제 글을 참고 하십시요.

http://www.simpleisbest.net/archive/2005/10/27/278.aspx
#re: 문자열 이야기 (3) - StringBuilder에 대한 진실 혹은 거짓말 (III) / 초보 / 2006-11-07 오후 4:57:00
저도 StringBuilder 엄청 많이 써댔는데....
위에 어떤분은 줄맞추기 위해서 였다고 하는데..
저는 줄맞춤 + 기존 코드가 다 그렇게 되어있으니까...
하하. 제가 최고 무식한가봅니다.
많은 도움이 되었습니다.
#re: 문자열 이야기 (3) - StringBuilder에 대한 진실 혹은 거짓말 (III) / 김대우 / 2007-04-10 오후 4:29:00
좋은 글 감사합니다. 아주 유익 했네요.
#re: 문자열 이야기 (3) - StringBuilder에 대한 진실 혹은 거짓말 (III) / jinos / 2007-09-05 오전 10:32:00
댓글의 수준도 역쉬 높습니다...
한수 배우고 있습니다.
#re: 문자열 이야기 (3) - StringBuilder에 대한 진실 혹은 거짓말 (III) / Gleam™ / 2008-10-06 오후 5:37:00
오랜만에 블로깅을 하면서 쓰신글을 보고 있습니다.

질문 사항이 좀 있어서요...
스트링빌더의 성능에는 공감이 가는데요.. 그렇다면 Sting.Concat() 과 String.Format()의 성능 관계는

string logpath = @"{0}\debug_{1}.txt";
logpath = string.Format(CultureInfo.CurrentCulture, logpath, ConfigurationManager.AppSettings["ErrLogPath"], DateTime.UtcNow.ToString("yyyyMMdd_HHmmss_fffffff", CultureInfo.CurrentCulture));

위와 같은 코드를
string logpath = ConfigurationManager.AppSettings["ErrLogPath"].ToString(CultureInfo.CurrentCulture) + @"{0}\debug_" + DateTime.UtcNow.ToString("yyyyMMdd_HHmmss_fffffff", CultureInfo.CurrentCulture) + ".txt";
로 변경하면 성능 향상이 있을까요??

#re: 문자열 이야기 (3) - StringBuilder에 대한 진실 혹은 거짓말 (III) / 블로그쥔장 / 2008-10-06 오후 7:57:00
글쎄요. 둘의 차이가 거의 없어 보입니다만...
String.Format은 내부적으로 StringBuilder.AppendFormat을 호출하기 때문에 이 경우에는
String.Concat이 미세하게 더 낫지 않을까 생각 됩니다만...
실제로는 테스트를 해봐야 알겠지요. -_-;