차세대 웹 서비스 프레임워크, WCF 프로그래밍

2006. 11. 27. 16:39 IT 및 개발/.NET FX & Visual C#
차세대 웹 서비스 프레임워크
WCF 프로그래밍

닷넷에 기반한 개발자들이라고 기술적인 안도감에 빠져 살 수는 없다. 올해 초 발표된 닷넷 프레임워크 2.0에 이어 벌써 닷넷 프레임워크 3.0 RC (Release Candidate) 버전이 발표되었다. 지난 8월호 칼럼에서 닷넷 프레임워크 3.0에 대해 간략히 소개한 바가 있다. 이번 호에서는 닷넷 프레임워크 3.0의 WCF(Windows Communication Foundation)의 기본에 대해 설명하고자 한다. 지면이 한정되어 있는 관계로 WCF의 모든 면모를 살필 수 없음을 독자들은 이해해 주기 바란다. 또한 이 글은 2006년 9월에 발표(?)된 닷넷 프레임워크 3.0 RC1 (release candidate 1)을 기준으로 작성했으므로 최종 버전에서는 바뀌는 내용이 있을 수 있다.

WCF 기본 프로그래밍 모델
WCF는 닷넷 프레임워크 3.0에 포함된 닷넷 플랫폼의 차세대 통신 인프라이다. 지금까지 닷넷 플랫폼은 ASMX 기반의 웹 서비스와 닷넷 리모팅(Remoting)을 이용하여 클라이언트와 서버 사이의 통신을 해 왔다. 물론 이들은 앞으로도 계속 지원될 것이지만, 이들을 통합한 프로그래밍 모델을 가지고 있는 WCF가 앞으로 닷넷 플랫폼의 기본 통신 인프라가 될 것은 너무나도 자명하다. WCF의 기본 사상은 마이크로소프트가 전략적으로 밀고(?) 있는 웹 서비스 기반의 SOA(Service Oriented Architecture) 기반이기 때문에 WCF의 중요성은 더욱 높다고도 할 수 있다.

WCF는 크게 특정 기능을 제공하는 서비스와 이것을 사용하는 클라이언트로 구성되어 있다. 서비스는 어떤 비즈니스 컴포넌트이거나 검색, 데이터 조회, 리포트 등 다양한 기능을 제공한다. 그리고 이 서비스가 어떤 기술로 구현되어 있는지는 중요하지 않다. SOA가 지향하듯이 서버는 서비스를 제공하고 이 서비스에 대한 인터페이스만이 공개되어 있을 뿐이다. 클라이언트는 공개된 인터페이스를 통해 서비스를 호출하고 서비스가 제공하는 기능을 사용할 수 있는 것이다.

서비스와 서비스가 제공하는 인터페이스를 정의하기 위해서 WCF는 종점(end-point)이란 용어를 사용한다. 종점이란 서비스를 액세스하기 위해 필요한 주소(Address), 계약(Contract), 그리고 바인딩(Binding)을 말한다. 주소란 서비스에 접근하기 위한 인터넷 주소가 되겠다. http://www.simpleisbest.net/echo/services 와 같은 일반적인 URL일 수도 있고, WCF에서만 통용되는 net.tcp://wcf_server:9082/echo/services 와 같은 주소일 수도 있다. WCF의 계약은 서비스가 어떤 메소드를 제공하고 그 메소드의 매개변수는 무엇인지를 명세한 인터페이스를 말한다.

마지막으로 바인딩이란 주어진 인터페이스를 메시지로 변환하는데 필요한 메시지 프로토콜을 의미한다. 인터페이스의 메소드를 호출하는데 필요한 매개변수 등을 XML로 표시할 것인가, 아니면 바이너리로 표시할 것인가 그리고 XML로 표시한다면 어떤 인코딩을 사용할 것인가 등등을 명세 하는 것 역시 바인딩이다.
이렇게 서비스가 서비스 종점을 표시하면 클라이언트는 종점 정보로부터 서비스를 찾아서 메소드(WCF 용어로는 operation이다)를 호출할 수 있게 된다. 메소드 호출은 WCF 바인딩을 준수하는 일련의 메시지가 되며 이 메시지는 WCF 바인딩이 명시하는 프로토콜에 의해 서비스에게 전달되는 것이다.

하나의 서비스는 2개 이상의 종점을 가질 수 있다. 즉, 하나의 서비스가 2개 이상의 WCF 계약을 구현(implement)하거나 단일 WCF 계약을 구현하더라도 2개 이상의 바인딩을 가질 수 있다는 뜻이다. <그림 1>은 WCF 서비스와 종점(endpoint) 사이의 관계를 보여주고 있다.


<그림 1> WCF 서비스와 종점(endpoint)

WCF 서비스 프로그래밍
항상 그러하듯이 코드를 봐야만 감이 잡히는 관계로 곧 바로 예제를 작성해 보도록 하겠다. 가장 먼저 해야 할 부분은 WCF 서비스의 계약, 즉 인터페이스를 정의하는 것이다. 인터페이스는 일반적인 닷넷에서의 인터페이스 정의와 동일하다. 다만, 이것이 WCF에서 사용될 때 WCF 런타임이 인식하도록 Contract, OperationContract 등의 특성(attribute)를 지정해 주기만 하면 된다.

먼저 System.ServiceModel.dll 어셈블리를 참조에 추가하고 using 문을 사용하여 System.ServiceModel 네임스페이스를 사용하도록 하자. 그리고 <리스트 1>과 같이 서비스에 사용할 인터페이스(계약; Contract)를 정의하고 인터페이스에는 Contract 특성을, 메소드에는 Operation Contract 특성을 명시해 준다. 이 두 특성을 명시할 때 추가적인 다양한 옵션을 줄 수 있지만 지면 관계상 인터페이스의 네임스페이스만을 명시하도록 했다.
계약이 정의되었다면 이제 이 인터페이스를 구현할 서비스 클래스를 작성해야 한다. 서비스 클래스는 단순히 앞에서 정의한 ITimeSerivce 인터페이스를 구현하기만 하면 최소 조건을 만족한다. <리스트 2>는 구현된 서비스 클래스를 보여주고 있다.

<리스트 1> WCF Contract 정의
[ServiceContract(Namespace="http://simpleisbest.net/wcf/example/timeservice")]
public interface ITimeService
{
  [OperationContract]
  DateTime GetServerTime();
}

<리스트 2> WCF 서비스 클래스 구현
public class TimeService : ITimeService
{
  public DateTime GetServerTime()
  {
    return DateTime.Now;
  }
}

WCF 서비스 호스팅
이제 남은 것은 구현된 서비스 클래스를 호스팅(hosting) 하는 서비스 호스트를 작성하면 된다. 서비스 호스트는 System.ServiceModel 네임스페이스의 ServiceHost 클래스의 인스턴스를 생성하는 것으로 다음과 같이 작성할 수 있다.

Uri baseAddress = new Uri(“http://localhost:18080/WCFServices/Time Service”);
ServiceHost _Host = new ServiceHost(typeof(TimeService),baseAddress);

ServiceHost 객체를 생성할 때는 ServiceHost가 호스트 할 서비스 클래스의 타입을 명시해야 한다. 만약 클라이언트로부터 WCF 메시지가 도착하면 이 클래스의 인스턴스를 자동적으로 생성하고 해당 서비스 메소드(operation)를 호출하게 된다. 서비스 클래스 타입이 아닌 서비스 클래스의 인스턴스를 생성하고 이 인스턴스를 ServiceHost 에게 넘겨주면 그 인스턴스가 모든 서비스 호출을 서비스하는 Singleton 역할을 수행하게 될 것이다. 이는 닷넷 리모팅의 Singleton 호출 타입과 매우 비슷하다.

서비스 클래스의 타입 외에도 베이스 주소를 ServiceHost에게 넘겨 주어야 하는데, 이 베이스 주소는 추후 서비스의 주소(address)를 결정할 때 참조된다.‘ 참조’만 될 뿐 이것이 서비스의 주소로 결정되는 것이 아님에 유의하자. 실제 서비스의 주소는 종점(endpoint)을 명세할 때 결정된다.

서비스 호스트를 생성했으면 이제 서비스에 사용될 종점을 명시해야 한다. 종점은 다음과 같이 ServiceHost 클래스의 AddServiceEndPoint 메소드를 호출함으로써 정의할 수 있다.

_Host.AddServiceEndpoint(typeof(ITimeService), new BasicHttpBinding(),“ ”);

AddServiceEndPoint 메소드는 <그림 1>에서 나타낸 바대로 종점을 정의하는 서비스의 계약(contract), 바인딩(binding) 그리고 주소(Address)를 매개변수로 취한다. 위 코드에서 계약은 앞에서 ServiceContract 특성을 통해 정의한 ITimeService 인터페이스이고(TimeService 클래스가 contract 가 아님에 유념하자), 바인딩은 WCF에서 기본으로 제공되는 바인딩 중 하나인 BasicHttpBinding을 사용하고 있으며, 주소는 빈 문자열을 사용했다.
AddServiceEndPoint 메소드의 주소에는 절대 경로의 URL 혹은 상대 경로의 URL을 모두 명시할 수 있는데, 상대 경로가 주어지면 ServiceHost 를 생성할 때 사용되었던 베이스 URL을 기준으로 주소가 결정되게 된다.

WCF에서 기본으로 제공되는 바인딩들은 BasicHttpBinding, WSHttpBinding, NetTcpBinding, NetNamedPipeBinding, NetPeerTcpBinding NetMsmqBinding 등이 존재한다. 이들 바인딩에 따라 WCF를 통해 수행할 수 있는 기능에 제약이 있다. 예를 들어 BasicHttpBinding은 XML을 기본으로 사용하여 SOAP 1.x 의 직렬화(serialization)을 사용한다. 즉, 우리가 일반적으로 알고 있는 XML 웹 서비스와 그대로 호환되는 바인딩이다. 반면 WSHttpBinding은 SOAP 을 확장한 표준인 WSSecurity, WS-Routing, WS-AutomicTransaction 등을 모두 사용할 수 있기 때문에 세션 기능, 트랜잭션 기능 등을 모두 사용할 수 있다. 하지만 여전히 WSHttpBinding은 XML 기반으로 직렬화를 수행한다. 반면 NetTcpBinding은 트랜잭션, 세션 등의 WCF 기능을 거의 모두 사용할 수 있을뿐더러 바이너리 인코딩을 기본적으로 사용하므로 성능 상의 이점을 가지고 있다. 이 외에도 P2P에 사용할 수 있는 NetPeerTcpBinding 과 MSMQ를 트랜스 포트(transport)로서 사용하는 NetMsmqBinding 등이 제공된다. 대부분의 닷넷 요소들이 그러하듯이 필요에 따라 커스텀 바인딩을 작성할 수도 있다.

종점의 주소는 어떤 바인딩이 사용되는가에 따라서 달라진다. BasicHttpBinding 이나 WSHttpBinding은 http: 로 시작하는 URL 주소를 사용하지만 NetTcpBinding은 net.tcp: 으로 시작하는 주소를 사용해야 한다. 종점까지 명시했다면 이제 남은 건 호스트를 Open 함으로써 서비스를 시작할 수 있다. ServiceHost의 Open 메소드를 호출하면 WCF 런타임을 필요에 따라 TCP, HTTP, NamedPipe, MSMQ 등 적절한 트랜스포트 계층의 리스너(listener)를 별도의 스레드로 구동하게 되고 클라이언트의 호출을 받을 수 있는 준비가 완료되는 것이다. 서비스 를 종료하기 위해서는 ServiceHost 클래스의 Close 메소드를 호출하면 된다. 서비스 호스트를 구현하는 전체 코드와 이를 사용하는 메인 코드는 <리스트 3>과 같다.

<리스트 3> WCF 서비스 및 호스트 구현
internal class TimeServiceHost
{
  internal static ServiceHost _Host = null;

  // 서비스를 시작한다.
  internal static void StartService()
  {

    // 서비스의 베이스 URL
    Uri baseAddress = new Uri("http://localhost:18080/WCFServices/TimeService");

    // 서비스를 구현하는 서비스 클래스를 서비스 호스트에 등록한다.
    _Host = new ServiceHost(typeof(TimeService), baseAddress);

    // 서비스의 엔드 포인트 설정
    _Host.AddServiceEndpoint(typeof(ITimeService), new BasicHttpBinding(), "");

    // 서비스를 Open 한다.
    // 이로써 서비스에 대한 Listener가 구동되게 된다.
    _Host.Open();
  }

  // 서비스 호스트를 중단한다.
  internal static void StopService()
  {
    // 서비스를 중단한다.
    if (_Host.State != CommunicationState.Closed)
    {
      _Host.Close();
    }
  }
}

class Program
{
  static void Main(string[] args)
  {
    Console.WriteLine("Simple WCF Service Host...");

    // 서비스를 시작한다.
    TimeServiceHost.StartService();
    Console.WriteLine("TimeService is started...");

    // 키 입력을 기다린다.
    Console.WriteLine();
    Console.WriteLine("Press any key to stop the service");
    Console.ReadKey();

    // 서비스를 종료한다.
    TimeServiceHost.StopService();
  }
}

이제 서비스에 대한 구현을 완료하였다면 테스트를 해 볼 차례이다. 작성한 서비스 프로그램을 구동시키고 서비스 주소를 브라우저에 입력해 보자. ASP.NET 웹 서비스(ASMX)를 작성한 것처럼 친절한 HTML이 우리를 반겨 줄 것이다(<화면 1> 참조).


<화면 1> 서비스에 대한 Description 표시

지금까지 WCF 서비스를 작성하는 방법에 대해 알아 보았다. 지금까지 필자가 소개한 WCF 서비스 작성 방법은 순전히 코드를 사용한 것이었다. 하지만 일부는 코드를 사용하지 않고 configuration을 사용할 수도 있다. 물론 WCF 계약을 정의하거나 구현하는 것은 코드에 의존해야 하겠지만 서비스의 종점을 명시하는 것은 개발할 때와 deploy 할 때가 매우 다를 수 있다. 그래서 서비스의 종점과 같은 부분은 configuration 파일(app.config 혹은 web.config)을 명시하여 구현이 완료된 후에도 서비스의 주소, 바인딩을 변경할 수도 있다.

다음은 서비스 호스트의 configuration 파일에 추가될 수 있는 WCF 서비스에 대한 설정이다. 이 설정은 WCFSampleService.TimeService 란 이름의 서비스가 어떤 계약과 바인딩, 그리고 주소를 갖는가 명시되어 있다. Service 요소의 name 속성을 명시할 때는 반드시 서비스를 구현하는 서비스 클래스의 네임스페이스를 포함하는 전체 이름이 사용되어야 함에 주의해야 한다. 이제 이 설정이 configuration 파일에 추가되면 <리스트 3>의 코드에서 AddServiceEndPoint 메소드를 호출하는 코드는 불필요하다.

<system.serviceModel>
  <services>
    <service name="WCFSampleService. TimeService">
      <endpoint contract="WCFSampleService.ITimeService" binding="wsHttpBinding" address="svc" />
    </service>
  </services>
</system.serviceModel>

<리스트 3>의 코드를 그대로 둔 채로 위의 configuration 설정을 추가할 수도 있는데, 이는 <그림 1>과 같이 하나의 서비스가 2개 이상의 서비스 종점을 가질 수 있기 때문에 가능한 것이다. 하지만 두 종점은 주소가 달라야 한다. <리스트 3>에서 주어진 상대 주소와 configuration의 상대 주소가 서로 다름에 유의 하도록 하자.

WCF 서비스를 호스팅 하기 위해 반드시 별도의 애플리케이션이 필요하지는 않다. 닷넷 리모팅과 같이 WCF 서비스 역시 IIS 에서 호스팅 할 수 있다. 이를 위해서는 .asmx 파일과 비슷한 역할을 하는 .svc 파일을 정의하면 된다. 이 .svc 파일에 다음과 같이 서비스 클래스를 명시하기만 하면 IIS와 ASP.NET 의 HTTP 핸들러에 의해 WCF 서비스를 호스팅 할 수도 있다.

<%@ServiceHost Service="WCFSampleService.TimeService"%>

물론 IIS에 호스팅 하고자 하는 WCF 서비스는 ASP.NET 프로젝트의 App_Code 디렉터리나 별도의 DLL 어셈블리에 인터페이스와 이를 구현한 서비스 클래스를 구현해야 할 것이다.

IIS에 WCF 서비스를 호스팅하는 경우에는 종점의 주소는 .svc 파일로 결정되지만, 바인딩이나 Contract을 명시해 줄 필요가 있다. 이를 위해 앞에서 언급한 종점에 대한 configuration 설정을 web.config에 추가해 주어야 한다.

WCF 클라이언트 프로그래밍
이제 서비스를 구현했으니 클라이언트를 작성해 보자. WCF 클라이언트가 서비스를 호출하기 위해서는 WCF 종점 정보가 있으면 된다. 종점 정보는 ServiceEndPoint 클래스를 통해 명시되므로 이 클래스의 인스턴스를 일단 생성한다. 종점을 구성하는 계약(contract), 바인딩(binding), 주소(address)가 모두 명시되어야 함은 물론이다.

ServiceEndpoint endpoint = new ServiceEndpoint(
  ContractDescription.GetContract(typeof(ITimeService)),
  new BasicHttpBinding(),
  new EndpointAddress("http://localhost:18080/WCFServices/TimeService"));

이제 종점으로부터 클라이언트와 서비스를 연결하는 채널 객체를 생성해야 한다. 예상 같아서는 Channel 이란 이름으로 시작하는 어떤 클래스가 있을 것 같지만, 구체적인 채널 클래스는 존재하지 않으며 채널을 생성하는 ChannelFactory 클래스만이 존재한다. 이 팩토리 클래스에 의해 채널이 생성되고 채널 객체는 어떤 클래스가 아닌 WCF 계약이 명시하는 인터페이스만을 구현하고 있다.

ChannelFactory<ITimeService> factory = new ChannelFactory<ITimeService>(endpoint);
ITimeService svc = factory.CreateChannel();

이제 WCF 서비스를 위한 ITimeService 인터페이스가 구해졌으므로 이 인터페이스를 통해 서비스를 호출하기만 하면 끝난다. 클라이언트의 전체 코드는 <리스트 4>와 같 다. 간단하지 않은가?

<리스트 4> WCF 클라이언트 코드
class ClientProgram
{
  static void Main(string[] args)
  {
    ServiceEndpoint endpoint = new ServiceEndpoint(
      ContractDescription.GetContract(typeof(ITimeService)),
      new BasicHttpBinding(),
      new EndpointAddress("http://localhost:18080/WCFServices/TimeService"));

    using (ChannelFactory factory = new ChannelFactory(endpoint))
    {
      ITimeService svc = factory.CreateChannel();
      DateTime dt = svc.GetServerTime();
      Console.WriteLine("Result = {0}", dt);
    }
  }
}

그렇다 간단하지 않다. 웹 서비스나 닷넷 리모팅으로 많은 프로젝트를 경험한 독자라면 항상 개발할 때 서버의 URL과 운영할 때의 서버 URL이 다르기 때문에 URL을 하드 코드 하는 것이 얼마나 좋지 못한가를 잘 알고 있을것이다. MS의 WCF 개발팀이 짱구(?)가 아닌 다음에야 이렇게 모든 것을 코드로 해결하는 방식만을 제공할 리 없다. WCF 클라이언트 역시 configuration을 통해 서비스의 종점을 다음과 같이 명시할 수 있다.

<system.serviceModel>
  <client>
    <endpoint name="TimeService"
      contract="WCFSampleService.ITimeService"
      binding="basicHttpBinding"
      address="http://localhost:18080/WCFServices/TimeService"/>
  </client>
</system.serviceModel>

이 configuration에서는 클라이언트에서 사용할 서비스에 대한 종점 정보를 표시하고 있다. 주목할 부분은 endpoint 요소의 name 속성으로서 ChannelFactory 클래스에 의해서 참조될 때 사용된다. name 속성의 값은 service 요소의 name 속성과는 달리 임의의 값을 사용할 수 있다.

위와 같은 configuration 설정을 클라이언트의 app.config 혹은 web.config에 지정하고 나면 이제 ChannelFactory를 다음과 같이 생성할 수 있게 된다. ChannelFactory의 생성자에게 주어진 매개변수가 ServiceEndPoint 객체가 아닌 configuration의 endpoint 요소의 이름임을 유의하자.

ChannelFactory<ITimeService> factory = new ChannelFactory<ITimeService>("TimeService")

WCF 프록시를 사용한 WCF 클라이언트
예전부터 웹 서비스나 닷넷 리모팅을 이용한 클라이언트/서버 통신을 많이 해본 독자라면 지금까지 필자가 설명한 WCF 클라이언트 프로그래밍 모델에 상당한 불만을 갖게 될 것이다. WCF가 ASP.NET의 ASMX를 대체해 갈 것으로 예상되는 시점에서 클라이언트를 작성하는데 이렇게 많은(?) 코드를 작성해야 한다면 불만일 수 밖에 없을 것이다. 게다가, 앞에서 필자가 설명한 클라이언트 코드는 커다란 가정을 두고 있다. 그것은 바로 WCF 서비스에 대한 인터페이스, 즉 ITimeService 인터페이스에 대해서 서비스와 클라이언트가 모두‘알고 있어야 한다’는 점이다. 이를 위해 ITimeService 인터페이스는 서비스와 클라이언트가 모두‘참조’할 수 있는 별도의 어셈블리 내에 존재해야 하며, 더욱 좋지 못한 것으로 이 어셈블리를 클라이언트에 배포해야만 한다.

인터페이스 외에도 서비스의 종점이 사용하는 바인딩 역시 클라이언트가 미리 알고 있어야만 클라이언트 코드를 작성할 수 있게 된다. 기업의 인트라넷에서 WCF를 사용할 것이라면 모르겠지만, 인터넷에 무수히 널려 있을 수 있는 다양한 서비스에 대해 각 서비스가 어떤 바인딩을 사용하고 있는지, 서비스의 주소가 무엇인지 등을 알아내야 한다면 Service Oriented Architecture 와는 아주 거리가 먼 딴나라 이야기가 되고 말 것이다. WCF는 ASP.NET 웹 서비스처럼 WSDL을 제공하여 서비스의 인터페이스, 바인딩, 주소 등에 대해 설명하는 기능은 없을까? 또 wsdl.exe 와 같은 유틸리티가 있어서 클라이언트를 위한 프록시 코드를 생성해 주는 유틸리티는 없을까?

왜 없겠는가? WCF 도 서비스를 기술하기 위한 구조를 가지고 있고 역시 WSDL에 의존하고 있다. 한시라도 잊어서는 안 될 것이 WCF 역시 XML 기반이라는 점이다. WCF 에서는 서비스를 기술하는 요소로서 서비스 Description 이란 용어를 사용하고 있다. 서비스 Description은 서비스 호스트를 통해 접근할 수 있으며 클라이언트가 필요로 하는 서비스 종점에 대한 상세한 정보를 제공한다. 다음 예제 코드는 서비스 호스트 클래스(ServiceHost)가 제공하는 Description 속성으로부터 서비스의 종점 정보를 표시한다.

Console.WriteLine("Information of Hosted Services....");
int i=1;
foreach (ServiceEndpoint ep in _Host.Description.Endpoints)
{
  Console.WriteLine("Endpoint #{0}", i++);
  Console.WriteLine(" Contract: {0}", ep.Contract.Name);
  Console.WriteLine(" Address: {0}", ep.Address.Uri.ToString());
  Console.WriteLine(" Binding: {0}", ep.Binding.Name);
}

클라이언트가 서비스에 대한 정보를 얻고자 한다면 서비스의 URL 뒤에 wsdl 매개변수를 명시하면 된다. 필자의 예제라면 http://localhost:18080/WCFServices/TimeService?wsdl 이란 URL로서 서비스의 WSDL을 얻을 수 있다.

출처 : 마이크로소프트웨어 2006년 10월호