[ 출처 : http://www.androider.co.kr/apps/board/view.do?tablecode=1279955671808&seqno=14 ]
S*통신사 프로젝트를 수행할 무렵, 오픈을 앞두고 성능테스트를 한적이 있었다.
로드런너로 주요 로직들의 퍼포먼스를 측정하는 단계였는데, CPU나 메모리 과부하가 심해서 계속 성능 테스트에 통과하지 못하는 상황이 발생하였다.
AS-IS 시스템이 Struts 1.x 에 iBatis 버전으로 작성 되어 있었고 TO-BE 시스템은 로직변경과 개발 인원이 많아진 관계로 단순 Controller 에서 비즈니스 로직을 녹이고, VO 없이 Map을 이용한 Direct Call 방식으로는 개발이 좀 힘들듯 하여 Vo를 두고 Struts 를 확장하여 새롭게 프레임웍을 구성하였다.
새 프레임웍은 Controller, Service, DAO로 나누고 중간에 Interface로 Loose 하게 엮어놓았고 ClassLoader를 이용하여 Interface간 결합 방식으로 Spring에서 쓰이는 구조를 가져간 것 이외에는 성능 테스트를 통과하지 못할 이유가 없었다고 판단했기 때문에 성능진단팀에게 프레임웍상의 문제가 아니라는 것을 증명할 필요가 있었다.
성능진단 팀에서는 프레임웍의 문제일것 같다고 주장하고, 나는 내가 잡아놓은 프레임웍 아키텍쳐엔 문제가 없다고 생각했기 때문에 내쪽에서 그 부분을 증명하기 위한 근거를 제시해줘야 했었다.
(결과적으로는 성능 진단시의 Connection 수치, Request, Session, Transation 수치 조정이 잘못되었기에 발생한것으로 판별되었다.)
어쨋든, 새 프레임웍 이전에 비슷한 방식의 속도측정을 하기 위해 동일한 JNDI 를 이용한 JSP, Servlet, Struts (JDBC)와 Struts + iBatis 방식으로 수행 대상 Query 를 적용하여 테스트를 하게 되었다.
그 때 당시의 결과값은 없기때문에 로컬에 Tomcat 6.0으로 다시 테스트 해본 결과를 알아보자.
환경 : JDK5.0
DB : MySQL 5.x (오라클도 상관없다)
Struts : 1.x
로컬에서 테스트를 하였고, 정확한 측정을 위해 하나의 어플리케이션을 수행 한 후 PC를 리부팅 한후 다시 두번째 어플리케이션을 수행하였다.
대상은 아래와 같다. 동일한 테이블에 동일한 쿼리를 수행하여 수행시간 측정 및 화면 로드 시간을 측정하였다.
화면 로딩 계산은 Javascript 프로파일링(time.js - http://remysharp.com/2007/04/20/performance-profiling-javascript 참조)을 사용하였고 내부 쿼리 수행은 밀리세컨을 구해 수행 시간을 계산하였다.
시간을 구하는 위치는 각각의 소스상에 동일하게 구현되어있다.
1. 순수 JSP 로 JNDI 룩업 후 십만건 데이터 로드
2. Servlet 방식으로 코딩된 십만건 로드
3. Struts 에서 JDBC 코딩
4. Struts + iBatis 를 이용한 데이터 로드
와 같은 방식으로 속도를 측정하였고 이번 주제에서는 JSP와 Servlet간의 비교를 해본다.
먼저 JSP를 살펴보자.
속도 측정하는 구간은 1,2,3,4 모두 동일한 소스레벨에서 측정하며, 다 측정 된 후 100000만건이 html 로 그려진 후 전체 로드 시간을 출력해본다.
<%@ page language="java" contentType="text/html;charset=euc-kr" %>
<%@ page import="javax.naming.*,javax.sql.*,java.util.*,java.net.*,java.sql.*"%>
<script src="../js/time.js" type="text/javascript"></script>
<%! public static int cnt = 1; %>
<%
// 아래는 속도 측정을 위한 변수이다.
java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
java.text.SimpleDateFormat sdf1 = new java.text.SimpleDateFormat("mm:ss.SSS");
long t1;
long t2;
long t3;
java.util.Date logT1;
java.util.Date logT2;
java.util.Date logT3;
String sT1;
String sT2;
String sT3;
t1 = System.currentTimeMillis();
logT1 = new java.util.Date(t1);
sT1 = sdf.format(logT1);
System.out.println("[******************** JSP "+cnt++ +" 회차 수행 ********************]");
System.out.println("[1:PAGE LOAD]=[" + sT1 + "]");
%>
<html>
<head>
<title>DB Test</title>
</head>
<body>
****
<br>
<h2>JSP DBConnection 입니다. </h2>
<br><br><br>
<table border="1" width="600">
<%
String rowseq = "";
String title = "";
String tab = "";
String tabcd = "";
int i = 0;
Context ctx = null;
DataSource ds = null;
Connection conn = null;
PreparedStatement stmt = null;
ResultSet rs = null;
try {
ctx = new InitialContext();
ds = (DataSource) ctx.lookup("java:comp/env/jdbc/bmtDS");
conn = ds.getConnection();
if (conn != null) {
t2 = System.currentTimeMillis();
logT2 = new java.util.Date(t2);
sT2 = sdf.format(logT2);
System.out.println("[2:Connection Time ]=[" + sT2 + "]");
StringBuffer sbSql = new StringBuffer();
sbSql.append("SELECT rowseq, tab, tabcd, title FROM bmt_100000 ");
stmt = conn.prepareStatement(sbSql.toString());
rs = stmt.executeQuery();
while (rs.next()) {
rowseq = rs.getString(1);
tab = rs.getString(2);
tabcd = rs.getString(3);
title = rs.getString(4);
i = i + 1;
%>
<tr>
<td><%=i%></td>
<td><%=rowseq%></td>
<td><%=tab%></td>
<td><%=tabcd%></td>
</tr>
<%
}
rs.close();
stmt.close();
//conn.close();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (rs != null) {
rs.close();
}
if (stmt != null) {
stmt.close();
}
if (conn != null) {
conn.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
t3 = System.currentTimeMillis();
logT3 = new java.util.Date(t3);
sT3 = sdf.format(logT3);
System.out.println("[3:Process 종료]=[" + sT3 + "]");
%>
</table>
</body>
</html>
<br><br><br>
<h2>
<%
long elapsedT = t3 - t1;
java.util.Date elapsedDate = new java.util.Date(elapsedT);
String elapsedS = sdf1.format(elapsedDate);
System.out.println("[4:Time -Start : " + sT1 + " -End : " + sT3 + "]\n[처리시간 : " + elapsedS + "]\n\n\n");
out.println("elapsed time : " + elapsedS);
%>
</h2>
<script type="text/javascript">
time.start('page load');
window.onload = function() {
time.stop('page load');
time.report();
}
</script>
로컬에 BMTWeb 이라는 Project를 세팅한 후 /test/sampleDS.jsp 라는 이름으로 저장하였다.
이클립스에서 Tomcat 을 추가한 후 Server 등록을 하였고, Server.xml 에는 아래와 같이 DataSource를 선언하였다.
<Resource name="jdbc/bmtDS" auth="Container"
type="javax.sql.DataSource"
driverClassName="com.mysql.jdbc.Driver"
url="jdbc:mysql://localhost:3306/DB명 ?characterEncoding=euckr"
username="userId "
password="password " />
GlobalNamingResources를 이용하기 위해서 META-INF에 context.xml 을 정의하였다.
<?xml version="1.0" encoding="UTF-8"?>
<Context>
<!-- Default set of monitored resources -->
<WatchedResource>WEB-INF/web.xml</WatchedResource>
<Resource name="jdbc/bmtDS"
auth="Container"
type="javax.sql.DataSource"
driverClassName="com.mysql.jdbc.Driver"
loginTimeout="10"
maxWait="5000"
username="userId "
password="password "
testOnBorrow="true"
url="jdbc:mysql://localhost:3306/DB명 ?characterEncoding=euckr">
</Resource>
</Context>
web.xml은 특별히 수정할 부분이 없고, 브라우저 상에서 http://localhost:8080/BMTWeb/test/sampleDS.jsp 를 호출한후 시간을 측정하였다.
최초 1번을 제외하고 총 10번의 수행시간을 측정해보았다.
처리시간은 최초 페이지 호출시간과 Connection 완료 후 Process가 종료된 시간을 뺀 것이다.
로딩시간은 브라우저상에서 데이터가 다 출력 된 이후 첫 시작부터 브라우저 출력 시간을 측정한것이다. 즉, 10만건의 데이터가 브라우저상에 다 출력된 이후의 시간이다.
데이터가 10만건이나 되다보니 (사실 실무에서 화면에 10만건 뿌리는 상황은 거의 없다) 브라우저에 도달하기 이전에 데이터 처리 시간도 1초단위나 되고 로딩 시간 또한 11초 이상부터 14초대까지 다양하게 나왔다.
커넥션이 1 인 경우를 감안하면 이정도 압박이면 메모리나 CPU가 상당히 소모될 것이라 예상할 수 있다.
좀 더 정확하게 측정하려면 메모리 변화, CPU 변화 및 concurrent user (동시 요청 처리) 의 증가로 따져보아야 확실히 차이를 볼 수 있지만 그 부분은 전문적인 툴을 이용해야 가능한 부분이기에 일단 속도 측정만을 수행해본다.
다음은 Servlet 으로 위의 처리를 했을 경우를 측정해보자.
JSP를 작성한 프로젝트에 web.xml 을 열고 아래와 같이 수정해본다.
<servlet>
<servlet-name>DbConnectionServlet</servlet-name>
<servlet-class>com.bmt.dbtest.DbTestServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>DbConnectionServlet</servlet-name>
<url-pattern>/servlet/DbConnectionServlet</url-pattern>
</servlet-mapping>
어플리케이션 하위에 /servlet/DbConnectionServlet 로 서블릿 요청이 오면 com.bmt.dbtest.DbTestSevlet이 수행되도록 매칭하였고 실제 수행 소스는 아래와 같다. (시간 측정 소스 부분은 위의 JSP와 같은 영역에서 수행한다..소스가 너무 길어지므로 생략한다)
public void doGet(HttpServletRequest request, HttpServletResponse response) {
java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
java.text.SimpleDateFormat sdf1 = new java.text.SimpleDateFormat("mm:ss.SSS");
long t1;
long t2;
long t3;
java.util.Date logT1;
java.util.Date logT2;
java.util.Date logT3;
String sT1;
String sT2;
String sT3;
int i = 0;
t1 = System.currentTimeMillis();
logT1 = new java.util.Date(t1);
sT1 = sdf.format(logT1);
System.out.println("[******************** Servlet "+cnt++ +" 회차 수행 ********************]");
System.out.println("[1:PAGE LOAD]=[" + sT1 + "]");
Context ctx = null;
DataSource ds = null;
Connection conn = null;
PreparedStatement stmt = null;
ResultSet rs = null;
try {
ctx = new InitialContext();
ds = (DataSource)ctx.lookup("java:comp/env/jdbc/bmtDS");
conn = ds.getConnection();
if (conn != null) {
t2 = System.currentTimeMillis();
logT2 = new java.util.Date(t2);
sT2 = sdf.format(logT2);
System.out.println("[2:Connection Time ]=[" + sT2 + "]");
StringBuffer sbSql = new StringBuffer();
sbSql.append("SELECT rowseq, tab, tabcd, title FROM bmt_100000 ");
stmt = conn.prepareStatement(sbSql.toString());
rs = stmt.executeQuery();
response.setContentType("text/html;charset=euc-kr");
out = response.getWriter();
out.println("<html>");
out.println("<script src='../js/time.js' type='text/javascript'></script>");
out.println("<body>");
out.println(" ****<br>");
out.println("<h2>Servlet JDBC 입니다.</h2>");
out.println("<br><br><br><table border=1 width=600>");
while(rs.next()){
i = i +1;
out.println("<tr>");
out.println(" <td>" + i + "</td>");
out.println(" <td>" + rs.getString(1) + "</td>");
out.println(" <td>" + rs.getString(2) + "</td>");
out.println(" <td>" + rs.getString(3) + "</td>");
out.println("</tr>");
}
rs.close();
stmt.close();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if( rs != null ) {
rs.close();
}
if( stmt!=null ) {
stmt.close();
}
if( conn!=null ) {
conn.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
t3 = System.currentTimeMillis();
logT3 = new java.util.Date(t3);
sT3 = sdf.format(logT3);
System.out.println("[3:Process 종료]=[" + sT3 + "]");
out.println(" </tr>");
out.println(" </table>");
out.println("</body>");
out.println("</html>");
out.println("<br><br><br>");
long elapsedT = t3 - t1;
java.util.Date elapsedDate = new java.util.Date(elapsedT);
String elapsedS = sdf1.format(elapsedDate);
System.out.println("[4:Time -Start : " + sT1 + " -End : " + sT3 + "]\n[처리시간 : " + elapsedS + "]\n\n\n");
out.println("elapsed time : " + elapsedS);
out.println("<h2>");
out.println("elapsed time : " + elapsedS);
out.println("</h2>");
out.println("<script type='text/javascript'>");
out.println("time.start('page load');");
out.println("window.onload = function() { ");
out.println("time.stop('page load'); ");
out.println("time.report(); ");
out.println("}");
out.println("</script>");
out.close();
풀소스는 첨부 파일을 참조하기 바란다.
아래는 서블릿에 접근하여 최초 1번 실행 이외에 시간을 측정해 정리하였다..
http://localhost:8080/BMTWeb/servlet/DbConnectionServlet
위의 JSP 와 비교해보면 큰 차이가 없음을 알수있다.
물론 미묘하게 차이가 있지만 1초 단위의 차이는 없을 뿐더러, 오히려 브라우저의 로딩 시간은 서블릿이 안정적인(큰 변화없이) 것을 볼 수 있다.
첨부한 소스에 정확한 횟차별 수행 시간과 소스가 있으므로 해당 소스를 다운받아 이클립스로 프로젝트를 생성하여 띄워보자.
기회가 된다면 다른 전문적인 툴을 이용하여 유저를 늘려가며 테스트를 해보고 결과를 올리도록 하겠다.
일단 JSP, Servlet, Struts, iBatis 10만건 데이터 로드 시간 측정 1탄인 JSP와 Servlet 간의 속도 비교는 여기까지 하고 2탄에서 Struts 기반, Struts + iBatis 기반과의 비교를 통해 어떠한 차이가 있는지 알아보도록 하자.