성태의 닷넷 이야기
홈 주인
모아 놓은 자료
프로그래밍
질문/답변
사용자 관리
사용자
메뉴
아티클
외부 아티클
유용한 코드
온라인 기능
MathJax 입력기
최근 덧글
[정성태] VT sequences to "CONOUT$" vs. STD_O...
[정성태] NetCoreDbg is a managed code debugg...
[정성태] Evaluating tail call elimination in...
[정성태] What’s new in System.Text.Json in ....
[정성태] What's new in .NET 9: Cryptography ...
[정성태] 아... 제시해 주신 "https://akrzemi1.wordp...
[정성태] 다시 질문을 정리할 필요가 있을 것 같습니다. 제가 본문에...
[이승준] 완전히 잘못 짚었습니다. 댓글 지우고 싶네요. 검색을 해보...
[정성태] 우선 답글 감사합니다. ^^ 그런데, 사실 저 예제는 (g...
[이승준] 수정이 안되어서... byteArray는 BYTE* 타입입니다...
글쓰기
제목
이름
암호
전자우편
HTML
홈페이지
유형
제니퍼 .NET
닷넷
COM 개체 관련
스크립트
VC++
VS.NET IDE
Windows
Team Foundation Server
디버깅 기술
오류 유형
개발 환경 구성
웹
기타
Linux
Java
DDK
Math
Phone
Graphics
사물인터넷
부모글 보이기/감추기
내용
<div style='display: inline'> <h1 style='font-family: Malgun Gothic, Consolas; font-size: 20pt; color: #006699; text-align: center; font-weight: bold'>제니퍼 닷넷 - Ninject DI 프레임워크의 성능 분석</h1> <p> 세상이 복잡해지다 보니, 예전에는 고려할 대상에도 없었던 새로운 것들이 나타나기도 합니다. 그중의 하나가 바로 IoC 컨테이너, 또는 Dependency Injection 프레임워크라고 불리는 것들인데요.<br /> <br /> 며칠 전에, 지인 한 분이 고객사의 성능 컨설팅을 의뢰받았는데 이에 대한 보고자료로 '제니퍼 닷넷'을 써보고 싶다는 연락이 왔습니다. 그쵸... 아무래도 썰렁한 텍스트 보고서보다는, 그래픽 요소가 가미되면 훨씬 더 ^^ 감동을 줄 수 있을 테니까요.<br /> <br /> 웹 애플리케이션 개발을 막 완료한 그 고객사는 내부적으로 Dog Food 식으로 테스트를 하는 과정에서 10명 정도의 사용자 수준에서도 CPU 부하가 심각해지는 등의 성능 저하를 겪었다고 합니다. 개인적으로도 재미있는 사례가 될 듯하여 ^^ 같이 만나서 '제니퍼 닷넷'을 통해 문제점 분석을 하기 시작했습니다.<br /> <br /> <hr style='width: 50%' /><br /> <br /> 해당 응용 프로그램의 구조는 MVC 3 + Ninject를 사용하는 특징이 있었습니다. MVC Controller에는 별다르게 복잡한 메서드 호출 없이 전형적인 Biz + Dac 구조의 DB 호출을 이용한 로직만을 포함하고 있었고, DB 호출 자체도 그다지 느리지 않았습니다. <br /> <br /> 좀 더 문제를 추적하기 위해, <a target='tab' href='http://www.sysnet.pe.kr/2/0/1203'>method_all_profile 옵션</a>으로 프로파일링을 해보았습니다.<br /> <br /> 다행히 여기서 문제점이 대충 들어났는데요. 다음과 같이 Ninject.Web.Mvc.NinjectDependencyResolver.GetService 하위에 Resolve 관련해서 엄청난 중첩 호출이 발생하고 있었습니다.<br /> <br /> <img alt='ninject_profile_1.png' src='/SysWebRes/bbs/ninject_profile_1.png' /><br /> <br /> 마지막에 <>c__DisplayClass4.<Create>b__2 메서드 호출 하위로도 재귀적으로 반복되는 것을 확인할 수 있었는데, 아래의 그림에서 다시 보면,<br /> <br /> <img alt='ninject_profile_2.png' src='/SysWebRes/bbs/ninject_profile_2.png' /><br /> (이미지의 일부 내용은 의도적으로 검정색 처리를 해서 숨겼습니다.)<br /> <br /> 총 3600개가 넘는 함수 호출 중에서 실제로 MVC Controller의 Action 메서드에서 수행한 것은 3553 - 3457 = 96개의 호출에 불과했던 것입니다. (참고로, 어떤 페이지는 총 17845번의 호출 중에서 104번의 호출만 실제 업무에 필요한 호출인 경우도 있었습니다.) <br /> <br /> 즉, 나머지 Ninject의 작업이 과다하게 발생하는 것이 문제였습니다. 전체적으로 웹 페이지 호출에 대한 X-View 정보를 보니 다음과 같이 응답시간과 CPU 시간이 비슷하게 높은 것으로 나옵니다.<br /> <br /> <img alt='ninject_profile_3.png' src='/SysWebRes/bbs/ninject_profile_3.png' /><br /> <br /> 결국 CPU 부하의 주된 요인은 aspx에 대해 한 번 호출할 때마다 Type Resolve 작업이 매번 발생하고, 게다가 그것이 매우 CPU-intensive한 작업이라는 점입니다. 문제를 더욱 심각하게 만든 것은, 하나의 aspx 페이지가 렌더링 되는 클라이언트 측에서 ajax 호출을 통해 다수의 서버 페이지를 다시 호출하는 것이었습니다.<br /> <br /> 파악해 보니, 로그인 페이지 한 번에 aspx 요청들이 18개나 발생되었습니다. (그리고, 그 하나의 aspx 요청들마다 다시 Ninject의 Type Resolving 작업이 발생했습니다.)<br /> <br /> 이런 경우는 어쩌면 '아는 것이 병'이었을 수도 있습니다. DI 프레임워크인 Ninject를 쓰지 않았다면 괜찮았을 수도 있고, 혹은 Ajax 호출로 쪼개지 않았다면 괜찮았을 수도 있습니다. 다시 말해, '전통적인 웹 애플리케이션 제작 방식'이었다면 이런 정도의 웹 애플리케이션을 만드는 데 있어 성능 문제는 없었을 것입니다.<br /> <br /> <hr style='width: 50%' /><br /> <br /> 원인은 대충 그렇다 치고... 그렇다면 Ninject가 저런 식으로 무거웠다면 실제로 많은 불평들이 있었을 텐데, 그렇지 않았다는 점이 이상했습니다. 그래서 좀 더 살펴보았는데요.<br /> <br /> 가만히 보니, 유독 느린 호출들은 MVC Controller 안에 Injection되는 인터페이스들의 숫자가 상대적으로 더 많았습니다. 그래서, 위의 3457개의 호출이 발생하는 MVC Controller 안에 일부러 (호출은 안하지만) 2개의 Injection 인터페이스를 생성자에 추가했더니... 실제 Biz 호출은 96개로 동일했지만, 전체적인 호출 건수는 39455건으로 10배 이상 증가를 했습니다.<br /> <br /> 다시 의문이 생겼습니다. 비례적으로 증가하지 않았다는 사실이지요.<br /> <br /> 그래서, 추가했던 2개의 인터페이스 중에서 하나씩 번갈아 다시 독자적으로 추가해 보면서 부하가 어느 정도로 심한지 체크해 보았습니다. 편의상, 해당 인터페이스들을 IA, IB, IC라고 해보면 결과는 다음과 같았습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > IA: 3553 호출 IB: 12931 호출 IC: 27718 호출 </pre> <br /> 전체 합치면 44202로 39455보다 많지만... 그런 세세한 차이는 무시하고 대충 비율적으로 IC 인터페이스의 Injection 작업이 가장 무겁다는 것을 짐작할 수 있습니다.<br /> <br /> 이제, IC와 IA에 어떤 주요한 차이점이 있는지 NinjectModule을 상속받은 타입의 Load 메서드에서 해당 인터페이스에 대해 지정된 Concrete 타입을 알아내서 비교를 해보면 될 것 같습니다. 다행히, IC와 IA 모두 하나의 Assembly 안에 정의된 타입들이어서 상이한 Assembly 간의 차이점까지 살펴볼 필요는 없어졌습니다.<br /> <br /> 추적 결과, 문제가 더욱 간단해 지더군요. IA를 구현한 클래스는 내부에 다시 Injection 될 인터페이스를 가지고 있는데 그 수가 하나에 불과했습니다.<br /> <br /> 하지만, IC는 달랐는데... 대충 다음과 같이 복잡한 Injection 구조를 가지고 있었습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > IC - ICda - IUM - IUMda - IOM - ICodM - ICodMda - IOM - IOMda - IUMda - IMM - IMMda - IComM - IComMda - IAM - IAMda - IUM </pre> <br /> <br /> 내부적으로 다시 IAM은 IUM을 포함하고 / IUM은 IOM을 포함하고 있어서 최종적으로 다음과 같이 펼쳐질 수 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > IC - ICda - IUM - IUMda - IOM - IOMda - IUMda - ICodM - ICodMda - IOM - IOMda - IUMda - IMM - IMMda - IComM - IComMda - IAM - IAMda - IUM - IUMda - IOM - IOMda - IUMda - ICodM - ICodMda </pre> <br /> 결론은, 중첩된 Injection이 많을수록 그에 대한 Type Resolving 시간이 급격하게 늘어난다는 점입니다.<br /> <br /> <hr style='width: 50%' /><br /> <br /> 그래도 혹시, 환경상의 차이로 인해 그럴 수 있으니 위와 같은 정보를 바탕으로 간단하게 프로젝트를 구성해서 확인하는 것이 좋을 것 같습니다. (향후, 좋은 예제 코드로 쓰일 수도 있고. ^^)<br /> <br /> 다음의 글에 따라서, MVC 없이 단순 WebForm에 Ninject로 IA, IC와 동일한 구조로 작성을 해보았는데요.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > How can I implement Ninject or DI on asp.net Web Forms? ; <a target='tab' href='http://stackoverflow.com/questions/4933695/how-can-i-implement-ninject-or-di-on-asp-net-web-forms'>http://stackoverflow.com/questions/4933695/how-can-i-implement-ninject-or-di-on-asp-net-web-forms</a> </pre> <br /> Ninject 관련 DLL들의 참조를 추가하고, Global.asax의 코드를 다음과 같이 입력해 주었습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > public class Global : NinjectHttpApplication { protected override IKernel CreateKernel() { var kernel = new StandardKernel(); kernel.Load(Assembly.GetExecutingAssembly()); return kernel; } } </pre> <br /> 이후, 문제가 되었던 IC, IA 인터페이스에 관해서 유사하게 구현해 주고 이를 테스트할 수 있는 2개의 웹 페이지를 만들었습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > public partial class TestIA : Ninject.Web.PageBase { [Inject] public IA A { get; set; } protected void Page_Load(object sender, EventArgs e) { A.Do(); } } public partial class TestIC : Ninject.Web.PageBase { [Inject] public IC C { get; set; } protected void Page_Load(object sender, EventArgs e) { C.Do(); } } </pre> <br /> 그 결과, TestIA.aspx 호출에는 773번의 호출이 있었던 반면, TestIC.aspx 호출에는 14169번의 호출이 있었습니다. 범인이 확실해졌군요. 중첩된 Injection을 하는 경우 Ninject는 Type Resolving에 심각한 CPU 사용량을 보이고 있었습니다.<br /> <br /> <hr style='width: 50%' /><br /> <br /> 그런데... 모든 IoC 컨테이너들이 이런 영향이 있을까요? 그래서 마이크로소프트의 Unity 컨테이너를 위와 동일한 상황으로 만들어 보았습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > patterns & practices - Unity ; <a target='tab' href='https://github.com/unitycontainer/unity'>https://github.com/unitycontainer/unity</a> Using Unity in ASP.NET - a Simple Example ; https://docs.microsoft.com/en-us/previous-versions/msp-n-p/ff650806(v=pandp.10) Unity And Asp.Net WebForms ; <a target='tab' href='http://dotnetninja.wordpress.com/2008/05/21/unity-and-aspnet-webforms/'>http://dotnetninja.wordpress.com/2008/05/21/unity-and-aspnet-webforms/</a> ASP.NET Web Services Dependency Injection using Unity ; <a target='tab' href='http://ruijarimba.wordpress.com/2011/12/27/asp-net-web-services-dependency-injection-using-unity/'>http://ruijarimba.wordpress.com/2011/12/27/asp-net-web-services-dependency-injection-using-unity/</a> </pre> <br /> 역시 동일하게 IA, IC 인터페이스를 구현하여 테스트해 보았는데, TestIA.aspx은 526번의 호출이 있었던 반면, TestIC.aspx인 경우 4860번의 호출이 있었습니다. 10배 정도 차이가 나는군요.<br /> <br /> 물론, 호출 횟수만으로 Unity와 Ninject의 성능을 비교하는 것은 말이 안됩니다. 함수 하나가 가질 수 있는 라인 수는 다양하기 때문에, 좀 더 확실한 성능 비교를 위해서는 부하테스트가 필요했습니다.<br /> <br /> CPU 100%가 치도록 부하 테스트를 걸었는데, 결과는 다음과 같이 Ninject가 1000 TPS 처리량을 보여준 반면, Unity는 약 두배 정도의 2000 TPS를 보여줌으로써 성능이 더 좋았습니다.<br /> <br /> [그림: Ninject 부하 테스트 시 1000 TPS]<br /> <img onclick='toggle_img(this)' class='imgView' alt='ninject_profile_4.png' src='/SysWebRes/bbs/ninject_profile_4.png' /><br /> <br /> [그림: Unity 부하 테스트 시 2000 TPS]<br /> <img onclick='toggle_img(this)' class='imgView' alt='ninject_profile_5.png' src='/SysWebRes/bbs/ninject_profile_5.png' /><br /> <br /> 음... Unity가 상대적으로 Ninject에 비해 성능은 좋았지만, 그것도 결국 Injection될 인터페이스가 증가함에 따라 호출 횟수가 커짐을 볼 수 있었습니다. 어찌 보면, 성능을 위한 웹 사이트라면 DI 프레임워크를 쓰는 것은 답이 아닙니다.<br /> <br /> <hr style='width: 50%' /><br /> <br /> 그렇다면, 이쯤에서 적절한 해결책으로 어떤 것이 있을까 생각해 봐야 합니다. 고민을 하는 중에, 다행히 지인이 좋은 개선안을 하나 내놓았습니다. 바로, Ninject의 바인딩을 한 번만 할 수 있도록 Singleton Scope을 가지도록 하는 것입니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > this.Bind<IA>().To<CIA>()<span style='color: blue; font-weight: bold'>.InSingletonScope();</span> </pre> <br /> 이렇게 되면, aspx 페이지에서 선언된 DI 속성에 넣어질 인스턴스들이 한 번만 생성되고 그것들이 재사용되어져서 중첩된 DI 속성들에 대해서 type resolving하는 시간이 처음 한 번만 소요되고 이후로는 그 부하가 0으로 떨어집니다.<br /> <br /> 하지만, 부작용은 있지 않을까요?<br /> <br /> 맞습니다. singleton이다 보니 해당 클래스에 '멤버 변수'를 가지고 동작해서는 안됩니다. 왜냐하면 다수의 스레드들이 동시에 해당 클래스를 접근해서 메서드를 호출하기 때문에 멤버 변수가 언제 어떻게 바뀌거나 사용될지 장담할 수 없기 때문에 적절한 동기화 처리를 함께 고려해야 합니다. 물론, 그런 경우에는 다시 동기화로 인한 잠김 문제가 발생하기 때문에 다시 성능 문제로 이어질 수 있습니다.<br /> <br /> 그러고 보면, 예전 COM+ 개체를 만들때 Stateless하게 만드는 것이 권장되었는데요. 그것이 진리가 아닌가 생각됩니다. <span style='color: blue; font-weight: bold'>DI 컨테이너를 사용한다면, 상태를 가지지 않은 Singleton 개체 운영이 답</span>이라는 것이지요. ^^ <br /> <br /> 다행이라면, 해당 고객사는 DI되는 모든 클래스들이 (완전히는 아니고) 대부분 멤버 변수가 하나도 없는 구조였습니다.<br /> <br /> 첨부된 파일은 IA, IC 인터페이스 예제로 <a target='tab' href='http://www.sysnet.pe.kr/bbs/DownloadAttachment.aspx?fid=730&boardid=331301885'>Ninject</a> 와 <a target='tab' href='http://www.sysnet.pe.kr/bbs/DownloadAttachment.aspx?fid=731&boardid=331301885'>Unity</a>로 구현한 예입니다. (위에서 부하테스트에 사용된 바로 그 예제입니다.)<br /> <br /> 기타, 다음의 글이 약간의 연관성이 있겠군요. ^^<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > MEF가 적용된 ASP.NET 웹 사이트를 제니퍼 닷넷으로 모니터링 해본 결과! ; <a target='tab' href='http://www.sysnet.pe.kr/2/0/1203'>http://www.sysnet.pe.kr/2/0/1203</a> MEF를 ASP.NET에 성능 손실 없이 적용하려면? ; <a target='tab' href='http://www.sysnet.pe.kr/2/0/1204'>http://www.sysnet.pe.kr/2/0/1204</a> </pre> </p><br /> <br /><hr /><span style='color: Maroon'>[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]</span> </div>
첨부파일
스팸 방지용 인증 번호
1089
(왼쪽의 숫자를 입력해야 합니다.)