login register Sysop! about ME  
qrcode
    최초 작성일 :    2005년 05월 18일
  최종 수정일 :    2006년 09월 29일
  작성자 :    Loner (유경상)
  편집자 :    Loner(유 경상)
  읽음수 :    26,815

강좌 목록으로 돌아가기

필자의 잡담~

이번 글은 약간 깁니다. 중간에 도저히 짜를 만한 데가 없어서... 블로그를 쓰는 것도 거의 잡지에 기사 쓰는 것 만큼이나 힘들군요. 그래도 서식이나 형식, 어투 등을 마음대로 할 수 있으니 재미를 붙여볼 만도 한데... 아웅...
현재 강좌의 원본 글의 링크는 http://www.simpleisbest.net/archive/2005/05/18/150.aspx 입니다.

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

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


해당 글과 관련하여 올라온 질/답 중 봐둘만한 내용
질문
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해야 맞는 건가요?
답변 부탁드려요....
답변 :
예제로 주신 코드만을 보고 판단하면 아래 코드가 훨씬 더 효율적으로 보입니다.
위쪽 코드는 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 생성시
적절한 크기를 주면 더 좋을 듯 싶습니다.

사실 이렇게 코드를 신경써 주면 좋지만 저렇게 까지 신경써 줘야 할 웹 사이트는 그다지 많지 않습니다.
한시간에 수십만명이 오고가는 사이트가 아니라면 아래쪽 코드 정도로 코드를 해줘도 무방할 듯 싶네요.

authored by


 
 
.NET과 Java 동영상 기반의 교육사이트

로딩 중입니다...

서버 프레임워크 지원 : NeoDEEX
based on ASP.NET 3.5
Creative Commons License
{5}
{2} 읽음   :{3} ({4})