728x90

.NET Framework 설치 실패!!

MS05-004(ASP.NET 취약점 보안 업데이트, KB887219), .NET Framework Service Pack 또는 hotfix 설치 등과
관련하여 .NET Framework 설치 시 실패 오류가 발생할 수 있습니다.

설치 실패 로그를 확인하여 원인과 해결 방법을 찾아야겠지만 많은 사례들을 살펴보면 기존에 설치되어 있는
.NET Framework 깨끗하게 지우는 방법을 권장하고 있습니다.

.NET Framework 를 수동으로 제거하는 방법에 대해서 몇 가지 방법이 소개되어 있지만 그 중에서
.NET Framework Cleanup Tool 이라는 녀석을 소개하고자 합니다.

이것은 .NET Framework setup Product Support team 에서도 .NET Framework 문제 해결을 위해
사용하고 있으며 문제 해결에 큰 성과가 있다고 합니다. 저 또한 Windows 2000 Server 에 .NET Framework
1.1과 2.0을 설치하는 과정에서 오류를 만났고 이 프로그램으로 문제 해결에 도움을 받았습니다.


[다운로드]
.NET Framework 1.0, 1.1, 2.0, 3.0 and 3.5 cleanup tool
http://astebner.sts.winisp.net/Tools/dotnetfx_cleanup_tool.zip

압축된 파일을 해제하고 별도의 설치 필요 없이 바이너리를 실행하면, 아래와 같은 화면을 볼 수 있습니다.
심플하죠! 설치된 모든 .NET Framework 를 제거가 필요할 때 Cleanup Now 버튼을 클릭하시면 됩니다.

사용자 삽입 이미지


















[주의사항]

1. CleanUp Tool을 실행하기 전에 반드시 프로그램 추가/제거에서 일반적인 프로그램 제거 방법을 통해
    기존에 설치되어 있는 .NET Framework 를 제거해야 합니다.

2. CleanUp Tool은 다른 버전의 .NET Framework 에 의해 사용된 공유된 파일과 레지스트리를 삭제합니다.


주의사항 없는 제거 툴이 어딨겠습니까?
이걸 꼭 사용해야 하는 이유는? 백업은 했냐? 설치 실패하면 롤백은 어떻게 할래?
이런 고민 안 해도 된다면 아무 생각없이 그냥 사용하십시오.


[참고자료]
Removal tool to fix .NET Framework install failures
http://blogs.msdn.com/astebner/archive/2005/04/08/406671.aspx

Microsoft .NET Framework 2.0 설치 문제를 해결하는 방법
http://support.microsoft.com/kb/908077/ko

.NET Framework 설치가 실패하여 수동 제거가 필요하다
http://support.microsoft.com/kb/320112/ko

Microsoft Security Bulletin MS05-004 - ASP.NET Path Validation Vulnerability (887219)
http://www.microsoft.com/technet/security/Bulletin/MS05-004.mspx


작성자 : Lai Go / 작성일자 : 2008.06.17

728x90
제 1강 : SQL Server 2005 .NET CLR 통합기능

이름 : 김종균
전) 벅스(주) 프로그래머, DBA
현) (주) 테크데이타 SQL Server Technical Support Engineer

개요

SQL 서버 2005에서는 .NET Framework2.0의 Common Language Runtime 기술을 이용하여 매우 향상된 데이터베이스 프로그램을 구현 할 수 있습니다. 이는 Microsoft Visual C#, Microsoft Visual Basic.NET, Microsoft Visual C++ 등의 CLR 언어를 통해서 저장프로시저, 사용자정의 함수, 트리거를 생성할 수 있을 뿐 아니라, 사용자 정의 데이터타입 및 집계의 생성을 가능하게 합니다.

TSQL의 그 능력의 한계

TSQL은 RDBMS만을 위한 언어이기 때문에 DML, DDL외의 프로그래밍 언어로서의 기능이 아주 미약합니다. TSQL에서 제공하는 함수는 제한적이며 (SQL2005에서 기능이 많이 추가되긴 하였지만), TSQL 로는 SQL 서버 외부 개체에 접근할 수 없습니다. 예를 들면 파일시스템 오브젝트를 통해서 파일의 내용을 읽고, 쓰는 등의 작업은 TSQL 로는 상상도 할 수 없습니다. 물론 SQL2000에서도 Extended Stored Procedure를 통해서 SQL외부 개체에 대한 핸들링을 할 수 있었으나, 상대적으로 어려운 C,C++프로그래밍에 대한 접근성과 in-process형태로 Sqlserver프로세스에서 실행되었기 때문에 자칫 확장 저장프로시져에 치명적인 코드결함이 있으면, SQL서버가 다운되는 경우도 있었습니다
하지만 CLR을 사용함으로써 SQL서버에서 기존에는 할 수 없었던 다양한 프로그래밍을 가능하게 합니다.

TSQL과 CLR의 몇 가지 장단점

  • TSQL은 인터프리터 방식이므로 느리다.
  • TSQL의 에러핸들링은 SQL2005에서 많이 향상되긴 했지만 비교적 좋지 않다. 
  • CLR은 컴파일 된 형태이므로 실행이 빠르다. 
  • CLR은 .NET프로그래밍 언어의 방대한 Class라이브러리를 사용하여 막강한 프로그래밍을 할 수 있다. 
  • TSQL은 대량의 데이터 액세스 혹은 데이터 조작 작업등에 유리하다. 
  • TSQL은 절차적 언어가 아니라 선언적 언어라서 문자열 조작, 데이터 포맷팅, 절차적 혹은 반복적인 로직에는 성능이 좋지 않다. 
  • CLR은 계산 집중적이거나 절차적,반복적 로직, 문자열 처리에서 좋은 성능을 나타낸다.

CLR 통합기능을 활용한 개발 절차

   1. SQL2005에서 CLR통합기능 옵션 활성화

   2. .NET Framework에서 지원하는 언어를 이용하여 클래스 작성

   3. 언어컴파일러를 통한 클래스 컴파일

   4. SQL서버에서 ASSEMBLY등록

   5. 등록한 ASSEMBLY 를 이용하여 사용자 개체 생성

.NET CLR통합기능 활성화

SQL2005는 보안상의 이유로 .NET CLR통합기능을 비활성화 해두고 있습니다. 따라서 CLR통합기능을 사용하기 위해서는 ‘Clr Enabled’ 라는 구성옵션을 활성화 해야 합니다. .NET CLR통합 기능을 사용할 수 있게 해주는 옵션을 활성화하는 방법은 두 가지가 입니다.

첫 번째 방법은, sp_configure 구성옵션 명령을 통해서 ‘clr enabled’ 라는 구성옵션을 ‘1’로 세팅해주는 것입니다. 이 옵션은 수정 즉시 적용되므로, SQL서버를 재 시작 할 필요가 없습니다.

SP_CONFIGURE 'clr enabled',1
RECONFIGURE WITH OVERRIDE
GO
SP_CONFIGURE
GO
name minimum maximum config_value run_value
----------------------- ------------ ------------ ------------ ------------
clr enabled 0 1 1 1
lightweight pooling 0 1 0 0

유의해야 할 사항은 clr enabled옵션이 해제되게 되면 등록된 모든 ASSEMBLY가 unload되게 됩니다.
그리고 clr enabled옵션과 lightweight pooling옵션은 병행해서 사용이 불가능합니다.

두 번째 방법은, SQL서버 노출영역 구성에서 기능에 대한 노출영역을 구성하는 것 입니다. 아래 그림과 같이 clr enabled 옵션을 체크 해주면 됩니다.

SQL서버 노출영역 구성

예제1 – 사용자 정의 스칼라 함수 구현

그럼, CLR을 통한 간단한 사용자 정의 함수를 생성하고 SQL서버에 이식시켜 사용해보는 예제를 구현해보면서, CRL을 이용한 SQL프로그래밍에 대한 감을 잡아보겠습니다.

예제로 email주소를 정합성을 검사해주는 사용자함수 클래스를 C#으로 구현해 보겠습니다. 소스코드 작성을 위해서 메모장을 이용하셔도 무방합니다만.. 가능하시면 Visual Studio를 이용하시면 보다 더 훌륭한 개발 인터페이스를 통해서 더 빠르고 쉽게 코딩이 가능합니다. 여기서, c#코드에 대한 설명은 하지 않습니다.

개발 도구를 사용하여 아래 c#코드를 작성합니다.

using System;
using System.Data;
using System.Data.SqlClient;
using System.Data.SqlTypes;
using Microsoft.SqlServer.Server;
using System.Text.RegularExpressions;


public partial class UserDefinedFunctions
{
[Microsoft.SqlServer.Server.SqlFunction]
public static bool emailCheck(string email)
{
string pattern=@"^[a-z][a-z|0-9|]*([_][a-z|0-9]+)*([.][a-z|" + @"0-9]+([_][a-z|0-9]+)*)?@[a-z][a-z|0-9|]*\.([a-z]" + @"[a-z|0-9]*(\.[a-z][a-z|0-9]*)?)$";
Match match = Regex.Match(email, pattern, RegexOptions.IgnoreCase);

if(match.Success) return true;
else return false;
}
};

[소스1] emailCheck.cs

소스코드 작성이 완료되면 c#컴파일러를 통해 cs파일을 dll형태로 컴파일 합니다.

csc.exe /target:library /out:emailcheck.dll emailcheck.cs

참고. csc컴파일러의 위치는 .NetFramework이 설치된 폴더에 있습니다.
C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727

자 이렇게 해서 emailcheck.dll 이라는 어셈블리 파일을 생성했습니다. 이 파일을 관리하시는 적절한 폴더에 복사 하십시요. 이 파일은 추후에 SQL2005의 ASSEMBLY로 등록하게 되면, 삭제해도 무관합니다.

다음 단계로 CLR사용자 함수 ASSEMBLY를 SQL서버에 ASSEMBLY로 등록합니다.

CREATE ASSEMBLY SQLER_UDF
FROM 'C:\TEMP\emailcheck.dll'
WITH PERMISSION_SET = SAFE
GO

CREATE ASSEMBLY는 이미 .dll파일로 미리 컴파일된 ASSEMBLY를 SQL2005 내부에서 사용할 수 있도록 업로드 혹은 바인딩 하는 작업입니다.


다음 단계로 ASSEMBLY를 매핑하여 사용자 정의 함수를 생성합니다.

CREATE FUNCTION UDF_VALIDATEEMAIL (@email as nvarchar (100))
RETURNS bit
AS EXTERNAL NAME SQLER_UDF.MyFunctions.emailCheck
GO

사용자 개체 생성시EXTERNAL NAME 지정규칙은 다음과 같습니다.
[SQL Server Assembly Name].[Fully Qualified Path to Class].[Static Method Name]

자. 그럼 이제 생성한 스칼라 사용자 정의 함수를 사용해보겠습니다.

SELECT DBO.UDF_VALIDATEEMAIL ('bellvirus1@naver.com')     --1
SELECT DBO.UDF_VALIDATEEMAIL ('bellvirus1@naver')           --0
SELECT DBO.UDF_VALIDATEEMAIL ('bellvirus1.com')               --0
GO

상기 절차로 .NET 프로그램 코드를 통한 UDF생성을 간단하게 살펴보았습니다.

예제2 – 스칼라 함수, 저장프로시져 및 트리거 구현

다음 예제로 테이블의 전체 행 수를 반환하는 스칼라 함수와, 레코드 셋을 가져오는 저장프로시져, 데이터 삭제에 대해 로그를 기록하는 트리거를 Visual Basic .NET 으로 구현 해보겠습니다.

Imports System
Imports System.Collections.Generic
Imports System.Text
Imports System.Data
Imports System.Data.SqlClient
Imports Microsoft.SqlServer.Server
Imports System.Data.SqlTypes

Public Class MySQLClass
<SqlFunction(DataAccess:=DataAccessKind.Read)>_
Public Shared Function ReturnOrderCount() As Integer
Using conn As New SqlConnection("context connection=true")
conn.Open()
Dim cmd As New SqlCommand("SELECT COUNT(*) AS 'ORDERCNT' FROM SALES.SALESORDERDETAIL", conn)
Return CType(cmd.ExecuteScalar(), Integer)
End Using
End Function


Public Shared Sub GetSalesOrderDetail()
Using conn As New SqlConnection("context connection=true")
conn.Open()
Dim cmd As New SqlCommand("SELECT COUNT(*) AS 'ORDERCNT' FROM SALES.SALESORDERDETAIL", conn)
SqlContext.Pipe.ExecuteAndSend(command)
End Using
End Sub


<Microsoft.SqlServer.Server.SqlTrigger()>_
Public Shared Sub writeDeleteLog()
Dim oTc As SqlTriggerContext
oTc = SqlContext.TriggerContext
If (oTc.TriggerAction = TriggerAction.Delete) Then
Dim oFi As New System.IO.StreamWriter("c:\temp\log.txt", True)
oFi.Write("something deleted !")
oFi.Close()
End If
End Sub

End Class

[소스2] MySQLClass.vb

코드 작성이 완료되면 컴파일러를 통해 DLL형태의 파일을 생성합니다.

vbc /target:library /out:c:\temp\MySQLClass.dll c:\temp\MySQLClass.vb

그런 다음에 SQL서버에서 ASSEMBLY로 등록합니다.
이번 예제에서는 ASSEMBLY생성시에 PERMISSION_SET옵션을 EXTERNAL_ACCESS를 지정하였는데, 그 이유는 writeDeleteLog 메서드가 SQL 외부 개체에 액세스 할 필요가 있기 때문입니다.

여기서 잠깐 PERMISSION_SET옵션에 대해 알아보고 넘어 가겠습니다.

  • SAFE (기본옵션) : 등록하게 되는 어셈블리에서 실행한 코드는 파일, 네트워크, 환경변수 또는 레지스트리와 같은 외부 시스템 리소스에 액세스할 수 없습니다.
  • EXTERNAL_ACCESS : 파일, 네트워크, 환경 변수 또는 레지스트리와 같은 외부 시스템 리소스에 액세스할 수 있습니다.
  • UNSAFE : SQL Server 인스턴스의 내부 리소스와 외부 리소스 모두에 제한 없이 액세스할 수 있습니다.
CREATE ASSEMBLY SQLER_CLASS
FROM 'C:\TEMP\MYSQLClass.dll'
WITH PERMISSION_SET = EXTERNAL_ACCESS
GO

생성 명령을 실행하였으나 아래와 같은 오류가 발생합니다.


메시지10327, 수준14, 상태1, 줄1 어셈블리'MySQLClass'에PERMISSION_SET = EXTERNAL_ACCESS에대한권한이없으므로어셈블리'MySQLClass'에대한CREATE ASSEMBLY가실패했습니다. 어셈블리는DBO(데이터베이스소유자)에게EXTERNAL ACCESS ASSEMBLY 권한이있고데이터베이스에TRUSTWORTHY 데이터베이스속성이있는경우또는어셈블리가현재인증서로서명되어있거나EXTERNAL ACCESS ASSEMBLY 권한이있는관련로그인을소유한비대칭키로서명되어있는경우에권한이부여됩니다.

사용자 정의함수나 저장프로시져에서 외부 개체에 접근하려면 Database의 TRUSTWORTHY 옵션을 활성화 해주어야 합니다. 아래 명령을 실행하여 외부 개체 접근을 허용해줍니다. 그런 다음 다시 ASSEMBLY를 생성해줍니다.

ALTER DATABASE AdventureWorks SET TRUSTWORTHY ON
GO

CREATE ASSEMBLY SQLER_CLASS
FROM 'C:\TEMP\MYSQLClass.dll'
WITH PERMISSION_SET = EXTERNAL_ACCESS
GO

SQLER_CLASS 어셈블리를 활용하여 사용자 정의 스칼라 함수를 생성합니다.

CREATE FUNCTION UDF_getOrderCount()
RETURNS INT
AS EXTERNAL NAME SQLER_CLASS.MySQLClass.ReturnOrderCount
GO

생성한 함수를 실행하여 보겠습니다.

SELECT DBO.UDF_getOrderCount() AS orderCount
GO
orderCount
-----------
121317

SQLER_CLASS 어셈블리를 활용하여 저장프로시져를 생성합니다.

CREATE PROC UP_getSalesOrderDetail
AS EXTERNAL NAME SQLER_CLASS.MySQLClass.GetSalesOrderDetail
GO

프로시져를 실행하여 보겠습니다.

EXEC UP_getSalesOrderDetail
GO

43659  1      4911-403C-98 1      776    1      2024.994
43659  2      4911-403C-98 3      777    1      2024.994
43659  3      4911-403C-98 1      778    1      2024.994
43659  4      4911-403C-98 1      771    1      2039.994
43659  5      4911-403C-98 1      772    1      2039.994
43659  6      4911-403C-98 2      773    1      2039.994
43659  7      4911-403C-98 1      774    1      2039.994
43659  8      4911-403C-98 3      714    1      28.8404
43659  9      4911-403C-98 1      716    1      28.8404

결과를 잘 반환해 줍니다.

다음으로 트리거를 생성하여 보겠습니다.
이 예제를 위해서 간단한 샘플 테이블을 생성하겠습니다.

CREATE TABLE TBL_TEST (idx int, name varchar(20))
GO
INSERT INTO TBL_TEST VALUES (1,'KIM')
INSERT INTO TBL_TEST VALUES (1,'LEE')
INSERT INTO TBL_TEST VALUES (1,'PARK')
GO

TBL_TEST테이블에 대해서 DELETE명령에 대한 트리거 생성합니다.

CREATE TRIGGER TRG_DELETE
ON TBL_TEST
FOR DELETE
AS EXTERNAL NAME SQLER_CLASS.MySQLClass.writeDeleteLog
GO

트리거가 잘 생성되었고, 실제로 데이터를 삭제하여 트리거가 정상적으로 동작하는지 살펴보겠습니다. 앞서 작성한 writeDeleteLog 메서드는 삭제 트리거가 발생하면 메모장에서 log.txt파일에 Something Deleted! 를 기록하게 됩니다.

DELETE FROM TBL_TEST WHERE idx = 1
GO

메모장에서 log.txt파일 확인

예제3. 사용자 정의 집계함수(Aggegation)의 구현

이번 예제에서는 각 행들의 특정 문자열 컬럼을 합해주는 함수를 만들어 보겠습니다.
이 함수는 숫자형 집계함수인 SUM()과 유사합니다 ^^
이번 예제는 온라인 도움말에서 제공하는 샘플을 사용합니다.

using System;
using System.IO;
using System.Data.Sql;
using System.Data.SqlTypes;
using System.Text;
using Microsoft.SqlServer.Server;

[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design",
"CA1020:AvoidNamespacesWithFewTypes"
, Scope = "namespace", Target =
"Microsoft.Samples.SqlServer")]

namespace Microsoft.Samples.SqlServer
{
[Serializable]
[Microsoft.SqlServer.Server.SqlUserDefinedAggregate(
Microsoft.SqlServer.Server.Format.UserDefined,       //use clr
serialization to serialize the intermediate result

IsInvariantToNulls = true,       //optimizer property
IsInvariantToDuplicates = false,       //optimizer property
IsInvariantToOrder = false,       //optimizer property
MaxByteSize = 8000)       //maximum size in bytes of persisted value
]
public classConcatenate : Microsoft.SqlServer.Server.IBinarySerialize
{
/// <summary>
///The variable that holds the intermediate result of the concatenation
/// </summary>
private StringBuilder intermediateResult;

/// <summary>
/// Initialize the internal data structures
/// </summary>
public void Init()
{
intermediateResult = new StringBuilder();
}

       
/// <summary>

///Accumulate the next value, nop if the value is null
/// </summary>
/// <param name="value"></param>
public void Accumulate(SqlString value)
{

if (value.IsNull)
{
return;
}
intermediateResult.Append(value.Value).Append(',');
}

/// <summary>
///Merge the partially computed aggregate with this aggregate.
/// </summary>
/// <param name="other"></param>
public void Merge(Concatenate other)
{
intermediateResult.Append(other.intermediateResult);
}

/// <summary>
/// Called at the end of aggregation, to return the results of the aggregation
/// </summary>
/// <returns></returns>
public SqlString Terminate()
{
string output = string.Empty;
//delete the trailing comma, if any
if (intermediateResult != null && intermediateResult.Length > 0)
output = intermediateResult.ToString(0, intermediateResult.Length - 1);
return new SqlString(output);
}
public void Read(BinaryReader r)
{
intermediateResult = new StringBuilder(r.ReadString());
}

public void Write(BinaryWriter w)
{
w.Write(intermediateResult.ToString());
}
}
}
[소스3] concat.cs

소스코드 작성이 완료되면 c#컴파일러를 통해 cs파일을 dll형태로 컴파일 합니다.

csc.exe /target:library /out:concat.dll concat.cs

concate.dll파일을 ASSEMBLY로 등록하고, 집계함수를 생성합니다.

CREATE ASSEMBLY STRINGHANDLES3
FROM 'C:\TEMP\concat.dll'

CREATE AGGREGATE [dbo].[Concatenate](@input nvarchar(4000))
RETURNS nvarchar(4000)
EXTERNAL NAME
STRINGHANDLES3.[Microsoft.Samples.SqlServer.Concatenate];
GO

테스트를 위한 샘플 테이블을 생성하고, 데이터를 몇 건 삽입합니다.

CREATE TABLETEST (IDX INT, NAME VARCHAR(10))
GO

INSERT INTO TEST VALUES (1,'AAA')
INSERT INTO TEST VALUES (2,'BBB')
INSERT INTO TEST VALUES (3,'CCC')
INSERT INTO TEST VALUES (4,'DDD')
INSERT INTO TEST VALUES (5,'EEE')
INSERT INTO TEST VALUES (6,'KKK')
GO

집계함수를 활용하여 쿼리를 실행합니다.

SELECT SUM(IDX)AS SUMIDX, dbo.concatenate(name) AS CONCATNAMES
FROM TEST
GO

SUMIDX      CONCATNAMES
---------  ----------------------------------
21          aaa,bbb,ccc,ddd,eee,kkk

(1개행적용됨)

예제4. 사용자 정의 테이블 값 함수의 구현

이번에는 특정 구분자를 이용하여 요소를 분리해주는 SPLIT함수를 구현해 보겠습니다.

using System;
using System.Collections;
using System.Data;
using System.Data.Sql;
using System.Data.SqlTypes;
using Microsoft.SqlServer.Server;


public sealed class StringSplitter
{
[SqlFunction(FillRowMethodName = "FillRow")]
public static IEnumerable Split(SqlString argument1, Char []
argument2)
{
string value;

if (argument1.IsNull)
value = "";
else
value = argument1.Value;

return value.Split(argument2);
}

[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design",
"CA1021:AvoidOutParameters")]
public static void FillRow(Object obj, out string stringElement)
{
stringElement = (string)obj;
}
}
[소스4] Split.cs

소스를 컴파일 합니다.

csc.exe /target:library /out:split.dll split.cs

어셈블리를 등록합니다.
CREATE ASSEMBLY STRINGHANDLES
FROM 'C:\TEMP\split.dll'
GO

사용자 정의 함수를 생성합니다.

CREATE FUNCTION DBO.UDF_SPILT (@INPUT NVARCHAR (4000), @DELIMETER
NVARCHAR(10))
RETURNS TABLE (ELEMENT NVARCHAR (MAX))
AS EXTERNAL NAME STINGHANDLE.[ StringSplitter]. Split;
GO

실행테스트

SELECT * FROM DBO.UDF_SPILT 'MSSQL/EXCHANGE/OFFICE/VISTA' , >'/');
GO

element
-----------------
MSSQL
EXCHANGE
OFFICE
VISTA
(4개행적용됨)

마치며..

간단한 예제 중심으로 SQL서버2005 .NET CLR통합 기능에 대해서 살펴보았습니다. 이처럼, TSQL만으로는 불가능 했던 프로그래밍을 가능하게 TSQL에 날개를 달아주는 것이 .NET CLR입니다. 언제나 그렇듯이, 좋은 기능을 제대로 사용하지 않거나, 남용하면 역효과를 가져 올 수 있습니다. TSQL만으로 구현 가능한 것들은 TSQL로 구현하는게 바람직 할 것입니다. 주어진 상황에 대해서 TSQL과 CLR 중 어떠한 것을 적용해야 효율적인가 에 대한 많은 고민과 테스트 과정을 통해서 올바른 판단을 해야 할 것입니다.

감사합니다.

728x90
[SQL 서버 2005 실전 활용] ② 닷넷과의 통합


한용희 (롯데정보통신) 20/07/2005
SQL 서버 2005의 가장 큰 변화라고 한다면 아마도 닷넷 프레임웍과의 통합일 것이다. 이제는 쿼리문을 C#을 이용해서 개발할 수 있을 뿐만 아니라 C#을 통해서 T-SQL이 하지 못하는 기능을 마음껏 확장할 수도 있다. 이번 글에서는 CLR에 통합된 SQL 서버 2005의 새로운 모습을 살펴본다.

지난 글에서는 T-SQL의 새로운 모습에 대하여 살펴 보았다. T-SQL은 언어 자체가 집합적 언어이기 때문에 여전히 데이터를 조작하고 접근하는데 있어서는 닷넷 언어 보다 더 좋은 성능을 나타낸다. 하지만 T-SQL은 절차적 언어이기 때문에 객체지향적 프로그래밍을 할 수 없다는 단점이 있다.

그러나 닷넷을 이용하면 더 이상 이 문제로 고민하지 않아도 된다. C#, VB.NET, Managed C++를 이용해서 얼마든지 객체지향적 프로그래밍이 가능하다. 또한 복잡한 로직이나 계산, 외부 자원 연동, 코드 재사용등에 있어서는 T-SQL 보다 더 좋은 접근성과 성능을 보여준다. 한마디로 .NET 프레임웍과의 통합은 T-SQL을 교체하는 개념이 아니라, 더욱 확장하고 강화하기 위하여 도입된 것이라고 보면 된다.

SQL Server 2005가 .NET 프레임웍과 통합되면서 안정성이 대폭 향상되었다. 이전 SQL Server 2000에서 확장 저장 프로시저를 C++를 이용해서 작성을 하는 경우, 간혹 잘못된 코드로 인하여 SQL Server 전체가 다운되어 버리는 경우가 있었다. 그래서 확장 저장 프로시저를 매우 신중하게 만들어야 했으며 만드는 과정 자체도 간단하지가 않았다.

하지만 SQL Server 2005에서는 기본적으로 .NET 프레임웍의 호스팅 모델을 따라간다. SQL Server 2005와 각각의 .NET 코드로 만들어진 확장 저장프로시저는 서로의 독립성을 보장한다. 서로 메모리를 직접적으로 침범할 수 없으며, 서로의 실행 환경을 침해 할 수도 없다. 각각 별도로 운영된다는 것이다.

<그림1>을 보면 .NET 프레임웍의 호스팅 모델이 와 있다. SQL Server와 외부 어셈블리는 서로 다른 도메인을 가지고 있어 자신의 독립적인 실행 환경을 보호한다. 그래서 이제는 확장 저장 프로시저 때문에 더 이상 SQL Server가 다운되는 일은 없다.

<그림1> .NET 프레임웍 호스팅

SQL Server는 자기 자신만의 특별한 쓰레드 스케줄링, 동기화, 잠금, 메모리 할당 정책을 가지고 있다. SQL Server 자체가 워낙 메모리를 많이 사용하고 성능이 중요한 기업용 애플리케이션이기 때문에 보통의 CLR에서 제공하는 정책을 따르지 않고 자기 자신만의 특별한 방식을 적용해서 운영을 한다.

약 외부 어셈블리가 CPU나 메모리를 과도하게 많이 써서 SQL Server를 운영하는데 지장을 준다면, SQL Server는 이를 즉시 탐지해 내고 해당 사용권을 외부 어셈블리로부터 뺏어온다. 이렇게 함으로써 SQL Server는 더 이상 외부의 간섭에 영향을 받지 않고, 자기 자신을 스스로 지속적으로 안정적으로 운영할 수 있는 능력을 가지게 되었다.

간단한 사용자 정의 함수 만들기
먼저 간단한 사용자 정의 함수를 C#으로 만들어 볼 것이다. 복잡한 표현식이나 계산을 요하는 작업의 경우 C#으로 만드는 것이 더 효율적이므로 이번 예제에서는 우편번호를 체크하는 간단한 정규식 표현 함수를 만들어 보자.

먼저 VS.NET을 시작하고 새로운 프로젝트로 SQL Server Project를 선택한다. CLREx이라는 새로운 프로젝트를 만들고 AdventureWorks DB 서버에 연결한 후 새로운 아이템으로 IsValidZipCode라는 사용자 정의 함수를 추가한다.

<화면1> SQL Server용 템플릿

<화면2> IsValidZipCode 초기 생성 화면

그러면 <화면2>와 같은 템플릿 코드가 들어있다. 여기에서 주의해서 보아야 할 것은 함수 위에 있는 속성 [SqlFunction]이다. 이 속성은 아래의 함수가 SQL에서 사용하는 사용자 정의 함수임을 컴파일러에게 알려주는 지시자이다. 이제 기본 코드는 지우고 아래와 같이 코딩을 하자.

using System;
using System.Data.Sql;
using System.Data.SqlTypes;

public partial class UserDefinedFunctions
{
  [SqlFunction]
  public static bool IsValidZipCode(SqlString ZipCode)
  {
    return System.Text.RegularExpressions.Regex.IsMatch(ZipCode.ToString(), @"^\d{3}-\d{3}");
  }
};

[ Partial Class ]  
사용자 정의 함수와 저장 프로시저에 보면 partial 못 보던 새로운 클래스 지시자가 있는 것을 볼 수 있을 것이다. 이는 Visual Studio 2005 에서 새로 나온 개념으로 클래스를 부분적으로 나누어 완성할 수 있는 기능이다. 이는 하나의 클래스를 여러 개발자가 여러 파일로 분할해서 만들 경우 나중에 합쳐줘야 하는 불편 없이, 이 지시자 하나면 컴파일러가 알아서 하나의 클래스로 인식해서 컴파일을 한다. 예를 들면 아래와 같이 하나의 클래스를 두 개의 파일로 나누어서 만들 수 있다.

// File: MyClassP1.cs
public partial class MyClass
{
  public void method1()
  {
  }
}
// File: MyClassP2.cs
public partial class MyClass
{
  public void method2()
  {
  }
}

MyClass라는 클래스는 두 개의 메소드를 갖는 클래스로서 이렇게 두 개의 파일로 나누어서 정의할 수 있다. 예제에 있는 사용자 정의 함수에 partial이라는 지시자가 붙음으로써 앞으로 만드는 모든 사용자 정의 함수는 하나의 클래스로 만든다는 의미가 된다.

간단하게 해당 문자열이 우편번호식인지 검사하여 결과를 리턴해 주고 있다. 이제 이 코드를 컴파일하여 배포까지 하자. 그러면 자동으로 SQL Server에 이 어셈블리가 등록이 된다. 배포를 성공적으로 끝내면 아래와 같이 테스트를 해보자.

select dbo.IsValidZipCode('333-333');
select dbo.IsValidZipCode('333-A33');

-----
1
(1 row(s) affected)
-----
0
(1 row(s) affected)

잘 작동하는 것을 볼 수 있을 것이다. 사용자 정의 함수를 만들어서 사용해 보았는데, 함수를 만들고 배포 하는 것이 간단하다는 것을 느꼈을 것이다. 그럼 SQL Server 내부에는 어떻게 등록이 되어 있는 것일까?

SELECT * FROM sys.assemblies;

sys.assemblies라는 뷰를 보면 해당 CLREx 이라는 어셈블리가 등록되어 있는 것을 확인할 수 있을 것이다.

SELECT * FROM sys.assembly_files;

sys.assembly_files에는 실제 어셈블리의 내용이 들어있다. 즉, DLL 바이너리 자체를 SQL Server안에 등록 한 것이다. 그러므로 한번 어셈블리를 SQL Server안에 배포를 하면 해당 DLL 파일은 없어도 무방하다.

위에서는 배포를 VS.NET을 이용해서 자동으로 배포를 하였지만, 수동으로 배포하는 방법도 있다.

CREATE ASSEMBLY UDF1
FROM '\\localhost\Projects\CLREx\CLREx\bin\Debug\CLREx.dll';

CREATE FUNCTION IsValidZipCode(@ZipCode nvarchar(10))
RETURNS bit
EXTERNAL NAME
CLREx.UserDefinedFunctions.IsValidZipCode;

위와 같이 먼저 어셈블리를 등록을 하고 해당 함수를 만들어 주면 수동으로도 등록을 할 수 있다.

이제는 저장 프로시저를 C#으로 만든다고?
이번에는 저장 프로시저를 만들어 보자. 저장 프로시저를 만들려면 먼저 SQL문장을 실행해서 결과를 리턴해야 한다. 그러기 위해서는 어셈블리가 DB에 접속을 해서 SQL문장을 보내주어야 한다. 일반적으로 ADO.NET을 이용해서 DB에 접속을 하지만 기존의 연결 방법을 사용할 경우에는 외부에서 접속해 들어오는 것이므로 성능상에 문제가 있다.

따라서 내부 접속을 위한 별도의 Data Provider가 필요한데 그것이 바로 SQL Server Managed Provider이다. 이 프로바이더는 SQL Server 내에서 실행되므로 별도의 접속을 맺을 필요 없이 빠르게 수행을 한다. 따라서 open, close와 같은 절차가 필요없는 Data Provider이다. 사용 방법은 아래와 같이 선언을 하면 된다.

using System.Data.SqlServer;

SQL Server Managed Provider에는 효과적인 작업을 위하여 몇가지 타입을 제공한다. SqlCommand, SqlPipe, SqlResultSet, SqlTransaction, SqlTriggerContext 와 같은 타입을 제공한다. 이중 대부분은 SqlClient에 있는 것과 동일하고 SqlPipe와 SqlTiggerContext가 이번에 새로 등장한 타입이다. SqlTiggerContext는 트리거 작성을 위한 타입이고 SqlPipe는 테이블과 같은 데이터를 호출 하는 쪽에 보내 줄때 사용하는 타입이다. 그러면 SqlResultSet과 뭐가 다르냐고 할 수도 있다.

SqlResultSet은 성능 문제로 인하여 사용을 권하지 않는 타입이고(이제는 없어질지도 모른다) SqlPipe가 성능상 더 좋은 타입이다. SqlPipe는 말 그대로 호출자에게 파이프로 물을 보내듯이 데이터를 받는 즉시 바로 보낸다. 성능면에서도 T-SQL의 저장 프로시저와 거의 비슷한 성능을 보여준다. 그러므로 앞으로 테이블 데이터를 리턴 받는 경우에는 SqlPipe를 써야 한다. 또한 .NET 저장 프로시저는 리턴값으로 int형과 void형만을 리턴 할 수 있으므로 어차피 SqlResultSet 형식으로 리턴하지도 못한다.

이번에는 직접 저장 프로시저를 만들어 보자. 이전에 만든 프로젝트에 저장프로시저를 하나 추가하고 아래와 같이 코딩을 한다.

using System;
using System.Data;
using System.Data.Sql;
using System.Data.SqlServer;
using System.Data.SqlTypes;


public partial class StoredProcedures
{
  [SqlProcedure]
  public static void SelectEmp(SqlInt16 val)
  {
    SqlCommand sqlCmd = SqlContext.GetCommand();

    sqlCmd.CommandText = "SELECT * FROM HumanResources.Employee "
      + "WHERE DepartmentID = @pDeptID";

    // 파라메터값 대입
    sqlCmd.Parameters.AddWithValue("@pDeptID", (Object)val);

    // SqlPipe를 이용하여 결과 리턴
    SqlContext.GetPipe().Execute(sqlCmd);
  }
};

이번 예제는 사용자 테이블에서 특정 부서의 사람들을 추출하는 저장프로시저이다. 이를 컴파일 하고 배포한 다음 아래와 같은 SQL문장으로 테스트 해 보면 결과를 볼 수 있을 것이다.

EXEC dbo.SelectEmp 4;

사용자 정의 데이터 타입을 이용하여 나만의 데이터 타입을 만들자
SQL Server에는 기본적으로 CHAR, INT와 같은 기본 데이터 타입을 지원한다. 여기에 더 확장을 하여 우리가 원하는 데이터 타입을 스스로 만들어서 추가할 수도 있다. 예를 들면, 위도, 경도, 포인트를 나타내는 데이터 타입이라든지 이메일 주소를 나타내는 데이터 타입을 새로 만들어서 추가할 수 있다. 포인트를 보면 10:30 과 같은 표현식을 수용하는 하나의 칼럼을 만들 수도 있다. 그런데 사실 이러한 표현은 기존의 칼럼을 두 개로 나누어서 x, y 좌표 값을 저장해도 된다. 구지 사용자 정의 데이터 타입(User-Defined Data Types)(UDT)을 안 만들어도 할 수는 있다.

하지만, 의미상 하나로 표현하는 것이 더 타당하고, 그 자료형과 관련된 많은 메서드나 행위가 필요할 때에는 하나의 데이터형으로 만드는 것이 바람직하다. 예를 들면 날짜 같은 데이터 타입을 년,월,일로 나누어서 3개의 칼럼에 저장하는 것 보다는 년월일 하나로 만들어서 하나의 칼럼에 저장하는 것이 더 의미상 더 타당하다는 것은 누구나 알고 있다.

또한 날짜와 관련된 많은 메서드와 제약사항들이 있기 때문에 이를 3개의 칼럼으로 나누어서 처리하는 것은 많은 불필요한 코드들을 필요로 한다. 예를 들면 월에 1월을 더하거나 빼는 연산과 같은 것들을 하나의 데이터 타입에 같이 넣어 두면 어디서나 손쉽게 끌어다 쓸 수 있다. SQL Server의 UDT는 데이터 자체뿐만 아니라 메서드도 같이 포함 할 수 있으므로 (사실 UDT는 클래스나 구조체로 정의한다) 이러한 구현이 가능하다.

자 그럼 여기서 이런 생각까지 하는 사람이 있을 수도 있다. “UDT를 클래스의 개념으로 볼 수 있으니, 이제는 객체를 그대로 DB에 저장 할 수 있다는 얘기군. 그럼 아예 사원(Employee) 객체를 통째로 DB에 저장해 볼까?” 여기까지 생각을 하면 “그동안 미들티어에서 했던 OR매핑(Object Relational Mapping)이 더 이상 필요 없는 진정한 객체지향의 DB가 탄생했군!” 이라고 생각하는 사람이 있을 수도 있다.

틀린 얘기는 아니다. 하지만 성능과 용량이 문제가 된다. UDT는 8KB라는 사이즈 제한이 있고, 인덱싱 처리의 제약, 그리고 데이터 업데이트시 부하가 있다. 그러므로 UDT는 그러한 복잡한 객체를 저장하는 데에는 적절하지 않다. 처음에 예를 들었던, 위도, 경도, 포인트와 같이 가벼운 객체를 저장할 때에만 이 UDT를 사용해야 한다.

UDT는 결국 클래스를 하드디스크에 저장하는 것이기 때문에 직렬화를 해야 한다. 직렬화를 위해서는 데이터의 크기가 중요하다. 기본적으로 .NET 환경에서는 값타입(Value Type)과 참조타입(Refernce Type)이라는 두 가지 타입이 있다. 값타입은 int, char과 같이 실제 데이터가 직접 있는 타입이고, 참조 타입은 string과 같이 실제 데이터가 아닌 데이터의 주소가 들어있는 타입을 말한다. 따라서 이들 데이터 타입에 따라 저장하는 방법도 달라진다.

값타입은 대부분 고정된 길이를 가지고 있으므로 컴파일러가 알아서 그 크기를 계산할 수 있지만, 참조 타입의 경우 그 크기가 얼마나 될지 모른다. 그래서 하드디스크에 얼마 정도의 공간을 할당해야 하는지 모르는 것이다. 그래서 참조 타입을 직렬화 하는 경우에는 사용자가 직접 그 방법을 정의를 해줘야 한다. 직렬화 방법을 정리해 보면 아래와 같이 3가지 방법이 있다.

◆ SerializedDataWithMetadata
값타입이나 참조 타입에 관계없이 어떤 데이터 타입도 저장 가능. 하지만 성능 면에서는 가장 느리다. 아마 Beta3에서는 없어질 포맷이다. 한마디로 사용하면 안 되는 포맷이다.
◆ Native
크기가 고정된 값타입의 데이터 형만 저장 가능. 가장 빠르다.
◆ UserDefined
값타입, 참조 타입 모두 사용가능. 하지만, 사용자가 데이터를 읽는 방법과 쓰는 방법을 정의해 주어야 한다.

위 3가지 포맷중 사용자 정의 포맷에서 읽기와 쓰기를 직접 구현하는 것이 간단하지가 않다. 약간 복잡하다. 그래서 이번 예제는 UDT를 소개하는 것이 목적이므로 Native 포맷을 이용하는 간단한 포인트 예제를 보여주려고 한다.

기존 CLREx프로젝트에 새로운 아이템으로 Point라는 사용자 정의 데이터 타입을 추가해 보자. 그러면 기본적인 코드들이 생성되어 있을 것이다. 모두 지우자. 현재 템플릿에서 생성된 코드는 옛날 방식의 코드이다. 기본적인 구조는 아래와 같다.

[Serializable]
[SqlUserDefinedType(Format.Native)]
public struct Point : INullable
{
  private Boolean is_null;
  private Int32 m_x;
  private Int32 m_y;

  // 기본 메서드
  public override string ToString() { ... }
  public bool IsNull { get; }
  public static Point Null { get; }
  public static Point Parse(SqlString s) {...}

  // 추가한 메서드
  public Int32 x {...}
  public Int32 x {...}
  public decimal DistanceTo(Point other) {...} // 두 포인트간 거리 구하기
}

이 메서드들을 채워주면 포인트 UDT가 완성 된다. 위의 가상코드를 보면 직렬화를 지원하고 Native 포맷으로 정의되어 있는 것을 볼 수 있을 것이다. 그리고 class가 아닌 struct로 선언한 것이 보일 것이다. 구지 class가 아닌 struct를 쓴 이유는 전통적으로 사용자 정의 데이터 타입은 구조체를 썼기 때문이다. 그 이유는 클래스는 힙에 데이터가 저장이 되지만 구조체는 그렇지가 않다. 따라서 클래스는 가비지 콜렉터가 쉽게 수거해 갈 수 있지만, 구조체는 그렇지 않다.

성능면에서 구조체가 약간 더 빠르다는 것이다. 또한 NULL값을 구현하는데 있어 구조체는 별도의 초기화 없이 기본적으로 모든 값을 기본값으로 초기화를 해준다. 예를 들면 숫자형은 모두 0으로 자동 초기화를 해준다. 그래서 데이터 형을 다루는 데에는 아무래도 클래스 보다는 구조체가 약간 더 편하다고 할 수 있다. SQL Server에서는 NULL이라는 값이 존재한다. 따라서 UDT를 만들때에는 NULL이라는 의미를 부여해 주어야 한다. 그래서 INullable 인터페이스를 상속받아서 NULL을 구현하고 있다.

포인트를 저장하기 위해서 X,Y값을 위한 공간을 마련하고 널값 체크를 위한 공간도 마련하였다. 그런데 사실 널값 체크를 위해서 위와 같이 별도의 저장 공간을 사용하는 것은 하드디스크 낭비가 될 수 있다. 그래서 어떤 사람들은 위와 같은 경우 Int32.MinValue를 널값 대신으로 사용하기도 한다. 즉 Int32의 최소값을 널값으로 대신 하는 것이다. 만약 포인트의 데이터형이 string형이면 이러한 불편이 없다. string형은 참조 타입이기 때문에 null이라는 값을 수용할 수 있기 때문이다.

Int32라는 데이터형은 값타입이기 때문에 NULL을 수용할 수가 없어 위와 같은 방법을 사용하였다. 어떤 방법을 사용하던 그것은 개발자의 몫이니 상황에 따라 적절한 방법을 사용하면 된다. 이번 예제에서는 하드디스크의 공간을 걱정 안해도 되므로 그냥 따로 널값 체크를 위한 데이터형을 따로 만들었다. 기본적인 메서드의 설명은 아래와 같다.

<표1> 기본적인 메쏘드 설명

실제 완성된 코드는 지면관계상 이달의 디스켓에 있으니 참고하기 바란다.
이제 이 UDT를 컴파일 하고 배포 하면 아래와 같이 테스트 할 수 있다.

DECLARE @a Point, @b Point;

IF @a is null
  PRINT 'null'
ELSE
  PRINT 'not null';

SET @a.x = 10;
SET @a.y = 20;

SET @b.x = 100;
SET @b.y = 110;

SELECT CAST(@a AS CHAR);
SELECT CAST(@b AS CHAR);

SELECT @a.DistanceTo( @b ); -- 두 점사이의 거리 구하기
-----------------------------------------------------------------
null
10:20
100:110
127

SUM, MAX와 같은 집합 함수만으로는 더 이상 충분하지 않다.
이번에 SQL Server의 CLR 통합 기능 중에서 제일 반가운 기능이 바로 이 기능이다. 기존에 MIN, MAX, SUM, COUNT, AVG 같은 집합 함수를 쓰다 보면 부족함을 느끼는 경우가 많다. 이러한 집합 함수가 있으면 좋은데... 하고 많은 사람들이 원했던 것이 사실이다. 이제는 이러한 집합 함수를 직접 만들어 쓸 수 있다. 만드는 방법은 UDT와 상당히 유사하다. 이번 예제에서는 최대 변이값을 구하는 함수를 만들 것이다. 즉, 최대값-최소값을 구하는 MaxVariance라는 함수 이다. 기존 프로젝트에 새로운 아이템으로 Aggregate를 추가하고 이미 있는 템플릿 코드는 역시 옛날 방식이므로 지운다. 기본적인 구조는 아래와 같다.

[Serializable]
[StructLayout(LayoutKind.Sequential)]
[SqlUserDefinedAggregate(Format.Native)]
public struct MaxVariance
{
  private Int32 m_LowValue;
  private Int32 m_HighValue;

  public void Init() {...}
  public void Accumulate(SqlInt32 Value) {...}
  public void Merge(MaxVariance Group){...}
  public SqlInt32 Terminate() { ... }
}

데이터형이 값타입 밖에 없으므로 Native 포맷으로 했으며, 최대값과 최소값을 저장하는 별도의 변수를 만들었다. 각 메소드별 설명은 아래와 같다.

<표2> 각 메쏘드별 설명

자세한 코드는 이달의 디스켓에 있으니 참고하기 바란다. 위의 사용자 정의 집합(User-Defined Aggregate)(UDA)을 컴파일 하고 배포한 후 아래와 같은 코드로 테스트 해 보자. 아래 코드는 전체 사원중에서 휴가시간이 가장 많은 사람과 가장 적은 사람의 차이를 나타낸 것이다.

SELECT dbo.MaxVariance(VacationHours)
FROM HumanResources.Employee;

SELECT MAX(VacationHours) - MIN(VacationHours)
FROM HumanResources.Employee;

-----------
99
(1 row(s) affected)
99
(1 row(s) affected)

위와 아래의 쿼리문을 대조해 보면 제대로 된 결과가 나왔음을 확인해 볼 수 있다.

클라이언트 ADO.NET의 개선점
이번에 ADO.NET 2.0으로 나오면서 SQL Server와 관련해서 크게 주목할 부분은 두가지가 있다. 하나는 비동기 호출기능과 하나의 연결로 다수의 커맨드를 실행하는 기능(Multiple Active Result Sets)(MARS)이다. 지난호에서 ADO.NET에서도 페이징 처리가 가능하다고 했는데, 그 기능이 이젠 없어질 예정이라서 이번에 제외했다.

더 이상 기다릴 필요 없는 비동기 호출
비동기 호출 기능은 기존에 쿼리 문장을 수행 시키고 결과가 올 때까지 기다려야 했단 불편을 없애고, 클라이언트는 결과가 올 때까지 나름대로의 작업을 할 수 있다. 그러므로 사용자는 쿼리 문장을 날리고 모레시계의 아이콘을 기다릴 필요 없이 다른 작업을 수행 할 수도 있다. 이때 처음 DB에 연결을 맺을 때 비동기 호출을 쓴다는 표시를 “Asynchronous Processing=true” 이와 같이 해주어야 한다. 간단한 예제를 보자. 전체 예제는 이달의 디스켓에 있다.

SqlConnection cnn = new SqlConnection(
  "Data Source=localhost;" +
    "Initial Catalog=AdventureWorks;" +
    "Integrated Security=SSPI;" +
    "Asynchronous Processing=true");

cnn.Open();
// 2초간의 딜레이 후 조회
SqlCommand cmd = new SqlCommand(
  "WAITFOR DELAY '00:00:02';SELECT * FROM Sales.Customer", cnn);

Console.WriteLine("작업 시작");
IAsyncResult iar = cmd.BeginExecuteReader();

while (!iar.IsCompleted) { Console.Write("*"); } //결과 올 때까지 별찍기

cmd.EndExecuteReader(iar);
Console.WriteLine("\n작업 끝");

--------------------------------------------------------------------
작업 시작
******************
작업 끝

위 예제는 고객 데이터를 조회 하는데 있어 비동기 호출을 이용하고 있다. 먼저 비동기 호출의 장점을 보려면 DB에서 시간이 오래 걸리는 작업을 돌려주어야 그 효과를 확실히 볼 수 있다. 그래서 2초간 딜레이를 주는 문장을 삽입하여 강제로 시간이 오래 걸리도록 하였다. 그리고 클라이언트는 결과가 올 때까지 계속 별을 찍다가 결과가 오면 끝내는 예제이다.

그런데 이번 예제에서는 간단히 하기 위해서 끝났는지 안 끝났는지를 알아보기 위하여 WHILE문에서 계속 체크를 하였지만, 실제 사용할 때에는 이렇게 할 필요 없이 비동기 콜백 함수를 만들어서 다 끝나면 저절로 그 함수가 호출되게 하는 것이 더 좋은 방법이 될 것이다.

하나의 연결로 다수의 쿼리 실행
기존 ADO.NET에서는 하나의 연결을 맺으면 하나의 커맨드만 실행가능 하였다. 그래서 다른 커맨드를 실행하려면 별도의 연결을 다시 맺어야만 했다. 하지만 이제는 하나의 연결로 다수의 커맨드를 실행 할 수 있다. 이렇게 함으로써 매번 새로운 연결을 안 맺어도 되므로 성능 향상이 있는 것이다. 구현하는 방법은 어렵지 않다. 그냥 쓰면 된다. 아래 예제를 보자.

// 하나의 연결
SqlConnection cnn = new SqlConnection(
  "Data Source=localhost;" +
  "Initial Catalog=AdventureWorks;" +
  "Integrated Security=SSPI;");
cnn.Open();

// 첫번째 실행
SqlCommand cmd1 = new SqlCommand(
  "SELECT * FROM Production.Location", cnn);
SqlDataReader dr1 = cmd1.ExecuteReader();

// 두번째 실행
SqlCommand cmd2 = new SqlCommand(
  "SELECT * FROM HumanResources.Department", cnn);
SqlDataReader dr2 = cmd2.ExecuteReader();

// 결과 출력
while (dr1.Read() == true && dr2.Read() == true)
{
  Console.WriteLine(dr1[0] + " | " + dr2[0] );
}
-------------------------------------------------------
1 | 1
2 | 2
3 | 3
4 | 4
5 | 5
.......

cmd1과 cmd2가 하나의 cnn이라는 연결을 공유해서 쓰고 있다. 전체 예제는 이달의 디스켓에 있다.

SQL Server의 변신은 무죄?
처음에 SQL Server가 닷넷 프레임웍(CLR)에 통합된다고 하였을 때 많은 사람들이 궁금증을 가지고 지켜보았다. 이제는 C#을 공부해야 하는가 하고 걱정하는 사람들도 있었다. 하지만 막상 뚜껑을 열어보니 CLR통합 이라는 기능은 T-SQL을 대체하는 기능이 아닌 좀더 확장하고 보강하기 위한 기능으로 보는 것이 좋다는 결과가 나왔다. SQL Server를 개발하는데 있어 기본은 T-SQL이다. 하지만 거기서 멈추지 않고 더욱 새로운 기능을 추가하고 확장하고 싶다면 .NET을 이용하면 된다. 다음 글에서는 DB 관리툴과 보안에 대해 소개할 것이다.@

* 이 기사는 ZDNet Korea의 제휴매체인 마이크로소프트웨어에 게재된 내용입니다.

+ Recent posts