지난 시간에는 쓰레드를 통한 비동기 프로그래밍의 원리와 구현에 대해 알아봤다. 이번 시간에는 쓰레드를 효과적으로 관리하기 위한 쓰레드 풀과 함께 서버 제작에 필요한 네트워크 기술을 설명한다. 쓰레드 풀을 이용한 비동기 프로그래밍과 소켓의 개념에 대해 알아 보고 최종적으로 게임 서버를 완성해 보자.
비동기 프로그래밍은 내부적으로 쓰레드를 이용한다. 그런데 비동기 호출을 할 때마다 새로운 쓰레드를 생성해서 작업을 하게 되면, 많은 비동기 호출이 일어날 때에는 쓰레드의 수가 너무 많아져서 오히려 컨텍스트 스위칭(context switching)하는 데 시간이 더 걸리는 현상이 일어난다. 이러한 현상을 해결하기 위해서는 적절한 쓰레드 수를 유지하는 것이 해결 방법이라 할 수 있을 것이다. 닷넷에서는 그러한 관리 방법으로 쓰레드 풀이라는 것을 이용한다. 이를 이용해 시스템의 CPU 사용량에 따라 항상 적절한 쓰레드 수를 유지시켜 준다.
쓰레드 풀이란 먼저 풀(pool)의 사전적인 의미는 스위밍 풀(swimming pool)처럼 물 웅덩이, 저수지라는 뜻이 있다. 다른 뜻으로는 카 풀(car pool)처럼 공동으로 이용하는 것이라는 뜻이 있다. 여기서는 두 번째의 공동으로 이용한다는 의미이다. 카 풀이라는 것이 에너지 절약을 위해서 이웃끼리 통근 시간 같은 때에 차를 같이 이용하는 것을 말한다. 쓰레드 풀도 이와 비슷한 것으로 쓰레드들이 시스템의 효율성을 높이기 위하여 집합적으로 모여 있는 것을 쓰레드 풀이라고 부른다.
쓰레드 풀은 쓰레드 생성 요청이 있을 때마다 그 쓰레드를 바로 생성하는 것이라 일단 큐에 그 쓰레드를 넣어 두었다가 쓰레드 풀이 그 요청을 처리할 수 있는 여유가 있을 때 큐에서 하나씩 꺼내서 처리를 한다. 닷넷 환경에서는 기본적으로 쓰레드 풀 안에서의 최대 25개의 쓰레드를 넣어 둘 수 있다. 이를 그림으로 나타내면 <그림 1>과 같다.
| <그림 1> 쓰레드 풀 |
쓰레드 풀의 사용 방법 닷넷에서 쓰레드 풀을 이용하기 위해서는 쓰레드 풀 클래스를 이용하면 된다. <리스트 1>은 쓰레드 풀 클래스의 메쏘드들이다.
| { // Constructors // Methods public static bool BindHandle(IntPtr osHandle); public virtual bool Equals(object obj); public static void GetAvailableThreads(ref Int32 workerThreads, ref Int32 completionPortThreads); public virtual int GetHashCode(); public static void GetMaxThreads(ref Int32 workerThreads, ref Int32 completionPortThreads); public Type GetType(); public static bool QueueUserWorkItem( System.Threading.WaitCallback callBack); public static bool QueueUserWorkItem( System.Threading.WaitCallback callBack, object state); public static System.Threading.RegisteredWaitHandle RegisterWaitForSingleObject( System.Threading.WaitHandle waitObject, System.Threading.WaitOrTimerCallback callBack, object state, int millisecondsTimeOutInterval, bool executeOnlyOnce); public static System.Threading.RegisteredWaitHandle RegisterWaitForSingleObject( System.Threading.WaitHandle waitObject, System.Threading.WaitOrTimerCallback callBack, object state, UInt32 millisecondsTimeOutInterval, bool executeOnlyOnce); public static System.Threading.RegisteredWaitHandle RegisterWaitForSingleObject( System.Threading.WaitHandle waitObject, System.Threading.WaitOrTimerCallback callBack, object state, long millisecondsTimeOutInterval, bool executeOnlyOnce); public static System.Threading.RegisteredWaitHandle RegisterWaitForSingleObject( System.Threading.WaitHandle waitObject, System.Threading.WaitOrTimerCallback callBack, object state, TimeSpan timeout, bool executeOnlyOnce); public virtual string ToString(); public static bool UnsafeQueueUserWorkItem( System.Threading.WaitCallback callBack, object state); public static System.Threading.RegisteredWaitHandle UnsafeRegisterWaitForSingleObject( System.Threading.WaitHandle waitObject, System.Threading.WaitOrTimerCallback callBack, object state, int millisecondsTimeOutInterval, bool executeOnlyOnce); public static System.Threading.RegisteredWaitHandle UnsafeRegisterWaitForSingleObject( System.Threading.WaitHandle waitObject, System.Threading.WaitOrTimerCallback callBack, object state, UInt32 millisecondsTimeOutInterval, bool executeOnlyOnce); public static System.Threading.RegisteredWaitHandle UnsafeRegisterWaitForSingleObject( System.Threading.WaitHandle waitObject, System.Threading.WaitOrTimerCallback callBack, object state, long millisecondsTimeOutInterval, bool executeOnlyOnce); public static System.Threading.RegisteredWaitHandle UnsafeRegisterWaitForSingleObject( System.Threading.WaitHandle waitObject, System.Threading.WaitOrTimerCallback callBack, object state, TimeSpan timeout, bool executeOnlyOnce); } // end of System.Threading.ThreadPool
|
| |
<리스트 1>을 보면 거의 모든 멤버가 static이고 public constructor가 없음을 볼 수 있을 것이다. 이는 닷넷에서는 하나의 프로세스당 한 개의 풀만을 허용하기 때문이다. 즉 모든 비동기 호출은 같은 하나의 풀을 통해 이뤄진다. 따라서 제 3자의 컴포넌트가 새로운 풀을 만들어서 기존의 풀과 함께 돌아감으로써 생기는 오버헤드를 줄일 수 있는 것이다. 쓰레드 풀의 큐에 새로운 쓰레드를 추가시키려면 다음과 같은 메쏘드를 이용한다.
public static bool QueueUserWotkItem ( WaitCallBack callBack ,object state);
우선 WaitCallBack이라는 대리자를 이용하여 처리할 함수를 등록하고, state를 이용하여 함께 넘길 파라미터를 지정해 준다.
public delegate void WaitCallBack( object state );
WaitCallBack 대리자의 형식이 반환 값은 없고, 인자로는 state 하나만을 받는 형식이다. 따라서 쓰레드를 사용할 함수는 이와 같은 signature를 가져야 한다. 이를 이용하여 0부터 3까지 출력하는 3개의 작업을 만들어 보자. 이를 실행하면 다음과 같이 보통 일반 쓰레드를 이용하는 것과 비슷한 결과 화면을 볼 수 있다. 3개의 작업이 동시에 이뤄지고 있다.
1번 작업 : 0 2번 작업 : 0 1번 작업 : 1 3번 작업 : 0 2번 작업 : 1 1번 작업 : 2 3번 작업 : 1 2번 작업 : 2 1번 작업 : 3 3번 작업 : 2 2번 작업 : 3 1번 작업 끝 3번 작업 : 3 2번 작업 끝 3번 작업 끝
이번에는 Thread.IsThreadPoolThread이라는 속성을 이용하여 정말 쓰레드 풀을 이용하고 있는지와, 현재 쓰레드의 고유 번호를 나타내주는 메쏘드인 GetHashCode를 이용하여 그 값을 확인해 보자.
| <리스트 2> 0부터 3까지 출력하는 4개의 작업 |
| |
| class Class1 { [STAThread] static void Main(string[] args) { WaitCallback callBack;
callBack = new WaitCallback(Calc); ThreadPool.QueueUserWorkItem(callBack,1); ThreadPool.QueueUserWorkItem(callBack,2); ThreadPool.QueueUserWorkItem(callBack,3); Console.ReadLine(); } static void Calc(object state) { for(int i= 0; i < 4; i++) { Console.WriteLine(“{0}번 작업: {1}”,state,i); Thread.Sleep(1000); } Console.WriteLine(“{0}번 작업 끝”,state); } }
|
| |
<리스트 2>에 <리스트 3>과 같은 코드를 추가한다. 결과는 다음과 같다.
Main thread. Is Pool thread:False, Hash : 2 1번 작업 thread. Is Pool thread:True, Hash : 7 1번 작업 : 0 2번 작업 thread. Is Pool thread:True, Hash : 8 2번 작업 : 0 1번 작업 : 1 3번 작업 thread. Is Pool thread:True, Hash : 9 3번 작업 : 0 2번 작업 : 1 1번 작업 : 2 3번 작업 : 1 2번 작업 : 2 1번 작업 : 3 3번 작업 : 2 2번 작업 : 3 1번 작업 끝 3번 작업 : 3 2번 작업 끝 3번 작업 끝
즉 메인 쓰레드는 쓰레드 풀에서 하는 작업이 아니며, 나머지는 쓰레드 풀 내에서 작업하고 있음을 볼 수 있을 것이다. 그리고 각자 다른 해시코드를 가지고 있으므로 각자 새로운 쓰레드를 생성해서 작업하고 있는 것이다. 이는 현재 CPU 사용량에 여유가 있었기 때문에 각자 하나씩의 쓰레드를 생성해서 작업을 한 것이다.
| // 메인 부분에 추가 Console.WriteLine(“Main thread. Is Pool thread:{0}, Hash: {1}”, Thread.CurrentThread.IsThreadPoolThread, Thread.CurrentThread.GetHashCode());
// 쓰레드 작업 부분에 추가 Console.WriteLine(“{0}번 작업 thread. Is Pool thread:{1}, Hash: {2}”, state, Thread.CurrentThread.IsThreadPoolThread, Thread.CurrentThread.GetHashCode());
|
| |
만약 CPU 사용량이 많아져서 컨텍스트 스위칭 시간이 더 걸릴거라 판단되면, 다른 쓰레드들은 큐에서 대기하다가 기존 작업이 끝나고 그 쓰레드를 재사용해서 작업을 하게 된다. 이에 대한 예를 보자. CPU 사용량을 높이려면 다음과 같은 함수를 추가한다.
int ticks = Environment.TickCount; while( Environment.TickCount - ticks < 500 );
Environment.TickCount 속성은 마지막 리부팅한 후부터의 시간을 millisecond 단위로 리턴해 준다. Thread.Sleep(1000)이라는 부분 대신 이 함수를 넣고 실행해 보면 <화면 1>과 비슷한 결과를 볼 수 있다.
| <화면 1> CPU 사용량을 높인 후의 쓰레드 푸 작동 화면 |
<화면 1>을 보면 CPU 사용량이 100%임을 확인할 수 있다. 그리고 결과를 보면 3번 작업이 1번 작업과 같은 해시코드를 사용하고 있다. 즉 같은 쓰레드를 재사용하고 있는 것이다. 그래서 1번 작업이 끝난 후에, 1번 작업이 쓰던 쓰레드를 3번 작업이 다시 사용하고 있는 것이다. 이처럼 쓰레드 풀이라는 것은 현재 시스템의 상황에 따라 적절히 쓰레드 개수를 유지시켜 줌으로써 효율성을 높이고 있다. 그럼 이제 정말 비동기 호출이 쓰레드 풀을 이용하는지 확인해 보자.
| class Class1 { public static void Calc() {
Console.WriteLine(“Is pool:{0}”, Thread.CurrentThread. IsThreadPoolThread); for(int i=1; i < 10 ; i++) { for(int j=0; j < 10000000 ; j++) {} // 많은 계산을 요구하는 작업 Console.WriteLine(“SubWork :{0}”,i); } }
[STAThread] static void Main(string[] args) { SubWork d = new SubWork(Calc); d.BeginInvoke(null,null); for(int i=0; i < 10 ; i++) { for(int j=0; j < 10000000 ; j++) {} // 많은 계산을 요구하는 작업 Console.WriteLine(“MainWork:{0}”,i); } }
|
| |
<리스트 4>는 지난 시간에 했던 예제이다. 그곳에 단지 쓰레드 풀임을 확인할 수 있는 문장을 하나 추가했을 뿐이다. 결과는 다음과 같다.
Is pool:True SubWork : 1 MainWork : 0 SubWork : 2 MainWork : 1 SubWork : 3 MainWork : 2 SubWork : 4 MainWork : 3 MainWork : 4 SubWork : 5 SubWork : 6 MainWork : 5 SubWork : 7 MainWork : 6 SubWork : 8 MainWork : 7 SubWork : 9 MainWork : 8 MainWork : 9
이제 비동기 호출이 쓰레드 풀을 이용하는 것이 확실해졌다. 그러면 모든 비동기 호출은 이와 같이 델리게이트를 만들어서 해야 할까? 그것은 아니다. 우리가 매번 비동기 호출을 위해서 델리게이트를 만들어야 한다면 그것 또한 귀찮은 일일 것이다.
그래서 닷넷에서는 미리 비동기 호출을 위한 함수들을 마련해 두고 있다. Begin×××, End×××로 표기되는 메쏘드들이 비동기 호출을 위해 미리 만들어 둔 함수들이다. 델리게이트의 BeginInvoke도 이와 같은 연장선에 보면 될 것이다. 우리는 소켓을 이용한 비동기 통신 방법에 대해 알아볼 것이므로 소켓과 관련된 비동기 함수들을 살펴볼 것이다. 그전에 소켓의 기본 개념부터 설명하겠다.
소켓이란? 일반적인 의미로 소켓이란, 전구의 소켓처럼 꽂는 구멍을 말한다. 즉 다른 것과 연결시켜 주는 구멍이다. 컴퓨터에서의 소켓도 이와 비슷한 의미이다. 네트워크의 다른 대상과 정보를 교환하기 위한 구멍인 것이다. 일종의 네트워크 자료 교환을 위한 파이프라인(pipeline)으로 생각하면 된다.
일반적으로 네트워크에서 정보를 주고받기 위한 주소로 IP 어드레스라는 것을 사용한다. 그런데 이 주소는 대개 하나의 컴퓨터에 한 개의 주소가 할당된다. 그런데 네트워크 정보 교환은 하나의 컴퓨터뿐만 아니라 여러 컴퓨터와 정보를 주고받아야 하므로 하나의 IP 주소로는 이 정보를 어디로 보내야 하는지 구분할 수 없다.
그래서 포트(Port)라는 개념을 쓴다. 이는 항구라는 뜻으로 각 네트워크 정보들이 통신하는 입구인 것이다. 일반적으로 HTTP는 80 포트를 사용하고, FTP는 21 포트를 사용한다. 그래서 어떤 한 컴퓨터에 네트워크 데이터를 보내더라도 포트 번호가 다르므로, HTTP용 데이터와 FTP용 데이터가 각각 제 자리를 찾아가는 것이다. 이를 그림으로 나타내면 <그림 2>와 같다.
| <그림 2> 포트의 개념 |
일반적으로 포트 번호는 0∼65535까지 쓸 수 있지만 0∼1024번까지는 80번이나 21번처럼 미리 정해진 포트 번호를 사용하므로 사용자가 임의의 포트 번호를 사용하려면 그 이상의 번호를 사용하면 된다.
<그림 2>를 보면 포트에 소켓이 연결되어 있음을 볼 수 있을 것이다. 특히 서버쪽을 보면 하나의 포트에 여러 개의 소켓이 달려있음을 볼 수 있을 것이다. 이는 다중의 클라이언트가 하나의 포트로 접속하기 때문이다. 각 클라이언트마다 이들의 데이터를 맡아서 중개해주는 파이프라인(소켓)이 따로 있어야 하기 때문에 하나의 포트에 여러 개의 소켓이 달려 있는 것이다.
그런데 여기서 한 가지 의문점이 있을 수 있다. 하나의 포트에 여러 개의 네트워크 데이터들이 몰려들어 올텐데 서버는 이를 어떻게 구분해서 각자의 전담 파이프라인(소켓)으로 보내주는 것일까? 이는 TCP/IP의 헤더를 보면 쉽게 해결이 된다.
| <표 1> TCP/IP 헤더 |
<표 1>을 보면 IP 프로토콜의 헤더에는 보내는 곳과 받는 곳의 IP 주소가 들어 있다. 한편 TCP 헤더에는 보내는 곳과 받는 곳의 포트 번호가 들어 있다. 이들 4가지의 정보는 서로의 데이터를 확실히 구분하는 기준이 되므로, 서버측에서는 이 헤더를 보고 각자에 맞는 소켓으로 데이터를 보내주는 것이다.
다시 <그림 2>를 보면 서버측의 포트 번호는 지정되어 있는 반면에 클라이언트측의 포트 번호는 일관성 없이 중구난방으로 아무 번호나 할당되어 있음을 볼 수 있을 것이다. 그 이유는 클라이언트 입장에서는 데이터를 보내야 하는 서버측의 포트 번호는 알아야 하지만 자신의 포트 번호는 그냥 비어있는 아무 번호나 써도 상관없다. 굳이 자신의 포트 번호를 미리 정하지 않아도 되는 것이다. 그래서 클라이언트가 서버로 연결할 때, 자신의 남는 포트 번호 중 아무나 한 개를 할당해서 소켓과 연결시켜 주는 것이다.
소켓의 구현 과정 소켓은 <그림 3>과 같은 일련의 과정을 거쳐 작업이 진행된다. 먼저 서버측에서는 소켓을 생성하고 그 소켓을 특정 포트 번호와 연결(bind)시킨다. 그리고 상대방으로 연결이 오기를 허락하는 듣기(listen) 작업을 수행한다. 그러다가 클라이언트가 접속을 하게 되면 서버는 이를 받아들이고(accept) 새로운 소켓을 만들어서 그 새로운 소켓이 계속 통신을 담당하게 하고 자신은 다시 듣기(lisetn)상태로 들어간다.
| <그림 3> 소켓의 구현 과정 |
그런데 이 때 한 가지 주의할 것이 있다. 하나의 포트에는 한 개의 소켓만 bind할 수 있다는 것이다. 여기서 조심해야 할 것이 bind라는 말이다. 하나의 포트에 여러 개의 소켓이 있을 수는 있지만 bind는 오직 한 개만 된다. 하나의 포트에 두 개의 소켓을 bind하려 하면 에러가 나면서 bind가 실패하게 된다.
그럼 왜 bind는 하나만 되는 것일까? 그 이유는 앞에서 보았듯이 데이터를 구분할 방법이 없기 때문이다. 데이터를 구분할 때 TCP/IP 헤더를 보고 구분한다고 했다. 그런데 하나의 포트에 두 개 이상의 소켓이 bind되면 이들 데이터를 구분할 방법이 없는 것이다.
예를 들어 <그림 2>에서 80번 포트에 HTTP와 FTP용 소켓 두 개를 bind시켰다고 해보자. 그러면 서버는 포트로 들어오는 패킷의 TCP/IP 헤더 정보를 보고 데이터를 구분하는데 그 헤더에는 IP와 포트 번호밖에 없다. 그래서 이 패킷이 HTTP용인지 FTP용인지 구분할 방법이 없는 것이다. 그래서 하나의 포트번호에는 하나의 소켓만 bind할 수 있다. 그러면 이제 실제로 간단한 소켓 통신 프로그램을 만들어 보자.
| [STAThread] static void Main(string[] args) { Socket listeningSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
IPAddress hostadd = Dns.Resolve(“localhost”).AddressList[0]; IPEndPoint EPhost = new IPEndPoint(hostadd, 7000);
listeningSocket.Bind(EPhost);
listeningSocket.Listen( 10 ); Console.WriteLine(listeningSocket.LocalEndPoint + “에서 접속을 listening하고 있습니다.”); Socket newSocket;
while(true) { newSocket = listeningSocket.Accept(); // blocking Console.WriteLine(newSocket.RemoteEndPoint.ToString() + “에서 접속하였습니다.”); byte[] msg = Encoding.Default.GetBytes(“접속해 주셔서 감사합니다.”); int i = newSocket.Send(msg); } }
|
| |
간단한 소켓 예제 <리스트 5>는 서버 소켓 예제이다. 먼저 TCP 방식의 소켓을 생성하고 7000번 포트에 bind한 후 listen하고 있다. 그러다가 클라이언트가 접속을 하게 되면, 클라이언트의 주소를 표시해 주고 메시지를 전송해 주고 있다.
| [STAThread] static void Main(string[] args) { Socket s = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); IPAddress hostadd = Dns.Resolve(“localhost”).AddressList[0]; IPEndPoint EPhost = new IPEndPoint(hostadd, 7000); s.Connect(EPhost); // blocking if ( s.Connected == true) { byte[] bytes = new byte[1024]; s.Receive(bytes); // blocking Console.WriteLine(Encoding.Default.GetString(bytes)); s.Shutdown(SocketShutdown.Both); s.Close(); } }
|
| |
<리스트 6>은 클라이언트의 코드이다. 클라이언트는 특정 포트와 bind할 필요가 없으므로 connect할 때 자동으로 임의의 포트가 할당된다. 서버로의 접속이 성공하면 메시지를 받아서 화면에 표시해 준다. 그럼 이제 이들의 결과 화면을 보자.
◆ 서버 화면 127.0.0.1:7000에서 접속을 listening하고 있습니다. 127.0.0.1:3912에서 접속하였습니다.
◆ 클라이언트 화면 접속해 주셔서 감사합니다.
서버 화면을 보면 클라이언트 측에서는 임의의 포트 번호에 소켓을 할당해서 접속하고 있다는 것을 확인할 수 있을 것이다. 그러면 이제 정말 하나의 포트에 여러 개의 소켓이 존재하는지 보자. 먼저 클라이언트를 두 개 실행시켜 서버에 접속하도록 하자. 다음은 서버 화면이다.
127.0.0.1:7000에서 접속을 listening하고 있습니다. 127.0.0.1:3916에서 접속하였습니다. 127.0.0.1:3917에서 접속하였습니다.
두 개의 클라이언트를 실행시켜서 7000번 포트에 두 개의 클라이언트가 접속을 했다. 이제 netstat -a라는 명령어를 ‘명령프롬프트’창에서 입력해 네트워크 상태를 확인해 보자.
C:\>netstat -a
Active Connections
Proto Local Address Foreign Address State TCP 한용희:7000 한용희:0 LISTENING TCP 한용희:7000 한용희:3916 CLOSE_WAIT TCP 한용희:7000 한용희:3917 CLOSE_WAIT
앞의 화면에서 다른 부분은 생략하고, 우리가 보기를 원하는 화면만 표시를 했다. 현재 로컬 컴퓨터의 7000번 포트의 상태를 보면 listening하는 상태가 있고, 이미 연결된 두 개의 정보가 나온다. 모두 같은 7000번 포트에 연결된 것들이다. 이로써 하나의 포트에 여러 개의 소켓이 있을 수 있다는 것을 확인할 수 있을 것이다.
이제 소켓에 대한 궁금증을 풀었다. 그런데 앞의 예제를 응용해서 게임 서버로 만들기에는 무리가 있다. 왜냐하면 accept할 때나 receive할 때 블러킹이 걸려서 다른 일을 하지 못하기 때문이다. 그러므로 우리가 지금껏 익혀온 비동기 호출을 이용해서 이 문제를 해결해 보자.
비동기 소켓 통신을 이용해 블러킹 해결 앞서 닷넷에서는 델리게이트를 따로 이용하지 않고서도 미리 준비된 Begin×××와 End×××를 이용해서 비동기 프로그래밍을 할 수 있다고 했다. 이를 이용해 앞서 만든 예제에 적용해 보자(<리스트 7>).
| class Class1 { static void AcceptCallBack(IAsyncResult ar) { Socket listener = (Socket)ar.AsyncState; Socket newSocket = listener.EndAccept( ar ); Console.WriteLine(newSocket.RemoteEndPoint.ToString() + “에서 접속하였습니다.”); byte[] msg = Encoding.Default.GetBytes(“접속해 주셔서 감사합니다.”); int i = newSocket.Send(msg);
} [STAThread] static void Main(string[] args) { Socket listeningSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
IPAddress hostadd = Dns.Resolve(“localhost”).AddressList[0]; IPEndPoint EPhost = new IPEndPoint(hostadd, 7000); listeningSocket.Bind(EPhost);
listeningSocket.Listen( 10 ); Console.WriteLine(listeningSocket.LocalEndPoint + “에서 접속을 listening하고 있습니다.”);
while(true) { IAsyncResult ar = listeningSocket.BeginAccept ( new AsyncCallback(AcceptCallBack), listeningSocket); // non-blocking ar.AsyncWaitHandle.WaitOne(); } } }
|
| |
먼저 예제에서 블러킹이 되었던 accept 부분을 비동기 함수인 BeginAccept로 바꾸었을 뿐 결과는 동일하다. 만약 이 프로그램을 윈도우폼으로 만들었다면 accept할 때 윈도우가 움직이는 것을 보면 확실히 블러킹되지 않았다는 것을 확인할 수 있을 것이다. 그러나 여기서는 간결한 예제를 위해서 콘솔 프로그램으로 만들었다.
<리스트 8>은 클라이언트를 비동기 방식으로 수정한 것이다. 이번에는 connect와 receive 두 개를 비동기 방식으로 만들었다. 결과는 먼저 예제와 동일하다. 이 예제들은 간단하기 때문에, 별 어려움이 없을 것이라 생각한다. 그러면 이 비동기 통신이 쓰레드 풀을 이용하는지 직접 확인해 보고 쓰레드 풀에 남아 있는 쓰레드의 갯수에 대해 알아보자.
| class Class1 { static byte[] bytes = new byte[1024]; static void ConnectCallBack(IAsyncResult ar) { Socket s = (Socket)ar.AsyncState;
if ( s.Connected == true) { s.BeginReceive(bytes, 0, bytes.Length, SocketFlags.None, new AsyncCallback( ReceiveCallBack) , s); // non-blocking } } static void ReceiveCallBack(IAsyncResult ar) { Socket s = (Socket)ar.AsyncState;
int nLength = s.EndReceive(ar); if ( nLength > 0 ) // 0보다 작다면 접속이 끊어진 것이다. { Console.WriteLine(Encoding.Default.GetString( bytes ) ); } }
[STAThread] static void Main(string[] args) { Socket s = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); IPAddress hostadd = Dns.Resolve(“localhost”).AddressList[0]; IPEndPoint EPhost = new IPEndPoint(hostadd, 7000);
s.BeginConnect(EPhost, new AsyncCallback(ConnectCallBack) , s); // non-blocking
Console.ReadLine(); s.Shutdown(SocketShutdown.Both); s.Close(); } }
|
| |
I/O completion ports <리스트 8>에 다음과 같은 코드를 추가해서 <리스트 9>와 같이 현재 쓰레드의 상태에 대해 알아보자. ShowThreadInfo()라는 함수를 만들었다. 이는 현재 쓰레드의 해시코드, 쓰레드 풀인지 여부, 그리고 남아있는 쓰레드 풀의 여분 갯수를 표시한다. 앞서 쓰레드 풀은 시스템에 따라 적절한 쓰레드 갯수를 유지시켜 준다고 했다.
| <리스트 9> 쓰레드의 상태를 알아보기 위한 코드 |
| |
| static void ShowThreadsInfo() { int workerThreads, completionPortThreads; Console.WriteLine(“Thread HashCode: {0}”, Thread.CurrentThread.GetHashCode()); Console.WriteLine(“Is Thread Pool? : {0}”, Thread.CurrentThread.IsThreadPoolThread);
ThreadPool.GetAvailableThreads(out workerThreads, out completionPortThreads); Console.WriteLine(“Available Threads”); Console.WriteLine(“WorkerThreads: {0}, CompletionPortThreads: {1}”, workerThreads, completionPortThreads); Console.WriteLine(); }
static void ConnectCallBack(IAsyncResult ar) { Socket s = (Socket)ar.AsyncState; ShowThreadsInfo(); if ( s.Connected == true) { s.BeginReceive(bytes, 0, bytes.Length, SocketFlags.None, new AsyncCallback( ReceiveCallBack) , s); // non-blocking } Thread.Sleep(2000); }
static void ReceiveCallBack(IAsyncResult ar) { Socket s = (Socket)ar.AsyncState;
ShowThreadsInfo();
int nLength = s.EndReceive(ar); if ( nLength > 0 ) // 0보다 적다면 접속이 끊어진 것이다. { Console.WriteLine(Encoding.Default.GetString( bytes ) ); } }
|
| |
기본적으로 25개가 최대인데 쓰레드 풀에서 쓰레드를 하나씩 돌릴 때마다 이 최대 수치는 줄어들게 된다. 이를 표시해 주는 함수가 GetAvailableThreads라는 함수이다. 앞에서 Connect의 콜백 함수의 경우 비동기 호출 후 바로 끝나는 것을 막기 위해서 2초간 잠시 잠을 재웠다. 어떤 결과가 나올 것인가? 그냥 생각하기로는 connect에서 쓰레드 하나 쓰고 receive에서 쓰레드 하나 쓰니 남아있는 쓰레드 갯수는 23개가 돼야 할 것이다. 과연 그럴까?
Thread HashCode : 30 Is Thread Pool? : True Available Threads WorkerThreads : 24, CompletionPortThreads : 25
Thread HashCode : 33 Is Thread Pool? : True Available Threads WorkerThreads : 24, CompletionPortThreads : 24
쓰레드 풀인 것은 확인이 됐고 문제는 남아있는 쓰레드 개수이다. 비동기 호출인 receive를 했는데도 WorkerThread 개수가 변함이 없다. 대신 completionPortThread에서 숫자가 하나 줄었다. 왜 이런 현상이 일어나는 것일까? 그것은 또 다른 쓰레드 풀을 사용했기 때문이다.
앞에서 프로세스당 하나의 쓰레드 풀이 존재한다고 했는데 사실 하나가 더 존재한다. 그것은 바로 I/O 전용으로 또 하나의 쓰레드 풀, 즉 I/O completion 포트용 쓰레드 풀이다. 이는 I/O 작업 전용의 쓰레드 풀로서 I/O 작업을 완료했는지 안 했는지에 대한 체크를 담당하게 된다. 그럼 왜 I/O 전용 쓰레드 풀을 사용하는 것일까? 이것을 쓰는 것이 성능이 더 좋기 때문이다.
그러나 이 기능을 사용하려면 Winsock에서 이 기능을 지원해야만 한다. 그래서 앞 프로그램을 윈도우 95나 윈도우 98에서 실행하면 이들 운영체제의 Winsock에는 이 기능이 없기 때문에 닷넷에서는 자동으로 I/O completion 포트용 쓰레드 풀 대신에 workerThread를 이용해서 처리를 하게 해 준다.
그러나 윈도우 NT/2000/XP의 경우 Winsock2가 설치돼 있는데 이 Winsock2가 IOCP 기능을 지원하므로 별도의 IOCP용 쓰레드 풀을 가동해서 일을 처리하게 된다. 과거 비주얼 C++로 IOCP를 구현하려면 복잡하게 코딩을 해야 했으나 닷넷에서는 손쉽게 비동기 호출 중, 네트워크 I/O 관련 함수를 호출하면 자동으로 IOCP를 이용하게 되어 있어 보다 손쉽게 코딩을 할 수 있다.
Deadlocks 비동기 함수를 이용하는 데 있어 한 가지 주의 사항이 있다. <리스트 10>을 보자.
| class ConnectionSocket { public void Connect() { IPHostEntry ipHostEntry = Dns.Resolve( “localhost”); IPEndPoint ipEndPoint = new IPEndPoint( ipHostEntry.AddressList[0], 7000 );
Socket s= new Socket( ipEndPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp ); IAsyncResult ar = s.BeginConnect(ipEndPoint, null, null); s.EndConnect(ar); Console.WriteLine(“비동기 호출 완료”); } }
class Class1 { [STAThread] static void Main(string[] args) { for(int i=0; i < 30 ; i++) { ThreadPool.QueueUserWorkItem(new WaitCallback(PoolFunc) ); } Console.WriteLine(“ThreadPool큐에 30개 적재”); Console.ReadLine(); } static void ShowThreadsInfo() { int workerThreads, completionPortThreads;
Console.WriteLine(“Thread HashCode: {0}” ,Thread.CurrentThread.GetHashCode()); ThreadPool.GetAvailableThreads(out workerThreads, out completionPortThreads); Console.WriteLine(“WorkerThreads: {0}, CompletionPortThreads: {1}”, workerThreads, completionPortThreads); Console.WriteLine(); }
static void PoolFunc(object state) { ShowThreadsInfo(); ConnectionSocket connection = new ConnectionSocket(); connection.Connect(); } }
|
| |
먼저 비동기 호출을 하는 connectionSocket이라는 클래스를 만들었다고 하자. 그런데 어떤 사람이 이 클래스를 쓰면서 이를 쓰레드 풀 내에서 호출하기로 했다. 그래서 그는 30개 쓰레드를 연속으로 만들고 이를 쓰레드 풀에 적재하였다. 그리고 각 쓰레드가 비동기 호출을 하는 이 클래스를 사용하였다. 서버는 먼저 만든 서버를 그대로 이용하기로 하자. 어떤 결과가 나올 것인가? 다음 결과를 보자.
ThreadPool큐에 30개 적재 Thread HashCode : 3 WorkerThreads : 24, CompletionPortThreads : 25 Thread HashCode : 18 WorkerThreads : 23, CompletionPortThreads : 25
Thread HashCode : 19 WorkerThreads : 22, CompletionPortThreads : 25
Thread HashCode : 1 WorkerThreads : 21, CompletionPortThreads : 25
Thread HashCode : 20 WorkerThreads : 20, CompletionPortThreads : 25
Thread HashCode : 21 WorkerThreads : 19, CompletionPortThreads : 25 Thread HashCode : 22 WorkerThreads : 18, CompletionPortThreads : 25
Thread HashCode : 23 WorkerThreads : 17, CompletionPortThreads : 25
Thread HashCode : 24 WorkerThreads : 16, CompletionPortThreads : 25 Thread HashCode : 25 WorkerThreads : 15, CompletionPortThreads : 25
Thread HashCode : 26 WorkerThreads : 14, CompletionPortThreads : 25
Thread HashCode : 29 WorkerThreads : 11, CompletionPortThreads : 25
Thread HashCode : 30 WorkerThreads : 10, CompletionPortThreads : 25
Thread HashCode : 31 WorkerThreads : 9, CompletionPortThreads : 25
Thread HashCode : 32 WorkerThreads : 8, CompletionPortThreads : 25
Thread HashCode : 33 WorkerThreads : 7, CompletionPortThreads : 25 Thread HashCode : 34 WorkerThreads : 6, CompletionPortThreads : 25 Thread HashCode : 35 WorkerThreads : 5, CompletionPortThreads : 25
Thread HashCode : 36 WorkerThreads : 4, CompletionPortThreads : 25
Thread HashCode : 17 WorkerThreads : 3, CompletionPortThreads : 25
Thread HashCode : 4 WorkerThreads : 2, CompletionPortThreads : 25
Thread HashCode : 5 WorkerThreads : 1, CompletionPortThreads : 25
Thread HashCode : 7 WorkerThreads : 0, CompletionPortThreads : 25
실행을 해 보면 프로그램이 멈춰버릴 것이다. WorkerThread가 0이 되면서 프로그램이 더 이상 작동 안 하는 데드록(deadlock) 현상이 일어난다. 왜 이런 현상이 일어나는 것일까? 먼저 어떤 사용자가 쓰레드 풀을 사용하면서 30개의 쓰레드를 쓰레드 풀에 적재를 했다. 쓰레드 풀의 기본적인 최대치는 25인데 한꺼번에 30개의 쓰레드를 적재해 버린 것이다.
그래서 이미 쓰레드 풀은 포화 상태가 되었다. 그런데 비동기 호출인 BeginConnect를 하려고 큐에 적재를 했는데, 이미 차지하고 있는 쓰레드 풀에서 빈 공간이 나올 기미가 안 보이는 것이다. 이미 connect 함수에서는 EndConnect 함수를 이용해 비동기 호출이 끝나기를 블럭되면서 기다리고 있는데 끝나질 않으니 한없이 기다리게 된다. 그렇다고 끝날 수도 없다. 이미 쓰레드 풀은 포화 상태이기 때문에 더 이상의 비동기 호출이 끼어들 자리가 없기 때문이다.
이 문제를 해결하기 위해서는 BeginConnect라는 비동기 호출을 동기 호출 함수로 바꿔주거나 처음 쓰레드 30개를 적재할 때, 한꺼번에 적재하지 말고 비동기 함수가 실행될 여지를 남겨주기 위해서 앞의 for문에서 Thread.Sleep(1000);이라는 문장을 주어 잠시 기다려 주면 비동기 호출이 실행될 여지가 있어서 데드록이 발생하지 않는다. 이러한 현상은 일반적으로 쓰레드 풀 내의 쓰레드가 비동기 호출이 끝나기를 기다릴 때 발생한다. 그러므로 쓰레드 풀과 비동기 호출을 같이 쓸 때는 주의해야 한다.
게임 서버 소개 지금까지 소개한 내용을 가지고 본격적으로 온라인 게임 서버를 만들어 보겠다.
네트워크 데이터 통신 방법 플래시와 소켓 통신을 하는데, 데이터 통신 방법은 단순하게 문자열로 보내고 받는 방법을 택하였다. 원래 플래시에는 XMLSocket이라는 것을 제공한다. 이는 XML 데이터를 위한 소켓으로 데이터를 XML 방식으로 보내야만 하는 것이다. 그러나 게임과 같이 속도가 중요한 프로그램에서는 XML로 데이터를 처리하면 이를 파싱하는 데 오버헤드가 있어 바람직하지 않다. 그래서 플래시의 XMLSocket을 이용하기는 하지만 이를 파싱하지 않고 데이터를 콤마로 구분한 문자열로 보내서 쓰는 방법을 택하였다.
이때 주의할 것은 플래시의 XMLSocket은 맨 마지막에 문자열의 끝임을 나타내주는 ‘\0’ 표시가 있어야 제대로 받아들인다. 그래서 서버에서 데이터를 전송할 때 데이터의 끝에 ‘\0’을 추가해 주었다. 서버에서 네트워크 데이터를 보낼 때는 보통 서버에 접속한 모든 사용자에게 데이터를 전송하는데 자기 자신을 포함하는 경우가 있고, 자기 자신을 제외한 나머지에게 데이터를 전송할 경우가 있어 브로드캐스트(broadcast) 함수를 두 가지로 만들었다.
| <그림 4> 로그인 부분 |
| <그림 5> 대기실 부분 |
사용자 처리 방법 각 사용자마다 이를 담당하는 user 클래스를 따로 만들었다. 이 클래스의 멤버는 <리스트 11>과 같다.
| class User { private Socket m_sock; // Connection to the user private byte[] m_byBuff = new byte[50]; // Receive data buffer public string m_sID; // ID 이름 public string m_sTank; // 탱크 종류 public string m_sTeam; // 팀 종류 public int m_nLocation; // 방에서의 자신의 위치 public Point m_point; // 자신의 위치 }
|
| |
각 사용자마다 자신의 네트워크 데이터를 처리할 소켓을 가지고 있고, 자신의 각종 정보를 가지고 있다. 메인에서는 이들을 arraylist로 유지해 새로운 사용자가 들어올 때마다 리스트에 추가해 준다.
방 관리 본 게임 서버에는 방이 하나밖에 없다. 최초에 들어온 사람이 방장이 되는 것이다. 이렇게 만든 이유는 간단하게 만들기 위해서이다. 본 게임 서버를 소개하는 목적이 소스를 이해하는 데 있으므로 가능한 최소한의 기능만 구현하여 소스 코드 크기를 줄였다. 아마 이 소스를 분석해 보면 쉽게 여러 개의 방도 만들 수 있을 것이다. 방이 하나밖에 없으므로 이미 게임중이면 다른 사용자가 들어오지 못하게 하였다.
| <그림 6> 게임 시작전 초기화 부분 |
happy4u
2004. 8. 25. 14:57
2004. 8. 25. 14:57
내용이 상당이 많으므로 관심 있는 분만 클릭하세요~
..more
>접기 비동기 프로그래밍이라는 것은 한 가지 일을 할 때 그 일이 끝날 때까지 기다리는 것이 아니라 그 일은 그 일 나름대로 진행하면서 동시에 자신의 일을 계속 할 수 있는 것을 말한다. 이러한 기능이 가능하게 하려면 결국 쓰레드를 써야만 한다. 이 쓰레드를 이용해 비동기 프로그래밍 기법을 흉내내 보자.
쓰레드를 이용한 비동기 프로그래밍 <리스트 1>은 쓰레드를 이용해 다른 작업을 동시에 하는 것을 보여준 예이다. 0부터 9까지 출력하는 프로그램으로 별개의 쓰레드를 하나 더 돌려서 이들을 동시에 처리하고 있다.
|
<리스트 1> 0부터 9까지 출력하는 프로그램 |
| |
| class Class1 { public void DoSubWork() { // 서브 작업 for(int i =0 ; i < 10 ; i++) { // 많은 계산을 요구하는 작업 for(int j=0; j < 10000000 ; j++) {} Console.WriteLine(“SubWork :{0} “,i); } Console.WriteLine(“부 작업 완료”); }
// 해당 응용 프로그램의 주 진입점이다. [STAThread] static void Main(string[] args) { // TODO: 여기에 응용 프로그램 시작 코드를 추가 Thread t = new Thread (new ThreadStart(Class1.DoSubWork)); t.Start();
// 메인 작업 for(int i =0 ; i < 10 ; i++) { for(int j=0; j < 10000000 ; j++) {} // 많은 계산을 요구하는 작업 Console.WriteLine(“MainWork:{0} “,i); } Console.WriteLine(“메인 작업 완료”); } }
|
|
이 프로그램의 결과는 컴퓨터 사양에 따라 차이가 나고, 할 때마다 다른 결과가 나올 수 있으나 대략 다음의 결과와 비슷할 것이다. MainWork : 0 SubWork : 0 SubWork : 1 MainWork : 1 SubWork : 2 MainWork : 2 MainWork : 3 SubWork : 3 MainWork : 4 SubWork : 4 MainWork : 5 SubWork : 5 MainWork : 6 SubWork : 6 SubWork : 7 MainWork : 7 SubWork : 8 MainWork : 8 MainWork : 9 메인 작업 완료 SubWork : 9 부 작업 완료
즉 쓰레드를 이용하면 이처럼 두 가지 작업을 동시에 처리할 수 있다. 그런데 쓰레드의 생성자를 보면 ThreadStart라는 델리게이트를 취하고 있음을 볼 수 있을 것이다. 이 ThreadStart 델리게이트의 형식을 보면 다음과 같다. public delegate void ThreadStart();
리턴형은 void이고, 인자로는 아무런 값을 받지 않는 델리게이트이다. 따라서 우리가 쓰레드를 사용해 다른 함수를 가동할 때는 인자도 없고 리턴 값도 없는 함수만을 사용해야 된다는 얘기다. 그런데 세상사라는 것이 그리 간단하지 않은 것이, 앞의 경우만 보더라도 0부터가 아닌 임의의 숫자로 시작하고 싶어서 그 숫자를 인자로 넘기고 싶을 때가 있을 것이다. 이럴 때에는 어떻게 해야 할 것인가? <리스트 2>의 예제를 보자.
<리스트 2> 원하는 수부터 9까지 출력하는 프로그램 |
| |
| class CSubWork { private int start; public CSubWork(int i) { start = i; } public void DoSubWork2() { // 서브 작업 for(int i =start ; i < 10 ; i++) { for(int j=0; j < 10000000 ; j++) {} // 많은 계산을 요구하는 작업 Console.WriteLine(“SubWork :{0} “,i); } Console.WriteLine(“부 작업 완료”); } }
class Class1 { [STAThread] static void Main(string[] args) { CSubWork = new CSubWork(5); Thread t = new Thread ( new ThreadStart (c.DoSubWork2) ); t.Start();
// 메인 작업 for(int i =0 ; i < 10 ; i++) { for(int j=0; j < 10000000 ; j++) {} // 많은 계산을 요구하는 작업 Console.WriteLine(“MainWork:{0} “,i); } Console.WriteLine(“메인 작업 완료”); } }
|
|
즉 비동기적으로 함수를 랩핑할 수 있는 클래스를 새로 만들어서 데이터 관리를 하면 되는 것이다. 그러므로 따로 인자를 넘기지 않더라도 클래스에 그 값을 줘서 해결할 수 있다. 다음은 <리스트 2>의 결과이다. MainWork : 0 SubWork : 5 MainWork : 1 SubWork : 6 MainWork : 2 SubWork : 7 MainWork : 3 SubWork : 8 MainWork : 4 SubWork : 9 부 작업 완료 MainWork : 5 MainWork : 6 MainWork : 7 MainWork : 8 MainWork : 9 메인 작업 완료
인자가 필요할 때에는 이렇게 해서 문제를 해결했는데, 이제 남은 또 다른 가능성은 리턴 값이 필요할 때이다. 되돌려 받는 값이 없다면 caller 측에서는 호출하고 잊어버리면 그만이다. 이를 ‘fire-and-forget style programming’이라고 한다. 그런데 되돌려 받는 값이 있다면 문제가 되기 시작한다. 상대방에게 비동기적으로 수행하라고 일을 시켜 놓았으니, 언제 끝날지 모르기 때문이다. 이를 확인하는 방법은 크게 두 가지가 있는데, caller 측에서 비동기 호출이 끝났는지 확인해서 끝났다면 리턴 값을 받아오는 방법과 caller 측에서 델리게이트를 넘겨주고 호출된 쪽에서 연산이 다 끝나면 그 델리게이트를 호출해줘서 리턴 값을 받아오는 방법이 있다. 이를 그림으로 나타내면 <그림 1>과 같다. | <그림 1> 비동기 호출로부터 결과값을 받는 방법 |
이번에는 비동기 부분에서 넘겨온 리턴 값을 가지고, 메인 부분에서 그 부분을 시작 값으로 하여 출력하는 프로그램을 만들 것이다. 지금까지 쓰레드 부분을 메인 부분에서 만들어 줬는데, 이번에는 메인 부분의 코드를 간결하게 하기 위하여 새로운 랩핑 클래스를 만들고 그곳에서 쓰레드를 담당하게 할 것이다. 쓰레드를 시작하는 함수는 Begin×××라는 이름을 붙여주고 결과 값을 받아 오는 함수는 End×××라고 붙여주자. <리스트 3>을 보자. | // 콜백 함수 델리게이트 public delegate void CallBack(int result);
// 리턴 값을 처리하기 위한 클래스 class CSubWork2 { private int start; // 시작 값 private CallBack callback; // 콜백 함수 델리게이트 public bool isCompleted; // 연산이 끝났는가? caller 측에서 물어볼 때 대답해 주기 위해서 public int ret; // 결과 값
public CSubWork2(int i, CallBack d) { start = i; callback = d; isCompleted = false } public void DoSubWork2() { ret = Calc(start); isCompleted = true if ( callback != null ) callback(ret ); }
protected int Calc(int s) { // 서브 작업 for(int i =s ; i < 10 ; i++) { for(int j=0; j < 10000000 ; j++) {} // 많은 계산을 요구하는 작업 Console.WriteLine(“SubWork :{0} “,i); } Console.WriteLine(“부 작업 완료”); return s+1; } }
class CAsync { CSubWork2 w; public void BeginWork(int i, CallBack d) { w = new CSubWork2(i, d); Thread t = new Thread(new ThreadStart ( w.DoSubWork2 )); t.Start(); }
public int EndWork() { // 결과 값이 나올 때까지 블럭 do { if ( w.isCompleted == true ) break }while(true); return w.ret; }
public bool IsCompleted() { return w.isCompleted; } }
|
|
리턴 값이 준비돼 있는지 안 돼 있는지 확인하기 위해 IsCim pleted하는 메쏘드를 준비했다. 또한 콜백(callback) 형식으로 델리게이트를 넘길 때를 위해 그에 대한 델리게이트도 마련해뒀다. CAsync 클래스의 BeginWork 함수에서 인자와 콜백 함수를 넘겨주게 되는데 이때 만약 콜백 함수가 필요없다면 null을 넘겨주면 된다. 그러면 대신 IsCompleted하는 함수로 비동기 연산의 종료 여부를 확인할 수 있다.
한편 EndWork 함수에서는 비동기 연산이 종료되지도 않았는데 이 함수를 호출하면 준비되지 않는 결과 값을 가져가는 오류를 미연에 방지하기 위해 결과 값이 나올 때까지 블럭되게 한 후, 결과 값이 나오면 리턴하도록 했다. 그럼 먼저 caller 측에서 비동기 연산의 종료를 확인하는 예제를 보자(<리스트 4>). <리스트 4> 비동기 연산 종료 여부 확인하기 |
| |
| CAsync a = new CAsync(); a.BeginWork(4,null);
while(!a.IsCompleted()) { // 메인 작업 Console.Write(“.”);
} for(int i = a.EndWork() ; i < 10 ; i++) { for(int j=0; j < 10000000 ; j++) {} // 많은 계산을 요구하는 작업 Console.WriteLine(“MainWork:{0} “,i); } Console.WriteLine(“메인 작업 완료”);
|
|
이번 예제에서는 콜백 함수가 필요없으므로 BeginWork 함수에서 null을 넘겨줬다. 그리고 대신 IsCompleted 함수를 이용해 종료 여부를 확인하는 동안 메인에서는 자신의 일을 할 수 있도록 했다. 종료된 후에는 EndWork 함수를 이용하여 결과 값을 가져와서 메인 부분의 일을 처리했다. 이 프로그램의 결과는 다음과 같다.
................................................................ ................................................................ ................................................SubWork : 4 SubWork : 5 SubWork : 6 SubWork : 7 ................................................................. ................................................................. ..................................SubWork : 8 SubWork : 9 부 작업 완료 MainWork : 5 MainWork : 6 MainWork : 7 MainWork : 8 MainWork : 9 메인 작업 완료
이번에는 콜백 함수를 이용해 결과 값을 받아 오는 예제를 살펴보자(<리스트 5>). 콜백 함수에서는 비동기 함수의 리턴 값을 인자로 받아 와서 그 일을 하고 수행하고 있다.
| class Class1 { // 비동기 함수가 대신 호출해 줄 콜백 함수 public static void CallMe(int ret) { // 메인 작업 for(int i = ret; i < 10 ; i++) { for(int j=0; j < 10000000 ; j++) {} // 많은 계산을 요구하는 작업 Console.WriteLine(“MainWork:{0} “,i); } Console.WriteLine(“메인 작업 완료”); }
[STAThread] static void Main(string[] args) { CAsync b = new CAsync(); b.BeginWork(4,new CallBack(CallMe));
for(int i=0; i<400; i++) { // 아무 작업 수행 Console.Write(“.”); } } }
|
|
이 방법을 쓰면 caller 측에서 일일이 확인하지 않아도, 호출된 비동기 함수 부분에서 다 끝났다고 알려주는 격이 된다. 결과는 다음과 같다. ................................................................................ ................................................................................ ....................................................SubWork : 4 SubWork : 5 SubWork : 6 SubWork : 7 ................................................................................ ................................................................................ ............................SubWork : 8 SubWork : 9 부 작업 완료 MainWork : 5 MainWork : 6 MainWork : 7 MainWork : 8 MainWork : 9 메인 작업 완료
이상이 비동기 프로그래밍의 대략적인 내부 구현이다. 실제 닷넷 프레임워크 내부적으로는 이보다 훨씬 더 복잡하게 진행이 되지만 대략적인 것은 이와 비슷하다. 그럼 이제는 실제 델리게이트를 이용해 비동기 프로그래밍을 해 보자. 델리게이트를 이용한 비동기 프로그래밍의 실제 이번 예제는 앞서 우리가 만든 프로그램과 비슷하다. 시작 숫자를 인자로 넘겨주면 그 숫자부터 프린트하는 프로그램이다. 이를 비동기 호출로 하기 위해 그에 대한 델리게이트를 다음과 같이 선언했다. public delegate void SubWork(int i);
반환형을 다루는 예제는 조금 뒤에 다룰 것이므로, 지금은 반환형이 없는 델리게이트를 이용하자. 인자로는 시작 숫자를 넘겨줬다. 우리가 이렇게 델리게이트를 만들면 지난 시간에 소개했듯이 컴파일러는 이를 바탕으로 하여 다음과 같은 클래스를 만들게 된다. public class SubWork : System.MulticastDelegate { // 생성자 public SubWork (object target, int32 methodPtr);
public void virtual Invoke( int i );
public virtual IAsyncResult BeginInvoke( int i, AsyncCallback callback, Object object);
public virtual void EndInvoke( IAsyncResult result); }
BeginInvoke는 앞에서 BeginWork와 비슷한 역할을 한다. 먼저 델리게이트가 받을 인자가 오고, 그 다음에 콜백 함수가 오고, 추가로 상태를 지정할 수 있는 인자를 쓸 수 있다. 이는 추가 정보를 넘겨줄 필요가 있을 때에만 쓰는 것이므로, 필요없다면 안 써도 된다. 그럼 이를 실제로 테스트해 보자(<리스트 6>). <리스트 6> 델리게이트를 이용한 비동기 호출 |
| |
| public delegate void SubWork(int i); class Class1 { public static void DoSubWork(int start) {
for(int i=start; i < 10 ; i++) { for(int j=0; j < 10000000 ; j++) {} // 많은 계산을 요구하는 작업 Console.WriteLine(“SubWork :{0}”,i); } }
[STAThread] static void Main(string[] args) { SubWork d = new SubWork(DoSubWork); d.BeginInvoke(3,null,null);
for(int i=0; i < 10 ; i++) { for(int j=0; j < 10000000 ; j++) {} // 많은 계산을 요구하는 작업 Console.WriteLine(“MainWork:{0}”,i); } } }
|
|
앞서 우리가 한 예제와 동일한 기능을 하는 예제이다. 델리게이트에는 기본적으로 BeginInvoke라는 메쏘드가 있어 프로그래머가 손수 쓰레드 관련 코딩을 하지 않고도 손쉽게 비동기 호출을 할 수 있게 해 준다. 결과는 다음과 같다. MainWork : 0 SubWork : 3 MainWork : 1 SubWork : 4 SubWork : 5 MainWork : 2 MainWork : 3 SubWork : 6 MainWork : 4 SubWork : 7 MainWork : 5 SubWork : 8 MainWork : 6 SubWork : 9 MainWork : 7 MainWork : 8 MainWork : 9
이와 같이 fire-and-forget형 프로그래밍의 경우 간단하지만 만약 반환 값을 다뤄야 할 경우는 약간 복잡해진다. 닷넷 플랫폼에서는 반환 값을 다루기 위한 4가지 스타일의 프로그래밍 기법을 제공한다. 1. Use Callbacks : 콜백 델리게이트를 이용해 비동기 부분에서 연산이 다 끝나면 델리게이트를 호출해 주는 방식 2. Poll Completed : caller 부분에서 연산이 다 끝났는지 IsCompleted라는 속성을 이용하여 계속 확인해 보는 방식 3. Begin Invoke, End Invoke : caller 측에서 결과 값을 받기 위하여 블러킹돼 기다리는 방식 4. Begin Invoke, Wait Handle, End Invoke : 앞의 방식과 비슷하나 wait handle에서 제한 시간을 설정해 줌으로써 계속 블러킹되는 것을 방지할 수 있다.
그럼 이들에 대한 예제를 하나씩 살펴보도록 하자. 이 예제는 SubWork에서 반환 값을 주는데, 역시 앞의 예제와 비슷하게 main 부분에서는 시작 값으로 사용될 값을 넘겨주게 된다. 먼저 <리스트 7>을 통해 1번의 경우부터 보도록 하자.
| public delegate int SubWork2(int i); class Class1 { public static int DoSubWork2(int start) { for(int i=start; i < 10 ; i++) { for(int j=0; j < 10000000 ; j++) {} // 많은 계산을 요구하는 작업 Console.WriteLine(“SubWork :{0}”,i); } return start + 1; }
public static void CallMe(IAsyncResult ar) { SubWork2 d = (SubWork2) ((AsyncResult)ar) .AsyncDelegate; int result = d.EndInvoke(ar); for(int i=result; i < 10 ; i++) { for(int j=0; j < 10000000 ; j++) {} // 많은 계산을 요구하는 작업 Console.WriteLine(“MainWork:{0}”,i); } }
[STAThread] static void Main(string[] args) { // 여섯번째 예제 Console.WriteLine(“*** 여섯번째 예제 ***”); SubWork2 d2 = new SubWork2(DoSubWork2); d2.BeginInvoke(3,new AsyncCallback(CallMe),null);
// 아무런 작업 for(int i =0; i<300;i++) Console.Write(“.”);
// 그냥 끝나면 안되므로 키입력 까지 대기 Console.Read (); } }
|
|
이번에는 콜백 함수를 이용해서 함께 넘겨주고 있다. 이 콜백 델리게이트의 형식을 보면 다음과 같다. public delegate void AsyncCallback(IAsyncResult ar)
즉 리턴 값은 없고, 인자로 IAsyncResult라는 것을 받고 있다. 이는 닷넷 프레임워크에서 콜백 함수를 호출할 때 그 인자를 자동으로 넘겨주므로 걱정하지 않아도 된다. 이때 넘어오는 인자에서 AsyncDelegate라는 속성을 이용하면 해당 델리게이트를 받아올 수 있다. 따라서 이를 이용해 EndInvoke를 호출하여 결과 값을 받아오는 것이다. 결과는 다음과 같다.
SubWork : 3 SubWork : 4 SubWork : 5 ................................................................................ ................................................................................ .............................SubWork : 6 SubWork : 7 SubWork : 8 SubWork : 9 ................................................................................ ...............................MainWork : 4 MainWork : 5 MainWork : 6 MainWork : 7 MainWork : 8 MainWork : 9
<리스트 8>은 2의 경우로서 caller 측에서 계속 폴링(polling)하면서 연산이 다 끝났는지 확인하는 방법이다.
| SubWork2 d3 = new SubWork2(DoSubWork2); IAsyncResult ar = d3.BeginInvoke(3,null,null); while( !ar.IsCompleted ) { Console.Write(“.”); }
int ret = d3.EndInvoke(ar);
for(int i= ret; i < 10 ; i++) { for(int j=0; j < 10000000 ; j++) {} // 많은 계산을 요구하는 작업 Console.WriteLine(“MainWork:{0}”,i); }
|
|
caller 측에서 IAsyncResult의 IsCompleted 속성을 이용하여 계속적으로 연산이 끝났는지 안 끝났는지 확인하고 있다. 확인하는 동안 caller 측에서는 계속 다른 작업을 할 수 있다. 결과는 다음과 같다. ................................................................................ ................................................................................ ...........................................MainWork : 5 MainWork : 6 MainWork : 7 MainWork : 8 ................................................................................ ................................................................................ ......................................MainWork : 9 SubWork : 3 SubWork : 4 SubWork : 5 ................................................................................ ................................................................................ ......................................SubWork : 6 SubWork : 7 SubWork : 8 SubWork : 9 MainWork : 4 MainWork : 5 MainWork : 6 MainWork : 7 MainWork : 8 MainWork : 9
<리스트 9>는 3의 경우인데, 이는 결국 caller 측에서 비동기 호출된 부분이 끝날 때까지 블러킹되고 있으므로, 별로 권장하는 방법은 아니다. 이렇게 되면 비동기 호출을 할 의미가 없기 때문이다. 그러나 이도 결과 값을 받는 방법의 하나이므로 알아두자. 이번 예제는 콘솔 프로그램이 아닌 윈도우용 프로그램으로 만들었다. EndInvoke를 실행했을 때 caller 측이 블러킹된다는 것을 보여주기 위해서 윈도우용 프로그램으로 만들었다.
<리스트 9> Begin Invoke, End Invoke |
| |
| public class Form1 : System.Windows.Forms.Form { private System.Windows.Forms.TextBox textBox1; private System.Windows.Forms.Button button1; public delegate void SubWork(); public void DoSubWork() { // 서브 작업 string str; for(int i =0 ; i < 100 ; i++) { for(int j=0; j < 10000000 ; j++) {} // 많은 계산을 요구하는 작업 str = String.Format(“SubWork :{0}\r\n”,i); textBox1.Text += str; } }
[STAThread] static void Main() { Application.Run(new Form1()); }
private void button1_Click(object sender, System.EventArgs e) { SubWork d = new SubWork(DoSubWork); IAsyncResult r = d.BeginInvoke(null,null); d.EndInvoke(r); } }
|
|
이 프로그램을 실행하고 ‘비동기 연산 시작’ 버튼을 누르면, 비동기 연산을 시작한다. 그러나 연산 결과가 바로 나오지도 않을 것이며 윈도우가 움직이지도 않을 것이다. 모든 연산이 끝난 후에 한꺼번에 나올 것이다. 이는 EndInvoke라는 함수 때문에 블러킹돼버려서 그런 것이다. 그래서 연산 결과를 표시하기 위해 윈도우 화면 갱신도 못하고 마치 다운된 것처럼 멈췄다가 결과가 나오게 된다. <화면 1>이 그 결과 화면이다. | <화면 1> 윈도우에서 비동기 연산을 한 결과 화면 |
이번에는 마지막 방법인 4에 대해 알아보자. 이 방법은 3번 방법과 비슷한데 한 가지 다른 점은 time out을 정해줄 수 있어서 마냥 블러킹되는 것이 아니라 일정 시간이 넘어버리면 끊어버릴 수 있는 기능을 제공한다. 이번에는 비동기 연산을 0.1초 안에 해내지 못하면 블러킹을 해제하고 main 부분을 실행하는 프로그램을 만들어 본다.
<리스트 10> Begin Invoke, Wait Handle, End Invoke |
| |
| SubWork2 d4 = new SubWork2(DoSubWork2); IAsyncResult ar2 = d4.BeginInvoke(3,null,null);
if ( ar2.AsyncWaitHandle.WaitOne(100,false) == false ) { ret = 4; Console.WriteLine(“도중 차단”); } else { ret = d4.EndInvoke(ar2); }
for(int i= ret; i < 10 ; i++) { for(int j=0; j < 10000000 ; j++) {} // 많은 계산을 요구하는 작업 Console.WriteLine(“MainWork:{0}”,i); }
|
|
<리스트 10>은 caller 측에서 결과 값을 받기 위해서 0.1초간만 기다리고 그 안에 결과 값을 받지 못하면 블러킹을 중지하고 자신의 코드를 수행하는 예제이다. 결과는 다음과 같다. 결과에서 확인할 수 있는 것처럼 비동기 호출이 0.1초 안에 연산을 끝내지 못했기 때문에 main은 블러킹을 멈추고 자신이 할 일을 하고 있다. SubWork : 3 SubWork : 4 SubWork : 5 SubWork : 6 SubWork : 7 도중 차단 MainWork : 4 MainWork : 5 MainWork : 6 SubWork : 8 SubWork : 9 MainWork : 7 MainWork : 8 MainWork : 9 리턴 값을 이용하지 않을 경우 이상으로 비동기 호출에서 결과 값을 얻기 위한 4가지 방법을 소개했다. 그러나 비동기 호출에서 결과 값을 받기 위해 꼭 리턴 값을 이용해야 하는 것은 아니다. 우리가 인자로 넘겨줄 때 ref형이나, out형으로 넘겨주면 꼭 리턴 값을 이용하지 않더라도 그 결과 값을 받는 방법이 된다. 그러나 여기서 한 가지 주의할 것은 인자(parameter) 값의 업데이트도 비동기적으로 일어난다는 것이다. <리스트 11>을 보자. | public delegate void SubWork3(ref int i);
public static void DoSubWork3(ref int i) { i *= 2; }
[STAThread] static void Main(string[] args) { Console.WriteLine(“*** 열번째 예제***”); int v = 42; SubWork3 d5 = new SubWork3(DoSubWork3); IAsyncResult ar3 = d5.BeginInvoke(ref v,null,null); ar3.AsyncWaitHandle.WaitOne(); Console.WriteLine(“EndInvoke를 호출하기 이전의 값 :{0}”,v); d5.EndInvoke(ref v,ar3); Console.WriteLine(“EndInvoke를 호출하고 난 후의 값:{0}”,v); }
|
|
이번에는 값을 ref형으로 넘기고 있다. 만약 비동기 호출이 아닌 동기 호출이라면, 처음에 결과 값이 84가 나와야 할 것이다. 그러나 비동기 호출이기 때문에, 파라미터값의 업데이트도 비동기적으로 하므로 EndInvoke를 호출해야만 값의 업데이트가 일어난다. 그 결과는 다음과 같다. EndInvoke를 호출하기 이전의 값 : 42 EndInvoke를 호출하고 난 후의 값 : 84
그런데 여기서 만약 reference type을 파라미터로 넘기게 되면 비동기 호출 부분에서는 이 값을 실시간적으로 업데이트를 한다. 즉 EndInvoke를 호출할 필요없이, 자신이 연산을 하면서 값을 지속적으로 갱신을 하는 것이다. 이번에는 reference type인 byte형 배열을 파라미터로 넘겨서 그 값을 확인해 보겠다. <리스트 12>를 보자.
<리스트 12> reference type을 파라미터로 넘긴 예 |
| |
| public delegate void SubWork4(byte[] b);
public static void DoSubWork4(byte[] b) { for(byte i=0; i < b.Length; i++) { for(int j=0; j < 10000000 ; j++) {} // 많은 계산을 요구하는 작업 b[i] = (byte)(i*i); } }
[STAThread] static void Main(string[] args) { Console.WriteLine(“*** 열한번째 예제***”); byte[] b = new byte[10]; SubWork4 d6 = new SubWork4(DoSubWork4); IAsyncResult ar4 = d6.BeginInvoke(b, null, null); // ar4.AsyncWaitHandle.WaitOne(); for(int i=0; i< b.Length; i++) Console.WriteLine(“b[{0}]={1}”,i,b[i]); }
|
|
이 예제는 byte형 배열을 만든 후(이때 모든 배열의 값은 0으로 자동 초기화된다) 이를 비동기 호출로 넘겨주면, 비동기 연산 부분에서는 이 배열의 값을 넣어주는 작업을 하는 예제이다. <리스트 12>에서 결과 값이 다 나오도록 기다리는 부분을 주석 처리했는데, 그렇게 한 이유는 비동기 호출이 값을 실시간으로 고치는지 확인해 보기 위해서이다. 그 주석 처리한 부분의 주석을 없애 버리면 연산이 다 끝날 때까지 기다리기 때문에 언제나 정확한 값을 얻을 수 있을 것이다. 그러나 우리는 과연 비동기 호출이 값을 실시간으로 바꾸는지 확인하기 위한 것이므로 주석 처리를 했다. 결과 값은 컴퓨터 사양에 따라 다르고 할 때마다 다른 값이 나온다. 다음은 필자 컴퓨터에서 실행한 결과이다. 결과를 보면 5번 방까지는 제대로 들어갔는데, 그 이후로 값이 안 들어와 있다. 이는 caller 측에서 비동기 연산이 아직 다 끝나지 않았는데, 이는 그 값을 꺼내봤기 때문이다. b[0]=0 b[1]=1 b[2]=4 b[3]=9 b[4]=16 b[5]=25 b[6]=0 b[7]=0 b[8]=0 b[9]=0
쓰레드 풀을 이용하자 이번 호에서는 쓰레드를 이용한 비동기 프로그래밍의 원리와 4가지 구현 방법에 대해 알아봤다. 그런데 여기서 아직 해결하지 못한 문제점이 남아 있다. 닷넷 프레임워크는 우리가 매번 비동기 호출을 할 때마다 쓰레드를 새로 만들어서 하는 것일까? 만약 그렇다면, 쓰레드라는 것이 적당한 수가 유지된다면 문제가 안 되지만 과도한 쓰레드의 생성은 오히려 쓰레드를 교체하면서 생기는 컨텍스트 체인지 오버헤드(context change overhead)가 있을 것이다. 닷넷 프레임워크에서는 이 문제를 쓰레드 풀(pool)을 이용해서 해결하고 있다. 다음 연재에서는 이 쓰레드 풀을 이용한 비동기 호출에 대해 알아보고, 최종적으로 게임 서버를 완성할 것이다. @
happy4u
2004. 8. 19. 21:07
2004. 8. 19. 21:07
Isolation level | 수많은 트랜잭션에 의해 데이터는 끊임없이 변경 및 ACCESS되고 있으며 이러한 과정 속에 검색되는 데이터 값의 어디까지를 내 트랜잭션에서 처리할 것인가를 결정하기 위해 ASE에서는 isolation level를 지정할 수 있다 isolation은 다른 트랜잭션에 의해 변경되고 있는 dirty page를 읽을 수 있는가? 하나의 트랜잭션에서 검색 되는 데이터의 일관성을 유지할 것인가? 하나의 트랜잭션에서 발생되는 phantom 현상을 허용할 것인가에 따라 isolation의 level을 1,2,3으로 정의한다 o dirty read? o nonrepeatable read? o phatom read? o isolation level? o isolation 설정 방법? o nonrepeatable 발생을 해결하려면? o phantom read 발생을 해결하려면? Dirty Read? 트랜잭션1이 데이터를 변경후 트랜잭션을 종료(commit또는 rollback)하지 않은 상태에서 트랜잭션2가 변경된 데이터로 접근할 수 있다
트랜잭션1이 변경한 데이터에 대해서 commit할 지 rollback할지 트랜잭션2는 알 수 없으므로 rollback과 함께 트랜잭션1이 종료한다면 트랜잭션2는 잘못된 데이터로부터 잘못된 처리를 할 수 있는 위험이 있다 이와 같이 업무 성격에 따라 Dirty Page를 다른 트랜잭션이 접근하는 것을 허용할 것인지, 안할 것인지에 따라 ioslation 0(허용)와 isolation 1(허용 안함)로 정의 할 수 있다 Nonrepeatable Read?
트랜잭션1이 select하고 있는 테이블에는 Shared lock이 걸려 있으므로 트랜잭션2는 트랜잭션1이 검색했던 페이지의 데이터를 Modify한 후 트랜잭션2를 종료(commit)할 수 있다. 이때 아직 트랜잭션을 종료하지 않은 트랜잭션1이 먼저 select한 데이터를 다시 검색했을때 같은 트랜잭션에서 같은 질의가 발생했음에도 불구하고 다른 데이터 값을 가져올 수 있다 이와 같이 동일한 트랜잭션에서 동일한 질의에 대해 검색되는 데이터 값에 대한 일관성을 유지할 것인가(isolation level 2), 아니면 유지하지 않을 것인가 (isolation level 1)에 따라 isolation level을 지정할 수 있다 Phantom Read? 트랜잭션1이 질의를 발생하여 조건에 맞는 row의 sets을 검색중인 상황에서 트랜잭션2가 같은 테이블의 다른 row의 sets(트랜잭션1이 검색하고 있지 않는 row의 sets)의 데이터 값을 변경한 후 트랜잭션을 종료 했다 트랜잭션2가 변경한 데이터 값이 우연히도 트랜잭션1의 조건절에 영향을 주는 데이터 값이라 아직 트랜잭션을 종료하지 않은 트랜잭션1이 동일한 질의를 실행시켰을 경우 처음 데이터보다 많은 rows의 set을 검색하게 된다 이와 같이 같은 트랜잭션에서 동일한 질의를 발생시켰을 때 더 많은 rows의 sets을 보게 되는 현상을 phantom 현상이라 한다 이와 같이 같은 트랜잭션에서 phantom현상을 허용할 것인지(isolation level 2) 허용하지 않을 것인지(isolation level 3)에 따라 isolation level을 정의할 수 있다 Isolation Level?
위의 세가지 현상을 허용할 것인지 허용하지 않을 것인지에 따라 isolation level을 4가지로 나눌 수 聆만?level이 높아질수록 더 많은 제약을 부여한다 Level 1 (Read Committed)가 SQL Server의 기본 격리 수준(Isolation Level)이다.
| Dirty Read
| Nonrepeatable Read
| Phantom Read | Level 0 READ UNCOMMITTED | Allowed
| Allowed
| Allowed | Level 1 READ COMMITTED | Prevented
| Allowed
| Allowed | Level 2 REPEATABLE READ | Prevented
| Prevented | Allowed | Level 3 SERIALIZABLE | Prevented
| Prevented | Prevented |
Level 0 변경되고 있는 데이터에 대한 정보를 실시간 보기 위한 노력으로 set transaction isolation level 0 명령에 의해 설정되며 select명령 실행 시 share page lock을 필요로 하지 않는다 Page read 시에 해당 page에 대한 어떠한 lock도 장해가 되지 않으며 select 문장의 holdlock option 또한 아무런 효과가 없다 현재 다른 트랜잭션에 의해 변경중인(exclusive page lock)page에 대해서도 read가 가능하다 Level 1 isql에서 default mode로 사용되며 select시 읽혀지는 page에 대해 순간적으로 Shared Page Lock이 걸리며 읽힌 후 바로 Lock이 해제된다 select문장에서 holdlock option을 주게 되면 page를 읽을 때 부여되는 Shared Page Lock이 바로 해제가 되지 않으며 commit/rollback 명령에 의해서만 해제된다 Level 2 같은 트랜잭션에서의 동일한 질의에 대해 동일한 결과 값을 보장해주기 위한 노력으로 select시 발생한 Shared Lock은 트랜잭션이 종료(commit or rollback)될 때까지 해제되지 않는다 Datarows lock scheme에서만 지원되며 같은 트랜잭션에서 동일한 질의에 대해 Phantom 현상을 허용한다 Level 3 실행되는 모든 select문에 대해 holdlock option을 준 효과를 가진다 따라서 트랜잭션이 종료(commit/rollback)되기 전까지는 read된 모든 page에 대해서 shared page lock이 해제 되지 않으므로 수많은 lock을 발생시킬 수 있으므로 주의해야 한다
보다 자세한 내용은 more를!!! |
출처 : http://www.jiic.com
..more
>접기 Isolation의 설정 방법? Syntax for Session-Level Isolation: set transaction isolation level { 0 | read uncommitted | 1 | read committed | 2 | repeatable read | 3 | serializable } Syntax for Statement-Level Isolation: select ... at isolation { 0 | read uncommitted | 1 | read committed | 2 | read repeatable | 3 | serializable } a @@isolation returns the isolation level for the session nonrepeatable 발생 해결하려면? isolation level을 2,3으로 올려야 한다 예) 현재의 isolation level은 1이고, 여러 page를 차지하는 table을 만든다 1> use sybsystemprocs 2> go 1> create table testTABLE ( 2> a char(255), 3> b char(255), 4> c char(255), 5> d char(255)) 6> go 1> insert into testTABLE values ('1','1','1','1') 2> insert into testTABLE values ('2','2','2','2') 3> insert into testTABLE values ('3','3','3','3') 4> insert into testTABLE values ('4','4','4','4') 5> insert into testTABLE values ('5','5','5','5') 6> insert into testTABLE values ('6','6','6','6') 7> insert into testTABLE values ('7','7','7','7') 8> insert into testTABLE values ('8','8','8','8') 9> insert into testTABLE values ('9','9','9','9') 10> insert into testTABLE values ('10','10','10','10') 11> insert into testTABLE values ('11','11','11','11') 12> insert into testTABLE values ('12','12','12','12') 13> insert into testTABLE values ('13','13','13','13') 14> insert into testTABLE values ('14','14','14','14') 15> insert into testTABLE values ('15','15','15','15') 16> insert into testTABLE values ('16','16','16','16') 17> insert into testTABLE values ('17','17','17','17') 18> insert into testTABLE values ('18','18','18','18') 19> insert into testTABLE values ('19','19','19','19') 20> insert into testTABLE values ('20','20','20','20') 21> go 1> select @@isolation 2> go ----------- 1 지금부터는 2개의 session A,B를 함께 사용해서 하십시요 ************* session A ************* 1> select @@isolation 2> go ----------- 1 1> -- (1) transaction 을 시작한다 2> begin tran 3> go 1> ---(3) a='1' 인 내용을 select한다. lock은 읽는 순간에만 걸린다 2> select * from testTABLE where a = '1' 3> go a -- b ---c ---d --- ---- --- ---- 1 ---1 ---1 ---1 1> ---(6) 위에서 select한 내용을 똑같이 시도한다. -- 그러나, 한 transaction안에서도 값은 위의 select와 똑같지 않다 -- 이것이 바로 nonrepeatable read 이다 2> select * from testTABLE where a = '1' 3> go a --- b ---c ---d ---- ---- ---- ----- 1 -- -1 ---1 ---123 1> ---(7) 2> commit tran 3> go ************* session B ************* 1> -- (2) transaction 을 시작한다 2> begin tran 3> go 1> -- (4) session A 에서 읽었던 a='1' 인 내용을 update한다. -- lock은 execluseve로 commit tran을 만날때 까지 풀리지 않는다 2> update testTABLE set d = '123' where a = '1' 3> go 1> ---(5) commit 을 하면, 위의 update로 인한 lock이 풀리고 -- 다른 session에서 update의 변경값을 읽을 수 있다 2> commit tran 3> go 위의 것은 Isolation level 1인 경우입니다. Isolation level 2로 바꾸고 하면 (3)번이 lock을 걸어 (4)번 수행이 되지 않습니다. 그래서 (6)번,(7)번 수행하고 lock이 풀려야 (4)번 수행됩니다 이처럼 Isolation level 2에서는 한 transaction이 1 row를 읽은 것이 transaction이 끝날 때 까지 lock을 걸어 수행이 같은 transaction에서 그 1 row를 다시 읽어도 같은 내용이 읽힙니다
phantom read 발생 해결하려면? isolation level 3으로 해결할 수 있습니다 예) 현재의 isolation level은 1이고, 여러 page를 차지하는 table을 만든다 1> use sybsystemprocs 2> go 1> create table testTABLE ( 2> a char(255), 3> b char(255), 4> c char(255), 5> d char(255)) lock datarows 6> go 1> set transaction isolation level 3 2> go 1> insert into testTABLE values ('1','1','1','1') 2> insert into testTABLE values ('2','2','2','2') 3> insert into testTABLE values ('3','3','3','3') 4> insert into testTABLE values ('4','4','4','4') 5> insert into testTABLE values ('5','5','5','5') 6> insert into testTABLE values ('6','6','6','6') 7> insert into testTABLE values ('7','7','7','7') 8> insert into testTABLE values ('8','8','8','8') 9> insert into testTABLE values ('9','9','9','9') 10> insert into testTABLE values ('10','10','10','10') 11> insert into testTABLE values ('11','11','11','11') 12> insert into testTABLE values ('12','12','12','12') 13> insert into testTABLE values ('13','13','13','13') 14> insert into testTABLE values ('14','14','14','14') 15> insert into testTABLE values ('15','15','15','15') 16> insert into testTABLE values ('16','16','16','16') 17> insert into testTABLE values ('17','17','17','17') 18> insert into testTABLE values ('18','18','18','18') 19> insert into testTABLE values ('19','19','19','19') 20> insert into testTABLE values ('20','20','20','20') 21> go 지금부터는 2개의 session A, B를 함께 사용해서 하십시요 ************* session A ************* 1> -- (1) 현재 isolation level은 2 입니다 1> select @@isolation 2> go ----------- --- 2 2> begin tran -- transaction 을 시작 합니다 3> go 1> ---(3) 조건 < 9 인 내용을 보았습니다 2> select * from testTABLE where convert(int,a) < 9 3> go a ----------------------------------------------------------------------- 1 2 3 4 5 6 7 8 1> ---(6) 조건 < 9 인 내용을 보았습니다 -- session B의 영향으로 -- 위의 select결과로 보이지 않던 0가 보인는 phantom현상이 발생했습니다 2> select * from testTABLE where convert(int,a) < 9 3> go a ----------------------------------------------------------------------- 1 2 3 4 5 6 7 8 0 1> ---(7) 2> commit tran 3> go ************* session B ************* 1> -- (2) 번호순 대로 수행하세요 2> begin tran 3> go 1> -- (4) 2> insert into testTABLE values ('0','0','0','0') 3> go 1> ---(5) 2> commit tran 3> go 위의 것은 Isolation level 2인 경우이다 Isolation level 2인 경우, 번호순서대로 수행이 가능하나 Isolation level 3로 바꾸고 하시면 (3)번이 lock을 걸어 (4)번 수행이 되지 않는다 그래서 (6)번,(7)번 수행하고 lock이 풀려야 (4)번 수행된다
happy4u
2004. 8. 16. 21:03
2004. 8. 16. 21:03
미리 보는 유콘 T-SQL의 새로운 기능들
편집자 메모: 이 기사에서 설명된 기능들은 최종 확정된 사항이 아니며, 최종 제품 릴리즈에서는 이 기사에서 설명한 대로 되지 않을 수도 않으며, 경우에 따라서는 최종 제품 릴리즈에 아예 포함되지 않을 수도 있다. ………………………………………………………………………………………. 독자들은 대개 Visual Basic .NET이나 C#과 같은 CLR(Common Language Runtime) 기반의 언어들을 통하여 프로그램이 가능한 객체를 개발하는 새로운 기능이 코드명 유콘(Yukon)의 차기 SQL 서버 버전에 포함된다는 사실에 대하여 들었을 것이다. 그 결과로 마이크로소프트가 T-SQL을 개선하는 데 드는 노력을 줄일 것이라고 생각할지도 모르겠지만, 실제로는 그렇지 않다. CLR 기반 프로그래밍은 T-SQL이 취약한 영역에서 T-SQL을 보완해 준다. T-SQL은 지금까지 늘 데이터 처리와 집합 기반의 연산에 있어서 강력한 면모를 보여 왔으며, 여전히 그 영역에서는 대적할 자가 없다. 그렇지만 복잡한 알고리즘이나 반복 처리 등이 포함된 영역에서는 CLR 언어가 더 강력하다. 마이크로소프트가 여러 가지로 T-SQL 개선을 위해 노력을 기울이고 있는 사실이 입증하듯이, T-SQL은 앞으로도 오랫동안 활용되고 발전할 것이다. 마이크로소프트는 오래 전부터 프로그래머들이 필요로 하고 요구해 왔던 사항들에 대한 해답을 유콘에서 제공하고 있다. 유콘에서는 T-SQL 분야에 다양한 측면에서의 개선이 이루어지고 있다. 유콘 베타1에서의 관계형 엔진의 개선에는 재귀 쿼리 기능, PIVOT, UNPIVOT 연산자, 보다 기능적인 TOP 절, CROSS APPLY 연산자, OUTER APPLY 연산자 등이 지원되는 CTE(Common Table Expressions)가 포함되어 있다.
유콘에서는 INSERT, UPDATE, DELETE 문에서도 아웃풋을 반환할 수 있게 되며, WAITFOR 명령어를 사용하여 데이터 조작 명령어인 DML((Data Manipulation Language)가 적어도 한 행에 대하여 작업을 수행할 때까지 기다렸다가 다음 명령어로 진행하도록 할 수도 있고, 또는 통지(notification)가 큐에 도달하기를 기다렸다가 다음 명령어로 진행하도록 할 수도 있다.
트리거에서 오브젝트를 삭제하는 것과 같은 DDL(Data Definition Language) 관련 이벤트들을 잡아낼 수도 있으며, DML 이벤트가 발생할 때 통지를 받을 수도 있다. Try/Catch 블록에 기반을 둔 새로운 오류 처리 메커니즘의 지원으로 이전에 비해 훨씬 더 효율적으로 오류들을 관리할 수 있게 되었다. 데이터 타입에 있어서도 중요한 개선 사항들이 있는데, 새로운 XML 데이터 타입이 지원되고, 일자 데이터 타입과 시각 데이터 타입을 별도의 데이터 타입으로 지원하며, MAX라는 옵션이 지원됨으로써 varchar, nvarchar, varbinary 등과 같은 동적 컬럼에서 대형 오브젝트들을 훨씬 더 세련되게 처리할 수 있게 되었다. 그리고 마지막으로, BULK라는 새로운 rowset provider가 지원됨으로써 사용자들이 이전에 비해 보다 용이하게 파일들을 액세스할 수 있게 되었다(Rowset provider를 사용하면 사용자가 데이터 소스를 관계형으로 액세스할 수 있으며, 이는 provider에 대하여 쿼리의 결과로서 테이블을 얻게 된다는 것을 의미한다).
이번 기사는 사전 검토 차원의 기사이기 때문에 기술적으로 깊은 부분까지 설명할 수도 없고 상세한 구문에 대하여 설명할 수도 없지만, 새로운 기능들에 대하여 주요 내용들을 알려 줄 수는 있다. 유콘 베타 테스트 프로그램에 참여하고 있는 분들의 경우에는 유콘에 포함된 온라인 설명서와 Yukon Beta Readiness Kit에 포함되어 있는 "유콘 T-SQL Enhancements" 기술 문서에서 보다 자세한 내용들을 참고하기 바란다. 유콘의 발표에 가까워질수록 다음 기사들에서는 보다 상세한 내용들을 다루게 될 것이다.
CTE의 기능 뷰와 파생된 테이블(derived table)의 기능을 결합할 수 있다면 좋겠다는 생각을 해 본 적이 있는가? T-SQL에서 재귀 쿼리를 작성할 필요가 있었던 적은 없는가? CTE(Common Table Expressions)를 사용하면 이 두 가지가 모두 가능해진다. CTE는 명명된 테이블 식 (named table expression)이며 그 뒤에 쿼리가 온다. CTE에는 두 가지 유형이 있는데, 하나는 비재귀(nonrecursive) CTE이고, 다른 하나는 재귀(recursive) CTE이다. 비재귀 CTE는 파생된 테이블과 뷰의 특성을 혼합한 기능이다. 파생된 테이블과 함께 사용하면 쿼리에 대하여 별칭(alias)을 부여할 수 있고, 쿼리의 결과로 얻어지는 컬럼에 대해서도 별칭을 제공할 수 있으며, CTE는 외부 쿼리(outer query)가 종료된 이후에는 남아 있지 않는다. 뷰와 함께 사용하면 외부 쿼리에서 테이블 식을 두 번 이상 참조할 수 있다. 대개 두 번 이상 테이블 식을 참조할 필요가 있고 테이블 식이 데이터베이스에 남아 있는 것을 원하지 않을 때 비재귀 CTE를 사용하게 될 것이다. 예를 들어, 각 년도별로 Northwind 데이터베이스의 Orders 테이블로부터 해당 년도의 총 주문 수량과 그 전년도의 총 주문 수량을 알고자 한다고 가정해 보자. [리스트1]에 있는 코드는 CTE를 사용하여 이 결과를 얻어 내는 방법을 보여 준다. [리스트 1]의 callout A를 보면 WITH 키워드 다음에 나오는 것이 CTE의 별칭(YearlyOrders)과 결과 컬럼 별칭들이다. Body인 callout B에서 사용자가 쿼리에 대해 부여한 별칭이 사용되고 있으며, 이 쿼리는 년도별 주문 수량 정보를 반환한다. 외부 쿼리에서는 YearlyOrders의 두 인스턴스인 CurYear와 PrevYear간에 외부 조인(outer join)을 수행하여 금년도 주문과 전년도 주문을 매칭시켜 보여 준다. [그림1]에는 [리스트1]의 코드를 수행한 결과가 나와 있다. 유콘 이전의 SQL 서버 버전에서 이와 같은 결과를 얻으려면 두 가지 방법을 사용할 수 있다. 하나는 뷰를 만들고 쿼리에서 그 뷰를 두 번 참조하는 것인데, 이 방법은 데이터베이스에 남아 있는 오브젝트를 생성할 수 밖에 없는 방식이다. 또 다른 방법은 파생된 테이블을 두 개 사용하는 것인데, 이 경우에는 코드를 두 번 작성해야 한다.
재귀 CTE는 T-SQL 쿼리에서 재귀 호출 기능이 가능하도록 해 준다. 재귀 CTE의 body에는 적어도 두 개의 쿼리(member라고도 함)가 포함되며, 이 쿼리들은 UNION ALL 연산자로 연결된다. Anchor member는 SQL 서버가 한 번 호출하는 쿼리이다. Recursive member는 CTE의 이름을 참조하는데, 이는 이전의 작업 단계에서 반환된 결과 집합을 나타낸다. 쿼리가 빈 집합을 반환할 때까지 SQL 서버가 반복적으로 Recursive member를 호출한다. 비재귀 CTE의 경우에는 외부 쿼리(outer query)가 CTE의 이름을 참조하지만, 재귀 CTE의 경우에는 참조가 Anchor member를 호출한 결과와 모든 Recursive member의 호출 결과들에 대하여 UNION ALL을 수행한 결과가 된다.
[리스트 2]에 재귀 CTE의 예제가 나와 있다. 이 코드의 목적은 Northwind 데이터베이스에 있는 Employees 테이블을 검색하여 지정한 직원과 지정한 직원의 모든 직간접 부하직원들을 반환하는 것이다. Employees 테이블에는 아홉 명의 직원들에 대한 정보가 저장되어 있으며, 모든 직원들은 한 명의 관리자에게 보고를 하는 체계이며, 관리자의 employee ID는 ReportsTo 컬럼에 저장된다. Andrew (employee ID 2)는 최상위 관리자이기 때문에 ReportsTo 컬럼의 값이 NULL이다. 만약 Andrew와 그의 직간접 부하직원을 요청하면 결과에는 아홉 명의 직원들이 모두 포함될 것이다.
이것이 어떻게 동작하는지를 이해하기 위하여 [리스트 2]에 있는 코드를 살펴 보자. SQL 서버는 callout A에서 Anchor member를 호출하며, 그 결과 Andrew에 대한 행과 Andrew가 시작점이기 때문에 0이라는 값을 포함하는 lvl이라는 의사 컬럼(pseudo column)을 반환한다. Callout B에서 Recursive member는 Employees 테이블을 이전 단계에서 얻은 결과와 조인하게 되며, 그 결과 이전 단계에서 반환된 직원들의 부하직원들이 반환된다. 재귀 쿼리에서 lvl 컬럼은 해당 직원이 관리 체인 상에서 Andrew로부터 얼마나 떨어져 있는지를 보여 주기 위하여 이전 값에 1을 더해서 lvl 컬럼 값을 반환한다.
Recursive member를 첫 번째 호출(invocation)하면 Andrew의 부하직원들인 Nancy, Janet, Margaret, Steven, Laura가 반환되며, 이들의 lvl 컬럼 값은 1이 된다. 두 번째 호출하면 첫 번째 호출에서 반환된 직원들의 부하직원들이 반환되는데, 그들은 Michael, Robert, Anne이며 이 때 그들의 lvl 컬럼의 값은 2가 된다. 세 번째 호출하면 빈 결과 집합을 반환하게 되는데, 그 이유는 마지막에 반환된 직원들은 부하직원들이 없기 때문이며, 이 경우 재귀 호출이 종료된다. CTE의 이름을 참조하는 callout C의 외부 쿼리는 Anchor member의 결과를 포함하여 모든 호출의 통합 결과를 반환한다. [리스트 2]의 코드를 수행하여 얻어지는 결과가 [그림 2]에 나와 있다. 유콘 이전 버전의 SQL 서버에서 동일한 결과를 얻기 위해서는 직원 계층 구조를 설명하기 위하여 데이터베이스에 중복 데이터를 저장해 두거나 아니면 커서와 임시 테이블을 사용하고 반복 수행 코드를 작성해야 하는데, 그렇게 하면 시간도 많이 걸리고 유지 보수도 더 어려워진다.
PIVOT과 UNPIVOT 새로운 PIVOT 연산자를 사용하면 행을 컬럼으로 회전시킬 수 있으며, 이 때 원한다면 그에 따른 집계를 수행할 수도 있다. 예를 들어 Orders 테이블로부터 직원별 년간 주문 수량을 반환하고자 한다고 가정해 보자. 단순한 GROUP BY 쿼리를 작성하여 그 정보를 얻을 수도 있기는 하지만, [그림 3]에서와 같이 직원별로 하나의 행이 반환되도록 하며 별도의 컬럼에 각 년도의 주문 수량을 표시하고자 한다고 가정해 보자. PIVOT 연산자를 사용하면 사용자가 보고자 하는 년도를 지정하기만 하면 원하는 정보를 쉽게 얻을 수 있다.
WITH EmpOrders(orderid, empid, orderyear) AS (SELECT OrderID, EmployeeID, YEAR(OrderDate)FROM Orders) SELECT * FROM EmpOrders PIVOT(COUNT(orderid) FOR orderyear IN([1996], [1997], [1998])) AS P ORDER BY empid
이 쿼리에서는 CTE를 사용하여 PIVOT 연산자와 함께 사용하고자 하는 컬럼들을 격리시킨다. CTE 대신 파생된 테이블을 사용할 수도 있지만, 쿼리가 CTE를 오직 한 번만 참조하기 때문에 이 경우에는 두 가지 방법에 차이가 별로 없다.
PIVOT 연산자의 body에 있는 FOR 절에는 컬럼의 이름을 지정하는데, 이 쿼리에서는 orderyear이며, 이 값이 바로 결과로 얻어지는 컬럼들을 회전시키는 기준이 되는 컬럼이다. FOR 절 앞에는 결과 컬럼 값 계산에 사용하고자 하는 집계 함수를 지정하는데, 이 경우에는 각 주문 년도에 대한 주문 ID의 개수가 된다.
내부적으로는 PIVOT 연산자가 지정하지 않는 컬럼들을 기반으로 하여 GROUP BY를 수행하며, 이 경우에는 empid를 기반으로 하여 GROUP BY가 수행된다. 각각의 서로 다른 employee ID에 대하여 [1996], [1997], [1998] 세 개의 컬럼들을 결과로 얻게 되며, employee ID와 년도의 각 교차 지점에 주문 수량 정보가 표시된다.
UNPIVOT 연산자는 PIVOT 연산자의 반대이다. UNPIVOT 연산자는 컬럼들을 행들로 회전시킨다. 한 예로써 이전의 PIVOT 쿼리를 수행하고 FROM 절 바로 앞에 INTO #EmpOrders를 추가해서 #EmpOrders라는 이름의 임시 테이블을 만든다. 그리고 각 사원들에 대한 년간 주문 수량을 반환하고자 하며, 그 결과의 각 행에는 사원과 년도의 조합을 표시한다고 가정해 보자. [그림 4]에 생략된 형태가 나와 있다.
다음의 UNPIVOT 쿼리를 수행해 보자:
SELECT empid, CAST(orderyear AS int) AS orderyear, numorders FROM #EmpOrders UNPIVOT(numorders FOR orderyear IN([1996], [1997], [1998])) AS P ORDER BY empid, orderyear
Orderyear와 numorders 둘 다 결과 컬럼이 된다. Orderyear 컬럼에는 IN 절에 의해 각각의 컬럼 이름들로부터 파생된 1996, 1997, 1998 주문 년도들이 포함될 것이다.
Numorders 컬럼에는 [1996], [1997], [1998] 세 개 컬럼들에 현재 저장되어 있는 값들이 포함되며, 각각은 각 년도에 대한 주문 수량 정보를 나타낸다. CAST() 함수를 사용하여 주문 년도에 대한 컬럼 이름들을 가지고 있는 문자열을 정수로 변환한다.
새로운 구문에 익숙해지기만 하면 PIVOT과 UNPIVOT 쿼리를 작성하는 것은 간단하다. 유콘 이전 버전의 SQL 서버에서도 동일한 결과를 얻을 수 있기는 하지만 더 길고 복잡한 코드를 작성해야 한다.
TOP 기능의 개선 마이크로소프트는 유콘에서 TOP 옵션에 두 가지 중요한 측면에서 개선을 가져왔다. 첫 번째 추가된 기능은 TOP에 대한 인수로서 상수가 아닌 식(expression)을 기술할 수 있게 된 것이다. 아마도 많은 분들이 오랫동안 기다려 온 기능이 아닐까 싶다. 예를 들면 다음에서와 같이 쿼리가 반환하는 행의 개수를 변수로 지정할 수 있게 된 것이다: DECLARE @n AS int SET @n = 5 SELECT TOP(@n) * FROM Orders ORDER BY OrderID
그리고 TOP 다음에 나오는 괄호 안에 서브쿼리와 같은 더 복잡한 식을 지정할 수도 있게 되었다. PERCENT 옵션을 사용하지 않을 때에는 식의 결과는 행들의 개수를 지정하는 bigint 값이 되며, PERCENT 옵션을 사용하는 경우에는 결과는 전체 행에서 차지하는 백분율을 지정하는 0과 100 사이의 부동 소수점 값이 된다
또 다른 개선 사항으로는 사용자가 INSERT, UPDATE, DELETE 문에 TOP을 사용할 수 있게 되었다는 점이다. 예를 들어 다음 코드는 과거 이력 데이터가 저장되어 있는 큰 테이블에서 한 번에 만 개씩 행들을 삭제하는 쿼리이다.
WHILE 1=1 BEGIN DELETE TOP(10000) FROM Sales WHERE dt < '20000101' IF @@rowcount <10000 BREAK END
이 코드에서와 같이 하나의 대량 삭제 DELETE문을 작은 크기의 여러 개의 DELETE문으로 쪼개지 않으면 트랜잭션 로그가 상당히 많이 커질 수 있으며, 그 작업 중에 발생하는 행 잠금들이 전체 테이블 잠금으로 상향 조정될 수도 있다. DELETE 연산을 여러 개의 n 행들로 분리할 때는 n개 행씩 삭제하는 DELETE 작업은 각각 개별 트랜잭션으로 간주된다. 작업이 수행되는 동안 사용자가 트랜잭션 로그 백업을 수행하게 되면 트랜잭션이 종료된 후에는 SQL 서버가 그 트랜잭션 로그 공@?재사용할 수 있게 된다. 그리고 한 번 수행되는 DELETE 문의 삭제 행의 수가 작으면 대개의 경우 행 수준 잠금으로 처리되는데, 그 이유는 SQL 서버가 리소스 부족으로 인하여 전체 테이블 잠금으로 잠금 수준을 조정해야 하는 상황이 발생하지 않기 때문이다. SQL 서버 2000에서도 대량 삭제 DELETE 문을 여러 개의 DELETE 문들로 쪼개어 행들을 삭제할 수 있으며 유콘 이전 버전들에서는 SET ROWCOUNT 옵션을 사용하여 그렇게 할 수 있다. 그렇지만 새로 지원되는 TOP 옵션이 SET ROWCOUNT보다 나은 기능이다.
CROSS APPLY와 OUTER APPLY CROSS APPLY와 OUTER APPLY는 외부 쿼리(outer query)의 각 행에 대하여 테이블 값 함수를 호출할 수 있도록 해 주는 새로운 관계형 연산자들이다. 원하는 경우 외부 행의 컬럼을 함수의 인자(argument)로 사용할 수도 있다. 예를 들어 다음의 코드를 수행하면 fn_custorders()라는 이름의 사용자 정의 함수(UDF)가 만들어지며, 이 함수는 인수로서 customer ID와 number를 받아 들여서 입력 받은 고객에 대한 가장 최근의 주문 요청 횟수를 포함하는 테이블을 결과로 반환한다: CREATE FUNCTION fn_custorders (@custid AS nchar(5), @n AS int) RETURNS TABLE AS RETURN SELECT TOP (@n) * FROM Orders WHERE CustomerID = @custid ORDER BY OrderDate DESC, OrderID DESC
다음 쿼리는 CROSS APPLY 연산자를 사용하여 Customers 테이블에 있는 각 고객들에 대하여 가장 최근의 세 개의 주문을 반환한다.
SELECT C.CustomerID, C.CompanyName, O.* FROM Customers AS C CROSS APPLY fn_custorders (C.CustomerID, 3) AS O ORDER BY C.CustomerID, O.OrderDate DESC, O.OrderID DESC
이 쿼리는 결과 테이블에 현재 있는 91명의 고객들에 대하여 263개의 행을 반환한다. 이 쿼리의 경우에는 주문이 없는 고객들(FISSA와 PARIS)에 대해서는 fn_custorders() 함수가 빈 결과 집합을 반환하기 때문에 주문이 없는 고객들은 결과에서 제외된다.
만약 함수가 빈 결과 집합을 반환하는 데이터에 대하여 외부 쿼리로부터 행을 포함시키기를 원하면 CROSS APPLY 연산자 대신 OUTER APPLY 연산자를 사용하면 된다. 조건에 부합하지 않는 행들의 경우에는 함수가 반환하는 결과 컬럼들의 값은 NULL이 된다.
유콘 이전의 SQL 서버 버전들에서는 외부 쿼리의 각 행에 대하여 하나의 쿼리에서 테이블 값 함수를 호출할 수 없다. @n이 인수일 때 각 고객에 대하여 @n개의 가장 최근의 주문들을 반환하기 위해서는 동적 수행과 서브 쿼리를 사용하는 훨씬 복잡한 코드를 작성해야 하며, 그 솔루션은 성능 면에서도 좋지 않고 유지 보수 측면에서도 어려운 단점이 있다.
결과를 반환하는 DML문과 WAITFOR 유콘에서는 데이터를 변경하는 SQL 문들이 단순히 데이터를 수정하는 데 그치지 않고 데이터를 반환할 수도 있다. INSERT, UPDATE, DELETE문에 OUTPUT 절을 추가하여 반환하고자 하는 데이터를 지정할 수 있다. 트리거를 작성할 때 inserted 테이블과 deleted 테이블을 사용하는 방식과 유사하게 inserted 테이블과 deleted 테이블을 참조하여 확인하고자 하는 데이터를 지정하면 된다. WAITFOR 명령어도 개선된다. SQL 서버 2000과 그 이전 버전들에서는 WAITFOR 명령어에서 사용할 수 있는 옵션이 두 가지뿐이다. 하나는 다음 명령어로 진행하기 전에 기다리는 기간을 지정하는 것이고, 다른 하나는 다음 명령어로 진행하는 일시(datetime)를 지정하는 것이다. 그렇지만 이제 유콘에서는 SQL 문을 지정할 수 있으며 다음 명령어로 진행하기 전에 그 SQL 문이 적어도 한 행을 처리할 때까지 또는 지정한 timeout 값에 도달할 때까지 기다리도록 할 수 있다.
이 두 가지 기능을 결합하면 데이터 변경 작업이 행들을 처리할 때까지 기다릴 수 있으며 데이터 변경 작업으로부터 데이터를 돌려 받을 수도 있다. 예를 들어, Queue라는 이름의 테이블에 메시지 큐를 관리한다고 가정해 보자:
CREATE TABLE Queue( keycol int NOT NULL IDENTITY PRIMARY KEY, dt datetime NOT NULL DEFAULT(GETDATE()), processed bit NOT NULL DEFAULT(0), msg varchar(50) NOT NULL)
여러 프로세스들이 새로운 메시지들을 Queue 테이블에 insert할 것이다. 여러 개의 서로 다른 연결에서 다음 코드를 수행하면 된다:
WHILE 1 = 1 BEGIN INSERT INTO Queue(msg) VALUES('msg' + CAST(CAST(RAND()*9999999 AS int) AS varchar (7))) WAITFOR DELAY '00:00:01' END
위의 쿼리에서는 기존의 WAITFOR 사용 방식으로 WAITFOR 명령어를 사용하여 데이터를 insert하기 전에 1초씩 기다리도록 했다.
그런데 여기서 사용자가 여러 개의 메시지들을 처리하고 그 메시지들을 처리한 것으로 표시할 필요가 있다면, 여러 세션에서 [리스트 3]에 있는 코드를 수행하면 된다.
[리스트 3]의 코드는 세 가지 새로운 기능들을 사용하고 있다. 다음 명령어로 진행하기 전에 WAITFOR 명령어가 WAITFOR 명령어의 인수인 UPDATE 문이 적어도 한 행을 처리할 때까지 기다렸다가 COMMIT을 수행한다. UPDATE 문은 UPDATE된 행들의 새로운 데이터를 output으로 반환해 주며 쿼리에서 READPAST 힌트를 지정했기 때문에 잠금이 걸려 있는 행들은 그냥 지나간다.
유콘 이전의 SQL 서버 버전들에서는 READPAST 힌트를 SELECT 문에만 사용할 수 있지만, 유콘에서는 READPAST 힌트를 UPDATE 문과 DELETE 문에 대해서도 사용할 수 있으며, 그로 인해 여러 개의 프로세스들이 동시에 병렬 처리될 때 잠금이 걸려 있지 않은 행들은 처리하고, 다른 세션에서 처리 중이어서 잠금이 걸려 있는 행들은 그냥 지나갈 수 있게 되었다. 이와 유사하게 동시에 여러 프로세스들이 잠금이 걸려 있지 않은 이미 처리 완료된 메시지들을 삭제할 수 있다:
WHILE 1 = 1 WAITFOR(DELETE FROM Queue WITH (READPAST) OUTPUT DELETED.* WHERE processed = 1)
만약 유콘 이전의 SQL 서버 버전들에서 큐잉 기능을 제공하고자 하는 경우에는 SQL 서버 외부에서 로직을 구현해야 한다. 앞에서 보여 준 예제들은 새로운 WAITFOR 명령어와 결과를 반환하는 DML 기능을 사용한 매우 단순한 예제들이다. 유콘에서는 완전히 새로운 큐잉 구조가 추가되며 서비스 브로커(Service Broker)라는 큐잉 플랫폼도 구현되어 있다. 이 주제는 이 기사에서 다루기에 너무 큰 주제인 까닭에 이후에 서비스 브로커에 대하여 다루는 기사들을 참고하기 바란다.
DDL 트리거와 DML 이벤트 통지 유콘 이전의 SQL 서버 버전들에서는 DDL 이벤트들에 대하여 트리거를 발생시킬 수 없다. DBA들은 오랫동안 그런 기능들을 요청해 왔으며, 주로 그 목적은 감사(auditing)와 권한이 부여된 사용자들에 의한 스키마 변경 방지를 위한 것이었다.
유콘에서는 오브젝트들을 생성하거나 삭제하는 것과 같은 DDL 이벤트들에 대해서도 서버 차원 또는 데이터베이스 차원에서 트리거를 만들 수 있다. 트리거 내에서 EventData()라는 새로운 함수를 호출하면 이벤트와 관련되는 정보(예를 들어 이벤트를 발생시킨 프로세스 ID, 발생 시각, 수행된 SQL 문)를 액세스할 수 있으며, EventData() 함수는 XML 형태로 된 정보를 반환해 준다. 예를 들어 서버 차원에서 로그인과 관련되는 이벤트를 잡아 내고자 하는 경우에는 다음과 같이 트리거를 작성하면 된다:
CREATE TRIGGER audit_ddl_logins ON ALL SERVER FOR CREATE_LOGIN, ALTER_LOGIN, DROP_LOGIN AS PRINT 'DDL LOGIN took place.' PRINT EventData()
로그인 계정을 만들고 변경하고 삭제하는 다음 코드를 수행하면 트리거를 테스트해 볼 수 있다: CREATE LOGIN login1 WITH PASSWORD = '123' ALTER LOGIN login1 WITH PASSWORD = 'xyz'
DROP LOGIN login1 트리거 내에서 이벤트 데이터를 조사할 수도 있고, 작업을 롤백시키는 것과 같은 적절한 조치를 취할 수도 있다. 트리거를 삭제하고자 하는 경우에는 다음 코드를 수행하면 된다: DROP TRIGGER audit_ddl_logins ON ALL SERVER 이와 유사하게 데이터베이스 차원에서 특정 DDL 이벤트들 또는 모든 DDL 이벤트들을 잡아 내는 트리거를 사용할 수도 있다. 에를 들어 다음에 나오는 트리거는 트리거가 만들어진 데이터베이스에 대하여 수행되는 모든 DDL 이벤트들을 잡아 낸다:
CREATE TRIGGER audit_ddl_events ON DATABASE FOR DDL_DATABASE_LEVEL_EVENTS AS PRINT 'DDL event took place in database ' + DB_NAME() + '.' PRINT EventData()
테이블과 뷰를 만든 다음에 만든 테이블과 뷰를 삭제하는 다음 코드를 수행하면 트리거를 테스트해 볼 수 있다:
CREATE TABLE T1(col1 int) GO CREATE VIEW V1 AS SELECT * FROM T1 GO DROP TABLE T1 DROP VIEW V1
이벤트 정보를 반환하는 것뿐만 아니라, 코드가 트리거를 호출하고 DDL 이벤트가 발생했음을 알리는 메시지를 인쇄할 수도 있다.
트리거를 삭제하고자 하는 경우에는 다음 코드를 수행하면 된다:
DROP TRIGGER audit_ddl_events ON DATABASE
트리거는 동기적으로 동작하는데, 그것은 트리거 코드의 수행이 완료되기 전까지는 트리거 수행을 유발한 애플리케이션으로 컨트롤이 돌아가지 않는다는 것을 의미한다. 또한 유콘에서는 asynchronous event consumption도 도입된다. 여러 개의 서로 다른 애플리케이션들이 어떤 프로세스가 발생시킨 이벤트(DML 이벤트도 가능함)가 발생하는 경우에 통지를 받도록 구독할 수 있다. 그리고 그 이벤트를 활성화하는 코드를 발생시킨 애플리케이션은 구독 애플리케이션들 모두가 작업을 완료할 때까지 기다리지 않고 자신의 작업을 계속할 수 있다.
오류 처리 오랫동안 기다려 온 또 하나의 T-SQL 개선 사항은 오류 처리 기능이다. 유콘에는 TRY/CATCH 구조가 추가됨으로써 다른 개발 환경에서 지원되는 오류 처리 기능과 유사한 기능을 구현할 수 있다. 유콘 이전의 SQL 서버 버전에서는 연결이 비정상적으로 종료되었던 오류들을 이제 유콘에서는 잡아 낼 수 있으며, 그 오류들을 세련되고 구조화된 방식으로 처리할 수 있게 되었다. 한 예로서 T1 이라는 테이블을 만들어 보자: CREATE TABLE T1(col1 int NOT NULL PRIMARY KEY) {리스트 4}에 있는 코드는 TRY/CATCH 기능을 사용하여 테이블에 데이터가 insert될 때 발생할 수 있는 기본 키 위반 오류(Primary key violation error) 또는 데이터 변환 오류 등과 같은 오류들을 잡아 내는 방법을 보여 준다. TRY 구조는 트랜잭션을 갑자기 중단시키는 오류들에 대해서만 동작하기 때문에, XACT_ABORT 옵션을 ON으로 설정해야 하며 오류 처리 코드를 트랜잭션 내부에 작성해야 한다. TRY 블록에는 오류를 잡아내고자 하는 트랜잭션을 포함시킨다. 오류 발생 시에는 트랜잭션이 “failed” 상태가 된다. 사용자는 여전히 트랜잭션의 컨텍스트 내에 남아 있게 되며, SQL 서버는 잠금을 계속 유지하며 트랜잭션 작업을 되돌려 주지 않는다. 컨트롤은 가장 가까운 CATCH 블록으로 전달되며, CATCH 블록에서는 사용자가 오류를 조사하고 실패한 트랜잭션을 롤백한 다음에 적절한 조치를 취할 수 있다. [리스트 4]의 코드를 맨 처음 수행할 때에는 오류가 발생하지 않기 때문에 CATCH 블록이 활성화되지 않는다. [리스트 4]의 코드를 두 번째 수행하면 CATCH 블록이 기본 키 위반 오류를 잡아내게 된다. 그렇지만 첫 번째 INSERT 문을 주석 처리하고 두 번째 INSERT 문을 주석 해제한 다음에 코드를 다시 수행하면 어떤 일이 일어나는지를 확인해 보기 바란다. 그 경우에는 변환 오류가 발생하게 될 것이다.
데이터 타입과 BULK Rowset Provider 유콘은 데이터 타입에 있어서도 몇 가지 흥미로운 변화를 가져 온다. 그 중 하나는 새로운 XML 데이터 타입을 도입함으로써 XML 데이터를 변수와 테이블 컬럼으로 저장하고 관리할 수 있게 되었다는 점이다. 그리고 새로운 MAX 옵션을 사용하면 명시적인 길이를 지정하지 않고 varchar, nvarchar, varbinary와 같은 동적 컬럼들을 정의할 수도 있기 때문에, LOB 데이터 타입인 text, ntext,image 데이터 타입에 대한 보다 자연스러운 대안을 가질 수 있게 되었다. MAX 옵션을 사용하면 하나의 컬럼에 최대 2GB의 데이텨를 저장할 수 있다. 여기에서 보다 자연스럽다는 것은 새로운 MAX 옵션을 사용하면 유콘 이전 버전에서와 같이 WRITETEXT, UPDATETEXT, READTEXT 등의 명령어를 사용하지 않고 일반적인 DML 문을 사용하여 크기가 큰 동적 컬럼들을 처리할 수 있다는 것을 의미한다.
또한 유콘에서는 새로운 BULK rowset provider가 지원되는데, 이 기능을 사용하면 파일을 손쉽고 세련되게 행 집합들로 처리할 수 있다.
[리스트 5]에 있는 코드를 수행해 보면 이러한 변화들을 쉽게 이해할 수 있을 것이다. [리스트 5]에 있는 코드는 TestLargeObjects라는 테이블을 만들고 그 테이블에 하나의 행을 insert한다. 다음 코드를 수행하면 XML 컬럼 x가 SELECT 쿼리의 XML 결과로 업데이트된다:
UPDATE TestLargeObjects SET x = (SELECT * FROM Customers FOR XML AUTO) WHERE keycol = 1
다음과 같이 BULK provider와 OPENROWSET() 함수를 사용하면 텍스트 파일을 varchar(MAX) 컬럼으로 로드할 수 있는데, 이 때 파일 경로와 SINGLE_CLOB 옵션을 다음과 같이 지정하면 된다:
UPDATE TestLargeObjects SET vc = (SELECT vc FROM OPENROWSET( BULK 'c:\temp\textfile1.txt', SINGLE_CLOB) AS CLOB(vc)) WHERE keycol = 1
SINGLE_CLOB 옵션은 하나의 텍스트 파일을 처리하여 하나의 컬럼을 가지는 단일 행으로 반환되도록 처리한다는 것을 의미한다. 이와 유사하게 SINGLE_NCLOB 옵션을 지정함으로써 유니코드 형태의 파일을 nvarchar(MAX) 컬럼으로 로드할 수도 있고, 이진 파일을 SINGLE_BLOB 옵션을 사용하여 varbinary(MAX) 컬럼으로 로드할 수도 있다.
아마도 이 중 가장 여러분을 흥분시키는 기능은 일자 데이터 타입과 시각 데이터 타입을 별도로 지원하는 것일 것이다. 다음 코드를 수행하면 일자 컬럼과 시각 컬럼을 별도로 가지는 테이블이 만들어지고 그 테이블에 하나의 행이 insert된다:
CREATE TABLE DateTimeTest(datecol date, timecol time) INSERT INTO DateTimeTest VALUES(CAST('2003-11-01' AS date), CAST('10:30:59.999999' AS time))
새로 지원되는 일자 데이터 타입과 시각 데이터 타입은 CLR 기반이며, CLR 기반이라는 것은 그 데이터 타입들이 유콘이 지원하는 .NET 구조에 기반하여 개발되었다는 것을 의미한다. 그 결과로 별도로 지원되는 일자와 시각 데이터 타입이 다양하고 유용한 메서드와 속성(property)을 제공해 준다. 예를 들어 다음에 있는 쿼리에서처럼 ConvertToString() 메서드를 date와 time 양쪽에 적용하면 지정한 포맷 문자열에 따라 date와 time 값을 표시할 수 있다:
SELECT datecol::ConvertToString( 'MM/dd/yyyy')AS thedate, timecol::ConvertToString( 'HH:mm:ss.fff') AS thetime FROM DateTimeTest
[그림 5]를 보면 이 쿼리의 수행 결과가 어떻게 나오는지 확인할 수 있다. 결과 컬럼 thedate에는 MM/dd/yyyy 형태로 된 포맷화된 일자가 표시되며, thetime 컬럼에는 HH:mm:ss.fff 형태로 된 시각이 포함된다.
T-SQL의 미래 앞에서 보았듯이 유콘에는 많은 T-SQL 관련 개선 사항과 새로운 기능이 포함되어 있다. 유콘의 등장으로 독자들은 이전 버전에 비해 더 적은 양의 코드를 작성하여 이전 버전들에서와 동일한 결과를 얻을 수 있게 되며, 훨씬 더 좋은 솔루션들을 많이 확보할 수 있게 될 것이다. 집합 기반의 관계형 언어로서의 T-SQL은 지금까지도 항상 강력했었지만 앞으로는 훨씬 더 강력해질 것이다. [그림 1] 해당 년도와 그 전년도의 주문 정보 [그림 2] Andrew와 Andrew의 직간접 부하 직원들
[그림 3] PIVOT 기능을 활용한 직원별 년간 주문 정보 [그림 4] UNPIVOT 기능을 활용한 직원별 년간 주문 정보 [그림 5] 일자와 시각 값 조회 리스트 1 리스트 1: CTE를 사용하여 금년도와 전년도 주문 정보를 반환하는 쿼리 USE Northwind BEGIN CALLOUT A WITH YearlyOrders(orderyear, numorders) END CALLOUT A AS BEGIN CALLOUT B ( SELECT YEAR(OrderDate), COUNT(*) FROM Orders GROUP BY YEAR(OrderDate) ) END CALLOUT B SELECT CurYear.orderyear, CurYear.numorders, PrevYear.numorders AS prev FROM YearlyOrders AS CurYear LEFT OUTER JOIN YearlyOrders AS PrevYear ON CurYear.orderyear = PrevYear.orderyear + 1 ORDER BY orderyear
리스트 2 리스트 2: 재귀 CTE를 사용하여 Andrew의 부하직원들을 반환하는 쿼리 WITH EmpsCTE(empid, mgrid, fname, lname, lvl) AS ( BEGIN CALLOUT A SELECT EmployeeID, ReportsTo, FirstName, Lastname, 0 FROM Employees WHERE EmployeeID = 2 END CALLOUT A UNION ALL BEGIN CALLOUT B SELECT E.EmployeeID, E.ReportsTo, E.FirstName, E.Lastname, M.lvl + 1 FROM Employees AS E JOIN EmpsCTE AS M ON E.ReportsTo = M.empid END CALLOUT B ) BEGIN CALLOUT C SELECT * FROM EmpsCTE END CALLOUT C
리스트 3: TRY/CATCH를 활용한 오류 처리 예제 SET XACT_ABORT ON BEGIN TRY BEGIN TRAN INSERT INTO T1 VALUES(1) -- INSERT INTO T1 VALUES('two') COMMIT END TRY BEGIN CATCH TRAN_ABORT DECLARE @err AS int SET @err = @@error ROLLBACK IF @err = 2627 PRINT 'Primary key violation.' ELSE IF @err = 245 PRINT 'Conversion error.' ELSE PRINT 'Error ' + CAST(@err AS varchar(10)) + ' occurred.' END CATCH
리스트 4: 메시지들에 대하여 처리 표시를 하고 UPDATE 결과를 output으로 반환하는 코드
WHILE 1 = 1 BEGIN BEGIN TRAN WAITFOR(UPDATE Queue WITH (READPAST) SET processed = 1 OUTPUT INSERTED.* WHERE processed = 0) -- process messages' content COMMIT END 리스트 5: LOB 테스트 테이블을 만드는 코드 CREATE TABLE TestLargeObjects(keycol int NOT NULL PRIMARY KEY, x XML, vc varchar(MAX), nvc nvarchar(MAX), vb varbinary(MAX)) INSERT INTO TestLargeObjects(keycol) VALUES(1) 출처 : SQL 매거진 12월호
happy4u
2004. 8. 10. 16:46
2004. 8. 10. 16:46
happy4u
2004. 7. 1. 10:00
2004. 7. 1. 10:00
음... 테스트 해 봤는데 일단 테스트 SP에 대해서는 잘 되었음. --;; 이거 이렇게 쉽게 풀리면 문제 있는거 아닌가.... 웅.. 내가 담아온 포스트의 SP는 역슬래시 때문에 바로 실행하면 에러가 나서 수정한 내용 다시 올립니다. 최근에 이용해 볼 일이 있어 다시 해 봤는데... 치명적인 문제가 있네요... SP 문자 수가 4000을 넘어가는 경우 제대로 수행이 안됩니다. 특히 큰 문제는 원본 SP가 에러가 나면서 삭제된다는 문제가 있습니다. 꼭 주의하세요~ -------------------------------------- SET QUOTED_IDENTIFIER OFF GO SET ANSI_NULLS OFF GO CREATE PROCEDURE DECRYPTSP2K (@objName varchar(50)) --INPUT: object name (stored procedure, -- -- view or trigger) --Original idea: shoeboy <shoeboy@a -- dequacy.org> --Copyright ?1999-2002 SecurityFocus --adapted by Joseph Gama --Planet Source Code, my employer and my -- -- self are not responsible for the use -- of -- this code --This code is provided as is and for ed -- -- ucational purposes only --Please test it and share your results AS DECLARE @a nvarchar(4000), @b nvarchar(4000), @c nvarchar(4000), @d nvarchar(4000), @i int, @t bigint --get encrypted data SET @a=(SELECT ctext FROM syscomments WHERE id = object_id(@objName)) SET @b='ALTER PROCEDURE '+ @objName +' WITH ENCRYPTION AS '+REPLICATE('-', 4000-62) EXECUTE (@b) --get encrypted bogus SP SET @c=(SELECT ctext FROM syscomments WHERE id = object_id(@objName)) SET @b='CREATE PROCEDURE '+ @objName +' WITH ENCRYPTION AS '+REPLICATE('-', 4000-62) --start counter SET @i=1 --fill temporary variable SET @d = replicate(N'A', (datalength(@a) / 2)) --loop WHILE @i<=datalength(@a)/2 BEGIN --xor original+bogus+bogus encrypted SET @d = stuff(@d, @i, 1, NCHAR(UNICODE(substring(@a, @i, 1)) ^ (UNICODE(substring(@b, @i, 1)) ^ UNICODE(substring(@c, @i, 1))))) SET @i=@i+1 END --drop original SP EXECUTE ('drop PROCEDURE '+ @objName) --remove encryption --try to preserve case SET @d=REPLACE((@d),'WITH ENCRYPTION', '') SET @d=REPLACE((@d),'With Encryption', '') SET @d=REPLACE((@d),'with encryption', '') IF CHARINDEX('WITH ENCRYPTION',UPPER(@d) )>0 SET @d=REPLACE(UPPER(@d),'WITH ENCRYPTION', '') --replace SP execute( @d) --------------------------------------------------------- 테스트 쿼리 -------------------------------------------- CREATE PROCEDURE hello WITH ENCRYPTION AS PRINT 'Hello World!' GO exec sp_helptext hello GO exec dbo.DECRYPTSP2K 'hello' GO exec sp_helptext hello 암호화한 것을 만든 후 helptext로 확인한 후, DECRYPTSP2K 를 실행시키면, 암호화 된 내용을 풀어서 다시 만들어 주게 된다. 그 후 다시 helptext로 확인하면 내용이 보입니다. --;;
happy4u
2004. 6. 24. 15:55
2004. 6. 24. 15:55
음... 뭐가 어떻게 된건지 모르겠지만... 하여간... 예전에 엑셀 파일을 컨트롤 하는 프로그램을 전면적으로 수정하는 작업 도중에 새로 프로젝트 생성해서 그 프로젝트에서 Excel을 사용할 필요가 있어서, 참조에 Microsoft Excel을 참조를 했는데, Excel 객체 생성이 안되는 것이다... 예전 소스랑 동일하게 했는데 --;; 헤매다가 MS 문서를 찾았는데, 다음과 같이 using문을 써서 해결했다. using Excel = Microsoft.Office.Interop.Excel; using System.Reflection; 쩝... 걍 줄여서 쓰는거였군... using Microsoft.Office.Interop.Excel; 걍 이것만 참조하고 Application mXls = new Application(); 이렇게 생성해서 사용해도 문제 없음 출처 : http://support.microsoft.com/default.aspx?scid=kb;en-us;302084
happy4u
2004. 6. 9. 18:14
2004. 6. 9. 18:14
본 sp는 테이블의 데이터를 insert할 수 있는 insert문을 자동으로 만들어 주는 매우 유용한 sp입니다. 쩝... 생각보다 쉽게 찾았네요 --;; 첨부 파일 중 sp_generate_inserts.sql을 실행하시면 sp가 하나 만들어집니다. sp_generate_inserts.mht는 해당 홈페이지를 긁어 놓은 파일입니다. 혹시 홈페이지가 없어지면 대략 낭패이니까요. sp 이름이 'sp_generate_inserts' 입니다. 사용법은 아래와 같습니다. Example 1: | To generate INSERT statements for table 'titles': EXEC sp_generate_inserts 'titles' | Example 2: | To ommit the column list in the INSERT statement: (Column list is included by default) NOTE: If you have too many columns, you are advised to ommit column list, as shown below, to avoid erroneous results EXEC sp_generate_inserts 'titles', @Include_Column_List = 0 | Example 3: | To generate INSERT statements for 'titlesCopy' table from 'titles' table: EXEC sp_generate_inserts 'titles', 'titlesCopy' | Example 4: | To generate INSERT statements for 'titles' table for only those titles which contain the word 'Computer' in them: EXEC sp_generate_inserts 'titles', @From = "from titles where title like '%Computer%'" | Example 5: | To specify that you want to include TIMESTAMP column's data as well in the INSERT statement: NOTE: By default TIMESTAMP column's data is not scripted EXEC sp_generate_inserts 'titles', @Include_Timestamp = 1 | Example 6: | To print the debug information: EXEC sp_generate_inserts 'titles', @debug_mode = 1 | Example 7: | If you are not the owner of the table, use @owner parameter to specify the owner name: NOTE: To use this option, you must have SELECT permissions on that table EXEC sp_generate_inserts Nickstable, @owner = 'Nick' | Example 8: | To generate INSERT statements for the rest of the columns excluding images: NOTE: When using this otion, DO NOT set @include_column_list parameter to 0 EXEC sp_generate_inserts imgtable, @ommit_images = 1 | Example 9: | To generate INSERT statements for the rest of the columns excluding IDENTITY column: EXEC sp_generate_inserts mytable, @ommit_identity = 1 | Example 10: | To generate INSERT statements for the top 10 rows in the table: EXEC sp_generate_inserts mytable, @top = 10 | Example 11: | To generate INSERT statements only with the columns you want: EXEC sp_generate_inserts titles, @cols_to_include = "'title','title_id','au_id'" | Example 12: | To generate INSERT statements by ommitting some columns: EXEC sp_generate_inserts titles, @cols_to_exclude = "'title','title_id','au_id'" | Example 13: | To avoid checking the foreign key constraints while loading data with INSERT statements: NOTE: The @disable_constraints option will disable foreign key constraints, by assuming that the source data is valid and referentially sound EXEC sp_generate_inserts titles, @disable_constraints = 1 | Example 14: | To avoid scripting data from computed columns: EXEC sp_generate_inserts MyTable, @ommit_computed_cols = 1 |
출처 : http://vyaskn.tripod.com/code.htm#inserts
happy4u
2004. 6. 8. 15:50
2004. 6. 8. 15:50
구성 요소 서비스에서 응용 프로그램을 만들고 Drag & Drop으로 dll 파일을 등록하는 것과 command line에서 RegSvcs.exe fileName.dll로 등록하는 것과 동일하다. [assembly: ApplicationName("NewComTest")] 에 입력한 이름이 자동으로 응용프로그램 이름으로 등록된다. 현재 작업 중이라 성공적으로 완료되면 추가로 올릴 예정
happy4u
2004. 6. 4. 22:31
2004. 6. 4. 22:31
ADO.NET and SQL Server Performance Tips ADO.NET provides several different methods to access SQL Server data, including OLE DB.NET, ODBC.NET, SQLXML, and the SQL Server .NET data provider. Of all of these, the SQL Server .NET data provider is the fastest, as much as 30-40% faster than the others. The SQL Server .NET provider uses TDS (Tabular Data Stream, which is the native SQL Server data format) to communicate with SQL Server. The SQL Server .NET provider can be used to connect to SQL Server 7.0 and SQL Server 2000 databases, but not SQL Server 6.5 databases. If you need to connect to a SQL Server 6.5 database, the best overall choice is the OLE DB.NET data provider. [7.0, 2000] Added 2-25-2002 ADO.Net은 SQL Server에 접근하기 위한 방법으로 OLE DB.NET, ODBC.NET, SQLXML, SQL Server .NET와 같은 data provider를 제공한다. 이 중 Server .NET data provider가 가장 빠르며, 다른 방법에 비해 약 30~40% 빠르다. Server .NET data provider는 SQL Servr와의 통신에 TDS(Tabular Data Stream, which is the native SQL Server data format)를 사용한다. SQL Server .NET provider는 SQL Server 7.0과 SQL Server 2000 데이터베이스 접속에 사용할 수 있으나 SQL Server 6.5에는 사용할 수 없다. 만약 SQL Server 6.5 접속이 필요하다면 가장 좋은 방법은 OLE DB.NET data provider를 이용하는 것이다. ***** When using ADO.NET to make connections to SQL Server, always be sure you explicitly close any Connection, Recordset, or Command objects you have opened. While letting an object go out of scope will in affect close the object, it is not the same as explicitly closing an object. By explicitly closing these objects and setting them to nothing, you do two things. First, you remove the object sooner than later, helping to free up resources. Second, you eliminate the possibility of "connection creep". Connection creep occurs when connection or resource pooling is used and when connections are not properly closed and released from the pool. This helps to defeat the purpose of pooling and reduces SQL Server's performance. [7.0, 2000] Added 2-25-2002 ***** When you specify a server in an ADO.NET connection string, use the server's IP address, not the server's DNS name. By using an IP address instead of a DNS name, name resolution does not have to occur, reducing the amount of time it takes for a connection to be made. A server's IP address can be used to specify either a default or named instance of a server running SQL Server. [ 7.0, 2000] Added 2-25-2002 ADO.Net의 Connection String에 서버를 기술할 때, DNS 이름을 사용하지 말고 서버의 IP address를 사용하라. DNS이름 대신 IP address를 사용하게 되면, 이름 풀이(name resolution)를 필요로 하지 않기 때문에, 연결을 만드는 시간을 줄일 수 있다. 서버의 ip address를 기본 인스턴스 혹은 동작중인 SQL Server의 명명된 인스턴스를 기술하는 데 사용할 수 있다. ***** While SQL Server application roles are handy, they can also negatively affect your application's performance. The reason for this is that a connection to SQL Server using an application role cannot take advantage of connection pooling. In effect, connection pooling is turned off for any connections using application roles. If your application will be making many connections to SQL Server, avoid applications roles for your application's connections. [ 7.0, 2000] Added 2-25-2002 ***** To get the most out of connection pooling in ADO.NET, keep the following in mind when developing your applications: - Be sure than your connections use the same connection string each time. Connection pooling only works if the connection string is the same. If the connection string is different, then a new connection will be opened.
- Only open a connection when you need it, not before.
- Close your connection as soon as you are done using it.
- Don't leave a connection open if it is not being used.
- Be sure to drop any temporary objects before closing a connection.
- Be sure to close any user-defined transactions before closing a connection.
- Don't use application roles if you want to take advantage of connection pooling.
[ 7.0, 2000] Added 2-25-2002 ***** Disconnected recordsets in ADO.NET outperform disconnected recordsets in traditional ADO. ADO.NET is faster than ADO for disconnected recordsets because under ADO, COM marshalling between tiers requires that values in a recordset be converted to values recognized by COM. Under ADO.NET, data type conversion is not required, boosting performance. [ 7.0, 2000] Added 2-25-2002 ADO.NET의 비연결형 recordsets의 성능이 기존의 ADO의 비연결형 recordset보다 성능이 뛰어나다. ADO.NET의 비연결형 recordset이 ADO의 비연결형 recordset보다 빠르다. 이유는 ADO 하위 계층에서 recordset이 COM이 인식할 수 있는 값으로 변환되는 tier 간의 COM 마샬링(marshalling) 때문이다. ADO.NET 하위 계층에서는 데이터 타입 변환이 필요 없기 때문에 보다 좋은 성능을 발휘한다. ***** When possible, use the ExecuteNonQuery method with SQLCommand objects, as this is the most efficient way to execute queries from ADO.NET. Use output parameters with SQLCommand objects if you need to retrieve just a few values, or a single data row, instead of using more expensive techniques, such as a SQLDataAdapter, a SQLDataReader, or a strongly typed DataSet. [2000] Added 3-27-2002 Read an article about ADO.NET. 가급적 ExecuteNonQuery 메소드와 SQLCommand 객체를 사용하라. 이것이 ADO.Net에서 쿼리를 실행하는 가장 효과적인 방법이다. 몇개의 값을 얻어오거나 하나의 data row를 얻어와야 할 경우에는 SQLDataAdapter, SQLDataReader 혹은 DataSet과 같은 비용이 큰 방법 대신 SQLCommand 객체와 output parameters를 사용하라. 출처 : http://www.sql-server-performance.com
happy4u
2004. 6. 4. 22:07
2004. 6. 4. 22:07
7. ADO.Net을 ADO처럼 취급하지 말라 ADO와는 달리 ADO.Net은 비연결 모드에서 동작하도록 디자인 되었으며, 각 클라이언트를 위해 데이터의 독립적인 복사본을 유지 관리한다. ADO.Net 응용프로그램들은 캐시된 데이터 저장소들과 동작하도록 디자인되었으며, 데이터를 검색하고 수정하기 위해 신속하게 데이터베이스에 연결하고 연결을 해제한다. 6. SqlCommandBuilder를 사용할 필요가 없다. SQLCommandBuilder는 원본 데이터베이스에 DataSet 업데이트들을 전파하기 위해 DataAdapter가 사용하는 INSERT, UPDATE, 그리고 DELETE 문을 자동으로 생성한다. 그러나 SqlCommandBuilder를 사용하지 않고 대신에 여러분의 저장 프로시저를 DataAdapter의 InsertCommand, UpdateCommand, 그리고 DeleteCommand 속성에 연결함으로써 성능을 향상시킬 수 있다. 5. DataView를 간과하지 말라 DataView는 웹과 WinForms 응용프로그램으로 바인딩을 해 주고 DataTable로부터 정보의 일부를 얻을 수 있게 한다. 여러분은 계산된 컬럼을 사용함응로써 DataTable에서 데이터를 확장하기 위하여 DataView Expressions 속성을 사용할 수 있다. 4. DataSet을 사용할 필요가 없다. 만일 단일 테이블로부터 데이터를 검색하는 중이라면 DataSet에 독립적인 DataTable의 한 인스턴스를 생성함으로써 DataSet의 오버헤드를 피할 수 있다. 그 다음에 DataSet의 필요 없이 DataTable에서 데이터에 접근할 수도 있고 그것에 바인드를 할 수도 있다. 3. DataSet을 작은 데이터베이스로 여기지 말라 종종 초보 ADO.Net 프로그래머들은 DataSet을 기반 데이터베이스의 작은 버전으로 만들려고 시도한다. DataSet은 단지 응용프로그램이 필요로 하는 데이터를 담고 있는 로컬 데이터 캐시라고 갖누한는 것이 훨씬 좋다. 2. 연결 문자열을 내장하지 말라 여러분의 응용프로그램이 연결 문자열을 내장하면 보안에 문제를 야기할 수 있다. 만일 가능하다면 통합된 보안의 장점을 이용한다. 그것이 불가능하다면 Active Directory나 보안된 구성 파일에 응용프로그램의 연결 문자열을 저장하기 위해 통합된 보안을 사용한다. 1. 불필요한 데이터를 검색하지 않는다. 너무 많은 데이터를 검색하는 것은 응용프로그램의 성능을 저하시키는 가장 좋은 방법이다. 과도한 데이터는 네트워크를 혹사시키며 대규모의 웹 응용프로그램들이 필요로 하는 귀중한 시스템 자원을 낭비한다. 당신의 SELECT 구문들을 재검토하여 그것들이 적절한 WHERE 절을 사용하고 있으며 필요로하는 컬럼들만을 검색하는지 확인한다. 출처 : Windows & .Net Magazine 2003. 7
happy4u
2004. 6. 4. 11:40
2004. 6. 4. 11:40
요약이 문서에서는 JDBC용 Microsoft SQL Server 2000 드라이버를 사용하여 SQL Server 2000에 연결하는 방법을 설명합니다.
참고: JDBC용 Microsoft SQL Server 2000 드라이버의 설치 지침은 JDBC용 Microsoft SQL Server 2000 드라이버 설치 설명서를 참조하십시오.
JDBC용 Microsoft SQL Server 2000 드라이버를 설치한 후 연결 URL이나 JNDI 데이터 원본을 사용하여 프로그램에서 데이터베이스에 연결할 수 있습니다. 이 문서에서는 연결 URL을 사용하여 데이터베이스 연결을 구성하고 테스트하는 방법을 설명합니다.
데이터베이스에 연결하는 한 가지 방법은 JDBC 드라이버 관리자를 통해 DriverManager 클래스의 getConnection 메서드를 사용하는 것입니다. 이 메서드를 사용하는 가장 간단한 방법은 URL, 사용자 이름 및 암호가 포함된 문자열 매개 변수를 사용하는 것입니다. 다음 절에서는 JDBC 프로그램에서 JDBC용 Microsoft SQL Server 2000 드라이버를 로드하는 방법을 설명합니다.
맨 위로 CLASSPATH 변수를 설정하려면JDBC용 Microsoft SQL Server 2000 드라이버의 .jar 파일이 CLASSPATH 변수에 나열되어 있어야 합니다. CLASSPATH 변수는 Java Virtual Machine(JVM)이 컴퓨터에서 JDBC 드라이버를 찾을 때 사용하는 검색 문자열입니다. 드라이버가 CLASSPATH 변수에 없으면 드라이버를 로드하려고 할 때 다음 오류 메시지가 나타납니다.
java.lang.ClassNotFoundException: com/microsoft/jdbc/sqlserver/SQLServerDriver 다음 항목을 포함하도록 시스템의 CLASSPATH 변수를 설정합니다. - \설치 경로\Lib\Msbase.jar
- \설치 경로\Lib\Msbase.jar
- \설치 경로\Lib\Mssqlserver.jar
다음은 구성된 CLASSPATH 변수의 예입니다. CLASSPATH=.;c:\program files\Microsoft SQL Server 2000 Driver for JDBC\lib\msbase.jar;c:\program files\Microsoft SQL Server 2000 Driver for JDBC\lib\msutil.jar;c:\program files\Microsoft SQL Server 2000 Driver for JDBC\lib\mssqlserver.jar 맨 위로 드라이버를 등록하려면드라이버를 등록하면 JDBC 드라이버 관리자에게 로드할 드라이버를 지시하게 됩니다. class.forName 함수를 사용하여 드라이버를 로드하는 경우 드라이버의 이름을 지정해야 합니다. 다음은 JDBC용 Microsoft SQL Server 2000 드라이버의 드라이버 이름입니다. com.microsoft.jdbc.sqlserver.SQLServerDriver 다음 예제 코드에서는 드라이버를 등록하는 방법을 보여 줍니다. Driver d = (Driver)Class.forName("com.microsoft.jdbc.sqlserver.SQLServerDriver").newInstance(); 맨 위로 연결 URL을 전달하려면 연결 URL의 형태로 데이터베이스 연결 정보를 전달해야 합니다. 다음은 JDBC용 Microsoft SQL Server 2000 드라이버의 템플릿 URL입니다. 사용 중인 데이터베이스에 맞는 값으로 변경하십시오. jdbc:microsoft:sqlserver://servername:1433 다음 예제 코드에서는 연결 URL을 지정하는 방법을 보여 줍니다. con = DriverManager.getConnection("jdbc:microsoft:sqlserver://localhost:1433", "userName", "password"); 서버 이름 값은 IP 주소나 호스트 이름(네트워크가 호스트 이름을 IP 주소로 확인한다고 가정하는 경우)일 수 있습니다. 호스트 이름에 ping 명령을 실행하고 올바른 IP 주소와 함께 응답을 받는지 확인하여 서버 이름 값을 테스트할 수 있습니다.
서버 이름 뒤의 숫자 값은 데이터베이스가 수신하는 포트 번호입니다. 위에 있는 값은 예로 든 기본 값이므로 데이터베이스가 사용하는 포트 번호로 변경해야 합니다.
연결 URL 매개 변수의 전체 목록은 JDBC용 Microsoft SQL Server 2000 드라이버 HTML 도움말이나 온라인 가이드에서 "Connection String Properties" 절을 참조하십시오.
맨 위로 연결을 테스트할 예제 코드다음 예제 코드는 데이터베이스에 연결하고 데이터베이스 이름, 버전 및 사용 가능한 카탈로그를 표시합니다. 서버 속성을 사용 중인 서버에 해당하는 값으로 바꾸십시오. import java.*;public class Connect{ private java.sql.Connection con = null; private final String url = "jdbc:microsoft:sqlserver://"; private final String serverName= "localhost"; private final String portNumber = "1433"; private final String databaseName= "pubs"; private final String userName = "user"; private final String password = "password"; // Informs the driver to use server a side-cursor, // which permits more than one active statement // on a connection. private final String selectMethod = "cursor"; // Constructor public Connect(){} private String getConnectionUrl(){ return url+serverName+":"+portNumber+";databaseName="+databaseName+";selectMethod="+selectMethod+";"; } private java.sql.Connection getConnection(){ try{ Class.forName("com.microsoft.jdbc.sqlserver.SQLServerDriver"); con = java.sql.DriverManager.getConnection(getConnectionUrl(),userName,password); if(con!=null) System.out.println("Connection Successful!"); }catch(Exception e){ e.printStackTrace(); System.out.println("Error Trace in getConnection() : " + e.getMessage()); } return con; } /* Display the driver properties, database details */ public void displayDbProperties(){ java.sql.DatabaseMetaData dm = null; java.sql.ResultSet rs = null; try{ con= this.getConnection(); if(con!=null){ dm = con.getMetaData(); System.out.println("Driver Information"); System.out.println("\tDriver Name: "+ dm.getDriverName()); System.out.println("\tDriver Version: "+ dm.getDriverVersion ()); System.out.println("\nDatabase Information "); System.out.println("\tDatabase Name: "+ dm.getDatabaseProductName()); System.out.println("\tDatabase Version: "+ dm.getDatabaseProductVersion()); System.out.println("Avalilable Catalogs "); rs = dm.getCatalogs(); while(rs.next()){ System.out.println("\tcatalog: "+ rs.getString(1)); } rs.close(); rs = null; closeConnection(); }else System.out.println("Error: No active Connection"); }catch(Exception e){ e.printStackTrace(); } dm=null; } private void closeConnection(){ try{ if(con!=null) con.close(); con=null; }catch(Exception e){ e.printStackTrace(); } } public static void main(String[] args) throws Exception { Connect myDbTest = new Connect(); myDbTest.displayDbProperties(); }} 이 코드가 성공적으로 실행되면 다음과 비슷하게 출력됩니다. Connection Successful!Driver Information Driver Name: SQLServer Driver Version: 2.2.0022Database Information Database Name: Microsoft SQL Server Database Version: Microsoft SQL Server 2000 - 8.00.384 (Intel X86) May 23 2001 00:02:52 Copyright (c) 1988-2000 Microsoft Corporation Desktop Engine on Windows NT 5.1 (Build 2600: )Avalilable Catalogs catalog: master catalog: msdb catalog: pubs catalog: tempdb 맨 위로 기본 연결 문제 해결SQL Server에 연결할 때 나타날 수 있는 일반적인 오류 메시지는 다음과 비슷합니다.
java.sql.SQLException: [Microsoft][SQLServer 2000 Driver for JDBC][SQLServer]'user' 사용자가 로그인하지 못했습니다. 이유: 트러스트된 SQL Server 연결과 관련되지 않았습니다. 이 오류 메시지는 SQL Server 2000 인증 모드가 Windows 인증 모드로 설정된 경우 나타납니다. JDBC용 Microsoft SQL Server 2000 드라이버는 Windows NT 인증을 사용하여 연결하는 작업을 지원하지 않습니다. SQL Server의 인증 모드를 Windows 인증과 SQL Server 인증을 모두 허용하는 혼합 모드로 설정해야 합니다. java.sql.SQLException: [Microsoft][SQLServer 2000 Driver for JDBC]이 버전의 JDBC 드라이버는 Microsoft SQL Server 2000만을 지원합니다. SQL Server 2000으로 업그레이드하거나 다른 버전의 드라이버를 지정하십시오. 이 오류 메시지는 SQL Server 2000 이전의 SQL Server 버전에 연결하려는 경우 나타납니다. JDBC용 Microsoft SQL Server 2000 드라이버는 SQL Server 2000에서만 연결을 지원합니다.
happy4u
2004. 5. 31. 16:08
2004. 5. 31. 16:08
jetbrains에서 나온 제품으로... Visual Stdio .Net 2003 Add-in 제품으로 아래의 기능을 가지고 있습니다 ReSharper is an add-in for Visual Studio .NET 2003 that brings intelligent C# code editing, highlighting and refactoring features to Visual Studio. ReSharper aims to provide C# developers with the same level of power and productivity enhancements that have been available to Java developers using IntelliJ IDEA. 사이트에 나온 설명입니다. 대충 보면... ReSharper는 VS .NET 2003의 add-in되는 것으로, 지능적인 C# 편집, highlighting , refactoring 기능을 가지고 있습니다. ReSharper는 C# 개발자들에게 IntelliJ IDEA를 사용하는 자바 개발자들과 동일한 수준의 향상된 개발 생산성을 제공하는데 역점을 두었습니다. (제대로 번역했나 몰겠네요 ^^'') 하여간... freeware인거 같구요. C# 개발자이시라면 당장 설치해서 써 보세요. 그 편리함에 감동하실껍니다. 지금도 Eclipse 쓰면서 계속 감동 중인데... 동일한 수준은 아니더라도 대폭 향상된 기능을 사용하실 수 있으실껍니다. 제공되는 기능 1. Navigation 2. Coding Assistance * Reformat code * 기타 등등 3. Highlighting 4. Refactoring * Rename * Move Type * Change Signature * Introduce Variable * Extract Method Download와 보다 자세한 내용은 아래 페이지 참조 출처 : http://www.jetbrains.com/resharper/index.html
happy4u
2004. 5. 28. 21:15
2004. 5. 28. 21:15
Java Press ( http://www.gihyo.co.jp/magazines/javapress) 라는 일본의 Java전문 서적(2003년 2월판)에서 발췌한 Java performance tips입니다. 그중 Java 일반적사항에 관련하여 7개, String 관련2개, Collection관련 8개, IO관련2개등 총 4개 분야 19여개의 Tips에 대해 제가 나름대로 번역해본 자료입니다. 출처 : javaservice.net 김선필(piper2)님의 글입니다.
내용보기
>접기 Java performance tips Java Press ( http://www.gihyo.co.jp/magazines/javapress) 라는 일본의 Java전문 서적(2003년 2월판)에서 발췌한 Java performance tips입니다. 그중 Java 일반적사항에 관련하여 7개, String 관련2개, Collection관련 8개, IO관련2개, 등 총 4개 분야 19여개의 Tips에 대해 설명하겠습니다. 1. 일반적사항 관련 Tips ① 쓸데없이 Cast를 남발하면 바람직하지 않음. Loop구조에서 쓸데없이 cast를 남발하면 performance를 현저히 저하시킵니다. 예) 쓸데없이 cast를 남발한 바람직하지 않은 코드예 for(int i=0; i<list.size(); i++) { if ( ((String)list.get(i)).indexOf('a') ! =-1) { } else if(((String)list.get(i)).indexOf('b') ! =-1) { } else if(((String)list.get(i)).indexOf('c') ! =-1) { } else if(((String)list.get(i)).indexOf('d') ! =-1) { } ... } 예) 쓸데없는 cast를 제거한 좋은 코드예 for (int i=0; i<list.size(); i++) { String value = (String)list.get(i); if(value.indexOf('a') ! = -1) { } else if ((value.indexOf('b') != -1){ } else if ((value.indexOf('c') != -1){ } else if ((value.indexOf('d') != -1){ } ... } ② 쓸데없이 동기화를 행하면 바람직하지 않음 同期化(Synchronized)은 높은 cost입니다. 필요가 없을 시에는 사용하지 마십시요. ③ 쓸데없는 인스턴스생성은 바람직하지 않음 인스턴스수가 많지 않은 경우에는 별 문제가 되지 않겠지만, 많은 인스턴스를 생성 하는 경우에는 performance를 현저히 저하 시키므로 주의하십시요. 예) String 인스턴스를 2번생성한 바람직 하지 않은 코드예 String value = new String("문자열"); 예) 개량 후 코드예 String value = "문자열"; ④ 필요이상으로 Wrapper클래스를 사용하면 바람직하지 않음 Wrapper클래스(Integer, Boolean 등)을 클래스멤버로 사용하는 경우 인스턴스생성 이 필요하게 되므로, 기본적으로 Primitive형을 사용하는 편이 performance를 높 입니다. 예) Wrapper클래스를 사용한 코드예 public class Person { private Integer id; private Boolean isValid; }; 예) primitive형으로 치환한 코드예 public class Person { private int id; private boolean isValid; }; 또한 Wrapper클래스에는 없지만 java.math.BigDecimal 클래스는 Double 클래스 보다 정확한 부동소수연산이 가능하지만 performance를 저하시키므로 유의바랍니 다. ⑤ primitive형의 default값을 이용 int형 boolean형등의 primitive형은 선언시 default값이 초기화 됩니다. 이것을 이용하면 초기화처리를 생략할 수 있습니다. 덧붙여 말하면 int형은 0, float은 0.0, boolean형은 false로 선언시에 초기화 됩니다. 예) primitive형의 초기화처리를 행한 코드예 public class Person { private int id; private boolean isValid; public Person() { id = 0; isValid = false; } } 예) primitive형의 default값을 이용한 코드예 public class Person { private int id; private boolean isValid; public Person() { } } ⑥ 문자열을 숫자형으로 변환하는 방법 문자열을 숫자형으로 변환시에 각 Wrapper클래스(Integer,Double 등)의 static 메 소드인 parseXXX()를 이용합니다. valueOf()를 경유해서 XXXValue()를 실행하면 한번의 인스턴스를 생성하게 되어 불필요한 cost를 들게 합니다. 예) valueOf()를 이용하여 문자열을 숫자형으로 변환한 코드예 double doubleValue = Double.valueOf("1234.56").doubleValue(); int intValue = Integer.valueOf("123456").intValue(); 예) 개량한 코드예 double doubleValue = Double.parseDouble("1234.56"); int intValue = Integer.parseInt("123456"); ⑦ 필요이상으로 System.gc()를 사용하면 바람직하지 않음 필요이상으로 System.gc()를 이용하면 프로그램실행 performance가 저하됩니다. 2. String 관련 Tips ① 문자열 연결방법 +연산자를 이용하여 문자열을 연결하게 되면 심하게 performance가 저하됩니다. StringBuffer클래스를 사용하면 performance가 향상됩니다. 예) + 연산자에 의한 문자열연결 코드예 String result = ""; for (int i=0; i<loopNum; i++ ) { result + =i; } 예) StringBuffer클래스에 의한 문자열연결 코드예 StringBuffer buf = new StringBuffer(); for(int i=0; i<loopNum; i++) { buf.append(i); } String result = buf.toString(); 더욱이, 연결후의 문자열의 길이를 알고 있을 경우, StringBuffer클래스 생성시에 적당한 초기값을 주면 더욱더 performance가 향상됩니다. StringBuffer클래스는 용량이 부족하게 되면 내부적으로 버퍼사이즈가 자동으로 변 경되나 연결후의 사이즈를 알고 있는 경우에는 초기값으로 사이즈를 주면 그러한 처리과정을 생략할 수 있습니다. 예) StringBuffer buf = new StringBuffer(length); 표) 문자열연결에 관한 performance 측정 연결수: 1000 연결수 : 500 +연산자 250(ms) 7500(ms) StringBuffer 15(ms) 20(ms) StringBuffer(초기값有) 2(ms) 10(ms) CPU : Pentium 300MHz/ Memory :128M / OS : Linux/ J2SE:1.3.1 ② StringTokenizer클래스 문자열을 자를 때,StringTokenizer클래스를 이용하는 것이 편하기는 하지만 문자열 이 고정되어 있는 경우에는 독자적인 Tokenizer를 사용하는것이 효율적입니다. StringTokenizer클래스가 token을 반복할때, 매번 자를문자가 변경되어 있는지를 체크하기 때문입니다. 예) StringTokenizer클래스를 취하는 코드예 String word = "aaa,bbb,ccc,ddd,eee,fff"; StringTokenizer token = new StringTokenizer(word,","); List list = new ArrayList(); while(token.hasMoreTokens()) { list.add(token.nextToken()); } 예) 자를문자를 ‘,’로 고정한후 token을 추출한 코드예 String word = "aaa,bbb,ccc,ddd,eee,fff"; List list = new ArrayList(); int now=0; int next=0; while ( (next = word.indexOf(",",now)) > 0 ) { list.add(word.substring(now,next)); now = next + 1; } 또한 StringTokenizer클래스에는 hasMoreElements()와 hasMoreTokens() 두개의 메소드가 있는데, hasMoreElements()가 내부적으로 hasMoreTokens()를 호출하 므로 통상적으로 hasMoreTokens()를 이용 하도록 합니다. 참고) J2SE 1.4부터 새로운 메소드가 추가된것이 있는데, SringTokenizer 클래스의 token 추출과 같은 기능의 메소드로 java.lang.String 클래스의 split()메소드를 소 개합니다. String word = "aaa,bbb,ccc,ddd,eee,fff"; String [] result = word.split(","); 3. Collection관련 Tips ① 배열의 이용 데이타의 개수나 사이즈가 변동하는 경우에만 Collection계통의 클라스를 사용하며, 그외에는 배열을 사용합니다. ② J2SE1.2이상에서의 Collection이용 JDK1.1까지는 Vector클래스나 Hashtable클래스가 편리했으나, 이러한 클래스는 메소드가 동기화(synchronized) 되어 있습니다. 따라서 동기화가 필요없는 경우에는 비효율적입니다. J2SE1.2이상에서는 메소드가 동기화되어있지 않은 ArrayList클라스나 HashMap클 래스를 이용합니다. Vector클래스는 ArrayList로 Hashtable은 HashMap클래스로 바꿔이용합니다. 예) 구 Collection클래스 이용예 Vector vector = new Vector(); Hashtable table = new Hashtable(); 예) J2SE1.2이상 Collection클래스 이용예 List list = new ArrayList(); Map map = new HashMap(); 또한, J2SE1.2이상에서 Collection의 동기화가 필요한 경우에는 List list = Collection.synchronizedList(new ArrayList(..)); 위와 같이 사용합니다. ③ Collection size 초기화 Collection을 default 사이즈로 만들면, 필요시 자동적으로 사이즈가 확장되나 명확히 예측이 가능한 경우에는 사이즈를 초기화 하는 편이 훨씬 효율적입니다. 예) 사이즈를 지정하지 않고 Collection을 생성한 코드예 List list = new ArrayList(); HashMap map = new HashMap(); 예) 사이즈를 지정한 Collection 생성 코드예 List list = new ArrayList(num); HashMap map = new HashMap(num); ④ Iterator클래스보다 빠른 요소검사 Collection 사이즈를 명확히 알 경우에는 Iterator클래스의 next()와 비교하여, Iterator클래스의 hasNext()에 의한 요소종료 체크가 필요없으며 내부처리가 간단한 List클래스의 get()메소드를 추천합니다. 예) Iterator클래스의 next()에 의한 요소조사 Iterator iterator = array.iterator(); while (iterator.hasNext()) { Object object = iterator.next(); } 예) List클래스의 get()에 의한 요소조사 int size =array.size(); for (int i=0; i<size ; i++) { Object object = array.get(i); } ⑤ 요소삽입/삭제시 주의점 List도중에 요소를 추가/삭제할 경우에 내부적으로 배열의 copy가 행해집니다. 따라서 요소의 수가 많으면 많을 수록 copy하는 요소수도 많아져 결과적 으로 performance의 저하를 초래합니다. 내부적처리로써 [ (전체사이즈) - (삽입/삭제대상요소의 index)] 만큼의 요소가 copy되므로 아래의 예를 참조바랍니다. 예) List의 맨앞에 요소를 추가/삭제하는 경우 -- 속도가 느림. list.add(0, new Object()); list.remove(0); 예) List의 맨 뒤에 요소를 추가/삭제하는 경우 -- 속도가 빠름. list.add(new Object()); list.remove(list.size() - 1); ⑥ List요소 전체삭제 List요소를 전체삭제 할때, 통상 쓰는 clear()을 이용하지말고, 새롭게 List를 생성(초기화)하는 편이 효율적입니다. 왜냐하면, clear()는 내부적으로 보유하고 있는 배열의 전체요소에 null을 셋팅함으로써 전체삭제를 실현하기 때문입니다. 예) clear()에 의한 요소 전체삭제 List list = new ArrayList(); for(int i=0; i< num; i++) { list.add(new Integer(i)); } list.clear(); 예) List재작성에 의한 요소 전체삭제 List list = new ArrayList(); for(int i=0; i< num; i++) { list.add(new Integer(i)); } list = new ArrayList(); ⑦ 배열요소 copy방법 루프를 돌며 배열요소를 하나씩 copy할 경우에는 System.arraycopy(Object src,int srcPos,Object dest,int destPos,int length)를 이용합니다. 메소드의 인자는 아래와 같습니다. src - the source array. srcPos - starting position in the source array. dest - the destination array. destPos - starting position in the destination data. length - the number of array elements to be copied. 예) 루프를 돌며 배열소소를 copy하는 예 int[] buf = new int[num]; int[] copy = new int[num]; for (int i=0; i<num; i++) { copy[i] = buf[i]; } 예) System.arraycopy()에 의한 신속한 copy 예 int[] buf = new int[num]; int[] copy = new int[num]; System.arraycopy(buf,0, copy,0, num); ⑧ List에 보존되어 있는 object 요소를 배열에 넣는 방법 List클래스의 toArray()를 이용하여 List에 보존되어 있는 Object요소를 배열에 넣습니다. 예) 루프에 의한 object요소 copy 예 int size = list.size(); Integer[] result = new Integer[size]; for (int i=0; i<size; i++) { result[i] = (Integer) list.get(i); } 예) toArray()에 의한 신속한 copy int size = list.size(); Integer[] result = new Integer[size]; list.toArray(result); 4. IO관련 Tips ① 통상적으로 Stream계의 클래스를 사용함 java.io패키지에서 문자데이터 입출력시에는 Reader/Writer 클래스를 바이트단위 데이터 입출력시에는 InputStream/OutputStream 을 이용합니다. 다만, 아스키캐릭터의 경우에는 1문자가 1바이트이므로 InputStream/OutputStream 클래스를 이용할 수 있습니다. 약 100K 정도의 영문자로 이루어져 있는 텍스트파일을 가지고 예를 들겠습니다. 예) Reader계 클래스을 이용한 파일 입출력 예 FileReader in = new FileReader("test.txt"); int buf; while ( buf = in.read() != -1) { ....... } in.close(); 예) InputStream계 클래스를 이용한 파일 입출력 예 FileInputStream in = new FileInputStream("test.txt"); int buf; while ( buf = in.read() != -1) { ....... } in.close(); 위의 2가지 예를 비교한 performance 비교표는 아래와 같습니다. 사용클래스 처리시간(ms) FileReader 18000 FileInputStream 800 CPU : Pentium 300MHz/ Memory :128M / OS : Linux/ J2SE:1.3.1 InputStream계 클래스를 이용한 파일 입출력이 월등히 빠른 처리속도를 보임을 알 수 있습니다. ② Buffering을 이용 예) Buffering을 한 Reader계 클래스을 이용한 파일 입출력 예 FileReader in = new FileReader("test.txt"); BufferedReader bin = new BufferedReader(in); int buf; while ( buf = bin.read() != -1) { ....... } bin.close(); 예) Buffering을 한 InputStream계 클래스를 이용한 파일 입출력 예 FileInputStream in = new FileInputStream("test.txt"); BufferedInputStream bin = new BufferedInputStream(in); int buf; while ( buf = bin.read() != -1) { ....... } bin.close(); 위의 2가지 예를 비교한 performance 비교표는 아래와 같습니다. 사용클래스 처리시간(ms) BufferedReader 150 BufferedInutStream 80 CPU : Pentium 300MHz/ Memory :128M / OS : Linux/ J2SE:1.3.1 - the end-
|
|