본문 바로가기

codeStory

ASP 최적화에 대한 정리


코드 최적화는 신중하게 수행해야 합니다. ASP 스크립팅에서는 다른 프로그래밍 언어에서처럼 가장 많은 시간과 

리소스를 소비하는 응용 프로그램의 영역을 파악하는 것이 중요합니다. 이 정보는 위의 영역을 최적화하는데 있어서

효과적으로 사용할 수 있습니다.


여기서는 ASP 스크립트에서 성능 문제를 최소로 줄이는데 도움이 되는 몇 가지 팁을 소개합니다.

  • Global.asa에 개체를 작성하거나 각각의 ASP 스크립트에 요구에 따라 개체를 작성하여 응용 프로그램 영역에 위치시킴으로써 응용 프로그램 영역의 개체와 데이터를 캐시하십시오.
  • IIS 5.0에서는 기본값으로 설정되어 있는 ASP 버퍼링에 의해 Response.Write 호출의 출력을 결합하십시오. 시간이 많이 걸리는 작업을 수행하는 동안 버퍼링된 출력을 사용하는 응용 프로그램의 인지 성능을 개선하려면 응용 프로그램이 주기적으로 Reponse.Flush를 사용하여 사용자와의 접촉을 유지 관리해야 합니다.

    웹 응용 프로그램에 ASP 버퍼링을 사용 불가능으로 설정한 경우에는 개별 출력 문자열을 하나의 더 큰 문자열로 결합하여 Response.Write에 대한 호출 수를 줄임으로써 성능을 향상시킬 수 있습니다. 그러나 이를 달성하기 위해 확장 문자열 조작을 수행하면 성능은 개선되는 반면 문자열을 처리하는 시간은 더 오래 걸려 그 효과가 상쇄됩니다.

  • 응용 프로그램 영역이나 세션 영역에서 개체의 인스턴스를 만들 때 Server.CreateObject 대신 <object> 태그를 사용하십시오. IIS는 개체가 실제로 사용될 때까지 <object> 태그로 지정된 개체의 인스턴스 작성을 지연시키기 때문입니다. <object> 태그를 사용하면 스크립트가 해당 개체를 사용하기 전에는 응용 프로그램이 개체의 인스턴스를 만들지 않습니다. 반대로 Server.CreateObject를 사용하면 개체가 스크립트에서 사용되는지 여부와는 상관 없이 개체의 인스턴스가 즉시 작성됩니다.
  • 지역 변수를 사용하고 공용 변수를 가급적 사용하지 마십시오. 지역 변수 값에 액세스하기 위해 이름 공간 전체를 찾을 필요가 없기 때문에 ASP 스크립트 엔진은 공용 변수보다 지역 변수에 더 빨리 액세스할 수 있습니다.
  • 가능하면 HTTP 라운드 트립의 필요성을 최소화하기 위해 사용자 입력에 대해 클라이언트쪽 유효성 검사를 사용하십시오. 모든 기능이 갖추어져 있는 강력한 브라우저를 사용하면 더 중요한 작업에 사용하기 위해 서버쪽 리소스를 남겨둡니다. 응용 프로그램에 따라 데이터 손상을 방지하기 위해 서버에서 무결성 검사를 일부 수행해야 합니다.
  • 값을 두 번 이상 참조하는 경우에는 값 집합에서 개별 값을 지역 변수로 복사하십시오. 이렇게 하면 ASP는 각 참조나 모든 참조에 대해 조회 작업을 하지 않아도 됩니다.
  • 가능하면 전체 응용 프로그램의 세션 상태를 해제하십시오. 응용 프로그램에서 IIS 세션을 사용하지 않아도 되는 경우에는 인터넷 정보 서비스 스냅인을 사용하여 전체 응용 프로그램의 세션 상태를 사용 불가능으로 설정해야 합니다. IIS의 세션은 메모리에 남게 되고 세션에 할당된 메모리는 세션이 종료되거나 시간 초과될 때까지는 계속 사용됩니다. 동시에 많은 사용자가 응용 프로그램을 사용할 경우에는 서버 자원이 고갈되어 성능에 영향을 줄 수 있습니다.응용 프로그램의 일부에 세션 상태가 필요 없으면 @ENABLESESSIONSTATE 지시어를 사용하여 해당 페이지에 대한 세션 상태를 사용 불가능으로 설정해야 합니다.

    페이지에

    요소가 포함되어 있는 경우에는 가능한 세션 상태를 해제하는 것이 좋습니다. Internet Explorer를 포함하여 일부 브라우저는 별도의 스레드를 사용하여 프레임셋의 각 프레임을 처리합니다. 프레임셋 페이지에 대해 세션 상태를 사용하면, IIS는 독립된 요청을 처리하는 스레드를 강제로 직렬화하기 때문에 이 병렬 스레드를 사용하여 클라이언트쪽 성능에서 얻는 이점이 사라집니다.


  • 세션 상태에 의존하고 있는 경우에는 Session 개체와 세션 상태에 대량의 데이터를 두지 마십시오. IIS의 세션이 지속되면 세션에 할당된 메모리는 세션이 종료되거나 시간 초과될 때까지 계속 할당되어 있으므로 여러 사용자가 동시에 응용 프로그램을 사용하고 있으면 서버 자원이 고갈되어 성능에 영향을 줍니다.
  • 빈 Session_OnStart 또는 Session_OnEnd를 제공하지 마십시오.
  • IIS와 ASP 구성을 변경할 경우 그 영향을 주의깊게 살펴 보십시오. 자세한 내용은 IIS 구성 최적화를 참고하십시오.
  • ASP 페이지를 응용 프로그램의 일부로 실행하고 있는 경우에는 응용 프로그램을 응용 프로그램 디버깅을 위한 독립 프로세스로 지정하십시오. IIS 4.0에 도입된 프로세스 격리는 유용한 기능입니다. 그러나 프로세스 격리를 지원하기 위해 필요한 교차 프로세스 마샬링은 ASP 처리에 어느 정도의 오버헤드를 초래할 수 있습니다. 간단한 ASP 페이지에서는 오버헤드의 차이가 가장 중요하게 고려되지만 더 복잡한 페이지에서는 큰 문제가 되지 않습니다. 그러나 성능과 확장성을 최대화하려면 응용 프로그램이 충분히 디버그되고 IIS와의 종속 프로세스를 실행할 수 있을만큼 충분히 안정될 때까지 응용 프로그램을 독립 프로세스로 실행하는 방안을 고려해야 합니다.
  • 가급적 ReDimming 배열을 사용하지 마십시오. 배열이 처음 초기화될 때 전체 배열 크기를 할당하는 것이 더 효율적입니다.

위는 태오님의 홈페이지에서 발췌한 글입니다. 
ASP와 ASP.net 등에 대한 정보가 많은 사이트입니다. 


DB 관련 최적화 내용
1. cachesize를 지정한다.

ASP의 주요임무는 DB에서 쿼리셋을 가져와서 렌더링 하는 것이다. 
그 구조를 자세히 생각해보자(커서가 DB측에 있는 일반적인 경우)

SQLserver(또는 기타DB) → IIS의 asp프로세스 → ADO.recordset 쿼리셋을 DB와 synk 
→ movenext때마다 DB서버와 연동 → 매결과를 asp프로세스 메모리에 적재 
→ 최종결과를 HTML로 렌더링해서 IIS에 전달 → 클라이언트에게 flush

대략 이렇다. 단절된 레코드셋을 사용하거나 getRows로 배열로 환원하지 않는다면

'movenext때마다 DB서버와 연동' 

이란 엄청난 일이 매 페이지마다 일어난다(커넥션은 풀링하지만)
그렇다고 맨날 배열로 참조하기도 싫고 단절된 레코드셋을 만들기도 싫다면 어떻게 해야할까?

'ADO.recordset 쿼리셋을 DB와 synk' 이 부분을 'ADO.recordset 쿼리셋 전체를 웹서버메모리에 적재'

이렇게 해주면 'movenext때마다 DB서버와 연동' 이란 작업은 'movenext때마다 메모리참조'로 바뀌게 되어 비
약적인 성능향상이 이루어진다. 하지만 얻는게 있으면 잃는게 있는법이다. 속도는 향상되나 그만큼의 캐쉬메
모리가 IIS서버에게 필요하다. 
따라서 아무 쿼리셋이나 전부 메모리에 적재하는 것은 어리석다. 그렇다고 특정 byte만큼 적재하다간 레코드
의 일부만 적재되고 짤리는 일이 있어서 말이 안된다.
가장 현명한 캐쉬방법은 '적당한 레코드를 캐쉬에 적재한다' 로 생각해볼 수 있다.
즉 100개의 레코드를 부른다면 앞에 30개정도는 캐쉬에서 처리하고 뒤에 70개만 디비와 synk하는 방법도 있다

ADO.recordset은 이러한 유연한 사고를 지원한다.

recordset.casheSize=캐쉬할 레코드수

이러한 형식으로 사용할 수 있으며 기본값은 1이다.
즉 'select count(*) from board'와 같은 레코드가 1개만 반환되는 경우는 이미 캐쉬에 잡힌다는 뜻이다.
하지만 게시판에 흔히 쓰이는 'select top 10 * from board'와 같은 쿼리는 캐쉬를 따로 잡아줘야한다.
cacheSize를 사용할때 주의할 점은 반드시 레코드셋을 오픈하기 전에 설정해야만 의미가 있다는 것이다(당
연하게도!)

strSQL="select top 30 * from board"
rs.cacheSize=30
rs.open strSQL,conn,0,1,1

이런 식으로 사용되어야한다. 쿼리결과가 정확하게 몇개의 레코드를 반환할지 모를 경우에도 유용하다.
대충 생각하기에 100개 안쪽이라면

rs.cacheSize=100

이렇게 설정하면 100개만큼 메모리를 낭비하는가? 답은 아니다이다. 
생성된 레코드수만큼만 캐쉬하는 것이다. 
마지막으로 cacheSize를 사용할때 가장 유의할 점은 캐쉬될 용량이다.
레코드의 수라곤해도 그 레코드하나의 용량에 따라 캐쉬에 할당될 메모리의 양은 천차만별이다.

select userid,count(*) from board group by userid

이런 쿼리는 사실 cacheSize=500을 해도 그닥 두려울게 없다. 캐쉬될 용량은 다음과 같이 계산해볼 수 있다.

1record 용량 userid(12byte)+count(4byte)=16byte
캐쉬전체 용량 16byte * 500 = 8000byte = 8kbyte

잘해봐야 8k가 두렵지는 않다. 하지만 아래 쿼리를 보자.

select rowid,subject,contents,regdate,userid,hit from board

이것은 게시판을 읽어드릴때 일반적인 쿼리다. 100개만 캐쉬한다면 용량이 어떻게 될까.

1record 용량 rowid(4)+subject(100)+contents(평균1000)+regdate(4)+userid(12byte)+hit(2) = 1122byte
캐쉬전체 용량 1122byte * 100 = 112,200byte ≒ 112kbyte

이렇게 되면 얘기가 다르다. 10명의 동시접속자만 붙어도 1메가씩 떨어져나간다. 무서운 것이다.
따라서 캐쉬사이즈는 항상 그 용량을 고려해서 설정하는 습관을 들이도록 하자.
------------------------------------------------------------------------------
2. ADO페이징을 사용하지 않는다.

이 부분은 길게 설명하자면 한없이 길어지지만 간단히 설명하자면 
.PageSize, .AbsolutePage, .PageCount 시리즈를 사용하지 말라는 것이다.
그럼 어떻게? Mysql처럼 limit를 응용해서 두개의 top를 이용하는 것이다.
원리는 아래와 같다.
1. 전체 레코드 수를 얻어온다. ex) RowCount=rs(0) - select count(*) from board
2. 다음의 변수를 셋팅한다.
PageSize - 한페이지 몇개의 로우가 보일지(직접할당 'PageSize=13')
AbsolutePage - 현재 페이지(직접할당 'AbsolutePage=13')
PageCount - 전체 페이지수(계산식 'PageCount = CInt((RowCount-1)/PageSize)+1' )
3. 위의 변수를 이용해 쿼리를 구성한다.
쿼리예) board테이블의 keyField가 rowid인 경우
"select top " & PageSize & " * from board " &_
"where rowid not in ( select top " & ((AbsolutePage-1)*PageSize) & " rowid from board)"
왜 이용하지 말라고 하는가하면 레코드셋의 페이징은 전체 레코드에 대해서 처리하기 때문에 용량이 큰 테이
블에서는 너무 큰 부하를 주기 때문에 인덱스를 이쁘게 잡은 테이블에서 위의 조건으로 indexSeek을 하는 것
과는 너무나 성능차이가 나기 때문이다.
------------------------------------------------------------------------------
3. ado객체의 옵션까지 상세히 설정한다.

레코드셋이나 커넥션객체를 다룰때 뒤에 인자를 세밀하게 조정하라는 얘기는 대부분의 책에 씌여져있으니 
자세히 다루지는 않겠다.
샘플로 쿼리로부터 레코드셋을 오픈하는 경우와 쿼리로 커넥션객체를 작업하는 샘플만 보자.

rs.open strSQL,conn,0,1,1
conn.execute strSQL,,1+&H80

(필자는 먼가 그 상수값을 외우는게 오히려 귀찮아서 걍 원래 변수값을 외워서 쓰고 있다..=.=;)
레코드셋 샘플은 전진전용커서,읽기전용락,커맨드는 일반 텍스트 라는 뜻의 옵션이다(순서대로)
execute 샘플은 커맨드는 일반 텍스트이고 + 레코드는 반환하지 말아라 라는 뜻의 옵션이다.
------------------------------------------------------------------------------
4. IsClientConnected를 사용한다.

Response.IsClientConnected 이란 메써드를 이용하면 asp를 호출한 클라이언트가 여전히 그 창을 닫지 않고 
서버의 응답을 기다리고 있는지 확인할 수 있다.
근데 대체 이걸 왜 사용할까? 간단히 예를 들면 복잡한 DB작업이나, 결제모듈등의 작업을 하는 경우 작업의 
요청자가 지루함을 이기지 못하고 페이지를 닫아버릴 때가 있다.
이런 경우 여러가지 문제가 야기되기 때문에 긴시간을 처리하는 asp는 마지막에 위 메써드를 이용해서 여전
히 클라이언트가 대기중인지 아닌지를 판별하여 대기중이라면 처리를 완료하고 아니면 롤백시키는 식으로 
구조를 짤 수 있다.(정말 시간이 긴 처리라면 중간중간에도 삽입해서 계속 확인해갈 수도 있고^^)
------------------------------------------------------------------------------
5. getRows와 getString을 활용한다.

특별하게 레코드셋으로 필터링하거나 검색할것이 아니라 단순히 루프를 돌릴것이라면 구지 무거운 레코드셋
으로 할 필요도 없고 자원도 빨리 반환할수록 좋다.
일반적으로 getRows를 사용하기 전의 루프문을 보면 다음과 같다.

rs.open strSQL,0,1,1
if not rs.EOF then
do until rs.EOF
response.write rs(0)&"-"&rs(1)
rs.movenext
loop
end if
rs.close
set rs=nothing

이것을 getRows로 바꾸면 아래와 같다.

rs.open strSQL,0,1,1
if not rs.EOF then arrTemp=rs.getrows
rs.close
set rs=nothing
if isArray(arrTemp) then
for i=0 to Ubound(arrTemp,2)
response.write arrTemp(0,i)&"-"&arrTemp(1,i)
next
end if

레코드셋을 반환하는 시점도 훨씬 앞이고 게다가 일괄적으로 하나의 ASP블럭에서 여러가지 테이블 쿼리를 
처리하고 디자인 렌더링 부분에서는 배열루프로 처리할 수도 있다.
(물론 훨씬 빠르다)
getString도 많이 사용하는데 주로 단일 필드를 검색한 결과를 문자열로 만들때 편한다.
일반적인 사용방법은 아래와 같다.

SQLstring=rs.getString(2,,",","
","null")

구문이 좀 복잡해보이는가? 2는 adClipString라는 상수대신에 사용한 값인데 거의 대부분의 경우에 저 값이 들
어가야하고, 그 다음 비워둔곳은 레코드 몇개만 치환할까인데 비워두면 다 받아준다. 
그 다음은 필드와 레코드의 구분자를 무엇으로할까이고 마지막은 널인 값은 무엇으로 치환하나에 대한 옵션
이다.
------------------------------------------------------------------------------
6. 연결문자열을 최적화한다.

이 글은 아래에도 있는 내용이라 생략하겠다.(MS SQL연결문자열에 Net-Library와 디비서버지정 참조)
------------------------------------------------------------------------------
7. 통계테이블 활용

디비를 설계할때는 항상 비용개념을 생각해봐야한다. 즉 insert나 update가 많은가 아니면 select가 많을 것인
가에 대한 숙제다.
일반적으로 게시판은 select가 압도적으로 많기 때문에 insert시에 비용을 더 책정해주면 select가 아름다워진
다. 즉 count(*), top, max 등을 사용할 부분을 간단히 트리거를 걸어두어 통계만 수집하는 테이블을 따로 작성
해두면 select시에는 단순하게 indexSeek으로 조회할 수 있기 때문에 훨씬 비용이 절감된다.
너무 뜬구름잡는 얘기같으니 트리거의 샘플을 하나보자.

CREATE TRIGGER insertBoard ON [dbo].[board] 
After INSERT,UPDATE
AS
declare @boardid int, @boardrowid int,@boardCNT int,@regdate datetime,@subject varchar(100),@cnt tinyint
select @boardid=boardid from inserted
select top 1 @boardrowid=rowid,@regdate=regdate,@subject=subject from board where boardid=@boardid 
and active=1
select @boardCNT=count(*) from board where boardid=@boardid and active=1
select @cnt=count(*) from boardstat where boardid=@boardid
if @cnt>0
update boardStat set boardrowid=@boardrowid,regdate=@regdate,subject=@subject,boardCNT=@boardCNT 
where boardid=@boardid
else
insert into boardStat(boardid,boardrowid,subject,regdate,boardCNT)values
(@boardid,@boardrowid,@subject,@regdate,@boardCNT)

위의 샘플은 BOARDID란 값으로 하나의 BOARD테이블에서 여러개의 게시판을 처리해주는 테이블을 가정하
고 짠 트리거인데, 간단히 설명하자면 insert가 되면 inserted에서 값을 확인해서 boardStat란 테이블에 해당레
코드가 있으면 최신값으로 업데이트를 아니면 insert를 하라는 것이다.
위의 트리거가 있으면

select count(*) from board where boardid=19 and active=1

이런 쿼리는

select cnt from boardStat where boardid=19

이렇게 바뀌게 된다.
boardid에 인덱스가 이쁘게 잡혀있다면 성능차이는 설명할 필요가 없을것이다.
------------------------------------------------------------------------------
8. 단절된 레코드셋을 사용한다.

웹서버가 여러대인 경우 단절된 레코드 셋을 사용하면 디비서버의 부하를 웹서버가 가져가기 때문에 상당한 
성능향상이 일어난다. (하지만 앞서 말했던 getRows보다는 아니다) 간단히 소스의 변화만 보겠다.

call DB_Open()
call RS_Open()
rs.open sql,conn,0,1,1

위의 일반적인 소스를 아래와 같이 바꾼다.

call DB_Open()
call RS_Open()
rs.CursorLocation = adUseClient
rs.Open sql,conn,0,1,1
rs.ActiveConnection = Nothing
------------------------------------------------------------------------------
9. 간이트랜젝션 사용하기

여러가지 상황에서 트랜젝션이 필요한 경우가 많다.
특히 insert나 update등은 한번에 여러개의 테이블에 안전하게 데이터가 안착해야하는 케이스가 많은데, 
이런 경우 트랜젝션을 일일히 선언하거나, SP를 짠다는 것은 여간 귀찮은게 아니다.
가장 간단한 형태의 트랜젝션은 바로 쿼리문자열에 세미콜론으로 구분하여 한꺼번에 execute시키는 것이다.
아래와같은 형태가 된다.

strSQL="insert into a (a)values('바보');insert into b (b)values('너도')"
adoCn.execute strSQL,,1+&H80

이러한 구문은 MSsql2000에서 지원되고 저 두개의 쿼리를 하나의 트랜젝션으로 취급해준다.
------------------------------------------------------------------------------
10. ADOR객체를 사용하기

ADODB 객체에는 오만가지 객체가 포함되어있다(Connect,Command,Recordset,Record,Stream)
단지 레코드셋이 필요한데 저런게 다 필요하단말인가(게다가 단일 연결을 한번만 하는 페이지의 경우는 디비
연결시 연결문자열로도 충분하다)
이런 경우 경량화객체인 ADOR객체를 사용하는 것이 좋다(대부분 언제나 좋다)

set rsOBJ = server.createobject("ADOR.Recordset")
dbconnSTR = "Provider=SQLOLEDB; Data Source=서버이름;..."
sqlSTR = "select * from test"
rsOBJ.open sqlSTR, dbconnSTR, 0, 1,1
<내용출처: About님의 블로그,  http://msinterdev.org/blog/ >

ASP 관련 내용 뿐만 아니라 Java, C++ 에 대한 내용이 많은 사이트 입니다. 
최적화에 대한 내용을 찾다가 발견한 사이트 인데요, 종종 들려 여러정보를 섭렵하고 있습니다. 

위 글의 원본 출처가 불분명하여 한 곳을 더 표기합니다.
http://cafe.daum.net/aspdotnet 이 곳은 비공개로 운영되고 있어서 확인할 방도가 없었습니다.



'codeStory' 카테고리의 다른 글

float left, right시 브라우져에 따라 겹침 문제 발생  (0) 2010.01.27