지난 시간에는 쓰레드를 통한 비동기 프로그래밍의 원리와 구현에 대해 알아봤다. 이번 시간에는 쓰레드를 효과적으로 관리하기 위한 쓰레드 풀과 함께 서버 제작에 필요한 네트워크 기술을 설명한다. 쓰레드 풀을 이용한 비동기 프로그래밍과 소켓의 개념에 대해 알아 보고 최종적으로 게임 서버를 완성해 보자.
비동기 프로그래밍은 내부적으로 쓰레드를 이용한다. 그런데 비동기 호출을 할 때마다 새로운 쓰레드를 생성해서 작업을 하게 되면, 많은 비동기 호출이 일어날 때에는 쓰레드의 수가 너무 많아져서 오히려 컨텍스트 스위칭(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. 16. 08:02
2004. 8. 16. 08:02
happy4u
2004. 8. 13. 23:03
2004. 8. 13. 23:03
딱지 떼지 않는 공짜 주차장 '강남'
▣ 교대역 ▶공짜 주차장◀ ① 교대역 사거리에서 서울교육대학교 대로변. ② 교대 정문 우측으로 난 골목 안쪽 빌라촌. 로얄빌리지, 롯데빌라 등에 무료 주차 가능. ③ 서초종합시장 앞 대로변. 단속하기 힘든 지역인 만큼 잠깐 세우기엔 좋다. ④ 삼풍아파트 부근. ▶인근 최저가 주차장◀ ⓐ 서울교육대학부설초등학교 뒤편 우리은행 기숙사 앞 서초 공영 주차장. 최초 1시간 3000원. 이후에 10분 초과시 300원 추가. 평일 오후 8시 이후, 토요일 오후 3시 이후, 공휴일은 종일 무료다. 월 주차 8만원, 월 야간주차 4만원.
▣ 양재동 ▶공짜 주차장◀ ① 서초구청은 평일 오후 6시 이후, 토요일 오후 5시 이후, 공휴일은 종일 주차장을 무료 개방. ② 국민은행 양재동점. 평일 저녁 시간과 공휴일에는 주차료를 받지 않는다. ③ 주변 주택가는 거의 대부분 거주자 우선 주차 구역. 낮시간 잠깐 이용하는 데는 적합하다. ④ 양재동성당 진입로. 장시간 세울 수는 없지만 잠시 주차하기에 적당하다. ▶인근 최저가 주차장◀ ⓐ 양재 시민의 숲 내 매헌기념관 앞 주차장. 10분 당 300원. 오후 9시부터 다음날 오전 9시까지 무료.
▣ 압구정&청담 ▶공짜 주차장◀ ① 갤러리아백화점 비자카드를 가지고 있다면 백화점 내에 무료 주차 가능. ② 홍실상가 앞 대로변에 주말, 공휴일, 평일 오후 7새 이후 무료 주차 가능. ③ 낮에는 갤러리아백화점과 한양아파트 사이 대로변, 혹은 한양아파트 주차장에 주차한다. ④ 밤에는 갤러리아 본관과 갤러리아 명품관 사이 대로변을 이용하는 것이 낫다. ▶인근 최저가 주차장◀ ⓐ 신사전화국 앞 공영주차장. 야간에는 무료주차 가능. 주차비는 10분 당 800원.
▣ 역삼역~선릉역 ▶공짜 주차장◀ ① 잠시 주차하기에는 역삼역 사거리에서 LG아트센터로 이어지는 대로변이 좋다. 평일 낮에는 자리 싸움으로 주차하기가 힘들지만 주말에는 빈자리가 많다. 공식적으로는 주차 금지 구역이지만 주차 위반 딱지를 떼는 일은 드물다. ② 평일에는 충현교회 골목이 안전하다. 견인되거나 딱지를 뗄 위험 없이 주차 가능. 단, 교회 사람들로 붐비는 주말은 엄두도 내지 않는 것이 좋다. ③ 역삼역과 선릉역 사이에 위치한 볼보자동차 매장에 꽤 넉넉한 주차 공간이 확보되어 있다. 이용 시간은 매장 직원들이 모두 퇴근한 저녁 시간 이후. ④ 역삼역에서 선릉역 방향으로 가다 한국고등교육재단에 평일 오후 7시 이후 무료 개방. ⑤ 개나리아파트를 중심으로 한 아파트 단지 내. 역과 떨어져 있어 사람들의 왕래가 드문 굿 플레이스. ▶인근 최저가 주차장◀ ⓐ 선릉역에서 1분 거리에 위치한 보람상호신용금고 주변의 파라다이스주차장. 30분당 1000원. 월 주차 요금은 7만원.
▣ 강남역 ▶공짜 주차장◀ ① 씨티극장 사잇길로 올라가다 오른편 블루클럽 건물. ② 야간에는 스타벅스 뒤편 ELS어학원 주차장. ③ 토요일은 오후 3시 이후와 일요일, 공휴일은 쿠아 의류 숍과 보디숍 사이의 골목으로 직진, 아파트 부근 공영 주차장 이용이 무료. 평소에는 10분당 800원. ④ 교보빌딩 뒤편 아파트 주차장. 퇴근 후부터는 단속이 심하지만 낮 시간에는 이용할 만하다. 그 중 세종아파트와 진흥아파트가 가장 안전하다. ⑤ 오후 8시 이후에는 국기원 앞 도로, 국립도서관과 과학기술회관 사잇길이 이용할 만하다. ▶인근 최저가 주차장◀ ⓐ 과학기술회관 주차장. 10분당 500원.
딱지 떼지 않는 공짜 주차장 '강북'
▣ 신촌&연세대 인근 ※100% 견인되는 지역 1. 신촌 기차역 앞 보호지대 2. 연대 정문 3. 녹색극장 뒤 여관 ▶공짜 주차장◀ ① 이화여대 후문과 이화여대부속초등학교 사이 골목으로 들어가면 초등학교 뒷담을 따라 언덕길에 주차가 가능하다. 단, 아래쪽 주택가 앞에 마련된 주차 공간은 거주자 우선 주차 지역이므로 주차 위반 딱지를 뗀다. ② 연세대 치과병원이 있는 동문 쪽은 거주자 우선 주차 지역을 제외하고 모두 주차할 수 있다. 서문은 학군단이 있는 언덕에, 연희동과 이어지는 북문은 주차 공간이 많고 종일 주차가 가능하지만 번화가와 다소 거리가 있다는 것이 단점이다. ③ 신촌 기차역에서 신촌 현대백화점 쪽으로 내려오다 보면 ‘쫄병부대찌개’가 있다. 이 가게 양옆 골목에 주차 가능. 단, 길 위쪽에서 빈자리를 찾는다면 별 문제가 없지만 입구 쪽은 비탈이 심해 초보자들에겐 위험하다. ④ 신촌 로터리에서 서강대교 방향으로 100m 직진, 우측 파출소 안쪽 골목에 주차 가능. ▶인근 최저가 주차장◀ ⓐ 신촌 기차역에서 연세대 방향으로 직진, 고박사냉면부터 형제갈비 앞까지 도로변이 공영주차장. 요금은 10분당 700원. 평일 11:00~21:00 토요일 11:00~17:00를 제외한 시간과 휴일은 무료. ⓑ 창천교회 주차장이 유료 주차장 중에는 가장 저렴하다. 30분당 1000원.
▣ 홍익대학교 인근 ▶공짜 주차장◀ ① 공영 주차장 주변 카센터. 오후 9시 폐점 후 3~4대 정도 가능. ② 공영 주차장 골목 ‘There’s’와 ‘바이더웨이’ 사이 사거리 ‘홈 바’ 골목. 갓길 주차 가능. ③ 마포 평생학습관 뒤 빌라촌. 주로 자취생들이 거주해 주차 시비 염려가 없다. ④ 상권과 거주지를 잇는 골목, 거주자 우선 주차 지역에 오후 6시 이전 주차 가능. ⑤ 한국문화신문사 인근 주택가. ⑥ 홍대 정문에서 극동방송국 방향으로 가다 우측, 국희약국 골목 왼편. ⑦야간과 휴일에 무료로 개방하는 홍대 건너편 공영 주차장 골목.
▣ 종로 ▶공짜 주차장◀ ① 대형 서점은 도서 구입 금액에 따라 무료 주차가 가능하다. 특히 서점 회원 카드를 소지한 경우엔 도서를 구입하지 않아도 주차 가능. ② 교보생명 빌딩은 평일 야간과 주말에 무료 개방. ③ 동아일보와 광화문우체국 뒤편에 30분~1시간 정도 주차 가능. ④ 종묘공원 내 관광 차량 주차 지역 주변, 승용차 3대 정도 가능. ▶인근 최저가 주차장◀ ⓐ 종묘주차장, 탑골공원 맞은편 유료 주차장. 극장 밀집 지역이라 영화 티켓을 가져오면 50%까지 할인해준다.
▣ 시청 ▶공짜 주차장◀ ① 주말에는 삼성 본관이 무료. 주중에는 삼성생명 식당가나 쇼핑몰을 이용하면 주차 할인을 받을 수 있다. ② 프라자호텔은 이용객에 한해 최대 4시간까지 1000원으로 주차할 수 있게 한다. ③ 시청 근처 호텔 정문에서 주차 요원에게 자동차 키와 1만원을 주면 발레파킹을 해준다. 주차장까지 왔다갔다 하는 불편도 없고 차를 찾을 때도 편리하다. 호텔 이용객을 위한 서비스를 편법으로 활용하는 것이지만 안전하게 하루종일 세워둘 수 있는 몇 안 되는 방법이다.
▣ 대학로 ▶공짜 주차장◀ ① 이화동 서울사대부속중학교 옆 ② 대학로 극장가에 있는 현대자동차 건물 뒤편 ③ 휴일에는 창경궁 주차장이 무료. ▶인근 최저가 주차장◀ ⓐ 지하철 4호선 혜화역 KFC 뒤, 일마레 옆에 위치한 유료 주차장이 30분당 1000원 정도로 가장 싸다.
▣ 여의도 ▶공짜 주차장◀ ① 휴일에는 KBS와 SBS 방송국, 그리고 공영 주차장이 무료다. ② 평일 야간과 주말 한강시민공원 주차장을 무료 개방. ▶인근 최저가 주차장◀ ⓐ 한강시민공원 주차장. ⓑ 여의도 둔치 주차장은 월 주차의 경우 2만5000원 선.
※단속요원도 피해 가는 주차 틈새 주차단속이 없는 오후 9시부터 오전7시까지는 종로통 어디에 세워도 딱지를 떼지 않는다. 단, 청계천로와 동대문운동장 부근은 심야 단속 구간.
▣ 명동 ▶공짜 주차장◀ ① 백화점 카드 회원에게 우편으로 발송되는 주차권을 모아두었다 사용한다. 미리 챙기지 못했을 경우 백화점 신용판매과에서 얻을 수 있다. 30분 이내 주차는 무료. ② 을지로2가 지하보도 옆 ‘SK VIEW’ 건설 현장과 한화빌딩 사이 인쇄소 골목. 휴일에는 70%이상 자리가 비어 있다. 단, 장시간은 위험. ③ 포호아, 베니건스, 아바타몰 등에서 식사나 쇼핑을 하면 건물 지하 주차장을 1~2시간 무료로 이용할 수 있다. ▶인근 최저가 주차장◀ ⓐ 세종호텔 바로 옆 밀리오레 주차장. 주차 요금은 공영 주차장 수준. ⓑ 회현로 인송빌딩. 처음 30분은 무료, 이후 30분마다 추가 요금 1500원이 붙는다.
출처 : 다음 10IN10클럽
happy4u
2004. 8. 10. 16:46
2004. 8. 10. 16:46
happy4u
2004. 8. 3. 13:42
2004. 8. 3. 13:42
쩝... 뭐 말하지 않아도 다들 아실만한 책이죠.
읽은 시기에 비해선 너무 늦게 올린 책이지만...
첨 공부하는 분들한테도 좋고, 중급자들 한테도 좋은 책입니다.
저희 회사에도 두 권 샀었는데..
아래 사진처럼 됐습니다.
원인이야 여러 가지가 있겠죠...
워낙 책을 막 본건지... 책이 워낙 약하게 만들어져 있었던건지...
워낙 많이 본 것이던지...
회사 책이 많지만... 이렇게 처참한 모습을 한 책은 없지 않나 싶네요 --;;
그렇담 원인은 사람들이 많이 봤거나 책이 워낙 약하거나 둘 중에 하나인데...
이 출판사의 다른 책은 멀쩡한걸 보면... 그만큼 많이 봐서 이렇게 된게 아닌가 싶네요...
그래서 한 권씩 더 샀답니다. 쩝...
디카를 누구 빌려줘서 할 수 없이 새로 장만한 핸펀으로 찍은 사진이라 영 엉망이네요...
happy4u
2004. 8. 2. 22:42
2004. 8. 2. 22:42
Windows 2000에서 NETSH 명령을 사용하여 고정 IP 주소를 DHCP로 변경하는 방법 Windows 2000에서 netsh 명령을 사용하여 고정 인터넷 프로토콜(IP) 주소를 DHCP 할당 주소로 또는 그 반대로 변경하는 프로세스를 스크립트로 만들 수 있습니다. netsh 명령을 사용하여 이 작업을 수행할 때 컴퓨터를 다시 시작할 필요가 없습니다. 이 기능은 두 환경 간을 이동하며 한 환경에서는 정적으로 할당된 IP 주소를 사용하고 다른 환경에서는 DHCP가 할당하는 IP 주소를 사용하는 랩톱 컴퓨터에 특히 유용합니다.
..more
>접기 사용법: netsh [-a 별칭 파일] [-c 컨텍스트] [-r 원격 컴퓨터] [-u [DomainName\]UserName] [-p 암호 | *] [명령 | -f 스크립트 파일]
다음 명령을 사용할 수 있습니다. 이 컨텍스트에 있는 명령: ? - 명령 목록을 표시합니다. aaaa - `netsh aaaa' 컨텍스트의 변경 내용입니다. add - 항목 목록에 구성 항목을 추가합니다. delete - 항목 목록에서 구성 항목을 삭제합니다. dhcp - `netsh dhcp' 컨텍스트의 변경 내용입니다. diag - `netsh diag' 컨텍스트의 변경 내용입니다. dump - 구성 스크립트를 표시합니다. exec - 스크립트 파일을 실행합니다. help - 명령 목록을 표시합니다. interface - `netsh interface' 컨텍스트의 변경 내용입니다. ipsec - `netsh ipsec' 컨텍스트의 변경 내용입니다. ras - `netsh ras' 컨텍스트의 변경 내용입니다. routing - `netsh routing' 컨텍스트의 변경 내용입니다. rpc - `netsh rpc' 컨텍스트의 변경 내용입니다. set - 구성 설정을 업데이트합니다. show - 정보를 표시합니다. wins - `netsh wins' 컨텍스트의 변경 내용입니다. 다음 하위 컨텍스트를 사용할 수 있습니다. aaaa dhcp diag interface ipsec ras routing rpc wins 명령에 대한 도움말을 보려면 명령을 입력한 다음 공백을 입력한 후 ?을(를) 입력하십시오. === ip_in.cmd ======================================================= @echo off netsh -f ipaddr.txt
=== ip_out.cmd ====================================================== @echo off netsh -c interface dump > ipaddr.txt
====================================================================
happy4u
2004. 8. 2. 19:42
2004. 8. 2. 19:42
Head First라는 자바 프로그래밍 책에 나온 내용입니다. 뭔가 새로운 것을 공부할 때는 자기 전에 마지막으로 그 내용을 공부하세요. 즉, 일단 책을 덮은 다음에는 머리를 많이 써야 하는 일은 절대 하지 마세요. 머리에서 여러분이 읽고 배운 내용을 처리하려면 어느 정도 시간이 걸립니다. 몇 시간이 걸릴 수도 있죠. 어떤 것을 배운 다음 바로 다른 것을 공부하려고 하면 제대로 이해가 되지 않을 수 있습니다. 물론, 몸으로 하는 일은 별 상관없습니다. 자바에 대한 내용을 열심히 공부하고 나서 열심히 발차기 연습을 해도 자바 공부에는 별로 나쁜 영향을 끼치지 않습니다. 학습 효과를 극대화시키고 싶다면 잠들기 직전에 이 책을 읽어보세요(적어도 그림이라도 보면 확실히 도움이 될 것입니다.)
happy4u
2004. 7. 31. 13:27
2004. 7. 31. 13:27
저자 : 켄 핸더슨 (Ken Henderson) 역자 : 이종철 음... 이건 중급 이상의 SQL 관련 개발자가 읽으면 도움이 될 만한 내용입니다. 전체적으로 다른 책에서 보지 못한 좋은 내용들이 너무 많습니다. 일단 사서 필요할 때 reference로 사용하셔도 좋을 만한 책~~
happy4u
2004. 7. 31. 13:23
2004. 7. 31. 13:23
저자 : 케이시 시이라, 버트 베이츠
역자 : 서환수 정말 정말 좋은 책인거 같다. 처음 자바를 배우는 사람에게 가장 좋은 책이라고 확신할 수 있을 정도로 좋은 책인거 같다. 물론 어설프게 아는 사람들에게도 정리하기 좋은 책인거 같고... 난 요즘 C#으로 프로그래밍을 하지만, 객체지향 프로그램밍 경험이 거의 없어서, 객체지향 공부하는 셈 치고 보고 있는데 잼있다. 이제 한 반 본거 같은데.. 정말 강추하고 싶은 책~
happy4u
2004. 7. 28. 10:15
2004. 7. 28. 10:15
사진을 클릭하시면 원본 사이즈로 보실 수 있습니다. 연인의 거리에서 바라 본 홍콩섬의 야경
홍콩에서 젤로 유명한 페닌슐라 호텔
호텔로 돌아가는 길에 살짝... 왼쪽이 페닌슐라 호텔
happy4u
2004. 7. 26. 10:09
2004. 7. 26. 10:09
나만의 평가 전체적으로 어때요? : ★★★☆☆ 여자친구랑 볼만한가요? : ★★★☆☆ 얼마나 야해요? : ★☆☆☆☆ 멋진 장면들이 많아요? : ★★☆☆☆ 얼마나 감동적인가요? : ★★☆☆☆ 친구들한테 추천할만해요? : ★★★☆☆ 그럭저럭 볼만한 영화... 중간 중간 터지는 폭소는 일품이지만, 영화 스토리가 그다지 맘에 들진 않았다. 또한 장편의 CF를 보는 것 같다는 느낌도 들었고... 하지만 중간에 터지는 폭소는 ^^'' 지금 생각해도 웃긴다. 그냥 편안히 시원한 극장에서 시원스런 웃음을 웃을 수 있게 해 주는 영화인거 같다
happy4u
2004. 7. 20. 09:50
2004. 7. 20. 09:50
시작 > 실행 > regedit
HKEY_LOCAL_MACHINE/Software/CLASSES/Directory/shell 키 아래에 dos prompt라는 키를 하나 생성합니다. 우측창의 (기본값)에 원하는 바로가기 이름을 적습니다. 그리고 그 dos prompt라는 키의 하위에 command라는 키를 하나 더 만들어 마찬가지로 (기본값)에 "cmd.exe /k"를 적어 주면 됩니다. 레지스트리 편집기를 닫고 탐색기에서 어떤폴더에서나 마우스 오른쪽 버튼을 클릭하면 위에서 지정해놓은 바로가기 이름이 나타날 것입니다. 이것을 실행하면 해당 위치의 명령 프롬프트가 짜짠하고 뜹니다.
happy4u
2004. 7. 20. 06:17
2004. 7. 20. 06:17
연인의 거리 -> 캔톤로드 -> 구룡공원 산책 -> 하버시티 쇼핑 및 점심식사(2:30 ~ 3시 문화센터 2층 세레나데) -> 퍼시픽 플레이스 -> 8시 낭만의 거리(쇼구경) -> 저녁식사 -> 야시장 -> 호텔
연인의 거리는 낮에 제대로 본 적이 없어서 더위를 무릅쓰고, 연인의 거리로부터 출발하기로 했다. 호텔의 위치가 연인의 거리 끝 부분에 위치해 있어서 페리 터미널쪽으로 가기에 적당했다. 햇볕이 너무 강한게 흠이긴 했지만, 낮에 보는 낭만의 거리와 영화의 거리는 멋졌다. 첩밀밀에 나온 캔톤 로드를 거쳐서 구룡공원까지… 캔톤 로드로만 가면 구룡 공원 입구를 놓치기 쉬우니 조심… 그간의 피로 누적으로 공원을 많이 돌아보진 못했다.
원래 3일째는 마카오에 다녀오려 했으나, 이틀째 너무 많이 걸어서 정말 걷기 힘들정도라 편안히 하버시티에서 쇼핑이나 하기로 했다.
간단히 하버시티 쇼핑을 하고 점심은 홍콩관광청에서 소개한 세레나데에서 얌차를 먹었다.
그냥 들어가서 차 식히고 먹을 딤썸을 고르면 되는 것을 얌차를 먹겠다고 얘기를 하고 어떻게 주문해야 하냐고 물어보고 했다 --;;
앉아 있으면 딤섬을 가지고 다니는 분들이 무작정 갖다 놓으려고 하니 무턱대고 받지 말고 메뉴판을 달라고 하고 주문하는 것이 좋을 듯…
점심을 먹고 어제 가보려고 했던 퍼시픽 플레이스에 갔다. 별로 볼거 없었다.
저녁 8시에는 빛의 심포니라고 하는.. 18개의 건물에서 18분동안 쏘아 올리는 장관을 사진 찍기 위해 연인의 거리에서 기다렸는데 --;; 이 날은 왠일인지 하질 않았다…
저녁 식사는 근처 식당에서 간단히 하고, 택시를 이용해 템플 야시장으로 향했다.
음… 여기가 역시 가격이 저렴하긴 했다.
여기서 이쁘게 생긴 29달러짜리 핸드폰줄을 5개나 샀는데…
가격을 9달러밖에 깎지 못한게 좀 아쉽지만, 오랜만에 시장에서 물건 값을 깎는 재미도 느낄 수 있었다.
..more
>접기
연인의 거리 모습...
연인의 거리에서 바라 본 페닌슐라 호텔
clock tower
캔톤로드~~ 첩밀밀에 나온 거리라는데 --;; 기억이...
구룡공원의 모습~~
세레나데에서 먹은 딤섬들... 자스민 차는 거의 마약 수준인거 같았다. 엄청 마셨다..
8시의 레이저쇼를 기다리며... 역시 연인의 거리에서 바라 본 홍콩 섬
템플 스트리트의 야시장...
happy4u
2004. 7. 20. 06:08
2004. 7. 20. 06:08
하버 시티에 들어가면 보이는 안내 데스크~
리펄스 베이의 모습
점심을 먹은 식당의 메뉴판~
미드레벨 에스컬레이터..
스타 패리쪽에서 바라본 홍콩 섬의 야경~ 언제봐도 멋지다..
happy4u
2004. 7. 20. 06:05
2004. 7. 20. 06:05
오늘의 여정…
로얄가든 호텔 출발 –(도보)à 하버씨티 –(도보)à 침사추이 스타페리 –(페리)à 홍콩 스타페리 –(도보)à 센트럴 버스터미널 –(버스 6A)à 리펄스베이 –(버스 6)à 스텐리 마켓(주의:스텐리 플라자 다음 정거장임) (점심식사) –(버스 6X)à 센트럴 버스터미널 –(도보)à 미드레벨 에스컬레이터 –(에스컬레이터)à 꼭대기 --;; --(버스:40)à won choi Ferry pier –(도보)à won choi MTR역 –(MTR)à causeway bay MTR역 –(도보)à sogo 백화점 –(도보)à 타임 스퀘어 센터 –(도보)à causeway bay MTR역 –(MTR)à won choi MTR역(저녁식사) –(도보)à Harbour cruise pier –(유람선)à Harbour cruise pier –(도보)à won choi Ferry pier –(페리)à 침사추이 스타페리 –(도보)à 연인의 거리 –(도보)à 로얄가든 호텔
무지무지 지친 하루다… 엄청 걸었다.
오늘의 가장 멋진 장소!!! 리펄스 베이와 스텐리 마켓
오늘의 최악의 장소!!! 미드레벨 에스컬레이터
물론 나의 개인적인 소견일 뿐이다. 절대적으로 받아드리면 안됩니다.
일단 가장 좋았던 리펄스 베이…. 꼭 수영복을 가지고 가서 물에 들어갔으면 하는 생각이 절실했습니다. 정말 멋진 곳이더군요. 또한 스텐리는 낡은 건물들 사이로 엄청나게 큰 간판들이 빼곡한 홍콩이라는 나라의 한 지역이라는 생각이 전혀 들지 않는… 가보진 않았지만 지중해의 어느 한적한 장소처럼 정말 좋았습니다.
미드레벨 에스컬레이터의 경우… 끝이없는 에스컬레이터 내려오는 에스컬레이터도 있을거라고 생각했지만 --;; 없더군요. 끝이 어디일까하는 단순한 궁금증으로 인해 계속 타고 올라가면서 옆에 계단으로 내려오는 사람들의 모습을 보니 겁이 나더군요 --;;
아니나다를까 끝까지 올라간 관광객을 많이 보기 힘들더군요…
올라가서 왼쪽길로 조금 내려오다 보니 버스 정류장이 하나 보이던데… 센트럴, 코즈웨이베이. Won choi 세 군데로 가는 버스들이 모두 있더군요. 불행 중 다행…
코즈웨이 베이의 타임 스퀘어 근처는 서울의 명동을 생각나게 하는 거리이더군요. 유람선 시간 맞추느라 많이 돌아다니지는 못했지만, 슬슬 돌아다니기 좋은 곳이 아닐까 싶네요.
사진모음을 보시려면 'more'를...
..more
>접기
스타 패리에서 하버 시티로 들어가는 입구
낮에 바라본 홍콩섬의 모습...
리펄스 베이의 모습...
스텐리 마켓 입구
스텐리 마켓의 모습... 가게들에서 뿜어 나오는 냉기로 시원하게 쇼핑을 할 수 있다. 생각보다 별로 살게 없는듯 --;;
점심 먹은 곳~
스텐리 마켓 근처에 있는 식당들의 모습...
우리가 점심 먹은 곳~
미드레벨 에스컬레이터의 시작~~
줄지어 보이는 에스컬레이터...
우리나라 명동의 중심과 비슷한 타임 스퀘어...
happy4u
2004. 7. 20. 05:53
2004. 7. 20. 05:53
스타 패리에서 내려 바라본 홍콩 섬의 야경
홍콩에서 가장 좋다는 페닌슐라 호텔~~
happy4u
2004. 7. 20. 05:51
2004. 7. 20. 05:51
드디어 홍콩에 도착
다른 나라에 비해 입국 심사가 빠른거 같았다. 입국 심사를 받고 짐을 찾았다.
난 슈퍼씨티 패키지로 왔는데, 호텔까지는 리무진 버스를 이용하기로 했다. 버스 이용을 위해 B Gate로 나가서 오른편에 있는 B13 창구에서 버스표를 받으라고 했다.
나가보니 쉽게 찾을 수 있었다.
..more
>접기
아래 사진이 B13의 모습
버스표 교환을 위한 바우처를 제시했는데, 직원이 아주 친절하게 설명해 줬다. 버스는 잠시후에 출발하니 대기하라는 말과 귀국하는날 스티커를 가슴에 붙이고, 호텔에서 버스 운전사를 기다리라는 말과 함께…
잠시 후 버스를 타고 출발했다.
내가 묶을 호텔은 침사추이 동쪽이라 가는 길에 침사추이역 근처 호텔에 다 들려서 마지막에 도착했다 --;;
호텔의 위치는 생각보다 역에서 그리 멀지 않았다.
짐을 풀고… 일단 피크 타워에 가기로 했다. 슬슬 걸어서 침사추이역에 도착… 한 10분 걸린거 같다. 옥토퍼스카드를 사고 일단 MTR을 함 타보기로 했다.
MTR을 타고 센트럴역에 내렸다. 여기 저기 잘 나온데로 J2출구로 나와서 약 5분을 걸어가서 피크트램역에 도착했다.
피크 트램이 막 도착하고 있다.
역시 책에서 시킨데로, 피크 트램의 오른쪽에 앉아서 올라가며, 멋진 경치를 감상했다.
와… 이런 각도를 오르는 열차라… 마치 놀이공원에서 놀이기구를 타는 기분이었다.
시간적으로 아직 여유가 많아서 일단. 마담 투소 전시관에 들렀다. 슈퍼씨티 패키지의 옵션으로 이 전시관 입장권을 선택해서 무료로 들어갔다.
들어가면 성룡이 제일 처음 맞이하는데…. 돈내고 같이 사진 찍으란다 --;; 물론 싫다고 했다.
전시관을 둘러보고 난 시간이 한 6시 20분정도… 아직 야경을 보려면 시간이 좀 남았다.
야경을 보기 전에 피크 타워에서 내려다본 홍콩의 모습을 좀 보고
밥을 먹기로 했다. 피크 타워 6층에 있는 마르쉐에서 밥을 먹기로 했다. 들어가서 간단하게 식사를 하고, 어두워지기를 기다리면 내일 일정을 정리했다.
드뎌 기다리던 밤…. 멋진 야경을 볼 수 있었다.
내려갈때도 피크 트램을 타기로 하고 피크 트램을 탔다. 올라올때와는 또 다른 느낌이었다. 그 급경사를 내려가는 느낌은….
내려가는 길에 우연히 한국 가이드와 한국 관광객들과 같이 내려가게 됐다. 덕분에 걍 따라서 이층 버스를 타고 스타 패리로 와서 패리를 타고 쉽게 구룡쪽으로 넘어올 수 있었다.
호텔로 돌아가는 길에 찍은 구룡쪽 스타 패리 옆의 Clock Tower의 모습...
그리 많은 일정은 아니었지만 몸이 너무 피곤해서 오늘의 일정은 이걸로 마무리하기로 하고 호텔로 돌아왔다.
happy4u
2004. 7. 11. 09:55
2004. 7. 11. 09:55
장치관리자등이나 서비스를 통제하는데 특히 편리하다. compmgmt.msc : 컴퓨터 관리 devmgmt.msc : 장치관리자 diskmgmt.msc : 디스크 관리 dfrg.msc : 디스크 조각모음 eventvwr.msc : 이벤트 뷰어 fsmgmt.msc : 공유폴더 gpedit.msc : 로컬 컴퓨터 정책 lusrmgr.msc : 로컬 사용자 및 그룹 perfmon.msc : 성능모니터뷰 rsop.msc : 정책의 결과와 집합 secpol.msc : 로컬 보안설정 services.msc : 서비스
happy4u
2004. 7. 10. 13:26
2004. 7. 10. 13:26
저자 : Yuki Hiroshi 역자 : 김윤정 출판사 : 영진닷컴 음.... 아직 몇 챕터 보진 않았지만... 그간 보아온(정독한게 아니고 걍 ?어본 ^^'') 패턴 책 중 젤로 맘에 든다. ^^;; 아주 쉽게 내용 정리도 잘 해서 써 놓은 책인거 같다. 내외공이 약한 나에게 적당한 책이 아닌가 싶다. 사실 C#을 공부 중이라 C#으로 된 패턴책이면 더 좋겠지만... C#으로 된 패턴책 꽤 오래전에 산게 있어서 비교해가면서 보고 있는데... 비교가 안된다 --;; Java를 공부하지 않은 분이더라도 패턴 공부 첨 할때 시작하기 좋은 책 같습니다. 강추~~
happy4u
2004. 7. 10. 11:30
2004. 7. 10. 11:30
오픈 위키의 경우 추가 기능을 설치 할 수 있는데... http://openwiki.com/ow.asp?OpenWiki%2FExtensions 여기 보시면 정리가 잘 되어 있습니다. 물론 영어로 --;; 그래도 어렵지 않게 설치하실 수 있습니다. 이 중 매우 간단히 추가할 수 있고, 제가 해 본 건... ./Progress Bar Macro : 프로그레스바를 표현할 수 있는 Tag를 추가 ./Font Size : 폰트 사이즈 변경 가능한 Tag추가 ./Colours : 글자색 변경 가능한 Tag추가
happy4u
2004. 7. 10. 11:22
2004. 7. 10. 11:22
Database 준비가 끝났으면, 이제 Web 사이트를 만드는 일만 남았습니다. IIS에서 새로 웹 사이트를 만드시고, web root는 '\openWiki\owbase'를 웹 root로 잡으시면 끝입니다. 열어 보시면, 위키가 동작하는 것을 확인할 수 있습니다. 간단하죠?? 그 다음 추가로 하나 해 주실 작업이 있는데... 기본 encoding이 영문으로 되어 있어, 한글이 깨지는 현상을 확인하실 수 있습니다. 물론 수작업으로 인코딩을 한글로 바꾸시면 되지만, 귀찮으시겠죠?? 전 소스에서 encoding 지정하는 부분을 두 군데 바꿨더니 한글이 문제 없었는데... 그렇게 바꾸면 완벽하지 않은건지... www.whohwa.pe.kr 에 가면 openwiki 한글 지원되게 수정하는 코드가 있습니다. 긁어 왔습니다. ( 형 괜찮지?? ㅋㅋ) ------------------------------------- 한글 패치- owconfig_default.asp
OPENWIKI_ENCODING = "EUC-KR"
- owpreamble.asp 272 Line
gFS = Chr(179) ==> Chr(127)
- owpatterns.asp 97 Line
vAnyLetter = "[-,.()'# _0-9A-Za-z"&chr(127)&"-"&chr(255)&"]"
- xsl/owinc.xsl 15 Line
return (pData);
- owwikify.asp 520 Line
Function URLDecode(pURL) Dim vPos Dim result Dim tempHex Dim i pURL = Replace(pURL, "+", " ") for i = 1 to len(pURL) If Mid(pURL, i, 1) = "%" Then If LCase(Mid(pURL, i + 1, 1)) = "u" Then result = result & Chr(CLng("&H" & Mid(pURL, i + 2, 4))) i = i + 5 Else tempHex = CLng("&H" & Mid(pURL, i + 1, 2)) If tempHex > 127 Then result = result & Chr(Clng("&H" & Mid(pURL, i + 1, 2)) * &H100 + clng("&H" & Mid(pURL, i + 4, 2))) i = i + 5 Else result = result & Chr(CLng("&H" & Mid(pURL, i + 1, 2))) i = i + 2 End If End If Else result = result & Mid(pURL, i, 1) End If next URLDecode = result End Function
------------------------------------------------------------------- 출처 : http://aspqna.whohwa.pe.kr/ow.asp?%BF%C0%C7%C2%C0%A7%C5%B0 입니다.
|
|