SimpleIsBest.NET

유경상의 닷넷 블로그

2006년 10월호 칼럼 ::  Beginning WCF

by 블로그쥔장 | 작성일자: 2007-02-28 오후 10:28:00
이 글은 오래된 전에 작성된 글입니다. 따라서 최신 버전의 기술에 알맞지 않거나 오류를 유발할 수 있습니다. 저자는 이 글에 대한 질문을 받지 않을 것입니다. 하지만 이 글이 리뉴얼 되면 이 글에 대한 질문을 하거나 토론을 할 수도 있습니다.

이 글은 월간 마이크로소프트웨어(일명 마소) 2006년 10월호 Inside Developer 칼럼에 기고한 글입니다. WCF의 기본적인 프로그래밍 모델에 대한 내용입니다. WCF에 관련하여 2006년 10월부터 현재(2007년 3월)까지 쭈욱 글을 쓰고 있습니다. 허접한 글이지만 아무쪼록 도움이 되길 바랍니다.

이 글의 원본은 .NET Framework 3.0 RC1 버전을 기준으로 되어 있습니다. RTM 버전에 의해 변경된 사항이나 보다 최신 정보가 있다면 별도의 "필자주"로 그 내용을 표시했습니다.

주) 이 글은 마소 편집부에 제가 발송한 원고를 약간 편집하여 올린 글입니다. 따라서 마소에서 지면관계상 삭제된 되거나 편집되는 부분이 있을 수 있으므로 실제 마소에 실린 글과는 약간의 차이가 있을 수 있습니다.


Beginning WCF

Basic Programming of Windows Communication Foundation

최근 회사 동료가 돌잔치를 했다. 돌잔치의 하이라이트는 누가 뭐래도 돌잡이 이다. 대개 돌잡이로 지폐, 공책/연필, 실 등이 사용되어 왔지만 요즘에 추가된 돌잡이로 마우스가 자주 등장한다. 빌 게이츠나 다른 IT 대박의 주인공이 되라고 마우스가 돌잡이로 등장했다고 한다. 그런데 돌잡이의 주인공 꼬마가 덜컥 마우스를 집은 것이다. 동료들은 그에게 우스개 소리로 "아들이 프로그래밍을 한다고 하면 시킬 거야?" 하고 물었다. 그 동료의 대답은 필자에게 매우 착잡한 느낌을 주는 것이었다. 그의 대답은......

"SI 한다고 하면 말릴래요."

우리나라 SI(System Integration) 업계에 종사하는 개발자들(프리랜서를 포함하여)은 정말 열악한 상황에서 프로그래밍이란 것을 하고 있다. 우리나라의 SI 프로젝트의 개발 기간은 상당히 짧다. 우리나라 SI 프로젝트에서 기간이란 것은 번갯불에 콩을 볶아 먹고 우물가에서 숭늉을 마신 다음에 이를 쑤실 정도의 빠른 개발을 요구하곤 한다. 덕택에 개발자들의 야근은 기본이요, 주말 반납은 선택이다. 야근 수당이나 휴일 수당은 꿈도 꾸지 말아야 한다. 쥐꼬리(?) 만한 월급을 받아 들고 나면 여기 저기서 들려오는 임금 인상해달라고 하는 파업은 다른 나라 얘기 같이 들릴 뿐이다.

야근에 절어 살다 보니 주말이나 휴일은 몰려오는 피곤함으로 인해 쿨쿨 잠만 잔다. 이러니 가족과 시간 보내기란 쉽지 않다. 이러다 보면 아이들에게서나 와이프(혹은 애인)에게 좋은 대접 받기 힘든 건 인지상정... 어디 집에서뿐인가? 이렇게 죽을둥살둥 개발을 완료해 놓고 나면 PM이나 고객에게 좋은 소리라도 듣는가? 어디에 버그가 있니 화면이 안 이뿌네, 기능이 맞지 않네, 요구 사항이 변했네 등등으로 입맛이 싹 가시게 하곤 한다. 정말 미칠 노릇이다. 아... 정말로 GG 치고 나오고 싶은 상황 아닌가?

프로젝트가 이렇게도 빡센데, 기술은 어찌나 그렇게 빠르게 변하는지... 배워야 할 것이 많기도 하다. IT 업계의 특성상 기술은 내일이 멀다 하고 자주 바뀌곤 한다. 경쟁에서 살아 남으려면 새로운 기술을 습득하고 그것에 적응해야 한다. 예전의 ASP와 VB 6.0에 안주하던 개발자와 IT 회사들이 지금 어떤 지경에 이르렀는지 굳이 말하지 않아도 사례를 손쉽게 찾아 볼 수 있다.

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

필자주: 닷넷 프레임워크 3.0의 RTM은 2006년 11월에 이미 출시된 상황이고 여기에 소개된 코드는 RTM 버전에서도 완전히 동일하게 작동함을 확인하였다. RTM 버전에 대한 테스트는 Windows Vista 환경이다.

WCF 기본 프로그래밍 모델

WCF는 닷넷 프레임워크 3.0에 포함된 닷넷 플랫폼의 차세대 통신 인프라이다. 지금까지 닷넷 플랫폼은 ASMX 기반의 웹 서비스와 닷넷 리모팅(Remoting)을 이용하여 클라이언트와 서버 사이의 통신을 해 왔다. 물론 이들은 앞으로도 계속 지원될 것이지만, 이들을 통합한 프로그래밍 모델을 가지고 있는 WCF가 앞으로 닷넷 플랫폼의 기본 통신 인프라가 될 것은 너무나도 자명하다. WCF는 기본 사상은 Microsoft가 전략적으로 밀고(?) 있는 웹 서비스 기반의 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) 사이의 관계를 보여주고 있다.

WCF 서비스와 종점(endpoint)
그림1. WCF 서비스와 종점(end-point)

WCF 서비스 프로그래밍

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

먼저 System.ServiceModel.dll 어셈블리를 참조에 추가하고 using 문을 사용하여 System.ServiceModel 네임스페이스를 사용하도록 하자. 그리고  리스트 1과 같이 서비스에 사용할 인터페이스(계약; Contract)를 정의하고 인터페이스에는 Contract 특성을, 메쏘드에는 OperationContract 특성을 명시해 준다. 이 두 특성을 명시할 때 추가적인 다양한 옵션을 줄 수 있지만 지면 관계상 인터페이스의 네임스페이스만을 명시하도록 했다.

[ServiceContract(Namespace = "http://simpleisbest.net/wcf/example/timeservice")]

public interface ITimeService

{

    [OperationContract]

    DateTime GetServerTime();

}

리스트1. WCF Contract 정의

계약이 정의되었다면 이제 이 인터페이스를 구현할 서비스 클래스를 작성해야 한다. 서비스 클래스는 단순히 앞서 정의한 ITimeSerivce 인터페이스를 구현하기만 하면 최소 조건을 만족한다.  리스트 2는 구현된 서비스 클래스를 보여주고 있다.

public class TimeService : ITimeService

{

    public DateTime GetServerTime()

    {

        return DateTime.Now;

    }

}

리스트2. WCF 서비스 클래스 구현

WCF 서비스 호스팅

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

// 서비스의 베이스 URL

Uri baseAddress = new Uri("http://localhost:18080/WCFServices/TimeService");

 

// 서비스를 구현하는 서비스 클래스를 서비스 호스트에 등록한다.

ServiceHost _Host = new ServiceHost(typeof(TimeService), baseAddress);

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

필자주: ServiceHost 객체 생성시 서비스 타입 혹은 서비스 인스턴스를 주는 것은 WCF의 서비스 인스턴스 관리에 해당되는 내용으로써 2006년 12월호 Inside Developer 칼럼에서 다루는 내용이다.

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

필자주: 서비스의 베이스 주소는 서비스에 대한 기본 정보를 표시하거나 WSDL을 HTTP 를 통해 수신하는데도 사용된다.

서비스 호스트를 생성했으면 이제 서비스에 사용될 종점을 명시해야 한다. 종점은 다음과 같이 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 을 확장한 표준인 WS-Security, WS-Routing, WS-AutomicTransaction 등을 모두 사용할 수 있기 때문에 세션 기능, 트랜잭션 기능 등을 모두 사용할 수 있다. 하지만 여전히 WSHttpBinding은 XML 기반으로 직렬화를 수행한다. 반면 NetTcpBinding은 트랜잭션, 세션 등의 WCF 기능을 거의 모두 사용할 수 있을뿐더러 바이너리 인코딩을 기본적으로 사용하므로 성능 상의 이점을 가지고 있다. 이외에도 P2P에 사용할 수 있는 NetPeerTcpBinding 과 MSMQ를 트랜스포트(transport)로서 사용하는 NetMsmqBinding 등이 제공된다. 대부분의 닷넷 요소들이 그러하듯이 필요에 따라 커스텀 바인딩을 작성할 수도 있다.

필자주: WS-Routing은 obsolete 되었다. 대신 WCF에서는 WS-Addresing이 사용된다.
필자주: NetTcpBinding이 바이너리 인코딩을 사용하지만 여전히 메시지는 XML 기반이다. 닷넷 리모팅처럼 바이너리 포매터(BinaryFormatter)를 사용하여 바이너리 직렬화를 수행하지 않음에 유의하도록 한다.

종점의 주소는 어떤 바인딩이 사용되는가에 따라서 달라진다. BasicHttpBinding 이나 WSHttpBinding은 http: 로 시작하는 URL 주소를 사용하지만 NetTcpBinding은 net.tcp: 으로 시작하는 주소를 사용해야 한다.

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

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();

    }

}

리스트3. WCF 서비스 및 호스트 구현

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

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

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

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

<?xml version="1.0" encoding="utf-8" ?>

<configuration>

<system.serviceModel>

    <services>

        <service name="WCFSampleService.TimeService">

            <endpoint contract="WCFSampleService.ITimeService"

                binding="wsHttpBinding" address="svc" />

        </service>

    </services>

</system.serviceModel>

</configuration>

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

WCF 서비스를 호스팅 하기 위해 반드시 별도의 어플리케이션(exe)이 필요하지는 않다. 닷넷 리모팅과 같이 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(WCFSampleService.ITimeService)),

    new BasicHttpBinding(),

    new EndpointAddress("http://localhost:18080/WCFServices/TimeService"));

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

ChannelFactory<WCFSampleService.ITimeService> factory =

                        new ChannelFactory<WCFSampleService.ITimeService>(endpoint)

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

class ClientProgram

{

    static void Main(string[] args)

    {

        // 서비스 endpoint 를 명세한다.

        ServiceEndpoint endpoint = new ServiceEndpoint(

            ContractDescription.GetContract(typeof(WCFSampleService.ITimeService)),

            new BasicHttpBinding(),

            new EndpointAddress("http://localhost:18080/WCFServices/TimeService"));

        // 주어진 endpoint 정보를 이용하여 ChannelFactory를 생성한다.

        using (ChannelFactory<WCFSampleService.ITimeService> factory =

                    new ChannelFactory<WCFSampleService.ITimeService>(endpoint)) {

            // ChannelFactory를 통해 채널을 생성한다.

            // 이 채널은 contract에 해당하는 인터페이스를 구현하고 있다.

            WCFSampleService.ITimeService svc = factory.CreateChannel();

            // 서비스를 호출한다.

            DateTime dt = svc.GetServerTime();

            Console.WriteLine("Result = {0}", dt);

        }

    }

}

리스트 4. WCF 클라이언트 코드

그렇다 간단하지 않다. 웹 서비스나 닷넷 리모팅으로 많은 프로젝트를 경험한 독자라면 항상 개발할 때 서버의 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<WCFSampleService.ITimeService> factory =

                        new ChannelFactory<WCFSampleService.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을 얻을 수 있다. 이를 통해 서비스가 어떤 인터페이스를 어떤 바인딩으로서 제공하는지 알 수 있는 것이다.

웹 서비스와 비슷하게 WCF도 클라이언트 프록시 코드 생성을 위한 유틸리티를 제공한다. 이 유틸리티는 svcutil.exe 이라는 것으로 wsdl.exe를 사용하는 것과 비슷하게 사용할 수 있다. 예를 들어, 필자의 예제 서비스에 대한 클라이언트 프록시를 생성하기 위해서는 다음과 같은 명령을 수행하면 된다.

svcutil.exe http://localhost:18080/WCFServices/TimeService?wsdl

svcutil.exe를 수행함으로써 프록시 클래스의 소스 파일(TimeService.cs)과 configuration에 추가할 WCF 목록이 담긴 파일(output.config)이 생성되게 된다. 프록시 소스 안에는 서비스의 계약으로 사용된 ITimeService 인터페이스를 비롯하여 채널을 구현해 놓은 TimeSerivceClient 클래스가 포함되어 있다.

개발자 노트

닷넷 프레임워크 3.0 RC1 은 이전 버전과 다르게 기본적으로 WSDL 생성이 막혀있다. 따라서 이 글의 예제처럼 svcutil.exe를 사용하면 WSDL을 얻을 수 없으므로 당연히 오류가 발생하게 된다.

이를 해결하는 방법은 configuration 이나 코드를 통해 막혀 있는 WSDL 생성을 풀어 주면 되겠다. 코드를 사용하는 방법은  리스트 3의 호스트 코드에서 Open 메쏘드 호출 전에 다음과 같은 코드를 삽입하면 된다.

ServiceMetadataBehavior metadataBehavior = new ServiceMetadataBehavior();

// HTTP GET에 의해 메타데이터(description)을 액세스 하도록 허용한다.

metadataBehavior.HttpGetEnabled = true;

_Host.Description.Behaviors.Add(metadataBehavior);

이제 서비스는 WSDL을 생성할 수 있게 된다. Configuration을 사용하는 방법 역시 어렵지 않다. 상세한 내용은 http://go.microsoft.com/fwlink/?LinkId=65455 을 참조하기 바란다.

반드시 WSDL이 있어야만 프록시를 생성할 수 있는 것은 아니다. Svcutil.exe 는 매개변수로서 URL 뿐만 아니라 어셈블리를 취할 수도 있다. 이 경우, 어셈블리 내에 정의된 WCF의 계약 인터페이스를 찾아서 필요한 XSD 를 생성해 준다. 그리고 이 XSD 는 다시 svcutil.exe의 입력으로 사용될 수 있어서, 프록시 코드를 생성하는데 사용할 수도 있다. 이러한 방법을 사용하면 굳이 WSDL을 open 하지 않더라도 프록시 코드를 작성할 수 있으며 이 프록시 코드를 컴파일 하여 클라이언트를 작성하는데 사용하면 된다.

인터넷 상에서 제공되는 서비스라면 WSDL을 공개하여 임의의 클라이언트의 호출을 허용하도록 할 수도 있으며 비공개 방식으로 선정된 몇몇 클라이언트들에게만 XSD를 ‘배포’하여 인터페이스를 공개할 수도 있는 것이다.

필자주: 기본적으로 WSDL을 HTTP GET을 통해 조회하지 못하도록 막아 놓은 것은 RTM 버전에서도 여전하다.

이 프록시를 사용하기 위해서는 먼저 configuration 파일에 output.config 의 내용을 붙여 넣던가 아니면 다음과 같이 간단한 endpoint 설정을 추가한다.

<endpoint name="SvcUtil_TimeService"

    contract="ITimeService"

    binding="basicHttpBinding"

    address="http://localhost:18080/WCFServices/TimeService" />

설정에서 주의할 부분은 contract 속성이 svcutil.exe 가 생성한 인터페이스의 정확한 이름이어야 한다는 점이다. 필자의 예에서 네임스페이스를 명시적으로 주지 않았기 때문에 달랑 ITimeService 란 값이 설정되어 있음에 유의하자.

이제 프록시를 사용하여 서비스를 호출하는 것은 매우 쉬워진다. 단순히 TimeServiceClient 클래스의 인스턴스를 생성하고 서비스를 호출하기만 하면 끝이다.

using (TimeServiceClient proxy = new TimeServiceClient()) {

    DateTime dt = proxy.GetServerTime();

    Console.WriteLine("Result = {0}", dt);

}

위 코드는 약간의 함축적인 의미를 담고 있다. TimeServiceClient의 인스턴스를 생성할 때 생성자(constructor)에 어떤 매개변수도 주지 않았다면, WCF 런타임은 configuration 에서 명시된 endpoint 들 중 contract가 TimeServiceClient 클래스를 정의할 때 사용된 ITimeService 인 것을 찾는다. 만약 이러한 endpoint 가 발견되지 않는다면 InvalidOperationException 이 발생하게 된다. 필자가 앞서 endpoint를 정의할 때 왜 contract 속성의 인터페이스 이름을 네임스페이스를 포함하여 명확하게 주어야 한다고 했는지 이제 이해가 갈 것이다.

svcutil.exe 가 생성해주는 프록시 클래스(TimeServiceClient)는 기본적으로 configuration에 의존하지만 서비스의 종점을 코드로서 명시하는데 필요한 EndPoint 속성을 제공하고 있다. 이 속성은 configuration을 아예 사용하지 않고 ServiceEndPoint 클래스 타입으로서 앞서  리스트 4에서 처럼 코드에 의해 서비스의 종점을 정의할 수도 있다. 혹은 다음과 같이 TimeServiceClient의 생성자에 직접 바인딩과 서비스의 주소를 명시할 수도 있다. 계약이 명시되지 않은 이유는 이미 TimeServiceClient 클래스가 계약에 사용된 인터페이스(ITimeService)와 연계가 되어 있기 때문이다. 이 경우에도 configuration은 필요하지 않다.

using (Svc.TimeServiceClient proxy =

    new Svc.TimeServiceClient(

        new WSHttpBinding(),

        new EndpointAddress("http://localhost:6250/WCF_IISHost/Service.svc")))

{

    DateTime dt = proxy.GetServerTime();

    Console.WriteLine("Result = {0}", dt);

}

Welcome to WCF Programming World

지금까지 부족하나마 WCF에 대한 프로그래밍 기초에 대해 살펴보았다. 이 글에서 다룬 WCF 프로그래밍에 대한 것은 극히 일부에 지나지 않는다. WCF는 독자들이 생각하는 것보다 훨씬 더 많은 기능을 가지고 있다. WCF는 필자의 오랜 염원 이였던 분산 트랜잭션을 위한 통신 인프라(지금까지는 유일하게 DCOM 하나 뿐이었다)로 사용될 수도 있으며, 아주 간단하게 P2P 프로그램을 작성하는데도 사용할 수도 있다. Visual Studio 2005를 이용하여 보다 편리하게 WCF 서비스 및 클라이언트 작성 방법도 존재하지만 지면관계상 다루지 못한 점을 독자들은 이해해 주기 바라는 바이다. Visual Studio 에 대해서 일언방구도 하지 않은 이유는 개발 도구에 의존하다 보면 내부적으로 어떻게 작동하는지에 대한 지식이 전혀 쌓이지 않기 때문에 내공을 쌓는데 도움이 되지 않는다는 필자의 개인적인 견해 때문이기도 하다.

어찌 되었건…… 마이크로소프트는 차세대 웹 서비스 프레임워크로서 WCF를 밀고 나갈 것이 분명하다. 이제 닷넷 프레임워크 3.0이 발표되면서 닷넷 어플리케이션 간의 통신의 표준으로서 WCF가 사용될 것도 너무도 자명하다. WS-Security 나 MTOM(Message Transmission Optimization Mechanism) 등 웹 서비스의 표준을 추가적으로 지원해 왔던 WSE(Web Service Extension)도 WCF가 등장함에 따라 3.0 버전을 마지막으로 더 이상의 기능 추가는 없을 것이라는 것이 이를 반증하고 있다. ASP.NET 기반의 XML 웹 서비스와 닷넷 리모팅 그리고 WSE의 장단점을 잘 고려하여 설계된 WCF는 더 이상 선택이 아닌 필수가 될 것이다. 이런 WCF에 눈길 한번 주는 센스를 발휘하는 것이 경쟁의 시대에 살아가는 닷넷 개발자의 현명한 선택이 아닐까 싶다. 필자 역시 가능한 자주 WCF에 대한 글을 쓰고자 한다. 왜냐면…… 재미있으니까…… 



Comments (read-only)
#re: 2006년 10월호 칼럼 ::  Beginning WCF / 눈꽃천사 / 2007-03-01 오후 1:19:00
감사히 잘봤습니다.
다음 글 기대하겠습니다.
손가락 풀러 거거~
#re: 2006년 10월호 칼럼 ::  Beginning WCF / 탱옹 / 2007-03-02 오후 4:22:00
역시 유수석님이십니다.

항간에 WCF를 설명하는 다른 글들과는 확실히 차별되어 있네요.

잘 읽겠습니다(응? 이 말은 아직 안 읽고 립흘 달고 있다는 거?)
#re: 2006년 10월호 칼럼 :: Beginning WCF / 지현사마 / 2007-04-02 오후 7:17:00
꺄아~~ 너무 좋아 프린트 할래~