VisualStudio/WCF

[WCF] AppConfig 설명

usingsystem 2023. 1. 3. 15:17
728x90

이전 강좌에서는 간단한 Hello World 서비스를 작성해 보았고 그에 관련된 사항들을 살짝 맛보기로 살펴보았다. 하지만 이전 컬럼에서 작성한 코드를 살펴보면 상당히 불만족스러운 사항을 발견한 독자들이 있을 것이다. 예전에 ASP.NET으로 웹 서비스와 그 클라이언트를 작성해 본 독자라면 더더욱 그러할 것이다. 필자가 예상할 수 있는 불만사항은 이렇다. 뭐가 그렇게 복잡하게 코딩이 많이 들어가는가와 서비스의 주소나 바인딩 설정 등 하드 코딩 된 부분이 많다는 점일 것이다. 서비스의 주소를 서비스와 클라이언트가 코드 상에 직접 하드코드 하여 명시한 것과 MessageEncoding 속성 역시 코드 상에서 고정시켰던 것을 상기하면 무엇이 문제인지 알 수 있을 것이다. ASP.NET 웹 서비스 개발 경험자에게 또 한가지 심각한 불만사항은 클라이언트 개발을 위해 서비스의 코드(서비스 어셈블리 DLL)를 "참조" 해야만 한다는 점과 클라이언트 프록시 코드를 개발자가 직접 코딩 해야 한다는 점이 심각한 불만사항일 수 있겠다.

Microsoft의 WCF 팀이 바보들이 아닌 이상 개발을 그렇게 하도록 놔두었을 리는 없다. WCF는 하드 코딩을 최소한으로 하기 위해 다양한 설정을 configuration 파일에 명시할 수 있도록 준비하고 있다. 또한, 클라이언트가 서비스의 인터페이스를 참조하거나 서비스 구현으로부터 독립할 수 있도록 서비스의 프록시를 생성해 주는 다양한 방법을 제공한다. 이제부터 이러한 사항을 살펴보는 WCF 맛보기 2부를 시작해 보겠다.

Configuration 사용

이전 컬럼에서 사용했던 서비스 코드를 다시 한번 살펴보자. [리스트 1]의 코드는 매우 잘 작동하며 버그도 없을뿐더러 코드 또한 매우 예쁘게(?) 작성되어 있다. 하지만 이 코드가 단순한 예제가 아니라 실제 업무나 제품의 코드라면 심각한 문제를 가질 수도 있다.

리스트 1. Hello World 서비스의 구현 코드 및 호스트 코드

//----- Service Implementation Code -----
using System.ServiceModel;

namespace HelloWorldService
{
    // 서비스 Contract 선언
    [ServiceContract]
    public interface IHelloWorld
    {
        [OperationContract]
        string SayHello();
    }

    // 서비스 타입 구현
    public class HelloWorldWCFService : IHelloWorld
    {
        public string SayHello()
        {
            return "Hello WCF World !";
        }
    }
}

//----- Service Host Code -----
namespace HelloWorldHost
{
    class Program
    {
        static void Main(string[] args)
        {
            ServiceHost host = new ServiceHost(typeof(HelloWorldWCFService),
                new Uri("http://localhost/wcf/example/helloworldservice"));

            host.AddServiceEndpoint(
                typeof(IHelloWorld),    // service contract
                new BasicHttpBinding(),    // service binding
                "");

            host.Open();
            Console.WriteLine("Press Any key to stop the service");
            Console.ReadLine();
            host.Close();
        }
    }
}

[리스트 1] 코드의 문제점은 하드 코딩이 많이 들어 있다는 점이다. 가장 문제가 될 만한 것은 서비스의 주소이다. Hello World 서비스가 매우 유명해 져서 이 프로그램이 여러 회사나 개인에게 판매되는 제품이 되었다고 가정해 보자(유명한 프로그램들이 단순한 예제에서 시작하곤 했다). 고객들은 이 서비스가 리스닝 하는 서비스의 주소가 /wcf/example/helloworldservice 인 것에 불만을 가질 가능성이 대단히 높다. 고객들은 이 주소를 수정하고 싶을 것이고 이를 위해서는 이 서비스의 소스 코드가 있어야 할 것이다. 물론 소스를 수정한 후에는 코드를 다시 컴파일 해야 할 것이므로 소스 코드뿐만 아니라 컴파일에 필요한 Visual Studio 프로젝트 파일과 코드에 서명을 했다면 서명에 필요한 키 파일 등도 필요할 지도 모른다. 고객에게 소스와 관련 파일들을 제공하여 알아서 고치라고 말하거나 고객 별로 서로 다른 EXE 바이너리를 만들어 배포하거나, 최후의 수단으로 배째고 들어 누워버리는 방법을 생각해 볼 수 있겠다.

위 코드의 또 한가지 문제점으로 지적할 수 있는 것으로 서비스 종점을 추가하거나 바인딩 속성을 바꾸기 위해서는 "또" 코드를 수정해야 한다는 점이다. 이전 컬럼의 코드 예제에서 NetTcpBinding을 사용하는 종점을 추가하는 코드를 본 적이 있다. 이처럼 종점을 추가하기 위해서는 코드를 수정해야 한다면 서비스 개발자가 서비스의 수행 환경 및 배포 시나리오를 미리 예측하여 코드를 작성해야만 한다는 얘기와 같다. 서비스의 개발이 완료된 시점에서는 새로운 배포 시나리오를 적용하기 어렵게 되고 다시 개발자가 코드를 수정해 주어야만 한다. 또한 서비스가 배포되고 설치된 이후에 바인딩 설정을 바꾸고자 하는 경우도 문제다. 서비스가 배포된 직후에는 텍스트 기반의 메시지 인코딩이 아무런 문제를 일으키지 않다가 추후 다량의 데이터를 전송해야 할 일이 자주 생기면서 MTOM 인코딩 을 바꾸어야 할 지도 모른다. 이 때 또 다시 코드를 수정하고 컴파일 해야 하며 배포 역시 다시 해야 할 것이다. 너무나도 당연하게 서비스의 주소가 바뀌거나 바인딩 속성이 변경되면 이 서비스를 호출하는 클라이언트 역시 수정되어야 할 것이다. 이전 컬럼에서 보았던 클라이언트 코드 역시 다수의 하드 코딩이 사용되고 있음을 상기하자. 개발자에겐 악몽이 시작되고 있는 거나 다름 없다.

만약 서비스와 클라이언트의 하드 코드를 줄이고 개발이 완료된 후에도 개발자가 아닌 시스템 관리자 혹은 운영자에 의해 서비스의 주소를 바꾸거나 바인딩을 추가 하거나 바인딩의 속성을 바꾸는 작업이 가능하다면 보다 유연한 웹 서비스 시스템을 구축할 수 있을 것이다. 특히 서비스의 주소나 바인딩 설정을 개발자가 아닌 시스템 운영자나 관리자가 변경할 수 있다는 것은 개발자의 부담을 줄여줄 수 있으며 보다 유연한 시스템 운영환경을 제공해 줄 수 있다.

WCF는 바로 그러한 요구 사항을 최대한 만족시켜줄 수 있도록 어플리케이션 configuration(web.config 혹은 app.config)을 통해 서비스와 클라이언트의 다양한 설정을 수행할 수 있는 능력을 가지고 있다. WCF와 더불어 어플리케이션 configuration에 추가된 새로운 섹션은 <system.serviceModel> 섹션이다. 이 섹션의 주요 하위 요소(element)들은 다음과 같다.

<?xml version="1.0" encoding="utf-8"?>
<configuration>
    <system.serviceModel>
        <services>
            <…List of Services>
        </services>
        <client>
            <…List of Endpoints>
        </client>
        <bindings>
            <…List of Bindings>
        </bindings>
    </system.serviceModel>
</configuration>

<services> 요소는 서비스 호스트에서 사용하는 것으로써 서비스가 호스팅 하는 서비스들이 어떤 것이 있고 이 서비스의 종점들에 대한 목록을 설정하는데 사용되며 <client> 요소는 클라이언트가 접속하고자 하는 서비스의 종점의 목록을 설정하는데 사용된다. 또한 <bindings> 요소는 여러 바인딩의 속성을 설정하는데 사용된다. 이외에도 서비스나 클라이언트의 작동 방식을 설정하는 등의 요소들이 존재하지만 이 장에서는 다루지 않을 것이며 구체적인 용도나 예제가 나올 때마다 다루기로 하겠다.

Configuration을 이용한 서비스 설정

이제부터 WCF의 configuration을 어떻게 적용하는지 구체적으로 살펴보도록 하겠다. 먼저 서비스 측에서 코드가 아닌 configuration을 통해 서비스 호스트를 어떻게 구성하는지 살펴보도록 하자.

WCF 서비스가 어떤 계약을 사용하며, 주소는 어떤 것을 쓰는지 그리고 바인딩은 어떠한지를 나타내기 위해서는 <system.ServiceModel> 섹션의 <services> 요소에 <service> 요소를 추가하면 된다. HelloWorldHost 프로젝트에 app.config 파일을 추가하고 다음과 같이 설정 내용을 추가해 보자.

리스트 2. HelloWorld 서비스를 위한 간단한 configuration 설정

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <system.serviceModel>
        <services>
            <service name="HelloWorldService.HelloWorldWCFService">
                <endpoint
                    contract="HelloWorldService.IHelloWorld"
                    address="http://localhost/wcf/example/helloworldservice"
                    binding="basicHttpBinding"
                />
            </service>
        </services>
    </system.serviceModel>
</configuration>

[리스트 2]는 HelloWorldService.HelloWorldWCFService 클래스를 서비스 타입으로 하는 서비스를 추가하는 설정으로써 이 서비스의 종점이 HelloWorldService.IHelloWorld 인터페이스를 계약으로서 사용하며 서비스 주소는 http://localhost/wcf/example/helloworldservice 를, 바인딩으로 BasicHttpBinding을 사용하고 있음을 서비스 런타임에게 알리고 있다. 여기서 서비스의 이름, 즉 <service> 요소의 name 속성이 나타내는 이름이 매우 중요하다. ServiceHost 클래스의 인스턴스가 생성될 때 매개변수로 주어진 서비스 타입과 configuration에 명시된 여러 <service> 요소들 중 서비스 타입이 일치하는 설정을 읽어 들여 ServiceHost 객체를 초기화하기 때문이다. 따라서 name 속성은 반드시 설정하고자 하는 서비스 타입의 네임스페이스를 포함하는 전체 이름을 명시해야 한다. [리스트 1]에서 구현한 서비스 타입의 네임스페이스와 클래스 이름이 HelloWorldService.HelloWorldWCFService 임을 상기할 필요가 있다.

또한 [리스트 2]는 이 서비스의 종점에 대한 설정도 포함하고 있다. 서비스에 추가되는 서비스 종점이 사용하는 서비스 계약과 주소 그리고 바인딩이 모두 명시되어 있음에 주목하자. 주의할 점은 binding 속성에 사용된 바인딩의 이름이 BasicHttpBinding이 아닌 basicHttpBinding(소문자로 시작함에 유의) 이라는 점이다. WCF는 바인딩들에 대한 configuration 설정을 기본적으로 가지고 있으며 BasicHttpBinding 에 대한 설정 역시 미리 준비되어 있다. 이렇게 미리 준비된 바인딩의 카테고리의 이름이 basicHttpBinding 이며 소문자로 시작하는 이름을 선호하는 XML의 특성 때문에 basicHttpBinding 이란 이름을 사용하는 것이다.

[리스트 2]는 코드를 통해 서비스를 설정하는 것과 동등한 작업을 수행한다. 즉, [리스트 2]는 다음과 동등한 설정을 수행한다고 보면 되겠다.

ServiceHost host = new ServiceHost(typeof(HelloWorldWCFService));

host.AddServiceEndpoint(
    typeof(IHelloWorld),
    new BasicHttpBinding(),
    "http://localhost/wcf/example/helloworldservice");

그렇다면 [리스트 2]와 같은 설정이 언제 WCF에 의해 읽혀 들여지고 효과를 나타내게 되는 것일까? 앞서 언급한바 대로 ServiceHost 클래스의 인스턴스가 생성되면 이 클래스는 configuration 파일의 <services> 요소의 하위 <service> 요소들 중에서 ServiceHost 클래스의 생성자에 주어진 서비스 타입을 찾는다. 만약 ServieHost 객체가 이 타입을 <services> 내에서 찾게 되면 발견한 <service> 요소의 설정들을 이용하여 ServiceHost를 초기화를 수행한다. 즉, 서비스에 대한 설정이 읽혀 들여지고 사용되는 시점은 ServiceHost 객체의 생성자가 호출되는 때인 것이다. 따라서 방금 보인 위 코드를 [리스트 2]와 함께 사용하면 InvalidOperationException 예외가 발생하게 된다. 이유는 간단하다. Configuration에서 이미 종점이 선언되었지만 코드에 의해 동일한 서비스 주소를 사용하는 종점을 추가하려고 시도했기 때문이다. 이러한 오류를 피하기 위해서는 AddServiceEndpoint 메쏘드 호출을 코드 상에서 제거해야 한다.

이제 configuration을 설정하는 방법과 적용하는 방법을 간단히 살펴보았으므로 이전 컬럼에서 2개의 서비스 종점을 사용하는 예제와 동등한 설정을 configuration과 코드를 통해 작성하는 예제를 살펴보도록 하자. 다음의 [리스트 3]은 이전 컬럼에서 예제로 보였던 서비스 호스트 코드의 일부이다.

리스트 3. 2개의 서비스 종점을 사용하는 호스트 코드

static void Main(string[] args)
{
    ServiceHost host = new ServiceHost(typeof(HelloWorldWCFService),
        new Uri("http://localhost/wcf/example/helloworldservice"),
        new Uri("net.tcp://localhost/wcf/example/helloworldservice"));

    host.AddServiceEndpoint(
        typeof(IHelloWorld),    // service contract
        new BasicHttpBinding(),    // service binding
        "");            // relative address
    host.AddServiceEndpoint(
        typeof(IHelloWorld),    // service contract
        new NetTcpBinding(),    // service binding
        "");            // relative address

    host.Open();
    Console.WriteLine("Press Any key to stop the service");
    Console.ReadKey();
    host.Close();
}

[리스트 3]에서는 서비스 호스트에 베이스 주소를 사용했으며 BasicHttpBinding 과 NetTcpBinding을 각각 사용하는 2개의 서비스 종점을 사용하고 있다. 이러한 코드를 configuration을 통해 설정하면 [리스트 4]와 같다.

리스트 4. 두 개의 서비스 종점을 사용하는 서비스의 configuration

<configuration>
    <system.serviceModel>
        <services>
            <service name="HelloWorldService.HelloWorldWCFService">
                <host>
                    <baseAddresses>
                        <add baseAddress="http://localhost/wcf/example/helloworldservice"/>
                        <add baseAddress="net.tcp://localhost/wcf/example/helloworldservice"/>
                    </baseAddresses>
                </host>
                <endpoint contract="HelloWorldService.IHelloWorld"
                    address=""
                    binding="basicHttpBinding"
                />
                <endpoint contract="HelloWorldService.IHelloWorld"
                    address=""
                    binding="netTcpBinding"
                />
            </service>
        </services>
    </system.serviceModel>
</configuration>

[리스트 4]에서 관심을 가질 부분은 두 부분이다. 첫째로 <service> 요소에 새로이 등장한 요소로서 호스트의 베이스 주소를 명시하는 <baseAddresses> 요소와 서비스를 Open 하거나 Close 할 때의 타임아웃을 설정하는 <timeouts> 요소를 명시할 수 있다. [리스트 3]에서 서비스의 베이스 주소를 2개 명시했기 때문에 [리스트 4]에서도 2개의 베이스 주소를 설정으로써 명시했다. 두 번째로 관심을 가질 부분은 <endpoint> 요소를 2개 사용하여 서비스가 두 개의 서비스 종점을 사용하도록 설정한 부분이다. 코드를 사용할 때는 2회의 AddServiceEndpoint 메쏘드 호출을 수행한 반면 configuration을 사용할 경우에는 2개의 <endpoint> 요소를 사용하면 된다.

이제 [리스트 4]의 configuration을 사용하는 서비스 호스트 코드인 [리스트 5]를 살펴보도록 하자.

리스트 5. Configuration을 사용하는 서비스 호스트 코드

ServiceHost host = new ServiceHost(typeof(HelloWorldWCFService));

host.Open();
Console.WriteLine("Press Any key to stop the service");
Console.ReadKey();
host.Close();

[리스트 4]의 설정을 사용하는 코드는 당황스러울 정도로 간단하다. 서비스들의 베이스 주소를 설정하거나 AddServiceEndpoint 메쏘드를 호출하여 서비스 종점을 추가하는 코드는 사라지고 달랑 ServieHost 클래스의 인스턴스를 생성하고 호스트에 대해 Open 메쏘드와 Close 메쏘드를 호출하는 것이 전부인 코드가 되어 버렸다. [리스트 5]에서 ServiceHost 객체가 생성되는 시점에서 WCF 런타임은 configuration 파일을 참조하게 되고 configuration 에서 서비스 타입인 HelloWorldWCFService 클래스의 이름을 찾게 된다(네임스페이스를 포함하는 전체 이름을 찾는다). 물론 [리스트 4]에서 해당 서비스의 설정이 존재하므로 이 설정을 읽어 들여 필요한 베이스 주소 설정과 종점 추가 작업이 내부적으로 일어나게 된다. 이 때문에 추가적인 코드를 사용하여 종점을 설정할 필요가 없어진 것이다.

한 발만 더 나아가 보자. 앞서 바인딩을 설명할 때 바인딩의 속성을 설정할 수 있다고 했었다. 그리고 구체적인 예제로서 BasicHttpBinding 의 MessageEncoding 속성을 MTOM으로 설정한 예제코드를 보였었다. 다음 코드처럼 말이다.

ServiceHost host = new ServiceHost(typeof(HelloWorldWCFService));
BasicHttpBinding binding = new BasicHttpBinding();
binding.MessageEncoding = WSMessageEncoding.Mtom;

host.AddServiceEndpoint(
    typeof(IHelloWorld),    // service contract
    binding,            // service binding
    "http://localhost/wcf/example/helloworldservice");
......

이와 동등하게 configuration을 통해서도 바인딩의 속성을 설정할 수는 없을까? 왜 없겠는가? [리스트 4]에서 사용한 <endpoint>의 binding 속성은 사용할 바인딩의 종류만을 표시한 것일 뿐이다. 다시 말해 [리스트 4]에서 사용한 <endpoint> 요소를 코드로 이야기 하자면 바인딩 객체(이 경우 BasicHttpBinding 객체)를 생성하고 아무런 설정 없이 그대로 사용한 다음 코드와 같다는 말이다.

host.AddServiceEndpoint(
    typeof(IHelloWorld),
    new BasicHttpBinding(),
    "http://localhost/wcf/example/helloworldservice");

따라서 바인딩 속성은 바인딩 객체가 제공하는 기본값만을 사용하는 것이 된다.

Configuration 상에서 바인딩 객체의 속성을 설정하고자 하면 <bindings> 요소를 사용해야 한다. 이 요소는 각 바인딩 종류별로 바인딩에 대한 속성 설정을 지정하고 나열할 수 있으며 이렇게 나열된 바인딩 설정 중 하나를 <endpoint> 에서 참조하도록 할 수 있다. 다음은 구체적인 <bindings> 요소의 예제를 보여준다.

<system.serviceModel>
    <bindings>
        <basicHttpBinding>
            <binding name="MtomSetting" messageEncoding="Mtom" />
            <binding name="Others" ...... />
        </basicHttpBinding>
    </bindings>
    <services>
        ......
    </services>
</system.serviceModel>

<bindings> 요소 내에 <basicHttpBinding> 요소를 사용하여 BasicHttpBinding에 대해 적용되는 바인딩 설정들을 별도로 모아두고 있음을 알 수 있을 것이며 <basicHttpBinding> 하위에 <binding> 요소를 여러 개 두고 각 바인딩 설정의 구분은 name 속성을 이용하고 있음에도 주목하기 바란다.

<bindings> 요소에 나열된 다양한 바인딩 속성 설정들은 바인딩 속성을 설정하는 템플릿들에 지나지 않는다. 이렇게 나열된 템플릿들 중에서 하나를 실제 바인딩 객체가 사용하도록 지시할 필요가 있다. 이 때 사용되는 것이 <endpoint> 요소의 bindingConfiguration 속성이다. bindingConfiguration 속성에 <bindings> 내에 나열된 바인딩 설정의 이름을 명시하면 해당 바인딩 설정에 나열된 다양한 속성(이 경우, messageEncoding 설정밖에 없지만)이 바인딩 객체에 적용된다. 적용되는 시점은 종점이 서비스 호스트에 추가되는 시점이며 이 시점은 ServiceHost 클래스의 인스턴스가 만들어지는 시점과 동일하다.

<endpoint contract="HelloWorldService.IHelloWorld"
    address=""
    binding="basicHttpBinding"
    bindingConfiguration="MtomSetting" />

완전한 configuration 파일의 모습은 [리스트 6]과 같다. 이 설정을 사용하면 Hello World 서비스는 MTOM 메시지 인코딩을 사용하는 BasicHttpBinding 기반의 종점과 NetTcpBinding을 사용하는 종점을 갖는 서비스가 될 것이다.

리스트 6. Binding 설정을 수행하는 Configuration

<configuration>
    <system.serviceModel>
        <bindings>
            <basicHttpBinding>
                <binding name="MtomSetting" messageEncoding="Mtom" />
            </basicHttpBinding>
        </bindings>
        <services>
            <service name="HelloWorldService.HelloWorldWCFService">
                <host>
                    <baseAddresses>
                        <add baseAddress="http://localhost/wcf/example/helloworldservice"/>
                        <add baseAddress="net.tcp://localhost/wcf/example/helloworldservice"/>
                    </baseAddresses>
                </host>
                <endpoint contract="HelloWorldService.IHelloWorld"
                    address=""
                    binding="basicHttpBinding"
                    bindingConfiguration="MtomSetting"
                />
                <endpoint contract="HelloWorldService.IHelloWorld"
                    address=""
                    binding="netTcpBinding"
                />
            </service>
        </services>
    </system.serviceModel>
</configuration>

이제 마음을 가라 앉히고 좀 정리를 해보자. [리스트 4]과 [리스트 5]와 같은 코드를 사용하면 앞서 언급했던 하드 코드 문제와 관리적인 편의를 올릴 수 있을까? 그렇다. 컴파일 된 코드 안에는 하드 코딩 된 부분이 없으며 configuration 파일 내에서 서비스의 베이스 주소를 수정한다던가 종점에서 상대 주소(relative address)를 수정하여 서비스 주소를 수정할 수 있을 것이다. 그 뿐인가? 필요에 따라 종점을 추가하는 것 역시 코드를 전혀 수정하지 않고 <endpoint> 요소만 추가하면 된다. 또한 바인딩에 적용할 다양한 속성이 바뀌었을 때 코드를 수정하기 보다는 configuration에서 바인딩 설정을 변경하는 방법으로 코드에 대한 종속성을 없앨 수 있었다. 어떤가? 이젠 보다 유연한 설정이 가능한 Hello World 웹 서비스가 탄생하지 않았나?

 

출처 - http://taeyo.net/columns/View.aspx?SEQ=341&PSEQ=23&IDX=5

728x90