[프로그래밍 노트] 실전 성능분석 (분석 편): 최적화

변경 내역

  1. 2006.09.18 작성.

이 글은 월간 마이크로소프트웨어(일명 마소) 2006년 10월호 프로그래밍 노트 칼럼에 기고한 글입니다. 물론 구성이나 내용 상의 차이가 있을 수 있습니다.

퍼포먼스 향상을 위한 세 번째 접근방법은 위에서 말한 90% 통계의 이점을 이용하는 것이다. 이 경우 보통 개발 단계의 후반부에 있는 퍼포먼스 최적화 단계에 들어가기 전까지는 퍼포먼스에 신경 쓰지 않고 그냥 프로그램을 잘 분해된 형태로 개발한다. 퍼포먼스 최적화 단계 동안 프로그램을 튜닝하는 특별한 프로세스를 따른다.

– 리팩토링, 마틴 파울러

최재훈 | 복무를 마치고 복학한 지 일주일이 다 되어 간다. 거친 사회의 바람을 잠시 피해 학교로 돌아왔지만, 새로운 환경에 적응하랴, 원하는 미래를 얻기 위해 준비하는 과정이 쉽지만은 않다. 필자와 같이 학생 신분인 마소 독자가 많은 것으로 알고 있는데, 함께 카이젠(지속적인 개선을 추구하는 마음가짐)의 정신을 잃지 않았으면 한다.

성능분석의 마지막 기사를 어떻게 장식할까 고민하다가, 필자의 실제 경험담을 드러내는 것도 괜찮을 성 싶었다. 물론 관련자들을 배려해서 약간 각색했다. 나초보 씨는 오류 수집용 서버 어플리케이션의 개발자이다. 불행히도 이번에 출시한 "세계 최고의 워드 프로세서였으면 좋겠다."에 치명적인 결함이 있어서 버그 리포트가 급격히 늘어나고 있었다. 결함에 대한 패치는 출시 후 바로 제공됐지만, 아직 패치를 적용하지 않고 쓰는 사용자가 많았다. 오류 수집 서버는 분당 10건을 처리할 수 있는데 무려 30건이 전송되고 있었다. 덕분에 인심 써서 버그 리포트 버튼을 누른 고객들의 불만이 이만저만 아니었다.

회의가 열리고 문제를 해결하라는 압력이 들어왔다. "뭐가 문제인가?" 팀장의 추궁에 나초보 씨는 대답했다. "최초의 요구사항은 분당 10건의 처리량이면 충분하다고 했습니다. 그래서 동기 소켓 프로그래밍으로 구현했습니다. 소켓 통신 부분을 IOCP로 구현하면 문제가 해결될 것입니다.."

일주일이 지났고, 마침내 새로운 버전을 테스트할 시기가 왔다. 그러나 문제는 여전했다. "어째서?" 분석결과, 최초의 의견이 틀렸음을 알게 됐다. 병목은 소켓 통신 컴포넌트에서 일어나지 않았다. 단지, 데이터베이스에 데이터를 추가하는 속도가 느렸을 뿐이다. 데이터베이스 관리자가 불필요한 인덱스 3개를 제거하고 나자, 병목 현상은 사라졌다.

사람의 직관은 매우 훌륭한 도구이지만, 논리보다는 경험에 의존하는 경향이 있다. 이 때문에 합리적인 프로세스(작업 과정)이 필요하다. 성능 분석 및 개선 작업은 추측에 의존하기보다는 누구나 동의할 수 있는 방법으로 합리적이고 효과적으로 수행되어야 하며, 수치화된 객관적인 산출물을 내놓아야 한다. 지난 3달 동안 필자는 이 같은 합리적인 프로세스를 전달하려 노력했다. 이제 그 동안의 노력을 한데 엮어, 정리해보려 한다.

준비 작업

앞으로의 기사 내용을 체험하기 위해서는 크게 두 작업을 미리 해놓아야 한다. 첫째, 예제 어플리케이션을 설치해야 한다. 둘째, CLR Profiler나 DevPartner Performance Analysis Community Edition 또는 NProf와 같은 성능 분석용 도구를 설치해야 한다. 뿐만 아니라 NProf를 사용하는 경우에는 현재 출시 버전 0.91의 버그를 직접 수정해야 한다. 지난 달 기사에서 이 문제들을 다루었기 때문에, 똑 같은 작업을 반복하는 것은 지면 낭비일 것이다. 그러나 마소를 처음 접하거나 오랜만에 읽는 독자를 위해 필자 블로그에 선행 지식을 습득할 수 있는 글을 올려놓았다. 지난 달 기사를 읽었던 독자도 복습하면 좋을 것이다.

테스트용 샘플 데이터의 수집

필자의 블로그에서 예제 소스코드를 다운로드 받자. "C:\ContentWebService\SoapEnvelopes"에 샘플 데이터가 여러 개 있다. 필자가 임의로 제작한 데이터이지만, "실전 성능분석 (준비 편) ? 테스트 자동화"에서 언급한 원칙에 최대한 충실하려고 노력했다.

  • 적절한 샘플의 선택 불행히도 상용서비스가 아니기 때문에 실제 데이터를 수집하지는 못했다. 다만 유사한 프로젝트의 경험을 살리려 노력했다.
  • 오류를 발생시킬 수 있는 부적절한 데이터 데이터 파일 중 Invalid가 들어간 것이 두 개 있다. 각각의 메시지는 유효하지 않은 XML 데이터와 BASE64 문자열을 포함하고 있다.
  • 실제 상황과 유사한 각 샘플 빈도 보수적인 입장에서 전체 요청 중 15% 가량이 오류를 발생시킨다고 보자.
  • 자동화 앞으로 다룰 것이다. 이 기사에서는 Web Application Stress Tool을 이용해서 ASP.NET 어플리케이션의 성능 테스트를 자동화시켜 볼 생각이다.

  • 문서화 수치화된 통계를 어떻게 뽑아내면 좋을지에 대해 다룰 것이다.

  • <br />
    
    
    <a name="test_automation"></a><h2>테스트 자동화</h2>
    
    <p>샘플 데이터가 준비됐다. 이제 Content Web Service(예제 어플리케이션의 이름)에 샘플을 전송하고 프로필러로 분석할 차례다. NProf나 CLR Profiler를 띄우고 재미있는 성능 분석 작업을 해보자. 잠깐! 뭔가 빠진 것 같지 않은가? 그렇다. 아직 테스트 과정을 자동화하지 않았다. 
    
    성능 분석 작업은 단 한번에 이뤄지지 않는다. 현실은 우리의 바램을 냉정하게 무시한다. 당장의 문제를 해결하기 위해 똑 같은 테스트를 수 차례 반복하기 마련이다. 당장의 문제를 어떻게 해결했다고 해도, 어플리케이션이나 서비스가 진화(?)함에 따라 또 다시 성능 문제가 발생할 수 있다. 매번 똑 같은 작업을 반복하는 것이야 말로 진정한 귀차니미스트(?)의 적이라 할 수 있다. 차라리 초반에 문제가 잘못될 소지를 제거해버리자.
    
    <br />
    
    
    <a name="web_application_stress_tool"></a><h2>Microsoft Web Application Stress Tool</h2>
    
    지난 시간에는 Microsoft 사의 Application Center Test (통칭 ACT)을 소개했다. 무척 훌륭한 도구이지만 상용 소프트웨어이므로 이번 시간에는 Microsoft 사가 제공하는 스트레스 테스트 도구 Web Application Stress Tool (통칭, WAS)를 사용할 것이다. 물론 이 소프트웨어는 무료로 제공된다. ACT와 WAS는 매우 유사한 도구이다. 그러나 차이점 또한 많은데, 여기서는 그 중 하나에만 주목하면 된다. ACT에서는 테스트 지속 시간을 &quot;지정한 시간 동안 테스트 실행&quot;과 &quot;지정한 횟수만큼 테스트 실행&quot;으로 나누어서 지정할 수 있었다. 그러나 WAS는 &quot;지정한 횟수만큼 테스트를 실행&quot;하는 기능은 제공하지 않는다. 
    
    이 때문에 WAS를 사용하면, 이전과 현재 사이의 성능 향상 정도를 쉽게 파악하기 힘들다. 이것은 문서화 작업을 방해하는 요소이지만, 성능 개선 작업을 방해하지는 않는다. 더욱이 &quot;지정한 시간 동안 테스트 실행&quot; 기능이 없어도 성능 개선의 정도를 객관적인 수치로 표현할 수 있다. 단지 불편할 뿐이다. 이에 대해서는 기사 뒷부분에서 다룰 예정이다.
    
    반복회수를 지정할 수 없더라도, 충분한 회수를 실행시키면 병목지점을 파악하는 데 장애가 되지는 않는다. 만약 최초의 문제점이 개선되었다면, 추후 테스트에선 새로운 병목 지점이 발견될 것이다. 
    
    <br />
    
    <a name="recording"></a><h2>레코딩하기</h2>
    
    WAS를 처음 실행시키거나 &quot;New Script&quot; 버튼을 누르면 &lt;화면 1&lt;의 창을 볼 수 있다. Record를 선택하면 Record delay between requests 등 세 개의 옵션이 제시된다. 경우에 따라 옵션을 선택하면 되지만, 여기서는 아무것도 선택하지 않는다. Finish 버튼을 누르면 로컬 컴퓨터의 HTTP 세션이 모두 기록된다. 그러므로 연예계 소식을 보려고 웹 브라우저를 열어놨다면, Finish 버튼을 누르기 전에 모두 닫아야 한다. 이때 한가지 실수하기 쉬운 점이 있다. 고백하건대 필자도 기사를 쓰다가 한번 실수했는데, 쥬크온이나 구글 데스크탑 같은 일부 어플리케이션은 HTTP 통신을 하므로 엉뚱한 세션이 기록될 수 있다. Finish 버튼을 누른 다음 잠시 기다려보자. 행여나 뭔가 기록되기 시작한다면, 의심 가는 어플리케이션을 찾아서 끄면 된다.
    
    <br />
    
    <strong>&lt;화면 1&gt; WAS의 Record 기능</strong>
        <br />
        <a href="http://andromedarabbit.net/shared/uploads/ee-images/uploads/imaso/200610_stress_test_ii/001_new_script.png" rel="lightbox[roadtrip]"><img src="http://andromedarabbit.net/shared/uploads/ee-images/uploads/imaso/200610_stress_test_ii/001_new_script.png" alt="WAS의 Record 기능" width="500" /></a>
    
    
    <br />
    
    기록 준비가 됐으면 &quot;C:\ContentWebService\TheLittleSOAPClient&quot;의 VB 스크립트를 실행시키자. VB 스크립트는 &quot;C:\ContentWebService\SoapEnvelopes\MIMEMessage.txt&quot; 파일의 내용을 <a href="http://localhost/ContentManager로">http://localhost/ContentManager로</a> 전송한다. 폴더에 있는 모든 &quot;.txt&quot; 파일을 전송하는 것이 아니므로 매번 파일의 이름을 바꿔서 전송해야 하는 불편함이 따른다. 그러나 단 한번만 수고하면 되니 인내심을 갖자. 
    
    기록 작업은 약 3분 내로 끝날 것이다. 하지만 그마저도 귀찮은 독자를 위해 필자 블로그에서 WAS 데이터베이스를 <a href="http://imaso.googlecode.com/files/WAS.zip">다운로드</a> 받을 수 있게 해 놓았다. 이제 새로운 스크립트의 설정을 고치자. 간단하게 Test Run Time만 3분 정도로 잡는다. 테스트 시간은 전체 샘플 데이터를 최소한 3번 이상 보낼 수 있을 만큼 길어야 한다. 이러한 조치에는 여러 가지 이유가 있지만, 무엇보다도 최소 테스트 시에 JIT(Just In Time) 컴파일이 이뤄지는 점을 감안해야 하기 때문이다.
    
    <br />
    
    <strong>&lt;화면 2&gt; HTTP 세션 기록</strong>
        <br />
        <a href="http://andromedarabbit.net/shared/uploads/ee-images/uploads/imaso/200610_stress_test_ii/002_recording.png" rel="lightbox[roadtrip]"><img src="http://andromedarabbit.net/shared/uploads/ee-images/uploads/imaso/200610_stress_test_ii/002_recording.png" alt="HTTP 세션 기록" width="500" /></a>
    
    
    <br />
    
    <strong>&lt;화면 3&gt; WAS 설정</strong>
        <br />
        <a href="http://andromedarabbit.net/shared/uploads/ee-images/uploads/imaso/200610_stress_test_ii/003_was_settings.png" rel="lightbox[roadtrip]"><img src="http://andromedarabbit.net/shared/uploads/ee-images/uploads/imaso/200610_stress_test_ii/003_was_settings.png" alt="WAS 설정" width="500" /></a>
    
    
    <br />
    
    
    
    <a name="performance_analysis"></a><h2>성능 분석하기 </h2>
    
    비주얼 스튜디오로 &quot;C:\ContentWebService\ContentManager.sln&quot; 솔루션을 열자. DevPartner Performance Analysis Community Edition (통칭 PA, <cite><a href="http://andromedarabbit.net/wp/imaso_august_edited_content/">마소 8월호 기사 중 편집된 내용</a></cite> 참조)을 작동시킨 다음, WAS로 돌아가서 조금 전에 만든 Content Manager Script를 실행시키면 된다. PA가 띄운 인터넷 익스플로러 창을 닫으면 &lt;화면 4&gt;와 같은 분석 결과를 볼 수 있다.
    
    <a href="http://www.microsoft.com/downloads/details.aspx?FamilyID=a362781c-3870-43be-8926-862b40aa0cd0">CLR Profiler</a>로 분석하고자 할 때도 PA의 경우와 다르지 않다. 다만 닷넷 프레임워크의 실행 권한을 반드시 조정해줘야 하는데, 이 문제는 &lt;필자 노트 1&gt;에서 다루고 있다.
    
    <blockquote class="box">
        <h3>&lt;필자 노트 1&gt; NProf 성능 분석 팁</h3>
    
        필자는 이번 기사를 위해 DevPartner의 제품을 사용했다. 이러한 상용 제품을 사용하면 보다 편리하게 성능 분석을 할 수 있다. 그러나 라이센스 문제(자세한 내용은 필자 블로그를 <cite><a href="http://andromedarabbit.net/wp/imaso_august_edited_content/">참조</a></cite>하기 바란다.) 등 걸림돌이 많은 것이 흠이다. 지난 시간에 소개한 NProf를 사용하기로 결정할 독자도 있을 것이다. NProf는 아직 성숙되지 않은 어플리케이션이라 예기치 않은 문제와 마주치게 될 수도 있는데, 일반적인 해결법을 소개한다.
    
        NProf로 ASP.NET 어플리케이션을 분석할 수 없을 때, 크게 두 가지 원인을 생각해 볼 수 있다. <strong>첫째</strong>, 닷넷 프레임워크의 실행 권한이 낮게 설정되어 있기 때문일 수 있다. 이때는 machine.config 파일을 수정해야 한다. 닷넷 프레임워크 버전에 해당하는 설정 파일을 다음 경로에서 발견할 수 있다. 이것은 CLRProfiler에도 똑같이 해당하는 문제이다.
    
        <ul>
        <li><strong>v1.1</strong>  C:\WINDOWS\Microsoft.NET\Framework\v1.1.4322\CONFIG\machine.config</li></ul></blockquote>
        <li><strong>v2.0</strong>  C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\CONFIG\machine.config</p></li>
    
    
        <p>메모장 등으로 설정 파일을 열어서 &lt;processModel&gt; 요소의 값을 다음과 같이 바꿔주면 된다. 이때 중요한 것은 username의 기본값 &quot;machine&quot;을 &quot;SYSTEM&quot;으로 바꿔주는 것이다. 물론 분석 작업이 완료되면 원래 값으로 바꿔주는 것을 잊지 말아야 하겠다. 
    
        <pre class="brush: xml">
        &lt;processModel enable=&quot;true&quot; timeout=&quot;Infinite&quot; idleTimeout=&quot;Infinite&quot; shutdownTimeout=&quot;0:00:05&quot; requestLimit=&quot;Infinite&quot; requestQueueLimit=&quot;5000&quot; restartQueueLimit=&quot;10&quot; memoryLimit=&quot;60&quot; webGarden=&quot;false&quot; cpuMask=&quot;0xffffffff&quot; userName=&quot;SYSTEM&quot; password=&quot;AutoGenerate&quot; logLevel=&quot;Errors&quot; clientConnectedCheck=&quot;0:00:05&quot; comAuthenticationLevel=&quot;Connect&quot; comImpersonationLevel=&quot;Impersonate&quot; responseDeadlockInterval=&quot;00:03:00&quot; maxWorkerThreads=&quot;20&quot; maxIoThreads=&quot;20&quot;/&gt;
        </pre>
    
        <strong>둘째</strong>, 버그 수정한 NProf를 다운로드 받은 후 &quot;RegisterProfilerHook.bat&quot;을 실행시켜야 한다. 이 배치 파일은 &quot;nprof.hook.dll&quot;을 재등록하는 역할을 한다.
    
        <strong>마지막</strong>으로 NProf의 분석 결과에 System이나 Microsoft 같은 네임스페이스만 보이는 경우가 있다. 이것은 콘솔/윈폼/ASP.NET 어플리케이션 모두에 해당하는 사항이다. 크게 두 가지 이유를 생각해 볼 수 있는데, 하나는 의미 있는 통계를 내기엔 테스트 회수가 너무 적었을 수 있다. 또 다른 경우는 NProf가 분석을 마치고 결과를 제시하기까지 시간이 많이 걸리는데, 이 때문에 실제로는 분석이 제대로 이뤄졌음에도 사용자에게 보이지 않는 것이다. 이럴 때는 All Namespaces 항목만 보이므로 인내심을 갖고 어플리케이션의 네임스페이스가 뜰 때까지 기다려야 한다. 고려해야 할 사항이 많아서 부담된다면 PA를 사용하는 편이 나을 것이다. 
    
    
    <br />
    
    
    <a name="analysis_result"></a><h2>분석 결과</h2>
    
    &lt;화면 4&gt;와 &lt;화면 5&gt;는 각각 PA와 CLR Profiler의 분석 결과이다. 사실상 동일한 결과를 보여주고 있는데, ContentManager.PublishViaMIME.ProcessMessage() 메써드가 전체 실행시간의 86.3%, 그리고 전체 메모리 할량의 99.89% 차지하고 있음을 보여준다. 두 번째와 세 번째에 놓인 PublishViaMIME.resolveContentRefs 메써드나 PublishViaMIME.getSOAPEnvelope 메써드는 모두 ProcessMessage() 메써드 내부에서 호출된다. 이 예제에서 우리는 마틴 파울러가 말한 &quot;90% 통계&quot;의 원리(서문 참조)를 목격할 수 있다.
    
    <br />
    
    <strong>&lt;화면 4&gt; DevPartner Performance Analysis Community Edition의 분석 결과</strong>
        <br />
        <a href="http://andromedarabbit.net/shared/uploads/ee-images/uploads/imaso/200610_stress_test_ii/006_devpartner_first_s.png" rel="lightbox[roadtrip]"><img src="http://andromedarabbit.net/shared/uploads/ee-images/uploads/imaso/200610_stress_test_ii/006_devpartner_first_s.png" alt="DevPartner Performance Analysis Community Edition의 분석 결과" width="500" /></a>
    
    
    <br />
    
    <strong>&lt;화면 5&gt; CLR Profiler의 분석 결과</strong>
        <br />
        <a href="http://andromedarabbit.net/shared/uploads/ee-images/uploads/imaso/200610_stress_test_ii/007_clrprofiler_first_s.png" rel="lightbox[roadtrip]"><img src="http://andromedarabbit.net/shared/uploads/ee-images/uploads/imaso/200610_stress_test_ii/007_clrprofiler_first_s.png" alt="CLR Profiler의 분석 결과" width="500" /></a>
    
    
    <br />
    
    
    <a name="solving_problems"></a><h2>문제점 해결</h2>
    
    &lt;리스트 1&gt;은 병목현상이 벌어지는 resolveContentRefs 메써드의 변경 전과 후의 코드를 보여준다. 주석 상에 delete로 표시된 부분이 이전 코드이며, add로 표시된 곳이 수정된 코드이다. String::Concat 메써드(또는 + 연산)를 호출하던 곳을 System.Text.StringBuilder 코드로 대체해주었을 뿐이다. 물론 이것은 임시 조치일 뿐이다. 문자열 파싱을 위해 StringBuilder를 사용하는 것 자체가 사치나 다름없다. 마소 5월호에서 MIME Parser를 작성했을 때처럼 먼저 잘라내야 할 문자열의 시작 지점과 끝 지점의 인덱스를 아낸 후, 단 한번만 문자열을 생성하는 것이 가장 바람직하다. 그러나 여기서는 일단 StringBuilder로 만족하고 얼마나 성능이 개선되었는지 살펴보자.
    
    <br />
    
    <strong>&lt;리스트 1&gt; 성능 병목지점 수정하기</strong>
    
    <pre class="brush: c#">private void resolveContentRefs(Stream MIMEstream)
    

    {
    string contID;
    string content;

    MIMEstream.Position = 0;
    StreamReader sr =
    new StreamReader(MIMEstream, System.Text.Encoding.Default, true);
    
    // Read through the request stream, looking for the Content-ID headers
    // ignore the content of the root item (the soap envelope content)
    for (String Line = sr.ReadLine(); (Line != null); Line = sr.ReadLine())
    {
        // process every Content-ID *other* than the start Content-ID
        if ((Line.IndexOf(&quot;Content-ID: &quot;) &gt;= 0) &amp;&amp;
        (Line.IndexOf(startContentID) &lt; 0))
        {
            //save the Content-ID value, and the base64 encoded content
            contID = Line.Substring(Line.IndexOf(&quot;Content-ID: &quot;) + 12).Trim();
    
            //remove the starting and ending &quot;&lt;&quot; and &quot;&gt;&quot;
            if (contID.StartsWith(&quot;&lt;&quot;)) contID = contID.Substring(1);
            if (contID.EndsWith(&quot;&gt;&quot;)) contID = contID.Substring(0, contID.Length - 1);
    
            //get the encoded content immediately following the Content-ID header
            //(until the next MIME Boundary is reached)
            // content = &quot;&quot;; // delete
            StringBuilder sb = new StringBuilder(); // add
            Line = sr.ReadLine();
            while ((Line != null) &amp;&amp; (Line.IndexOf(MIMEBoundary) &lt; 0))
            {
                // content = content + Line; // delete
                sb.Append(Line);
                Line = sr.ReadLine();
            }
    
            // insertContent(contID, content); // delete
            insertContent(contID, sb.ToString());// add
        }
    }
    
    // now that we've processed all the MIME 'attachments', look through
    // the envelope and make sure there are no remaining unresolved href's
    if (SOAPenvelope.IndexOf(&quot;href=\&quot;cid:&quot;) &gt;= 0)
    {
        throw new Exception(&quot;Envelope contains unresolved content references&quot;);
    }
    MIMEstream.Position = 0;  //reset the stream position
    

    }

    </pre>
    
    <br />
    
    PA의 분석 결과(화면 6)부터 살펴보자. 전체 실행시간의 82.9%를 차지하던 resolveContentRefs 메써드의 비중이 37.5%까지 내려갔다. CLR Profiler의 분석 결과(화면 7)도 고무적이다. 18 GB라는 어마어마한 메모리를 요구하던 것이 단지(?) 720 MB만 사용했을 뿐이다.
    
    <br />
    
    <strong>&lt;화면 6&gt; DevPartner Performance Analysis Community Edition의 분석 결과 2</strong>
        <br />
        <a href="http://andromedarabbit.net/shared/uploads/ee-images/uploads/imaso/200610_stress_test_ii/008_devpartner_second_d.png" rel="lightbox[roadtrip]"><img src="http://andromedarabbit.net/shared/uploads/ee-images/uploads/imaso/200610_stress_test_ii/008_devpartner_second_d.png" alt="DevPartner Performance Analysis Community Edition의 분석 결과 2" width="500" /></a>
    
    
    <br />
    
    
    <strong>&lt;화면 7&gt; CLR Profiler의 분석 결과 2</strong>
        <br />
        <a href="http://andromedarabbit.net/shared/uploads/ee-images/uploads/imaso/200610_stress_test_ii/009_clrprofiler_second_s.png" rel="lightbox[roadtrip]"><img src="http://andromedarabbit.net/shared/uploads/ee-images/uploads/imaso/200610_stress_test_ii/009_clrprofiler_second_s.png" alt="CLR Profiler의 분석 결과 2" width="500" /></a>
    
    
    <br />
    
    
    <a name="setting_the_goal"></a><h2>성능 개선 작업의 목표를 설정하자.</h2>
    
    <em>제약조건이론</em>은 시스템에는 하나의 (많아야 두 개의) 병목 지점이 있다고 말한다. 시스템에서 중요한 것은 병목지점이고, 비병목 지점의 생산성(여기서는 성능으로 해석할 수 있다.) 감소는 전체 생산성에 아무런 영향을 미치지 않는다. 데이터베이스 계층에서 병목 현상이 벌어졌을 때, 소켓 통신 모듈을 IOCP로 바꿔본들 성능은 개선되지 않는다. 
    
    제약조건이론은 또 다른 현상을 예언하는데, 병목을 제거하면 또 다른 병목이 발생한다는 점이다. 최초의 병목 지점 resolveContentRefs 메써드는 전체 실행시간의 82.9%를 차지했다. 그러나 문제를 해결했을 때는 단지 37.5%만 차지했었고, 2위인 getSOAPEnvelope 메써드와 단지 10.9%의 차이만 났을 뿐이다. 만약 땜질 처방이 아닌 집중 치료를 했더라면, getSOAPEnvelope 메써드가 병목 지점이 됐을 것이다. 즉, 병목 지점이 이동하게 된다.
    
    이와 같이 병목 지점은 계속해서 이동하고, 병목 자체는 사라지지 않는다. 그러므로 병목 지점의 제거가 성능 개선 작업의 목표가 될 수는 없다. 목표는 절대적이어야 하고 수치로 표현할 수 있어야 한다. 예를 들어, &quot;현재 웹 어플리케이션은 분당 10회의 요청을 처리하는데 개선 작업 후에는 30회를 처리할 수 있어야 한다.&quot;는 좋은 목표가 될 수 있다.
    
    
    <br />
    
    
    
    <a name="writing_a_report"></a><h2>보고서 작성하기</h2>
    
    병목 현상이 가장 심한 곳을 찾아서 수정해보았다. 극적인 성능 향상이 있었다. 그러나 성능 개선의 목적이 달성되었는지 여부는 아직 확인되지 않았다. 우리는 여태까지 각 구간이 전체 실행시간에서 차지하는 상대적인 비중만을 보았을 뿐이다. 절대적인 기준에서 성능이 어느 정도 향상되었는지 확인한 것은 아니다.
    
    앞서 ACT와 WAS를 비교할 때, WAS에는 &quot;지정한 횟수만큼 테스트를 실행&quot;하는 기능이 없다는 사실을 지적했다. 이것은 절대적인 성능 분석을 방해하는 요소이다. 무조건 지정한 시간만큼 테스트가 진행되기 때문에 성능 개선 전과 후의 실행시간은 같을 수밖에 없다. 여러분이 ACT가 아닌 WAS를 사용한다면 다른 방법이 필요하다.
    

    해결책은 의외로 간단하다. 같은 회수의 요청을 처리하는 시간을 비교하는 대신에 같은 시간에 얼마의 요청을 처리하는지 측정하면 된다. 가장 무식한 방법은 Global.asax의 Application_BeginRequest 메써드와 Application_EndRequest 메써드를 오버라이딩해서 카운트를 세는 것이다. 마소 8월호 "성능 분석하기(2) ? CPU Profiler"에서 제시한 방법대로 이러한 기능을 구현하면 된다.

    커스터마이징된 카운터를 작성하는 것은 여러 가지 장점이 있다. 무엇보다 프로필러를 돌릴 수 없는 상용 서버에서도 카운터 기능을 활성화할 수 있다는 점이다. 그러나 게으른 필자 같은 개발자는 프로필러의 기능을 활용한다. PA의 Session Summary에는 메써드 호출 회수가 제시된다. &lt;화면 8&gt;과 &lt;화면 9&gt;는 성능 개선 전과 후의 메써드 호출 회수를 보여는다. 개선 전에는 3분 동안 8710번의 호출이 일어났는데 반해, 개선 후에는 같은 시간 동안 무려 9730번이나 메써드가 호출됐다. (9730 ? 8710) / 8710 * 100 = 약 12% 의 성능 개선이 이뤄진 것이다. 이런 식으로 성능 개선의 정도를 수치화시키면, 문서화하기 쉬워지고 자신의 성과를 구체적으로 내세우기 편해진다.
    
    <br />
    
    <strong>&lt;화면 8&gt; 성능 개선 전의 Session Sumamary</strong>
        <br />
        <a href="http://andromedarabbit.net/shared/uploads/ee-images/uploads/imaso/200610_stress_test_ii/010_calling_methods_first_s.png" rel="lightbox[roadtrip]"><img src="http://andromedarabbit.net/shared/uploads/ee-images/uploads/imaso/200610_stress_test_ii/010_calling_methods_first_s.png" alt="성능 개선 전의 Session Sumamary" width="500" /></a>
    
    
    <br />
    
    
    <strong>&lt;화면 9&gt; 성능 개선 후의 Session Sumamary</strong>
        <br />
        <a href="http://andromedarabbit.net/shared/uploads/ee-images/uploads/imaso/200610_stress_test_ii/011_calling_methods_second_s.png" rel="lightbox[roadtrip]"><img src="http://andromedarabbit.net/shared/uploads/ee-images/uploads/imaso/200610_stress_test_ii/011_calling_methods_second_s.png" alt="성능 개선 후의 Session Sumamary" width="500" /></a>
    
    
    <br />
    
    
    <a name="concluding_remarks"></a><h2>마치는 글</h2>
    
    우리는 정확한 문제 분석보다는 추측에 의존하기도 한다. 개발자의 귀차니즘을 비난하는 경우도 있지만, 필자는 다르게 생각한다. 누구나 더 잘 하고 싶어하고, 개발이라는 녹녹하지 않은 일을 하는 사람이라면 더욱 그럴 것이다. 다만 그 방법을 모를 뿐이다. 4회에 걸쳐 성능분석 문제를 다룬 것도 그런 개발자들의 궁금증을 완전히 해소하고 싶어서였다. 필자의 내공이 보다 강력했더라면, 짧고 굵게 핵심 사항을 전달할 수 있었을 텐데 아쉬움이 많이 남는다. 
    
    앞으로는 성능분석을 주제로 다루지는 않을 것이다. 하지만 핵심가치를 전달할 기회가 올 때마다 짧게나마 이 문제에 대해 언급할 생각이다. 1년 후에라도 궁금증이 발동하면 주저 말고 필자에게 문의해주면, 언제라도 최선을 다해 답변할 생각이다.
    

    참고 문헌

    최 재훈

    블로그, 페이스북, 트위터 고성능 서버 엔진, 데이터베이스, 지속적인 통합 등 다양한 주제에 관심이 많다.
    Close Menu