본문 바로가기

IT 살이/04. 기술 - 프로그래밍

개발 프레임워크 만들기 대장정 28 - Spring.NET의 Web Services 지원

Spring.NET IoC 컨테이너나 Spring.NET이 지원하고 있는 AOP 프로그래밍에 대해서 아직 해야할 얘기는 남아있다. 객체 타입 즉 singleton, prototype으로 설정하는 방법 및 객체의 생명주기에 대한 얘기, Attribute를 이용해서 AOP를 구현하는 방식등등. 필요한 얘기이기는 하지만 나중에 하기로 하자. 우선 전체적인 애플리케이션을 구성하는 구성 기술들을 Spring.NET이 어떻게 지원하는지를 알아본다.


■ Spring.NET 웹 서비스 구조


먼저 Spring.NET이 .NET의 Web Services를 어떻게 보완, 지원해주는지 알아보자. Visual Studio.NET 또는 WSDL 커맨드 툴을 이용해서 클라이언트측 프락시를 만들어서 웹 서비스를 사용했던 기존의 구조는 다음처럼 표현할 수 있겠다.

Spring.NET 팀에서는 이 구조가 문제가 있었다고 봤다는 거다. 문제라기 보다는 좀 더 효율적인 구조로 가고 싶었다는 것이다.

기존의 구조에서는 클라이언트측에서 생성되는 프락시가 곧바로 클래스로 구현되었다는 것이다. WSDL기반의 클래스이다. Spring.NET 팀에서는 프락시가 곧바로 클래스로 구현되는 대신에 서비스 인터페이스를 구현하는 구조로 가기를 원했었다. 그렇게 가면 추후에 웹 서비스 구현체의 수정을 좀 더 유연하게 할 수 있다는 것이다.

그림에서 "웹 서비스용 클래스"란 WebService, WebMethod같은 어트리뷰트를 가지고 있는 클래스를 말한다. 즉 일반 클래스에 웹 서비스 목적의 어트리뷰트가 섞인 상태이다.

Spring.NET 팀에서는 다음과 같은 구조의 웹 서비스 구조를 구현했다.

우선 서버측을 보면 "PONO"라는 것이 있다. 이것은 "plain old .NET object" 약자인데, 일반 클래스를 말한다. 자바 버전의 Spring에서는 POJO라는 용어를 사용하고 있다. 앞의 그림의 "웹 서비스용 클래스"라는 용어와 대비된다. 웹 서비스용 클래스에는 웹 서비스 노출에 필요한 어트리뷰트가 섞인 웹 서비스 전용 클래스이다. 그러나 PONO는 사용자 정의의 일반 클래스 객체이다. 즉 Spring.NET 웹 서비스에서는 웹 서비스로 노출시키기 위한 특별한 클래스가 존재하는 것이 아니라 일반 타입의 객체를 사용하고 있다는 것이다. 이 PONO는 웹 서비스로 노출될 수 있지만, 코드를 수정하지 않고 그대로 .NET Remoting, Enterprise Service(COM+)에 사용될 수 있다는 것을 Spring.NET 팀에서는 목표로 했다는 것이다.

그러나 웹 서비스로 노출될 PONO는 서비스 인터페이스를 정의하고 그것을 구현하고 있어야 한다. 만약 웹 서비스로 노출시키고 싶은 메소드와 .NET Remoting으로 노출시키고 싶은 메소드가 다르다면 다른 인터페이스를 사용하면 된다. 여러 개의 인터페이스를 구현하는 PONO에서 어떤 메소드를 노출시킬지를 인터페이스를 선택함으로써 변경할 수 있다.

그러나 Spring.NET이 웹 서비스 인프라를 완전히 새롭게 구현한 것은 아니다. 이 말은 웹 서비스 타겟용 객체로 PONO를 사용해도 결국은 WebService 또는 WebMethod 같은 어트리뷰트로 꾸며진 웹 서비스용 클래스를 만들어내야 한다는 것이다. 그림에 표현된 서버측 프락시는 이렇게 만들어진 최종 웹 서비스용 클래스이다. 클라이언트측에서는 이 서버측 프락시를 바라보게 된다.

다음은 클라이언트측 프락시 구조에 대해서 알아보자. 클라이언트측에서는 여전히 WSDL 기반의 프락시를 사용한다. 이것은 여전히 클래스로 구현된다. 그러나 서비스 인터페이스를 상속해서 또다른 프락시를 만들어 내고 있다. 해서 Spring.NET이 만들어내는 프락시는 프락시의 프락시( proxy for proxy )인 셈이다. 클라이언측 코드에서는 이 두번째 프락시 객체를 참조하게 된다. 서버측 및 클라이언트측의 프락시는 실행시 동적으로 만들어진다.

최종적으로 이런 구조를 만들어내기위해서 Spring.NET에서는 서버측과 클라이언트측에서 프락시 객체를 만들어내는 객체를 제공한다. 이 객체들은 팩토리 패턴을 이용해서 프락시 객체를 만들어낸다.

서버측 프락시 생성 객체 : WebServiceExporter, 클라이언트측 프락시 생성 객체 : WebServiceProxyFactory


■ Spring.NET의 프락시용 팩토리 객체


▷ WebServiceExporter

이 타입은 Spring.Web.Services 네임스페이하에 정의되어 있다. 서버측 프락시 생성 객체의 타입 이름을 보면 "웹 서비스를 노출"시키는 기능을 하는 객체라는 것을  표현하고자 한 것 같다. 이것은 PONO를 래핑하고 있는 서버측 프락시 객체를 웹 서비스로 노출시키는 객체로 해석될 수 있겠다. PONO 객체에 대한 정보를 받아들여서 클라이언트에 노출되는 웹 서비스용 클래스를 만들어내는데, 이 결과물이 서버측 프락시 객체이다.

▷ WebServiceProxyFactory

이 객체는 WSDL 기반의 프락시 클래스를 만들고 다시 서비스 인터페이스 정보를 이용해 클라이언트에서 사용할 웹 서비스 프락시 객체를 만들어낸다. (WebServiceProxyFactory를 사용하면 웹 서비스 메소드에서 반환하는 사용자정의 타입도 클라이언트측에 자동 생성해줄 수 있는 건가? 이것은 나중에 해봐야 겠다. )

런타임시 Spring.NET은 이 두 객체를 이용해서 앞의 그림 같은 최종 구조를 만들어내는 것이다. 런타임시의 웹 서비스 최종 구조가 생성되는 과정과 실행 구조를 보면 다음과 같다.

클라이언트 코드에서는 WSDL 정보와 서비스 인터페이스에 대한 정보를 WebSeriviceProxyFactory에 전네주고 프락시 객체를 요청한다(1번 절차). 팩토리 객체는 전달받은 정보를 이용해서 프락시 객체를 동적으로 생성해서 건네준다(2번절차). 이때 WSDL 정보는 노출된 웹 서비스에 접근할 수 있는 URL 형태로 주면 된다. 이 과정이 프로그램적으로 될 수도 있겠지만 설정을 이용할 수도 있다. 그래서 WebServiceProxyFactory를 <object/>로 설정하고 이 객체를 IoC 컨테이너에 요청하게 되면 이 팩토리 객체 자체에 대한 참조가 반환되는 것이 아니라 이 팩토리 객체가 생성한 객체 즉 클라이언트측 프락시에 대한 참조가 반환된다.

클라이언트 코드에서는 IoC 컨테이너에서 절달받은 프락시 객체를 통해서 서버측으로 HTTP 요청을 보낼 수 있다(3번 절차). 이 요청은 일반 ASP.NET 웹 서비스 처리 핸들러가 받는 것이 아니라 Spring.NET에서 제공하는 핸들러가 받도록 설정을 변경해야 한다. Spring.NET의 웹 서비스 핸들러는 IoC 컨테이너에게 PONO 정보를 건네주면서 WebSerivceExporter 객체를 요청한다(4번절차). IoC 컨테이너는 이 요청에 대해서 WebServiceExporter객체를 직접 보내주는 것이 아니라 PONO를 이용해서 웹 서비스용 클래스를 동적으로 만들어내서 핸들러에게 반환해준다(5번 절차). 핸들러는 클라이언트에서 요청한 서비스를 프락시에 요청하고 나서(6번 절차) 그 반환값을 이용해서 HTTP 반응을 만들어서 클라이언트 프락시로 보낸다(7번 절차). 클라이언트측 프락시는 결과를 클라이언트 코드로 넘겨주게 되고 이로써 한 사이클의 웹 서비스 요청/반응이 끝나게 된다. 휴 ~~.

따라서 객체들의 참조 구조를 간단하게 정리하면 다음과 유사한 구조로 되겠다.


■ 서버측에서 PONO를 웹 서비스로 노출하기


앞에서 팩토리 객체들을 프로그램적으로 호출해서 프락시 객체에 대한 참조를 얻을 수도 있지만, 설정을 통해서도 가능하다고 했다. 이 포스트에서는 설정을 통해서 얻는 방법을 알아본다. 왜냐고? 첫번째는 내맘이고, 둘째는 설정을 통한 방법에 익숙해지는 것이 IoC 컨테이너에 대한 개념이 좀 더 빨리 그려질 수 있을 것 같아서이다. 세번째는 설정을 통한 방법을 이해하면 API를 이용하는 방법은 혼자서도 할 수 있다(아닌가).

<objects xmlns="http://www.springframework.net">

  <object id="helloWorld" type="HelloWorldApp.HelloWorldService, HelloWorldApp">

    <property name="Message" value="안녕하세요!"/>

  </object>

  <object id="HelloWorldService" type="Spring.Web.Services.WebServiceExporter, Spring.Web">

    <property name="TargetName" value="helloWorld"/>

    <property name="Namespace" value="http://MySpringSample_WS/HelloWorldService"/>

  </object>

</objects>


<system.web>

  <httpHandlers>

    <add verb="*" path="*.asmx"

        type="Spring.Web.Services.WebServiceHandlerFactory, Spring.Web"/>

  </httpHandlers>

  <httpModules>

    <add name="Spring" type="Spring.Context.Support.WebSupportModule, Spring.Web"/>

  </httpModules>

</system.web>

첫번째 <object/>는 웹 서비스로 노출될 PONO 객체에 대한 정보이다. HelloWorldApp 네임스페이 아래에 HelloWorldService라는 이름의 클래스를 웹 서비스의 타겟 객체로 지정하고 있다. 이 객체에 대한 정의는 다음과 같다. 

namespace HelloWorldApp

{

    public interface IHelloWorld

    {

        string HelloWorld();

    }


    public class HelloWorldService : IHelloWorld

    {

        private string message;

        public string Message

        {

            set { message = value; }

        }


        public string HelloWorld()

        {

            return this.message;

        }


        public string SayNo()

        {

            return "No";

        }

    }

}

웹 서비스로 노출될 PONO는 서비스 인터페이스를 정의해야 한다고 했다. 코드에서는 IHelloWrold라는 인터페이스를 정의해서 구현하고 있다. 이 인터페이스는 PONO 객체의 메소드중에서 웹 서비스로 노출시킬 메소드를 지정하는 역할을 하기도 한다. 이 HelloWorldService에는 두 개의 메소드가 있지만, 이 코드에서는 메소드 HelloWorld()만을 노출시키고 있는 것이다. 설정에서는 PONO 객체 HelloWorldService를 id를 "helloWorld"로 지정하고 있다.

다음 <object/>에는 이 PONO에 대한 웹 서비스용 프락시 클래스를 만들어 낼 WebServiceExporter에 대한 정의가 있다. 어떤 PONO에 대한 프락시를 생성할지를 첫번째 <property/>의 TargetName으로 표현하고 있다. 이 설정에서는 앞에서 설정한 HelloWorldService에 대한 id를 지정해주고 있다. 설정에서는 노출될 웹 서비스의 네임스페이스도 지정해주는 설정이 있다.


■ 웹 서비스 요청에 대한 Spring.NET Http Handler 지정하기


Spring.NET은 웹 서비스에 대한 요청을 핸들링하는 사용자 정의 핸들러를 제공하고 있다: Spring.Web.Services아래에 있는 WebServiceHandlerFactory 이다. 앞의 설정에서는 클라이언트로부터의 asmx 파일에 대한 요청이 오면, ASP.NET가 제공하는 웹 서비스 핸들러 대신에 Spring.NET이 제공하는 WebServiceHandlerFactory가 작동하도록 하고 있다.

클라이언트에서 올라오는 asmx에 대한 HTTP요청에는 어떤 WebServiceExporter에 대한 요청인지를 알 수 있는 정보가 있다.

클라이언트에서 오는 HTTP 요청은 다음과 같은 형식의 URL을 갖는다. 

http://서버/~/서비스명.asmx

서비스명 : 서버측에 지정된 WebServiceExporter 객체의 id

앞의 요청 URL의 형식에서 "서비스명"에 해당하는 값이 web.config에서 지정된 WebServiceExporter의 id에 해당한다. asmx에 대한 요청을 받은 핸들러 WebServiceHandlerFactory는 요청 URL로부터 WebServiceExporter에 대한 정보를 얻고 IoC 컨테이너에 이 객체를 요청한다. IoC는 WebServiceExporter에 설정된 PONO 객체에 대한 정보를 통해서 프락시 객체를 생성해서 핸들러에게 건네줄 것이다. 이제 SOAP 프락시로부터 호출할 메소드, 메소드에 전달할 인자에 대한 정보를 구해서 프락시는 요청한 서비스 메소드를 호출하고 결과를 클라이언트에 보내주면 된다.


■ 클라이언트측의 WebServiceProxyFactory 객체 설정하기

이제 클라이언트에서의 WebServiceProxyFactory에 대한 설정을 보자. 서비스 인터페이스에 대한 정보와 노출된 웹 서비스에 대한 URL을 팩토리 객체에 설정하면 된다.

\par ?? \par ?? \par ??} -->

<object id="helloWorldService"

    type="Spring.Web.Services.WebServiceProxyFactory, Spring.Services">

  <property name="ServiceUri" value="http://localhost/HelloWorldWeb/helloworldservice.asmx"/>

  <property name="ServiceInterface" value="HelloWorldApp.IHelloWorld, HelloWorldApp"/>

</object>

이 설정에서는 helloworldservice.asmx를 ServiceUri 속성에 지정하고 있다. 이 값은 앞에서도 말한것처럼 서버측에 가서는 helloworldserivce라고 지정된 WebServiceExporter 객체를 찾는데 사용된다. 그리고 사용할 서비스 인터페이스로는 IHelloWorld로 지정하고 있다.

이제 이 팩토리 객체를 이용하는 코드이다.

IApplicationContext ctx = ContextRegistry.GetContext();


IHelloWorld helloWorld = (IHelloWorld )ctx.GetObject("helloWorldSerivce");

helloWorld.HelloWorld();


■ Spring.Calculator.Web.2005  예제 보기


앞 포스트에서도 보았지만 Spring.Calculator.Web.2005 프로젝트를 실행하면 default.aspx 페이지가 실행된다. 

default.aspx 페이지를 보면 다음과 같다.

<td align="center">

  <h2>

    <a href="calculatorService.asmx">CalculatorService</a>

  </h2>

  <br />

  <h2>

    <a href="calculatorServiceWeaved.asmx">CalculatorServiceWeaved</a>

  </h2>

</td>

물론 Spring.Calculator.Web.2005 프로젝트를 보면 asmx 웹 서비스 페이지는 없다.

이 예제에서는 웹 서비스의 클라이언트측 프락시는 사용하지 않고 있다. ASP.NET에서 제공하는 테스트 페이지가 클라이언트 코드 역할을 한다.

이 예제는 서버측의 WebServiceExporter만을 설정해서 사용하고 있는데, web.config를 보면 다음과 같은 설정이 있다. 

<context>

  <resource uri="config://spring/objects"/>

  <resource uri="~/Config/webServices.xml"/>

  <resource uri="~/Config/webServices-aop.xml"/>

</context>

설정이 webServices.xml과 webServices-aop.xml에 분리되어 있다. 설정을 이런식으로 분리할 수 있다는 것도 유용한 정보일 것이다. 이 두 파일을 보면 그곳에 서버측 프락시 팩토리에 대한 설정이 있다. 이 중에서 webServices-aop.xml의 내용이다.

<objects xmlns="http://www.springframework.net">


  <description>webServices-aop</description>


  <object id="calculatorServiceWeaved" type="Spring.Web.Services.WebServiceExporter, Spring.Web">

    <property name="TargetName" value="calculatorWeaved" />

    <property name="Namespace" value="http://SpringCalculator/WebServices" />

    <property name="Description" value="Spring Calculator Web Services" />

  </object>


</objects>

id가 calculatorServiceWeaved  로 되어 있고, TargetName 속성은  calculatorWeaved 으로 되어 있다.  이 속성이 참조하고 있는 calculatorWeaved 는 web.config에 정의되어 있는 객체이다.  이제 클라이언트 코드에서 calculatorWeaved가 참조하고 있는 객체에 대한 메소드를 호출하면 서버측에서 처리할 수 있다.