WCF(Windows Communication Foundation)는 기존 윈도우 세계에서 사용되는 다양한 통신 인프라를 통합하고 보완하면서도 새로운 기능들을 제공하고 있다. WCF가 ASP.NET 웹 서비스, 닷넷 리모팅, DCOM, MSMQ과 같은 기존 기술들의 프로그래밍 모델을 어떻게 통합하고 있는지는 지난 11월호에서 언급한 바가 있다. 이번 호에서는 WCF가 기존 ASP.NET 웹 서비스에 비해 새로이 제공하는 기능으로서 콜백(Callback)에 대해 살펴보기로 하겠다. WCF의 콜백에 대해 알아보기 전에 일반적인 콜백의 메커니즘과 콜백을 사용할 때 발생하는 일반적인 주의사항에 대해 먼저 살펴보기로 하자.
유경상 www.simpleisbest.net·필자는 현재 드윈 테크놀러지사에서 근무하고 있다. COM+, 닷넷 등 주로 마이크로소프트의 기술에 대한 교육과 컨설팅을 주로 맡고 있다. 늘 머리가 백발이 되더라도 기술서적을 집필하는 게 꿈이다.
콜백의 기본 개념
일반적인 의미에서 콜백이란 호출자(Caller)가 피호출자(Callee)를 호출하는 것이 아니라 피호출자가 호출자를 호출하는 것을 말한다(<그림 1> 참조). 콜백이 많이 사용되는 전형적인 예는 WIN32 API이다. 대개의 경우 응용 프로그램이 WIN32 API를 호출하는 것이 일반적이지만 때때로 윈도우 시스템이 응용 프로그램을 호출해야 할 때가 있다. 이때 응용 프로그램은 콜백 함수를 윈도우 시스템에게 알려주고 어떤 조건이 만족되면 윈도우 시스템이 콜백 함수를 호출해 준다. WIN32 API의 EnumWindow, SetTimer 함수 등이 콜백을 사용하며 윈도우 프로시저(window procedure) 역시 콜백 개념을 사용한다.
굳이 닷넷에서 비슷한 개념을 제시하라고 한다면 이벤트 정도가 되겠지만 정확한 의미에서 콜백과 닷넷의 이벤트는 약간 다르다. 닷넷 이벤트는 여러 이벤트 가입자(subscriber)에게 이벤트 핸들러를 호출해 주는 메커니즘이기 때문이다. 필자는 콜백의 비슷한 개념으로는 델리게이트를 매개변수로 취하는 메소드의 경우를 들고 싶다.

예를 들어 ThreadStart 델리게이트(delegate, 한글 MSDN에서는 대리자라고 표기하고 있다)를 매개변수로 취하는 Thread 클래스의 생성자는 스레드의 시작점을 ThreadStart 델리게이트로서 취하고 스레드가 시작될 때 매개변수로 받은 ThreadStart 델리게이트를 호출한다. 이처럼 닷넷 프레임워크가 응용 프로그램의 코드를 호출해 주는 상황이 바로 콜백이라고 할 수 있겠다. 비슷한 경우로는 비동기 호출을 처리할 때 작업의 종료를 알리기 위한 AsyncCallback 역시 콜백의 전형적인 예라고 할 수 있겠다.
<그림 2>는 전형적인 콜백 메커니즘을 보여주고 있다. 콜백 메커니즘의 순서로서 (1) 호출자는 콜백 메소드의 참조(함수 포인터 혹은 델리게이트)를 매개변수로 하여 피호출 메소드를 호출한다. (2) 피호출 메소드는 매개변수로 전달된 콜백 메소드에 대한 참조를 필드와 같은 곳에 기록해 둔다. (3) 이제 콜백을 수행할 어떤 조건(이 조건은 다양할 수 있다)이 만족되면 (4) 기록해 둔 콜백 메소드 참조를 이용하여 콜백 메소드를 호출하게 된다.
물론 모든 콜백이 <그림 2>와 같은 순서를 따르는 것은 아니지만 많은 경우 이와 같은 시나리오를 따르는 것이 일반적이다. 콜백을 수행할 조건을 만족하는지 지속적으로 검사하는 과정이 필요하기 때문에 별도의 스레드를 이용하는 경우가 대부분이며, 콜백 메소드를 호출하는 스레드 역시 조건을 검사하는 스레드이기 때문에

콜백 메소드는 서로 다른 스레드에서 호출되는 것이 일반적이다. 이렇게 다중 스레드를 사용하기 때문에 비동기(asynchronous) 작업을 수행할 때 비동기 작업이 완료되었음을 알리기 위한 방법으로 콜백 메커니즘이 많이 사용되곤 한다.
<리스트 1>은 콜백을 사용하여 비동기적으로 파일을 읽는 전형적인 예제 코드이다. 19번째 라인에서 파일 스트림에 대해 BeginRead 메소드를 호출할 때, 콜백을 위해 델리게이트가 사용되었음에 주목하자. 그리고 파일 읽기가 완료되면 콜백 메소드로 지정된 ReadDone 메소드가 호출되게 된다. 이 때 ReadDone 메소드를 호출하는 스레드는 Main 메소드가 수행되는 스레드와는 다른 스레드임에도 주의할 필요가 있다. 따라서 메인 스레드가 읽기가 완료되는 시점(35~37 라인)을 알기 위해 AutoResetEvent 동기화 객체를 사용한 것 역시 눈여겨볼 필요가 있다.
<리스트 1> 콜백을 사용한 비동기 파일 입출력 예제
// 응용 프로그램 메인 메쏘드 [STAThread] static void Main(string[] args> { int id = Thread.CurrentThread.ManagedThreadId;
Console.WriteLine("Open FileStream..."); stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize, FileOptions.Asynchronous);
Console.WriteLine("Start Async File Read... ThreadId={0}", id); stream.BeginRead(buffer, 0, buffer.Length, callback, null);
EndEvent.WaitOne(); }
// Read의 완료를 알리는 Callback 메쏘드 static void ReadDone(IAsyncResult ar) { int id = Thread.CurrentThread.ManagedThreadId; int read = stream.EndRead(ar);
if (read > 0) {
Console.WriteLine("read {0} bytes... ThreadId={1}", read, id);
// 버퍼를 어딘가에 사용한다...(생략)
stream.BeginRead(buffer, 0, buffer.Length, callback, null);
}
else {
Console.WriteLine("reading file done... ThreadId={0}", id);
EndEvent.Set();
}
}
WCF Callback Basic
비단 파일 입출력뿐만 아니라 피호출자가 호출자에게 어떤 사건을 알려줘야 할 상황은 실제 애플리케이션을 개발할 때 많이 발생하곤 한다. 대부분의 비동기 작업이 이러한 상황들 중 하나로서 이야기할 수 있겠다. 필자가 이번 칼럼에서 말하고자 하는 상황은 단일 프로그램에서뿐만 아니라 두 개 이상의 연결된(connected) 프로그램에서도 비동기 호출 상황은 발생할 수 있으며 이러한 상황을 해결할 수 있는 기능이 필요하게 된다.
예를 들어, 시스템에서 발생하는 비 정상적인 상황(디스크 부족, 메모리 부족, 서비스의 다운 등)을 모니터링 하는 클라이언트에게 알려주어야 하는 상황이나 시스템에서 발생하는 로그 메시지를 클라이언트에게 실시간으로 알려주는 로깅 시스템 등을 예로 들 수 있겠다. 이외에도 업무적으로 긴 처리 시간을 요하는 배치 작업의 경우, 클라이언트가 배치 작업이 종료될 때까지 기다리도록 하는 것은 좋은 사용자 경험(UX; User experience)이라 할 수 없다.
이러한 장시간의 처리 시간이 필요한 배치 작업은 클라이언트가 작업의 시작을 서버에 요구하고 서버는 작업을 시작한 후, 배치 작업의 완료를 클라이언트에게 알려주는 것이 이상적인 처리 방식이라 할 수 있다. 그 동안 클라이언트가 다른 작업을 할 수 있도록 말이다.
기존의 ASP.NET 웹 서비스는 웹 서비스 서버가 클라이언트에게 어떠한 사건(시스템의 변화, 로그 발생, 배치 작업의 종료 등)을 알려줄 적당한 방법이 없었다. 이 때문에 클라이언트는 주기적으로 사건의 발생을 확인하는 호출을 수행하곤 했다. 이러한 방식이 소위 말하는 폴링(polling) 방식이다. 폴링 방식의 접근 방법은 구현이 상대적으로 쉽지만 폴링 주기를 어떻게 정하는가에 따라 정확도의 차이가 크다.
정확도를 높이기 위해 폴링 주기를 짧게 할 수도 있을 것이다. 단일 컴퓨터 내에서라면 짧은 폴링 주기가 정확도를 올리는 효과를 낼 수도 있다. 하지만 네트워크에 의해 연결된 클라이언트와 서버는 폴링 주기를 너무 짧게 정하면 네트워크 혼잡(congestion)을 유발하기 때문에 문제를 유발할 수도 있다.
WCF는 ASP.NET 웹 서비스가 제공하지 않는 콜백 메커니즘을 제공한다. 물론 이전에도 닷넷 리모팅이나 DCOM, MSMQ 등은 콜백 기능 혹은 그와 유사한 메커니즘을 제공했었지만 방화벽을 자유롭게 통과할 수 없다는 단점을 가지고 있기 때문에 널리 사용하기 쉽지 않았다. 이젠 WCF를 통해 방화벽을 자유롭게 통과할 수 있는 웹 서비스 콜백이 가능하게 된 것이다.
Callback capable bindings
WCF에서 콜백을 지원하는 바인딩은 네 종류이다. WSDualHttpBinding, NetTcpBinding, NetNamedPipe Binding, NetPeerTcpBinding 등이 그들이다.
WSDualHttpBinding은 WSHttpBinding 과 동등하게 보안(Security), 신뢰할 수 있는 메시징(reliable mess aging), 트랜잭션(Transaction)을 모두 지원할 뿐 더러 서비스가 클라이언트를 호출할 수 있는 능력을 보유하고 있다. WSDualHttpBinding은 HTTP 프로토콜을 사용하므로 방화벽을 통과하기 매우 쉽다는 장점과 상호운영성(interoperability)이 뛰어나다는 장점을 가지고 있지만 성능 면에서 상대적으로 느리다는 단점을 가지고 있다.
반면 NetTcpBinding은 HTTP 기반의 바인딩에 비해 바이너리 인코딩을 사용하고 TCP/IP 라는 저 수준 트랜스포트만을 사용한다. 따라서 프로토콜 자체의 오버헤드를 갖지 않으므로 닷넷 리모팅에 동등한 성능을 낼 수 있을뿐더러 콜백 역시 가능한 바인딩이다.
NetNamedPipeBinding은 단일 컴퓨터 내에서 최적의 성능을 발휘할 수 있도록 명명된 파이프(Named Pipe)를 사용하는 바인딩이다. 명명된 파이프를 사용하므로 신뢰할 수 있는 메시징, 트랜잭션뿐만 아니라 양방향 호출, 즉 콜백이 가능하다.
NetPeerTcpBinding은 이름에서도 알 수 있듯이 P2P (Peer-to-Peer)에 적합한 바인딩이다. P2P를 지원하는 바인딩이기 때문에 당연히(?) 콜백을 지원한다. WCF의 P2P에 대한 지원은 상당한 분량의 내용을 다시 이야기해야 하므로 다음 기회에 별도로 다루기로 하고 이번 호에서는 더 이상 다루지 않을 생각이다.
이번 호에서는 WSDualHttpBinding을 사용하는 예제를 다루려고 한다. 서비스가 WSDualHttpBinding을 사용하도록 하기 위해서는 configuration 상에서 서비스의 종점(endpoint)를 명시할 때 wsDualHttpBinding 프로파일을 사용하도록 설정하면 된다. 다음은 configuration을 통해 WSDualHttpBinding을 사용하도록 설정한 예제이다.
Configuration 이 아닌 코드에 의해 프로그램적으로도 이러한 설정을 수행할 수 있다. 상세한 방법은 본지 2006년 10월호 필자의 칼럼을 참고하기 바란다.
Callback Contract
WCF에서 콜백은 앞서 콜백 메커니즘에서 보여준 예제 코드와 같이 함수 포인터나 메소드에 대한 참조를 사용하지 않는다. WCF가 서비스를 지향하기 때문에 콜백 역시 서비스에 기초하여 콜백 기능을 제공한다. 따라서 WCF에서 콜백을 사용하기 위해서는 콜백을 위한 인터페이스를 별도로 정의해야 한다. WCF는 인터페이스란 용어 대신 계약(contract)이란 용어를 사용하므로 콜백 계약(역시 좀 어색하다)이라 칭하는 것이 더 맞을 것이다.
콜백 인터페이스는 하늘에서 그냥 뚝 떨어지는 것이 아니라 어떤 서비스 계약의 콜백 계약이 어떤 것이라고 명시함으로써 정의된다. <리스트 2>는 콜백 계약의 예제를 보여주고 있다. ILongRunningJob 인터페이스는 배치 작업을 위한 서비스로서 긴 시간을 요구하는 배치 작업을 시작하거나 취소하는 메소드를 가지고 있다.
그리고 이 서비스 계약은 콜백 인터페이스를 명시하기 위해 ServiceContract 특성(attribute)에 Callback Contract 속성을 사용하고 있다. IJobResult 인터페이스는 콜백 계약으로 사용되고 있지만 ServiceContract 특성을 명시하지 않아도 된다.
<리스트 2> 서비스 계약(service contract)과 그에 따른 콜백 계약(callback contract)
Implementing Service
콜백을 가진 서비스라고 해서 특별한 구현이 필요한 것은 아니다. 일반적인 WCF 서비스를 구현하듯이 구현을 하면 된다. 다만 클라이언트에게 콜백 호출을 하기 위해 클라이언트 콜백에 대한 채널 객체를 알아낼 수 있다는 점이 다를 뿐이다.
WCF 서비스의 메소드(WCF에서는 이를 operation이라 한다)는 서비스 호출에 대한 문맥 정보를 사용할 수 있다. 이 정보는 OperationContext 클래스를 통해 제공되는데, 이 클래스의 사용법은 ASP.NET의 HttpContext와 매우 비슷하게 사용할 수 있다. 다음 코드는 서비스의 메소드 내에서 클라이언트 콜백에 대한 채널을 얻기 위한 코드이다.
// operation context를 얻는다.
OperationContext ctx = OperationContext.Current;
// callback을 위한 클라이언트 채널을 구한다.
// 이 경우 서비스에서 클라이언트에 대한 proxy를 갖게 되는 것이다.
IJobResult client = ctx.GetCallbackChannel
OperationContext 클래스의 GetCallbackChannel 메소드는 클라이언트에서 제공하는 콜백 인터페이스에 대한 채널을 반환하며 이 채널은 앞에서 선언한 콜백 인터페이스 타입으로서 사용할 수 있다. 콜백 인터페이스를 알아냈으니 이제 이 인터페이스를 통해 메소드를 호출하면 클라이언트에 대해 콜백을 수행할 수 있는 것이다.
필자가 구현한 예제는 긴 시간 동안 수행되는 배치 작업의 시작과 취소를 서비스로서 제공하는 WCF 서비스이다. <리스트 3>은 WCF 서비스의 구현을 보여주고 있다. JobStart 메소드가 호출되어 배치 작업의 시작이 요청되면 서버는 배치 작업을 추상화하고 있는 Job 클래스의 인스턴스를 생성하고 이를 구동시키고 곧바로 서비스 호출을 종료한다.
Job 클래스의 구현은 <리스트 4>에 나타난 바와 같이 별도의 스레드를 작성하여 이 스레드에서 배치 작업을 수행한다. 배치 작업이 정상적으로 종료되거나 클라이언트의 요청에 의해 취소된 경우, 배치 작업의 종료 상태를 콜백 인터페이스를 통해 클라이언트에게 알리도록 되어 있다. <리스트 3> <리스트 4>의 코드는 전체 코드의 일부이므로 전체 소스 코드는 이 달의 디스켓을 참조하기 바란다.
<리스트 3> ILongRunningJob 인터페이스에 대한 서비스 구현
<리스트 4> 배치 작업을 추상화하는 Job 클래스
Implementing Client
WCF 서비스에 대한 클라이언트를 구현하는 가장 쉬운 방법은 Visual Studio 나 svcutil.exe 유틸리티의 도움을 받는 것이다. 어떤 방법을 사용하든지 상관없이 WCF 서비스에 대한 프록시(proxy)가 생성될 것이다. 서비스에 콜백 인터페이스가 정의되어 있는 경우, 이 프록시는 적절한 콜백 인터페이스와 클래스 구현을 제공한다. 한 가지 알아두어야 할 점은 콜백 인터페이스의 이름이 서버에서 정의한 이름(IJobResult)이 아닌 서비스 인터페이스 이름(ILo ngRunningJob) 뒤에 Callback 이 붙은 이름이라는 것이다.
즉 <리스트 2>와 같은 서비스 계약이 서버에서 제공되는 경우, 콜백 인터페이스의 이름은 ILongRunningJob Callback 이 된다. 어차피 WCF가 서비스 기반이므로 닷넷의 인터페이스 이름은 중요하지 않다.
콜백을 사용하는 서비스를 호출하는 클라이언트는 반드시 콜백 인터페이스에 대한 구현을 제공해야만 한다. 즉, WCF 서비스가 콜백 계약을 명시하고 있다면 클라이언트는 반드시 콜백 계약에 대한 구현을 제공해야 한다. 콜백 인터페이스의 구현은 계약에 명시된 인터페이스, 즉 ILon gRunningJobCallback 인터페이스를 구현하는 클래스를 구현하기만 하면 된다.
<리스트 5>에서 보인 바와 같이 콜백 인터페이스를 구현하는 클래스는 일반적인 WCF 서비스의 구현과 동일하게 구현할 수 있다. 콜백 시나리오에서 클라이언트는 서비스에 대해서 ‘콜백 서비스’ 역할을 하기 때문이다. <리스트 5>는 간단하게 호출의 결과를 콘솔 윈도우에 표시하는 것 정도로만 구현되어 있다.
콜백을 사용하는 경우 서비스에 대한 프록시는 일반적으로 사용되는 ChannelFactory 클래스를 사용하지 않는다(2006년 10월호 Inside Developer 칼럼 참조). 대신 DuplexChannelFactory 클래스를 사용해야 하며 이 클래스는 콜백 인터페이스를 구현하는 클래스의 인스턴스를 매개변수로 요구한다는 점을 알아두어야 한다. 다음 코드는 DuplexChannelFactory 클래스를 구성하는 방법을 보여주고 있다.
JobResultHandler handler = new JobResultHandler();
InstanceContext ctx = new InstanceContext(handler);
DuplexChannelFactory
new DuplexChannelFactory
Visual Studio 나 svcutil.exe를 사용하는 경우, 생성된 프록시 클래스는 DuplexChannelFactory 클래스를 사용하도록 코드가 생성되므로 개발자가 직접 이 클래스를 제어할 필요는 없다. 하지만 프록시 클래스의 인스턴스를 생성할 때 반드시 InstanceContext 객체를 매개변수로 넘겨 주어야 하는 것은 변함이 없다.
<리스트 6>은 클라이언트 구현을 보여주고 있다. 서비스의 JobStart() 메소드를 호출하더라도 배치 작업이 종료될 때까지 기다리는 것이 아니라 곧바로 호출은 반환되기 때문에 다른 작업을 수행할 수 있음에 유의할 필요가 있다. 배치 작업이 종료되면 서비스는 콜백 인터페이스의 메소드인 OnJobEnd 메소드를 호출할 것이므로 클라이언트는 서비스의 통지(notification)이 도착할 때까지 다른 작업을 수행할 수 있는 것이다.
<리스트 5> 콜백 인터페이스를 구현하는 클래스 구현
<리스트 6> 클라이언트 구현
지금까지 WCF 상에서 콜백을 구현하는 것에 대한 기본적인 사항들을 살펴보았다. 이제 콜백을 구현할 때 기술적으로 문제의 소지가 있는 한두 가지 사항에 대해 살펴보도록 하겠다.
Solving Callback Timeout
서비스에서 클라이언트에게 콜백 호출을 할 때는 상당한 주의를 요구한다. 클라이언트의 생리 상 콜백 호출이 발생하기 전에 클라이언트가 종료될 수 있기 때문에 콜백 호출이 실패할 수 있다. 다시 말해서 서비스는 클라이언트가 서비스에 연결되어 있는지 검사할 필요가 있다는 말이 되겠다. <리스트 4>의 Worker 메소드의 구현처럼 단순히 콜백 인터페이스에 대해 호출을 수행할 때 클라이언트가 정상적으로건 비 정상적으로건 이미 종료된 상태라면 콜백 호출을 수신할 수 없을 것이다.
따라서 콜백 호출은 주어진 타임아웃 시간 동안 반복적으로 시도될 것이며(WSDualHttpBinding이 신뢰할 수 있는 메시징을 지원하기 때문이다), 타임 아웃이 지나면 예외가 발생되게 된다.
이러한 문제를 해결하기 위해서 서비스는 콜백 호출을 하기 전에 클라이언트가 여전히 연결되어 있는지 확인할 필요가 있다. WCF에서 클라이언트 콜백 인터페이스인 IJobResult 인터페이스는 ContextChannel 객체에 의해 암시적으로 구현하고 있다. 따라서 콜백 인터페이스는 IChannelContext 인터페이스로 형변환(casting)이 가능하고 IChannelContext 인터페이스는 채널의 연결 상태를 알려주는 State 속성을 제공하므로 이를 통해 클라이언트의 연결 여부를 파악할 수 있다. <리스트 7>은 콜백 호출 전에 클라이언트가 연결되어 있는지 확인하고 있다.
<리스트 7) 클라이언트 연결 확인 및 콜백 호출 오류에 대한 처리
<리스트 8> OneWay 설정이 수행된 콜백 인터페이스
<리스트 7>에서 특이한 점은 클라이언트의 연결 여부를 확인한 후에도 try ~ catch 문으로 콜백 호출에 대한 예외 처리를 하고 있음을 알 수 있을 것이다. 클라이언트의 연결 여부를 판단할 수 있는 경우는 클라이언트가 종료하면서 연결을 정상적으로 끊은 경우이다. 만약 클라이언트가 비정상적으로 종료되었거나 코딩 오류로 인해 클라이언트 측 프록시에 대해 Dispose를 호출하지 않은 경우에 서버는 클라이언트에 대한 연결이 끊겼다는 사실을 통지 받지 못한다.
비록 타임아웃에 의해 언젠가는 서비스가 클라이언트가 종료되었음을 알게 되겠지만 콜백을 호출하는 상황에서 클라이언트가 여전히 연결된 상태로 인식될 수 있기 때문에 콜백 호출은 여전히 예외를 유발할 수 있다. 이 때문에 <리스트 7>에서는 콜백 호출을 다시 try ~ catch 로 묶고 예외에 대한 처리를 해주는 것이다.
Deadlock Condition
필자가 지금까지 보여준 예제에서는 WCF 서비스 인터페이스의 구현 내에서 직접 콜백을 호출하지 않았다. 즉, ILongRunningJob 인터페이스의 JobStart 메소드에서 별도의 스레드를 작성한 후, 이 스레드 내에서 콜백 호출을 수행한 것이다. 이러한 구현은 이 글의 서두에서 설명한 대로 가장 일반적인 콜백 시나리오이다.
하지만 반드시 콜백이 이러한 시나리오에서만 사용되는 것은 아니다. 종종 서비스 인터페이스 메소드 내에서 곧바로 콜백을 호출해야 하는 상황도 발생하기 마련이다. 예를 들어 배치 작업을 취소하는 ILongRunningJob 인터페이스의 JobCancel 이 다음과 같이 구현되어 있다고 가정해 보자.
// 배치 작업을 취소한다.
위 코드는 서비스 메소드 내에서 곧바로 콜백을 호출하고 있다. 이런 상황은 WCF 런타임이 교착 상태(dead lock)에 빠지게 한다. 교착 상태가 발생하는 원인을 살펴 보자.
WCF의 서비스 인스턴스는 기본적으로 단일 스레드에 의해서만 액세스되도록 설정되어 있다. 이와 같은 설정은 서비스 타입에 명시되는 ServiceBehavior 특성(attri bute)의 ConcurrencyMode 속성에 의해 제어된다. ConcurrencyMode 속성의 기본값은 Single 로써 서비스 인스턴스를 액세스할 수 있는 스레드가 오직 하나라는 의미이다.
이러한 기능을 제공하기 위해 WCF 런타임은 각 서비스 인스턴스 당 하나의 잠금을 유지하고 있으며 하나의 스레드가 인스턴스를 액세스 하는 동안 이 인스턴스를 액세스하고자 하는 다른 스레드는 블로킹(blocking) 되게 된다. 다시 말해서 서비스가 호출되면 스레드는 서비스 인스턴스의 메소드를 호출하기 위해 서비스 인스턴스를 액세스 해야 하며 이는 곧 서비스 인스턴스가 잠기게 됨을 의미한다.
문제는 콜백이 발생할 때이다. 콜백 호출을 수행하면 서비스는 클라이언트에 대해 Request 메시지를 전송하게 된다. 그리고 클라이언트가 콜백 호출에 대해 Response 메시지를 서비스에게 발송하게 되면 이 메시지를 처리하기 위해 서버 측 스레드는 서비스 인스턴스를 액세스 해야만 한다. 하지만 이미 클라이언트로부터 메시지 호출을 처리하기 위해 서비스 인스턴스가 잠겨 있기 때문에 콜백에 대한 Response를 처리하기 위해 서비스 인스턴스의 잠금을 획득할 수 없게 되어 버린다. 이 때문에 교착 상태가 유발되는 것이다.
이러한 문제를 해결하는 방법은 세 가지가 있다. 첫째로 서비스 인스턴스가 여러 스레드의 동시 접근에 대해 안전하도록 구현하고 WCF의 인스턴스 ConcurrencyMode 속성을 Multiple 로 설정하는 것이다. 다음 코드는 Service Behavior 특성(attribute)을 이용하여 ConcurrentyMode를 설정한 예이다.
ConcurrencyMode 속성 값을 Multiple로 유지하기 위해서는 각별한 주의가 필요하다. 하나의 서비스 인스턴스가 여러 스레드(여러 동시 호출)에 의해 액세스 될 수 있기 때문에 필드 값에 대해 읽기 쓰기 작업이 동기화 문제를 유발할 수도 있으며 부주의한 코드는 새로운 교착 상태를 유발할 수도 있다.
ConcurrencyMode 속성의 값을 Reentrant 로 설정하는 것도 앞에서 언급한 교착 상태를 피하는 하나의 방법이다. Reentrant 모드는 순전히 콜백을 위해 제공되는 ConcurrencyMode 라고 보아도 될 만큼이나 특별한 처리를 수행한다. ConcurrencyMode 가 Reentrant 로 설정되면 기본적으로 Single 과 동일하게 하나의 서비스 인스턴스를 액세스 할 수 있는 스레드는 오직 하나이다(내부적인 잠금에 의해).
하지만, 서비스 메소드 내에서 콜백 호출을 수행하면 WCF 내부에서 유지되는 잠금은 풀리게 된다. 따라서 콜백 호출의 결과인 Response 메시지를 서비스가 처리할 수 있게 되는 것이다. 콜백 호출이 완료된 후에는 풀린 잠금은 잠기게 되어 Single 과 동일한 문맥 상에서 서비스의 처리가 가능해 진다.
ConcurrencyMode 가 Single 인 이유는 서비스의 구현을 보다 손쉽게 하기 위함이다. 하나의 서비스 인스턴스가 여러 클라이언트에 의해 동시에 액세스 되게 되면(Multiple 값 사용 시) 서비스를 구현하는데 소요되는 노력은 기하 급수적으로 늘어나기 때문이다. COM+ 에서도 트랜잭션을 사용하는 경우, 컴포넌트에 대한 동기화가 ‘필수’ 이상의 값으로 강제적으로 설정되는 이유도 트랜잭션 무결성(integrity)을 유지하기 쉽도록 하기 위함이다.
ConcurrencyMode를 Multiple 로 설정하기 위해 많은 노력이 필요하다면 Reentrant 값을 사용하는 것은 어떤가? 간단한 해결책처럼 보이지만 콜백을 하는 동안 잠금이 풀린다는 점에 유의할 필요가 있다. 잠시 잠금이 풀리는 사이에 또 다른 클라이언트의 호출이 발생하면 그 호출은 동일한 서비스 인스턴스를 액세스하기 위해 잠금을 획득하게 될 것이다. 이런 경우 하나의 서비스 인스턴스를 두 개 이상의 클라이언트가 동시 액세스하는 상황이 발생하므로 동기화 문제가 유발될 수 있음에 유의해야 한다.
프로그래밍의 단순함을 유지하면서도 교착상태를 해결하는 ‘간편한’ 방법은 없을까? 즉, ConcurrencyMode 를 Single 로 설정해 놓으면서도 서비스 메쏘드 내에서 콜백을 호출하는 방법 말이다. 콜백을 위해 다른 쓰레드를 생성하는 것을 생각해 볼 수도 있겠지만 그다지 간편해 보이지 않는다. 문제의 핵심은 콜백을 수행했을 때 클라이언트가 전송하는 콜백의 결과 메시지를 수신하고 처리해야 하고 이 때문에 교착상태가 발생한다는 점이다.
어떤 웹 서비스 호출이 결과를 반환하지 않는 경우, 굳이 그 결과 메시지를 수신하고 처리하는 과정을 밟을 필요가 없다. ASP.NET 웹 서비스 시절부터 제공되는 OneWay 웹 서비스 호출은 서비스를 호출하고 그 결과값을 받지 않기 때문에 서비스 호출은 서비스의 호출 결과와 무관하게 비동기적으로 호출되어 버린다.
WCF 역시 OneWay 호출을 지원하므로 호출 결과가 중요하지 않다면 콜백 메쏘드에 IsOneWay 속성을 추가할 수 있다. 우리의 예제에서 OnJobCanceled 콜백 메쏘드는 결과값을 반환하지 않으며 서비스에서 반환 값에 대해 관심을 기울일 필요도 없다. IsOneWay 속성을 설정하기 더 없이 좋은 상황인 것이다.
<리스트 8>과 같이 콜백 인터페이스의 OnJobCanceled 메소드에 IsOneWay 속성을 추가하고 그 값을 true로 설정하면 콜백 호출이 발생하더라도 콜백에 따른 Response 메시지를 수신하고 처리할 필요가 없기 때문에 잠금을 요구하지 않는다. 이제 OnJobCanceled 콜백은 서비스의 메소드 내에서도 아무런 문제를 유발하지 않고 호출이 가능하게 된 것이다.
- 스크립트 언어의 올바른 이해 자바스크립트의 재해석 (0)2007/05/02
- 코드의 웃음을 빼앗아가는 리펑토링(Refuctoring) (0)2007/04/21
- WCF Callback (0)2007/04/21
- 엔터프라이즈 아키텍처의 미래, The Next Applicat... (0)2007/04/20
- 선택이 아닌 필수 AOP(Aspect Oriented Programming) (0)2007/04/20

수안이의 컴퓨터 연구실



Leave your greetings.