소년포비의 세계정복!!

[스크랩] [펌] 닷넷 프로그래밍 최적화 기법 본문

프로그램 세상/C#

[스크랩] [펌] 닷넷 프로그래밍 최적화 기법

소년포비 2009. 3. 10. 10:17
[프로그래밍 최적화 ④] 닷넷 프로그래밍 최적화 기법

강영욱(닷넷채널 웹 매거진 발생, 비스무리 개설 및 운영)   2007/11/07
1부 | 개발 환경의 변화와 대응하는 프로그래밍 최적화의 재발견
2부 | OPP적 개발을 위한 C++ 프로그래밍 최적화 기법
3부 | 리팩토링을 이용한 자바 성능 최적화 기법
4부 | 성능 이슈 해결을 위한 닷넷 프로그래밍 최적화 기법
5부 | ARM과 파워pc에 기반한 임베디드 프로그래밍 최적화 기법
닷넷 기술이 정식으로 런칭된지 5년 째에 접어들고 있다. 5년 만에 버전은 1.0에서 1.1로 그리고 2.0으로 발전해 왔다. 내년 상반기에는 정말 엄청난 변화와 쓸 만한 기술을 가득 담은 프레임워크 3.0도 발표 될 예정이다. 닷넷은 자바나 다른 언어들에 비해서 상대적으로 어리다. 그 탓에 닷넷 기술을 사용하는 개발자들의 경우 사소한 실수로 성능적인 이슈를 만들어 내는 경우가 많다. 4부에서는 닷넷 프로그래밍의 성능적인 이슈를 예방할 수 있는 다양한 팁들을 소개한다.

닷넷 기술이 정식으로 런칭 된지도 벌써 4년이 넘어간다. 4년 동안 닷넷 기술이 걸어온 길을 보면 정말 숨 가쁘게 달려온 것 같다. 그 동안 버전이 1.0에서 1.1로 또 1.1에서 2.0으로 바뀌었고 내년에는 버전 3.0이 발표될 예정이다. 닷넷의 개발환경 자체는 비주얼 스튜디오라는 강력한 도구에 의해서 많은 부분들이 지원된다.

그 덕에 닷넷 개발자들은 프로젝트 파일이나 솔루션 파일의 구조나 작성방법 같은 기본적인 부분을 몰라도 개발하는데 전혀 지장이 없을 정도다.

때로는 지나치게(?) 편리한 개발 환경에 의존하다 보면 개발 생산성은 확실히 보장 받을 수 있을지 몰라도 성능에 관련된 부분들을 놓치기 쉽다. 항상 이 바닥의 지론은 개발자의 고생과 소프트웨어의 성능은 비례 관계에 있다는 점이다.

닷넷 자체는 기계어로 바로 번역되어 있는 상태가 아니라 중간 상태인 IL형태로 번역되어 있다. 실행 시마다 기계어로 번역되어서 실행되는 JIT(Just In Time) 컴파일러 방식으로 최적화 되어있다. 아무래도 중간에 한 단계를 거치는 탓에 VC++등에서 개발한 네이티브 프로그램들에 비해서는 성능이 떨어지는 것도 사실이다.

하지만 일반적인 소프트웨어를 개발하는데 있어서는 이슈가 되지 않는다. 성능 이슈의 대부분은 닷넷 자체의 문제라기 보다는 닷넷을 개발하는 방식의 문제나 네트워크 혹은 데이터베이스의 병목현상 때문인 경우가 대부분이다.

그 덕에 닷넷 자체에서 성능을 위해서 튜닝해야 할 부분은 사실 그리 많지 않다. 이제 부터 몇 가지 일반적인 튜닝 포인트들에 대해 살펴보자.

  StringBuilder의 사용

많은 사람들이 알고 있는 것처럼 string 타입의 연산에는 많은 오버헤드가 발생한다. string 타입의 연산을 하면 기존의 값이 바뀌는 것이 아니라 별도의 메모리를 새로 할당하고 새로 할당한 메모리에 연산의 결과를 넣는다.

string 타입의 연산을 반복해서 사용할 경우에는 계속 메모리를 할당하고 복사하는 동작을 반복하면서 쓸데없는 부하를 일으킨다. 그래서 대부분의 경우 string 타입의 연산이 잦은 곳에는 string을 사용하지 말고 StringBuilder를 사용하라는 권장을 어디서나 쉽게 들을 수 있는 것이다.

StringBuilder는 기본적으로 충분한 버퍼를 확보하고 계속 문자열을 추가(Append)하는 방식이기 때문에 기본적으로 문자열처럼 계속해서 메모리를 할당받지 않아도 된다. 때문에 보통 긴문자열이나 SQL 쿼리를 조합하는 등의 용도로 널리 사용하고있는 방식이다.

<그림 1> 삽입 string과 StringBuilder의 메모리 할당

StringBuilder를 사용하는 방법은 아주 쉽다. 먼저 기존의 string으로 구성된 <리스트 1> 예제를 살펴보자.

 <리스트 1> 일반적인 string 연산의 예


string a="aaa";
string b="bbb";
Console.WriteLine(a +b);



<리스트 1>을 StringBuilder를 사용하는 예제로 바꾸어 보면 <리스트 2>처럼 된다.

 <리스트 2> StringBuilder를 이용한 문자열 연산의 예


System.Text.StringBuilder sb = new
System.Text.StringBuilder();
sb.Append("aaa");
sb.Append("bbb");
Console.WriteLine(sb.ToString());



StringBuilder를 사용한다고 해서 무조건 성능에 도움이 되는 것은 아니다. StringBuilder의 경우 기본 버퍼 사이즈가 16바이트 밖에 되지 않는 탓이다.

만약 16바이트가 넘게 되면 String Builder가 알아서 32바이트로 다시 버퍼를 확장한다. 만약 32바이트마저도 넘게 되면 다시 64바이트를 확보한다. String Builder는 초기에 할당된 사이즈를 넘어서게 되면 계속해서 현재 메모리의 두 배에 해당하는 메모리를 확보하게 된다.

이때 기존에 할당받은 메모리를 두 배로 늘리는 것이 아니라 두 배의 메모리를 새로 확보해서 새로 확보한 메모리로 string을 옮기는 방식으로 동작한다.

즉 할당받은 메모리가 넘치게 되면 string을 사용하는 것이나 다름없는 셈이다.이를 피하려면 System.Text.StringBuilder sb = System.Text.StringBuilder(100); 처럼 처음에 StringBuilder를 생성할 때 생성자에 충분한 버퍼 사이즈를 지정하면 된다.

  DataReader의 활용

대부분의 프로젝트에서 DataReader는 꼭 필요한 경우가 아니면 잘 사용되지 않는 객체다. 대부분의 데이터베이스 작업은DataAdapter를 이용해서 DataSet에 결과를 담는 방식으로 구성되어 있다. 다양한 기능의 활용을 위해서는 DataSet을 이용하는 게 정답이겠지만 성능이라는 문제만 놓고 보았을 때는 DataReader를 사용하는 편이 올바른 선택이 될 것이다.

 <리스트 3> DataReader의 사용 예


SqlCommand cmd = new SqlCommand("select * from
categories", conn);
SqlDataReader dr = cmd.ExecuteReader();



DataReader는 DataSet에 비해서는 기능이 많이 떨어지지만 그 대신 구조가 간단해서 간단한 스키마정보와 순수 데이터만 존재한다. 반면에 DataSet은 모든 데이터의 상태정보까지 모두 유지해야 하기 때문에 상대적으로 메모리도 훨씬 더 많이 차지하고 생각보다 상당히 무거운 객체이다.

따라서 조회 위주의 대량 데이터인 경우 DataReader로 코드를 수정하면 상당한 성능상의 이점을 얻을 수 있다.

하지만 DataReader로 성능상의 이점을 얻으려면 몇 가지 주의할 점이 있다. DataReader는 열려있는 동안 계속해서 Connection객체를 독점해 버리는 문제가 있으니, 사용할 때만 열었다가 사용이 끝나면 잽싸게 DataReader.Close()를 호출해 주는 센스가 필요하다. 또 하나의 문제는 DataReader가 웹 서비스를 통해서 전달되지 않는다.

좀 더 정확하게 얘기 하자면 DataReader는 직렬화(Serialize)가 되지 않는다. 때문에 웹 서비스를 사용하는 곳에서는 기본적으로 DataSet이나 Array을 이용해서 데이터를 전달하는 방식을 원칙으로 하고 있다.

DataReader를 직접 웹서비스로 넘기는 것은 불가능 하지만 DataReader를 DataSet으로 변환해서 넘기는 것은 가능하다.

 <리스트 4> DataReader를 DataSet으로 변환하는 함수


/// <summary>
/// DataReader 를 DataSet 으로 변환합니다.
/// </summary>
/// <param name='reader'>변환할 DataReader</param>
/// <returns>Reader 의 내용으로 채워진 DataSet.</returns>
public static DataSet
ConvertDataReaderToDataSet(IDataReader reader)
{
DataSet dataSet = new DataSet();
try
{
do
{
// 새 DataTable 객체 생성
DataTable schemaTable = reader.GetSchemaTable();
DataTable dataTable = new DataTable();
if ( schemaTable != null )
{
// 레코드가 조회가 되었을 경우
for ( int i = 0; i < schemaTable.Rows.Count; i++ )
{
DataRow dataRow = schemaTable.Rows[ i ];
// DataTable 에 넣을 고유 컬럼명을 생성한다.
string columnName = ( string )dataRow[ "ColumnName"
]; // DataTable 에 컬럼을 추가
DataColumn column = new DataColumn( columnName, (
Type )dataRow[ "DataType" ] );
dataTable.Columns.Add( column );
}
dataSet.Tables.Add( dataTable );
// 방금 정의한 DataTable 에 데이터를 채웁니다.
while ( reader.Read() )
{
DataRow dataRow = dataTable.NewRow();
for ( int i = 0; i < reader.FieldCount; i++ )
{
dataRow[ i ] = reader.GetValue( i );
}
dataTable.Rows.Add( dataRow );
}
}
else
{
// 레코드가 반환되지 않았을 경우
DataColumn column = new DataColumn("RowsAffected");
dataTable.Columns.Add(column);
dataSet.Tables.Add( dataTable );
DataRow dataRow = dataTable.NewRow();
dataRow[0] = reader.RecordsAffected;
dataTable.Rows.Add( dataRow );
}
}
while ( reader.NextResult() );
}
finally
{
reader.Close();
}
return dataSet;
}



<리스트 4>에서 소개하고 있는 함수를 이용하면 DataReader를 DataSet으로 변환할 수 있다. 물론 일일이 루프를 돌면서 반복하는 까닭에 별도의 오버헤드는 발생한다. <리스트 4>에서 사용하는 방법은 COM+ 혹은 일반 애플리케이션에서는 성능상의 여유가 충분한데 데이터베이스의 병목현상이 발생할 때 사용하면 좋은 방법이다.

만약 닷넷 프레임워크 2.0을 사용한다고 하면 더 좋은 방법이 있다. DataTable.Load() 메소드를 사용하는 방법이다. 사용하는 방법은 DataTable.Load(DataReader dr)와 같이 Data Table을 먼저 생성해서 여기에 Load()에 DataReader 객체를넘겨주기만 하면 된다.

정말 이걸로 모든 것이 다 된다. 이렇게만 하면 DataTable이 알아서 스키마를 파악해서 DataTable의 컬럼 스키마를 생성하고 DataReader의 데이터로 DataTable을 채운다.

물론 성능상의 이점도 분명한데 반복문을 열심히 도는 것에 비해서 엄청난 속도 향상이 있다. 또 다르게 보면 DataAdapter를사용하지 않아도 된다는 장점도 있다.

사실 따지고 보면 ADO.NET 1.x에서 ADO.NET 2.0으로 변경하기만 해도 대량의 데이터를 다루는데 있어서 상당한 성능상의 이점을 가질 수 있다. ADO.NET 2.0에서는 내부적으로 인덱스 엔진을 거의 다시 작성하는 대공사를 했다고 한다.


<표 1>을 보면 데이터 건수가 많아질수록 ADO.NET 2.0의 효율성이 더 잘 보이고 있는데 1만 건 정도의 데이터를 처리할 때는 큰 차이가 없다가 10만 건 부터는 거의 두 배 차이가 나는 것을 볼 수 있다. 또 100만 건을 처리할 때는 사실 비교가 안 될 정도로 엄청난 차이를 보이고 있다.

  DataTableReader

앞서 알아본 DataReader는 DataSet에 비해서 그 구조가 극히 가볍고 효율적이긴 하지만 사용상의 제약과 불편함으로 인해많이 사용되지 않고 있다.

‘DataReader처럼 가벼우면서도 DataTable만큼이나 편리한 객체는 없을까?’라는 고민 해본 독자들이 있다면 그 해답으로DataTableReader를 추천한다. ADO.NET 2.0부터는 DataTableReader 객체가 추가되어 가벼운 데이터 객체로 활용할 수있는 방법을 제공해 주고 있다.

DataTableReader는 DataTableReader dtr = DataTable.CreateDataReader();와 같이 생성해서 사용할 수 있다. DataTableReader는 DataReader와 달리 Connection 객체를 독점하지 않는 덕에 사용이 편리하고 또 DataBinding이 가능하다.

DataTableReader는 조회성 데이터를 저장해 두거나 유지할 필요가 있을 때 유용하게 사용할 수 있을 것이다.

 <리스트 5> DataTableReader의 사용예


using (SqlConnection cn = new SqlConnection(cnStr))
{
SqlCommand cmd = new SqlCommand(sqlAllCustomers, cn);
SqlDataAdapter adpt = new SqlDataAdapter(cmd);
DataTable dtCustomers = new DataTable("Customers");
adpt.Fill(dtCustomers); DataTableReader dtRdr =
ds.CreateDataReader();
dgvCustomers.DataSource = dtRdr;
}



  SqlBulkCopy의 활용

많은 양의 데이터를 한꺼번에 INSERT나 UPDATE를 해야 할 경우 데이터베이스에는 꽤 긴 시간의 트랜잭션이 발생한다.트랜잭션의 특징상 길어지면 질수록 더 많은 시간이 걸리기 때문에 트랜잭션이 사용되는 작업은 최소한으로 유지해야만 한다.

트랜잭션을 최소한으로 유지하는 방법은 첫 째, DELETE 쿼리를 먼저 사용해서 먼저 삭제를 하고 일괄적으로 INSERT 하는 것이다. 이 방법은 UPDATE 작업을 할때 UPDATE 할 대상이 많을 경우 UPDATE 쿼리를 일일이 사용하는 것 보다 효과적으로 사용할 수 있다.

둘째, UPDATE 구문에서 JOIN문을 잘 활용하면 관련 있는 데이터를 한 번에 일괄 업데이트 할 수 있다.

 <리스트 6> JOIN문을 활용한 일괄 UPDATE


UPDATE Table1
SET Column1 = Table2.Column1
FROM Table1 JOIN Table2 ON Table1.code = Table2.code
WHERE 조건



<리스트 6>은 Table1의 Column1을 Table2의 Column1의 값으로 업데이트를 하는 구문이다. Table1과 Table2를 code라는 이름의 컬럼 값으로 join해서 일괄 업데이트 한다. 이와 같이 하게 되면 루프를 돌면서 한 레코드씩 업데이트를 하는 경우보다 비약적인 성능 향상을 볼 수 있는데 필자의 경우도 9분이 넘게 걸리던 업데이트 작업이 4초로 줄어든 경우를 겪었다.

아무튼 위에서 설명한 두 가지 방법은 쿼리에서 할 수 있는 방법이고 여러분들이 작업하고 있는 환경이 ADO.NET 2.0을 사용하고 SQL Server 2005를 사용하고 있다면 또 다른 방법을 사용할 수 있다.

셋째, SqlBulkCopy의 이용
흔히 용산에서 공시디를 살 때 보면 벌크시디라고 거의 헐값에 나오는 재품들이 있다. 벌크란 대량으로 생산해서 단가를 낮춘 제품들이다.

SqlBulkCopy도 대량으로 데이터를 처리해서 처리 시간을 획기적으로 낮출 수 있는데 세 개의 컬럼이 있는 간단한 테이블을 샘플로 100만건을 입력하는 테스트를 했었는데 필자의 노트북(CPU: 1.3G 메모리:1G)에서 딱 5초 걸리는 기염을 토해냈다. 엄청난 성능에 비해서 사용하는 방법도 너무 간단하다.

 <리스트 7> SqlBulkCopy의 활용


SqlBulkCopy bcp = new SqlBulkCopy(ConnectionString);
bcp.DestinationTableName = "DumpTable"
bcp.WriteToServer(ds);



SqlBulkCopy를 사용하려면 SqlBulkCopy의 생성자에 데이터베이스 연결문자열을 넘겨주고 DestinationTableName에 대상 테이블의 이름을 설정해준다. 그 뒤에 데이터가 들어 있는 DataSet을 WriteToServer에 넘겨주면 바로 작업은 끝난다. 이때 DataSet에 있는 DataTable의 컬럼명과 실제 물리적인 Table의 컬럼명이 일치해야 한다.

DataTable binary serialization option
DataTable이 닷넷 프레임워크 1.1까지는 웹 서비스로 전달되지 못했던 것이 닷넷 프레임워크가 2.0으로 버전업되면서 웹 서비스로 전달 될 수 있도록 바뀌었다. 또 다른 변화가 웹 서비스로 전달될 때 직렬화(Serialize) 과정을 거치게 되는 것인데 기본 값으로 직렬화를 거치면서 XML로 변환되어 전달된다.

웹 서비스의 문제점 중에 하나가 직렬화 과정에서 데이터 크기가 상당 부분 증가되어서 전달에 시간이 많이 걸리는 다는 것이다.

따라서 XML대신 객체를 바로 binary형태에 제일 가깝게 넘길 수 있으면 직렬화 과정에서 발생하는 오버헤드와 데이터 크기를 줄일 수 있다. 또, ADO.NET 2.0부터는 DataSet객체에 RemotingFormat이라는 속성이 새로 추가되었다.

DataSet.RemortingFormat은 열거형으로 제공되는 Serialization Format.XML나 SerializationFormat.Binary 중 하나를 선택할 수 있다. 이중에서 기본 값은 SerialzationFormat.XML이다.

<화면 1> XML 타입으로 직렬화된 결과

<화면 1>에서 기본 값인 XML 타입으로 직렬화 되었을 경우 모든 데이터가 텍스트 방식으로 변환된 것을 볼 수 있다. 이걸Binary 타입으로 직렬화 하게 되면 <화면 2>와 같은 결과를 볼 수 있다.

<화면 2> Binary 타입으로 직렬화된 결과

Binary 타입으로 직렬화 되었을 경우에 데이터를 눈으로 확인하기는 어렵지만 성능 향상에는 도움이 될 것이다. 하지만 이 방식의 경우 닷넷 기반끼리의 통신에만 사용할 수 있다는 한계가 있다.

이 외에도 ADO.NET이 2.0으로 버전업되면서 많은 기능들이 추가 되었는데 비동기 쿼리 작업이나 SqlNotification과 같은 기능들을 적절히 잘 활용한다면 응답시간의 개선에 상당한 도움이 될 것이다.

  웹 서비스의 데이터 압축

Binary 타입으로 직렬화 시켜서 성능을 개선할 수 있다고 했지만 정말 큰 데이터의 경우는 이 역시 어렵다. 서버와 클라이언트의 성능은 비약적으로 향상된 것에 비해서 네트워크 속도는 아직 큰 향상이 없는 탓에 거의 대부분의 네트워크에서 병목현상이 발생된다.

이 병목현상을 제거하기 위해서는 웹 서비스에서 데이터를 압축하는 방법을 사용해야 한다. 즉 데이터를 보내는 측에서 압축해서 보내고 받는 측에서 이 데이터를 풀어서 해석하면 되는 것이다.

데이터를 압축하지만 기본적인 웹 서비스의 포맷은 텍스트 방식의 XML로 데이터를 넘겨야 한다. 때문에 데이터는 압축해서 base64 방식의 문자열로 리턴 하도록 되어있다.

데이터를 압축하기 위해서 흔히 사용되는 방식은 System.IO.MemoryStream과 System.IO.Compression.DeflateStream을 이용하는 것이다. 먼저 데이터가 들어 있는 DataSet이 ds 이름으로 넘어 왔다고 가정할 경우 먼저 넘어 온 DataSet을 바이너리 포맷으로 변환해서 메모리 스트림으로 전환 압축 할 수 있는 준비를 해야 한다.

 <리스트 8> DataSet을 바이너리 포멧으로 전환한 뒤 배열로 전환


ds.RemotingFormat = SerializationFormat.Binary;
BinaryFormatter bf = new BinaryFormatter();
MemoryStream ms = new MemoryStream();
bf.Serialize(ms, ds)
byte[] data1 = ms.ToArray();



일단 여기 까지 준비 되었다면 실제로 그 뒤의 내용은 아주 간단하다.

 <리스트 9> 메모리 스트림을 압축하고 배열로 리턴


//데이터 압축
System.IO.MemoryStream objStream = new MemoryStream();
System.IO.Compression.DeflateStream objZip =
new System.IO.Compression.DeflateStream(objStream,
System.IO.Compression.CompressionMode.Compress)
objZip.Write(data1,0,data1.Length);
objZip.Flush();
objZip.Close()
return objStream.ToArray();



System.IO.Compression.DefateStream의 경우 데이터를 압축하거나 혹은 그 반대로 압축을 해제할 수 있는 기능이 있는데 기본적으로 CompressionMode에 의해서 기능이 결정된다.

- CompressionMode.Compress 데이터를 압축하는데 사용되는 모드이다.
- CompressionMode.Decompress 데이터 압축을 해제하는데 사용되는 모드이다.

 <리스트 10> 웹 서비스에서 데이터 압축의 활용 예


[WebMethod]
public string GetService()
{
//데이터 압축
byte[] data = CompressDataSet(ds);
//Base64로 형변환
return Convert.ToBase64String(data, 0, data.Length);
}
public byte[] CompressDataSet(DataSet ds)
{
//1. 데이터셋 Serialize
ds.RemotingFormat = SerializationFormat.Binary;
BinaryFormatter bf = new BinaryFormatter();
MemoryStream ms = new MemoryStream();
bf.Serialize(ms, ds);
byte[] inbyt = ms.ToArray();
//2. 데이터 압축
System.IO.MemoryStream objStream = new MemoryStream();
System.IO.Compression.DeflateStream objZS =
new
System.IO.Compression.DeflateStream(objStream,
ystem.IO.Compression.CompressionMode.Compress);
objZS.Write(inbyt, 0, inbyt.Length);
objZS.Flush();
objZS.Close();
//3. 데이터 리턴
return objStream.ToArray();
}



이렇게 데이터를 압축하고 나면 그 결과는 base64로 압축되어서 전송되는데 이럴 경우 압축률은 생각 이상으로 높다. 거의 대부분의 경우 90% 이상의 압축률을 보여주고 있으며 보통 97%까지도 압축되는 것을 볼 수 있다. 100MB의 데이터를 네트워크로 넘겨야 할 것을 3MB로 줄일 수 있는 엄청난 성능 향상이라고 할 수 있다.

<화면 3> 압축된 결과

  ASP.NET의 성능 개선

웹 개발에 있어서 가장 먼저 고려해야 할 부분이 바로 객체의 라이프 사이클이다. 웹이란 환경 자체가 모든 상태를 유지하기 어렵기 때문에 상태와 객체를 유지하는 일이다. ASP.NET에서 캐시를 잘 활용한다면 이런 문제를 상당부분 해결할 수 있는 방법이 된다.

응용 프로그램 캐시
ASP.NET의 응용 프로그램 캐시는 키와 값을 이용해서 원하는 데이터를 메인 메모리에 캐시 할 수 있는 방법을 제공해 준다. ASP.NET의 캐싱 알고리즘은 상당히 강력한데 시스템 메모리가 부족해지면 ASP.NET에서 알아서 거의 사용되지 않거나 불필요한 항목을 제거한다.

이런 기술을 MS에서는 ‘청소’기법이라고 하는데 필요에 따라서 청소의 우선순위를 개발자가 직접 지정할 수 있게 해서 중요한 항목의 경우 우선적으로 보호할 수 있는 방법을 제공해 준다.

Cache[“CacheItem1”] = “Cached Item 1”; 혹은 Cache.Insert (““CacheItem2”,“ Cached Item 2”);

캐시는 메모리 상에 그 값이 유지되기 때문에 만료 시간을 지정해서 일정 시간이 지나면 자동으로 제거할 수 있는 방법도 경우에 따라서 유용하게 사용할 수 있다. Cache.Insert()에서 TimeSpan을 이용해서 지정하는 것이다.

Cache.Insert“( CacheItem7”,“ Cached Item 7”, null, System.Web. Caching.Cache.NoAbsoluteExpiration, new TimeSpan(0, 10, 0));

캐시는 청소 기법에 의해서‘알아서’제거 될 수도 있기 때문에 중요한 데이터인 경우에는 반드시 높은 우선순위를 지정해 주어야 한다.

Cache.Insert(““CacheItem8”,“ Cached Item 8”,
null, System.Web.Caching.Cache.NoAbsoluteExpiration,
System.Web.Caching.Cache.NoSlidingExpiration,
System.Web.Caching.CacheItemPriority.High, null);

여기서 눈여겨 볼 부분이 System.Web.Caching.CacheItem Priority.High이다. 우선순위를 결정하는 부분으로써 CacheItemPriority는 열거형으로 제공된다. CacheItemPriority는 <표2>를 참조하기 바란다.


캐시 대신 Application, Session, State를 이용해도 된다고 말하는 독자가 있을지도 모르지만 역시 가장 가볍게 사용할 수 있는 방법은 캐시다. 전체적으로 걸리는 부하를 보면 Application >Session > Cache 순서로 부하를 일으킨다.

State는 웹 트래픽을 증가시키기 때문에 메모리에 부하를 일으키지는 않지만 성능이라는 관점에서 보면 똑같이 자제해야 할 대상이다.

페이지 출력 캐시
ASP 시절에는 모든 페이지가 인터프리터 방식으로 매번 라인 단위로 분석되고 해석되어서 실행되었다. 이 방식은 지금도 많이 사용되고 있는 PHP에서 사용되는 방식이다. ASP.NET은 ASP와는 달리 처음부터 컴파일 되어 있는 덕에 상당히 빠른 응답속도를 보여준다.

단 첫 번째 호출에 대해서는 인스턴스를 발생시키기 위해서 어쩔 수 없는 지연시간이 발생되긴 하지만 그래도 전체적인 성능 향상에 비해서는 아주 미미한 수준이다.

인스턴스가 발생되어 있는 상태에서 ASP.NET 페이지 요청이 들어오면 페이지를 출력하기 위해서 해당 페이지를 HTML로 변환하는 작업을 하게 되는데 이런 작업을 렌더링이라고 한다.

페이지 출력게시는 처리된 ASP.NET 페이지의 콘텐츠를 메모리에 저장해서 자주 호출되는 페이지의 렌더링 작업을 최소화 할 수 있게 해준다. 이렇게 하면 페이지 처리 주기를 다시 실행하지 않고도 ASP.NET에서 페이지 응답을 클라이언트에 보낼 수 있게 한다.

페이지 출력 캐시는 자주 변경되지 않지만 만드는 데 처리 시간이 많이 소요되는 페이지에 아주 효과적으로 사용할 수 있다. 특히 실적이나 자제 목록과 같이 자주 변경되지 않으면서 많은 양의 데이터를 처리하는 페이지인 경우에는 그 효과를 확연히 느낄수 있다.

페이지 출력 캐시를 사용하는 방식은 각 페이지에 대해 페이지 캐싱을 개별적으로 구성하는 것이다. 또, 캐싱 설정을 한 번 정의한 다음 여러 페이지에서 이러한 설정을 사용할 수 있는 캐시 프로필을 Web.config 파일에 만들 수 있다. 페이지 출력 캐시는 두가지 방식을 사용할 수 있다.

● 전체 페이지 캐시: 페이지 전체를 메모리에 저장해서 클라이언트의 요청에 사용한다.
● 부분 페이지 캐시: 페이지에서 지정된 일부를 캐시하고 자주 변경되는 부분은 동적인 상태로 유지한다.

페이지의 출력 캐시를 지정하는 방법은 두 페이지 지시자에서 설정하는 방식과 코드에서 직접 설정하는 방법 두 가지다. 이렇게 페이지 출력 캐시를 지정할 때 효과적으로 캐시를 유지하기 위해서 사용되는 부분이 바로 Duration 속성이다. Duration은 캐시의 유지시간을 지정해 준다.

● 페이지 지시자를 이용하는 방법:
<%@ OutputCache Duration=”60”VaryByParam=”None”%>
● 코드에서 직접 지정하는 방법: Response.Cache.SetExpires (DateTime.Now.AddSeconds(60));

부분 페이지 캐시도 역시 페이지 지시자와 코드에서 직접 지정하는 방법 중 선택해서 지정할 수 있다.

● 페이지 지시자를 이용하는 방법:
<%@ OutputCache Duration=”120”VaryByParam=”None”%>
● 코드에서 직접 지정하는 방법: 캐시하려고 하는 클래스의 메타데이터에 <PartialCaching(120)>를 지정한다.

캐시 기법을 적절히 잘 활용하면 적은 메모리를 이용해서 상당한 효과를 볼 수 있기 때문에 적절히 잘 이용해주는 가이드라인을 프로젝트 기획단계에서 부터 잘 제안해야 한다.

세션의 효율적인 사용
세션은 생각 보다 무척 신중하게 사용되어야 하는 메모리 자원이다. 세션은 개별적인 유저들에게 모두 할당되어야 한다. 때문에 유지되어야 할 정보는 최소한으로 유지되어야 하며 객체나 많은 양의 데이터를 올리는 것은 반드시 피해야 할 금기 사항 중에 하나이다.

특히 좋지 않은 방법 중에 ADO.NET의 Connection객체를 세션에 넣어두고 필요할 때 마다 호출하는 방법이 있다.

어차피 데이터베이스 연결은 ADO.NET에서 풀링(Pooling)되고 있기 때문에 별도로 유지할 필요가 없다. 괜히 세션의 사이즈를 키우는 결과만 가져온다.

ASP.NET으로 작업하다 보면 모든 페이지에서 세션에 접근할 필요가 없다는 사실을 알 수 있다. 웹 사이트에서 간단한 약관을 보여준다거나 회사소개 혹은 약도 같은 것을 보여주는 페이지는 세션과는 전혀 무관하게 사용될 수 있는 페이지 이다. 이런 경우 해당 페이지에서는 세션을 비활성화 해두면 성능에 도움이 된다.

이런 경우 페이지 지시자에서 <%@ Page EnableSession State=”false”%>와 같이 세션을 비활성화 시키면 된다. 세션에서 또 다른 이슈가 될 수 있는 부분은 세션을 inprocess로 사용할 것인가 혹은 out-of-process로 사용할 것인가하는 문제이다. out-of-process는 SQL Server를 통해서 여러 대의 웹서버의 세션을 공유하게 해주는 방법이다.

아무래도 inprocess방식에 비해서는 성능이 떨어지는 탓에 다수의 웹서버를 이용할 경우에만 한정적으로 사용해야 한다. 자세한 설정방법은 http://msdn.microsoft.com/library/kor/default.asp?url=/lib rary/KOR/cpguide/html/cpconaspstatemanagement.asp를 참조하기 바란다.

PostBack 사용의 최소화
ASP.NET에서는 상태를 유지하기 위해서 state를 사용한다. state는 hidden된 데이터 필드를 이용해서 서버와 클라이언트가 지속적으로 통신을 하면서 상태를 유지한다. 따라서 PostBack이 많이 발생되면 그만큼 네트워크 트래픽이 발생되기 때문에 최소한으로 사용하라고 한다.

굳이 기능이 필요 없는 페이지는 aspx대신 그냥 html을 사용하는 것도 좋은 방법이다. 또 서버 컨트롤을 사용 할 때에도 일반 html 컨트롤로 충분히 가능하다면 html과 자바 스크립트로 처리하는 편이 좋다(물론 개발자들은 서버 컨트롤을 사용하는 편이 개발 생상성이 높다고 반문할지도 모르지만 단순히 성능이라는 문제만 놓고 본다면 서버 컨트롤은 불필요하게 PostBack을 발생시킨다).

만약 서버 컨트롤을 사용하게 된다면 불필요한 경우에는 뷰 상태를 필요할 때만 활성화 시키고 사용한다. 뷰 상태를 지정하는데 있어서 와 같이 직접 지정할 수도 있고혹은 <%@ Page EnableViewState=”false”%>와 같이 지정해서 전체의 뷰를 비활성화 시키는 방법도 사용할 수 있다.

MS의 정책인지는 모르겠지만 절대 그들의 말처럼 MS의 기술은 결코 시대를 앞질러 나가지는 않는 것 같다. 오히려 현실의 요구에 충분히 만족시키는데 그 목표가 있는 것처럼 보인다. 현실적인 요구사항이 충분할 때 제품을 내놓는다. 때문에 먼저 시작한 업체들은 열심히 시장만 만들어 놓고 MS에 밀리는 경우가 종종 있는 것 같기도 하다.

그 덕에 MS를 기반으로 하는 개발자들은 아주 편하고 높은 생산성을 갖는 환경을 제공받고 있다. 그래서인지 오히려 개발자들은 성능을 저하시키는 막 코딩 혹은 날 코딩(?)과 같은 신조어들을 만들어 내가면서 비효율적인 코드를 생산하기도 한다.

좋은 생산성을 보장 받는 축복받은 환경에서 충분한 일정까지 보장 받으면 더없이 행복한 개발자들이겠지만 꼭 그렇지 않더라도 일반적인 성능 이슈들이라도 잘 숙지한다면 더 좋은 결과물을 얻어 낼 수 있을 것이다.

최근 MS에서 직접 개설한 MyMSDN(http://www.microsoft.com/korea/msdn/ mymsdn)과 같은 곳에서 질문을 해보는 것도 좋은 방법을 찾을 수 있는 한 방법이 될 것이다. @


참고자료
1. http://www.gosu.net 제2회 세미나 Power개발자를 위한 ADO.NET 자료
2. MyMSDN
3. Inside C# 2 -정보문화사
4. MSDN web page
5. http://msdn.microsoft.com/library/KOR/cpguide/html/cpconaspoptimization.asp
6. http://msdn2.microsoft.com/ko-kr/library/44e5wy6k(VS.80).aspx


* 이 기사는 ZDNet Korea의 제휴매체인 마이크로소프트웨어에 게재된 내용입니다.
출처 : 닷넷 (.NET) 프로그래머 모임
글쓴이 : 심재운 원글보기
메모 :
0 Comments
댓글쓰기 폼