본문 바로가기
Development/Visual Basic | VBScript | ASP

ASP 응용 프로그램의 문자열 처리 성능 향상

by Dev. Jkun 2010. 9. 20.
반응형

ASP 응용 프로그램의 문자열 처리 성능 향상

James Musson

Microsoft UK, Developer Services

2003년 3월

 

적용 대상:
   Microsoft Active Server Pages
   Microsoft Visual Basic

 

요약: 대부분의 ASP(Active Server Pages ) 응용 프로그램은 문자열 연결을 기반으로 HTML 형식 데이터를 작성한 뒤 사용자에게 표시합니다. 이 기사에서는 이러한 HTML 데이터 스트림을 만드는 여러 방법을 비교하여 특정 상황에서 어떤 방법이 다른 방법보다 우수한 성능을 제공하는지 살펴보도록 하겠습니다. 여기서는 개발자가 ASP 및 Visual Basic 프로그래밍을 잘 알고 있다고 가정합니다(11페이지/인쇄 페이지 기준).

목차

소개
ASP 디자인
문자열 연결
빠르고 쉬운 해결 방법
StringBuilder
기본 제공 방법
테스트
결과
결론

소개

ASP 페이지를 작성할 때 개발자는 실제로 ASP에 의해 제공되는 Response 개체를 통해 웹 클라이언트에 기록되는 형식의 텍스트 스트림을 만듭니다. 개발자는 여러 다른 방법으로 이러한 텍스트 스트림을 작성할 수 있으며 어떤 방법을 선택했는지에 따라 웹 응용 프로그램의 성능과 확장성에 큰 영향을 줄 수 있습니다. 필자는 다양한 상황에서 웹 응용 프로그램의 성능을 조정하려는 고객에게 도움을 제공한 바 있으며, 이 과정에서 성공을 거두기 위한 한 가지 방법이 HTML 스트림의 작성 방식을 변경하는 것이라는 사실을 알게 되었습니다. 이 기사에서는 몇 가지 일반적인 기술을 살펴보고 이러한 기술이 간단한 ASP 페이지의 성능에 어떤 영향을 주는지 테스트하도록 하겠습니다.

ASP 디자인

대부분의 ASP 개발자는 적절한 소프트웨어 엔지니어링 원칙을 따르면서 가능한 한 해당 코드를 모듈화합니다. 일반적으로 이 디자인은 별개의 특정 페이지 섹션을 모델링하는 함수가 들어 있는 여러 포함 파일의 형태를 가집니다. 대개 HTML 테이블 코드인 이러한 함수의 문자열 출력은 완전한 페이지를 작성하기 위한 다양한 조합으로 사용될 수 있습니다. 일부 개발자는 더 나아가 이러한 HTML 함수를 Visual Basic COM 구성 요소로 이동함으로써 컴파일된 코드가 제공할 수 있는 추가 성능 혜택을 얻고자 합니다.

 

이러한 디자인 방식이 분명 적절하기는 하지만 별도의 HTML 코드 구성 요소를 이루는 문자열을 작성하는 데 사용되는 방법은 실제 작업이 ASP 포함 파일 내에서 수행되는지 아니면 Visual Basic COM 구성 요소 내에서 수행되는지 여부에 관계없이 웹 사이트의 성능과 확장성에 상당한 영향을 줄 수 있습니다.

문자열 연결

WriteHTML이라는 함수에서 다음 코드 단편을 가져왔다고 가정해 보십시오. Data라는 매개 변수는 단순히 테이블 구조로 형식을 지정해야 할 일부 데이터(예: 데이터베이스에서 반환된 데이터)를 포함하는 문자열 배열입니다.

Function WriteHTML( Data )

Dim nRep

For nRep = 0 to 99
   sHTML = sHTML & vbcrlf _ 
         & "<TR><TD>" & (nRep + 1) & "</TD><TD>" _ 
         & Data( 0, nRep ) & "</TD><TD>" _ 
         & Data( 1, nRep ) & "</TD><TD>" _ 
         & Data( 2, nRep ) & "</TD><TD>" _ 
         & Data( 3, nRep ) & "</TD><TD>" _ 
         & Data( 4, nRep ) & "</TD><TD>" _ 
         & Data( 5, nRep ) & "</TD></TR>"
Next

WriteHTML = sHTML

End Function

대부분의 ASP 및 Visual Basic 개발자는 이 방법으로 HTML 코드를 작성합니다. sHTML 변수에 포함된 텍스트는 호출 코드로 반환된 다음 Response.Write를 통해 클라이언트에 기록됩니다. 물론 이것은WriteHTML 함수의 간접 참조 없이 페이지 내에 직접 포함되는 유사한 코드로 표현될 수 있습니다. 그러나 이 코드는 ASP와 Visual Basic, BSTR 또는 Basic String이 사용하는 문자열 데이터 형식이 실제로 길이를 변경할 수 없다는 점에 문제가 있습니다. 이것은 문자열의 길이가 바뀔 때마다 메모리의 원래 문자열 표시가 파괴되고 새 문자열 데이터를 포함하는 새 표시가 만들어지며, 결과적으로 메모리 할당 작업과 메모리 할당 취소 작업이 수행된다는 것을 의미합니다. 물론 ASP와 Visual Basic에서는 이러한 작업이 모두 자동으로 처리되기 때문에 이러한 비용이 즉각 드러나지는 않습니다. 메모리를 할당 및 할당 취소하려면 단독 잠금을 확보하는 기본 런타임 코드가 필요하기 때문에 비용이 많이 들 수 있습니다. 이것은 대규모 문자열 연결 도중에 발생하는 것처럼 많은 양의 메모리 블록을 가진 문자열이 빠른 속도로 연속하여 할당 및 할당 취소되는 경우에 특히 분명하게 나타납니다. 이러한 현상은 단일 사용자 환경에서 큰 문제가 되지 않을 수 있지만 웹 서버에서 실행되는 ASP 응용 프로그램과 같은 서버 환경에서 사용될 때는 심각한 성능 및 확장성 문제를 일으킬 수 있습니다.

 

그렇다면 위 코드 단편에서 얼마나 많은 문자열 할당이 수행되고 있습니까  정답은 16입니다. 위 코드에서 모든 '&' 연산자는 sHTML 변수가 가리킨 문자열을 파괴하고 다시 만들게 합니다. 필자는 문자열 할당에 많은 비용이 들고 문자열이 커질수록 이러한 현상이 더욱 가중된다는 것을 앞서 언급한 바 있습니다. 아래에서는 위 코드를 개선하여 이러한 문제를 해결할 수 있는 방법에 대해 알아보겠습니다.

빠르고 쉬운 해결 방법

문자열 연결의 영향을 줄이는 방법에는 두 가지가 있습니다. 첫 번째 방법은 조작하는 문자열의 크기를 줄이는 것이고 두 번째 방법은 수행되는 문자열 할당 작업의 개수를 줄이는 것입니다. 아래의 WriteHTML 코드는 수정된 버전입니다.

Function WriteHTML( Data )

Dim nRep

For nRep = 0 to 99
   sHTML = sHTML & ( vbcrlf _ 
         & "<TR><TD>" & (nRep + 1) & "</TD><TD>" _ 
         & Data( 0, nRep ) & "</TD><TD>" _       
         & Data( 1, nRep ) & "</TD><TD>" _ 
         & Data( 2, nRep ) & "</TD><TD>" _ 
         & Data( 3, nRep ) & "</TD><TD>" _ 
         & Data( 4, nRep ) & "</TD><TD>" _ 
         & Data( 5, nRep ) & "</TD></TR>" )
Next

WriteHTML = sHTML

End Function

 

언뜻 보면 이전 샘플과 이 코드 간에 차이점을 구별하기가 쉽지 않을 수 있습니다. 이 코드는 단순히 sHTML = sHTML & 뒤에 오는 전체 내용을 묶기 위해 괄호가 추가되었습니다. 이렇게 하면 우선 순위가 변경됨으로써 대부분의 문자열 연결 작업에서 조작되는 문자열의 크기가 줄어듭니다. 원본 코드 샘플에서는 ASP 컴파일러가 등호 오른쪽에 식이 있는지 확인하여 단순히 왼쪽에서 오른쪽으로 식을 평가합니다. 이 경우에는 계속 증가하는 sHTML이 포함된 반복 횟수 당 16개의 연결 작업이 수행됩니다. 반면, 새 버전에서는 작업을 수행해야 하는 순서를 변경함으로써 컴파일러에 힌트를 제공합니다. 이제 컴파일러는 식을 왼쪽에서 오른쪽으로 평가하며 괄호 안의 내용에서 밖에 있는 내용 순으로 평가합니다. 이 기술을 사용하면 증가하지 않는 더 작은 문자열이 포함된 반복 횟수 당 15개의 연결 작업과 증가하는 큰 sHTML이 포함된 하나의 연결 작업이 수행됩니다. 그림 1은 이러한 최적화된 연결의 메모리 사용 패턴이 표준 연결 방법과 비교하여 어떤 효과를 가지는지 보여 줍니다.

 

그림 1   표준 연결과 괄호로 묶인 연결 간의 메모리 사용 패턴 비교

 

이 기사의 뒷부분에 설명되어 있는 것처럼 괄호를 사용하면 특정 상황에서 성능과 확장성이 크게 달라질 수 있습니다.

StringBuilder

앞에서는 문자열 연결 문제에 대한 빠르고 쉬운 해결 방법을 살펴보았는데, 대부분의 경우 이 해결 방법을 통해 성능과 구현 노력 간의 가장 적절한 조화를 이룰 수 있습니다. 그러나 큰 문자열을 작성하는 작업의 성능을 개선하는 것이 중요시되는 경우에는 문자열 할당 작업 수를 줄이는 다른 방법을 취해야 하며 이를 위해서는 StringBuilder가 필요합니다. 이 클래스는 구성 가능한 문자열 버퍼를 유지 관리하고 해당 버퍼로의 새 텍스트 삽입을 관리하여 텍스트 길이가 문자열 버퍼의 길이를 초과할 경우에만 문자열 재할당을 수행하게 합니다. Microsoft .NET Framework에서는 해당 환경의 모든 문자열 연결 작업에서 권장되는 이러한 클래스(System.Text.StringBuilder)를 무료로 제공합니다. ASP 및 기존 Visual Basic 영역에서는 이 클래스를 액세스할 수 없으므로 직접 작성해야 합니다. 다음은 Visual Basic 6.0을 사용하여 만든 샘플StringBuilder 클래스입니다(간단하게 하기 위해 여기서는 오류 처리 코드를 생략함).

Option Explicit

' 버퍼의 기본 초기 크기 및 증가 인수
Private Const DEF_INITIALSIZE As Long = 1000
Private Const DEF_GROWTH As Long = 1000

' 버퍼 크기 및 증가
Private m_nInitialSize As Long
Private m_nGrowth As Long

' 버퍼 및 버퍼 카운터
Private m_sText As String
Private m_nSize As Long
Private m_nPos As Long

Private Sub Class_Initialize()
   ' 크기 및 증가 기본값 설정
   m_nInitialSize = DEF_INITIALSIZE
   m_nGrowth = DEF_GROWTH
   ' 버퍼 초기화
   InitBuffer
End Sub

' 초기 크기 및 증가량 설정
Public Sub Init(ByVal InitialSize As Long, ByVal Growth As Long)
   If InitialSize > 0 Then m_nInitialSize = InitialSize
   If Growth > 0 Then m_nGrowth = Growth
End Sub

' 버퍼 초기화
Private Sub InitBuffer()
   m_nSize = -1
   m_nPos = 1
End Sub

' 버퍼 증가
Private Sub Grow(Optional MinimimGrowth As Long)
   ' 필요한 경우 버퍼 초기화
   If m_nSize = -1 Then
      m_nSize = m_nInitialSize
      m_sText = Space$(m_nInitialSize)
   Else
      ' 증가
      Dim nGrowth As Long
      nGrowth = IIf(m_nGrowth > MinimimGrowth, 
            m_nGrowth, MinimimGrowth)
      m_nSize = m_nSize + nGrowth
      m_sText = m_sText & Space$(nGrowth)
   End If
End Sub

' 버퍼를 현재 사용된 크기로 줄이기
Private Sub Shrink()
   If m_nSize > m_nPos Then
      m_nSize = m_nPos - 1
      m_sText = RTrim$(m_sText)
   End If
End Sub

' 단일 텍스트 문자열 추가
Private Sub AppendInternal(ByVal Text As String)
   If (m_nPos + Len(Text)) > m_nSize Then Grow Len(Text)
   Mid$(m_sText, m_nPos, Len(Text)) = Text
   m_nPos = m_nPos + Len(Text)
End Sub

' 여러 텍스트 문자열 추가
Public Sub Append(ParamArray Text())
   Dim nArg As Long
   For nArg = 0 To UBound(Text)
      AppendInternal CStr(Text(nArg))
   Next nArg
End Sub
 
' 현재 문자열 데이터를 반환하고 버퍼 줄이기
Public Function ToString() As String
   If m_nPos > 0 Then
      Shrink
      ToString = m_sText
   Else
      ToString = ""
   End If
End Function

' 버퍼를 지우고 다시 초기화
Public Sub Clear()
   InitBuffer
End Sub

 

이 클래스에서 사용되는 기본 원칙은 문자열 버퍼로 작동하는 클래스 수준에서 변수(m_sText)가 유지되고Space$ 함수를 사용하여 공백 문자로 채움으로써 이 버퍼를 특정 크기로 설정한다는 것입니다. 추가 텍스트를 기존 텍스트에 연결해야 할 경우 버퍼의 크기가 새 텍스트를 보유할 만큼 충분한지 확인한 후 Mid$ 함수를 사용하여 텍스트를 올바른 위치에 삽입합니다. ToString 함수는 버퍼에 현재 저장된 텍스트를 반환하고 버퍼의 크기를 이 텍스트의 길이만큼 줄입니다. StringBuilder를 사용하기 위한 ASP 코드는 다음과 같이 표시됩니다.

Function WriteHTML( Data )

Dim oSB
Dim nRep

Set oSB = Server.CreateObject( "StringBuilderVB.StringBuilder" )
' 크기 및 증가 인수를 사용하여 버퍼 초기화
oSB.Init 15000, 7500

For nRep = 0 to 99
   oSB.Append "<TR><TD>", (nRep + 1), "</TD><TD>", _ 
         Data( 0, nRep ), "</TD><TD>", _ 
         Data( 1, nRep ), "</TD><TD>", _ 
         Data( 2, nRep ), "</TD><TD>", _ 
         Data( 3, nRep ), "</TD><TD>", _ 
         Data( 4, nRep ), "</TD><TD>", _ 
         Data( 5, nRep ), "</TD></TR>"
Next

WriteHTML = oSB.ToString()
Set oSB = Nothing

End Function

 

StringBuilder는 매번 사용할 때마다 인스턴스를 만들어야 하고 이를 포함하는 DLL을 첫 번째 클래스 인스턴스 작성 시 로드해야 하므로 이 클래스를 사용하는 것에 대한 일정한 오버헤드가 존재합니다. 또한StringBuilder 인스턴스에 대한 추가 메서드 호출을 하는 것과 관련해서도 오버헤드가 존재합니다.StringBuilder가 괄호로 묶인 '&'에 대해 수행되는 방법은 연결 수, 작성되는 문자열의 크기, StringBuilder 문자열 버퍼에 대한 초기화 매개 변수를 제대로 선택했는지 등의 여러 요소에 따라 달라집니다. 대부분의 경우 버퍼에서 필요한 공간을 예상보다 많이 잡아두는 것이 자주 공간을 늘리는 것보다 훨씬 더 나은 방법입니다.

기본 제공 방법

ASP에서는 단순히 Response.Write를 여러 번 호출하여 HTML 코드를 매우 신속하게 작성할 수 있는 방법이 포함되어 있습니다. 내부적으로 Write 함수는 매우 뛰어난 성능 특성을 제공하는 최적화된 문자열 버퍼를 사용합니다. 수정된 WriteHTML 코드는 다음과 같이 표시됩니다.

Function WriteHTML( Data )

Dim nRep

For nRep = 0 to 99
   Response.Write "<TR><TD>" 
   Response.Write (nRep + 1) 
   Response.Write "</TD><TD>"
   Response.Write Data( 0, nRep ) 
   Response.Write "</TD><TD>"
   Response.Write Data( 1, nRep ) 
   Response.Write "</TD><TD>" 
   Response.Write Data( 2, nRep ) 
   Response.Write "</TD><TD>"
   Response.Write Data( 3, nRep ) 
   Response.Write "</TD><TD>"
   Response.Write Data( 4, nRep ) 
   Response.Write "</TD><TD>"
   Response.Write Data( 5, nRep ) 
   Response.Write "</TD></TR>"
Next

End Function

 

이 코드가 최상의 성능과 확장성을 제공할 수 있지만 Response 스트림에 직접 기록되는 함수 안에 코드가 있고 이에 따라 호출 코드의 제어 수준이 저하되기 때문에 캡슐화가 다소 손상되었습니다. 또한 사용 가능한Response 스트림에 함수가 종속되기 때문에 이 코드를 이동(예를 들어 COM 구성 요소로 이동)하는 것이 더 어려워집니다.

테스트

위에 제공된 네 가지 방법은 문자열의 더미 배열에서 제공된 단일 테이블이 있는 간단한 ASP 페이지를 사용하여 각각에 대해 테스트되었습니다. 테스트를 수행하기 위해 100MB/초 네트워크 상에서 단일 서버(Windows 2000 Advanced Server, 이중 PIII-1000MHz, 256MB RAM)에 대해 단일 클라이언트(Windows XP Professional, PIII-850MHz, 512MB RAM)의 ACT(Application Center Test)가 사용되었습니다. ACT는 웹 사이트에 연결되는 사용자 5명의 로드를 시뮬레이트하기 위해 5개의 스레드를 사용하도록 구성되었습니다. 각 테스트는 20초의 준비 시간과 가능한 한 많은 요청이 이루어지도록 100초의 로드 시간으로 구성되었습니다.

 

WriteHTML 함수에 대한 코드 단편에 나온 것처럼 기본 테이블 루프에서 반복 횟수를 다양하게 함으로써 다양한 수의 연결 작업에 대해 테스트 실행을 반복했습니다. 각 테스트 실행은 지금까지 설명된 모든 연결 방법을 사용하여 수행했습니다.

결과

아래에 나온 일련의 차트는 각 방법이 응용 프로그램의 처리량에 미치는 영향과 ASP 페이지의 응답 시간을 보여 줍니다. 이러한 차트는 응용 프로그램이 지원할 수 있는 요청 수와 사용자가 페이지를 브라우저로 다운로드하는 데 걸리는 시간에 대한 몇 가지 정보를 제공합니다.

 

표 1   사용된 연결 방법의 약어

방법 약어설명
RESP 기본으로 제공되는 Response.Write 방법
CAT 표준 연결('&') 방법
PCAT 괄호로 묶인 연결('&') 방법
BLDR StringBuilder 방법

 

이 테스트가 일반 ASP 응용 프로그램의 작업 부하를 시뮬레이트하는 것과 관련하여 실제 상황과 다소 차이가 있지만 표 2에 나온 것처럼 420번의 반복에서도 페이지가 그렇게 크지 않다는 것을 분명하게 알 수 있습니다. 실제로는 이러한 수치의 높은 범위에 속하거나 이 테스트 범위의 제한을 초과할 수 있는 복잡한 ASP 페이지가 많이 존재합니다.

 

표 2   테스트 샘플의 페이지 크기 및 연결 수

반복 횟수 연결 수 페이지 크기(바이트)
15 240 2,667
30 480 4,917
45 720 7,167
60 960 9,417
75 1,200 11,667
120 1,920 18,539
180 2,880 27,899
240 3,840 37,259
300 4,800 46,619
360 5,760 55,979
420 6,720 62,219

그림 2   처리량 결과를 보여 주는 차트

 

그림 2에 나온 차트는 테스트된 전체 반복 범위에서 여러 Response.Write 방법(RESP)이 예상대로 최상의 처리량을 제공한다는 것을 보여 줍니다. 무엇보다도 놀라운 것은 표준 문자열 연결 방법(CAT)의 성능이 빠른 속도로 저하되고 괄호로 묶인 버전(PCAT)이 300번 이상의 반복에서 훨씬 더 나은 성능을 보인다는 것입니다. 약 220번의 반복에서 문자열 버퍼링으로 인해 StringBuilder 방법(BLDR)의 성능 향상이 이 방법의 고유한 오버헤드를 능가하기 시작하며, 따라서 이 지점 이상부터는 해당 ASP 페이지에서 StringBuilder를 사용하기 위한 추가 노력을 기울일만한 가치가 있을 것입니다.

 

그림 3   응답 시간 결과를 보여 주는 차트

 

그림 4   CAT가 생략된 응답 시간 결과를 보여 주는 차트

 

그림 3과 4의 차트는 TTFB(Time-To-First-Byte)로 측정된 밀리초 단위의 응답 시간을 보여 줍니다. 표준 문자열 연결 방법(CAT)의 응답 시간은 매우 빠른 속도로 증가하므로 그림 4에서는 이 방법을 포함하지 않고 차트를 반복함으로써 다른 방법 간의 차이점을 확인할 수 있게 했습니다. 표준 연결 방법(CAT)과 괄호로 묶인 연결 방법(PCAT)이 특정 임계값을 지난 후에 매우 빠른 속도로 증가하는 반면 여러 Response.Write방법(RESP)과 StringBuilder 방법(BLDR)은 반복이 증가함에 따라 선형 수열의 형태를 가진다는 것은 흥미로운 일입니다.

결론

이 기사에서는 여러 다른 문자열 작성 기술을 ASP 환경 내에서 적용하는 방법을 중점적으로 살펴보았지만 이러한 방법이 수동으로 XML 문서를 만드는 경우처럼 Visual Basic 코드에서 큰 문자열을 만드는 모든 시나리오에 적용된다는 것을 잊어서는 안 됩니다. 다음 지침은 각 상황에서 어떤 방법이 가장 적합한지 결정하는 데 도움이 됩니다.

  • 특히 기존 코드를 다룰 경우에는 괄호로 묶인 '&' 방법을 우선적으로 사용합니다. 이렇게 하면 코드 구조가 거의 영향을 받지 않으며 기대 이상으로 응용 프로그램의 성능이 향상된다는 것을 확인할 수 있습니다.
  • 필요한 캡슐화 수준을 손상시키지 않는 것이 가능하다면 Response.Write를 사용합니다. 이렇게 하면 불필요한 메모리 내의 문자열 조작을 방지함으로써 항상 최상의 성능을 얻을 수 있습니다.
  • 매우 크거나 연결을 많이 사용하는 문자열을 작성하려면 StringBuilder를 사용합니다.

이 기사에 나온 것과 정확하게 같은 종류의 성능 향상을 개발자가 경험하지 못할 수 있지만 필자는 지금까지 이러한 기술을 실제 ASP 웹 응용 프로그램에서 사용하여 아주 적은 노력으로 성능 및 확장성 모두에서 상당한 향상을 거둔 바 있습니다.

반응형

댓글