본문 바로가기

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

Soap 익스텐션(SoapExtension)을 고한다.

클라이언트와 서버와의 통신에 웹 서비스를 사용하고 있는 스마트클라이언트 애플리케이션을 제작하면서 처음 Soap 익스텐션이라는 것을 알게 되었다. 이 녀석을 사용하면 개발자의 코드를 거의 수정없이 웹 서비스 기능을 추가, 확장활 수 있게 된다.  Soap 익스텐션은 클라이언트측에서는 웹 서비스에 대한 프락시 클래스의 메소드가 호출될때 그리고 서버측에서는 웹 서비스 메소드가 호출될때마다 활성화되어 추가적인 역할을 수행하게 된다. 웹 서비스에 대한 요청 또는 응답시 웹 서비스의 기능을 확장할 수 있는 수단이라는 점에서는  ASP.NET의 Http Handler, Http module과 유사한 개념이라고 볼 수 있다. 

Soap익스텐션을 사용할 수 있는 예로는 웹 서비스 호출 전 후에 로깅을 한다든지 또는 SOAP 메세지를 압축해서 전달한다든지 하는 작업들이 있을 수 있다.

■ SOAP의 XML모습

네트워크 선을 따라서 오고 가는 SOAP 요청/응답의 XML형태를 좀 자세히 들여다 보겠다. 예를 위해서 웹 서비스에서 "add"메소드를 하나 노출시키자. 두 정수를 받아서 합을 리턴하는 간단한 웹 서비스 메소드이다. :

[WebMethod]

public int add( int a, int b)

{

  return a + b;

}

클라이언트측의 웹 서비스 프락시가 add 메소드를 호출할때 실제로 서버로 보내지는 SOAP 요청 메세지를 다음과 같다:

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

<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"

xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

xmlns:xsd="http://www.w3.org/2001/XMLSchema">

  <soap:Body>

    <add xmlns="http://tempuri.org/">

      <a>10</a>

      <b>20</b>

    </add>

  </soap:Body>

</soap:Envelope>

이 요청에 대해서 서버는 다음과 같은 응답을 내려보낸다:

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

<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"

xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

xmlns:xsd="http://www.w3.org/2001/XMLSchema">

  <soap:Body>

    <addResponse xmlns="http://tempuri.org/">

      <addResult>30</addResult>

    </addResponse>

  </soap:Body>

</soap:Envelope>

■ 웹 서비스 라이프 사이클

SOAP 익스텐션을 사용하면 이런 SOAP 메시지의 헤더나 바디 부분을 적절한 순간에 가공할 수 있다. ASP.NET SOAP 익스텐션 프레임워크는 개발자들로 하여금 이런 순간들에 참여하여 SOAP 메세지를 가로채서 가공한 후 메세지를 이후의 전달 경로로 다시 전달할 수 있도록 적절한 기회와 API를 제공한다. SOAP 익스텐션 프레임워크를 통해서 개발자들은 객체로 역직렬화할때 접근할 수 있고 다시 그 객체를 SOAP 메세지로 직렬화할때도 참여할 수 있다.

이 포스트에서는 웹 서비스 호출시 그리고 응답을 받는 동안 개발자가 참여할 수 있는 단계들을 설명하고, 그리고 각 순간들 개발자가 접근할 수 있는 API와 인자들의 특징을 설명하고자 한다. 첫번째 주제는 많은 아티클들에서 설명하고 있다. 다음 그림은 MSDN에서 볼 수 있는 것으로 웹 서비스의 라이프 사이클과 그리고 메세지가 직렬화/역직렬화되는 각 단계를 보여주고 있다.

웹 서비스의 라이프 사이클 #1 - 개요 그림

메세지가 클라이언트측의 프락시를 떠나서 서버의 웹 서비스 메소드에 전달되기까지의 단계는 다음과 같다.

프락시의 호출 인자들 -> BeforeSerialize( at Client) -> AfterSerialize ( at Client ) -> SOAP 요청 메세지

----네트워크---->

SOAP 요청 메세지 -> BeforeDeserialize( at Server) -> AfterDeserialize( at Server)-> 메소드로 인자 전달

서버측에서 클라이언트로 메세지가 전달되기까지의 단계는 그 역으로 된다.

메소드의 리턴값 및 아웃 인자들-> BeforeDeserialize( at Server) -> AfterDeserialize( at Server )->SOAP 응답 메세지

----네트워크---->

SOAP 응답 메세지 -> BeforeDeserialize( at Client ) -> AfterDeserialize( at Client )->프락시의 리턴값

■ SoapExtension 클래스

개발자들이 네트워크로부터 들어오는 SOAP 메세지 스트림, 네트워크로 나가는 메세지 스트림에 각 단계별로 접근하고 싶다면 SoapExtension 클래스를 상속해서 구현해야 한다. 상속 클래스에서 구현할 필요가 있는 주요 메소드들로는 다음과 같은 것들이 있다: GetInitializer, ChainStream, Initialize, and ProcessMessage. 다음 그림은 클라이언트측과 서버측에서 메소드가 호출되는 순서를 잘 보여 주고 있다. 이 그림은  주목해서 이해할 필요가 있다.

웹 서비스의 라이프 사이클 #2 - 상세 이해용

GetInitializer, Initialize 메소드는 초기화(initialization)할 수 있는 기회를 제공한다. 초기화는 메소드가 호출될때마다 클라이언트측, 서버측에서 한번씩 수행할 수 있는 기회가 있다.  ChainStream 메소드는 SOAP 메소드를 호출하는 메세지, 결과로 반환되는 메세지에 해당하는 스트림을 캡쳐할 수 있는 기회를 준다. ProcessMessage는 개발자가 추가 작업을 할 수 있는 익스텐션의 핵심적인 부분이다.

■ 메세지 스트림 전달하기  

ChainStream 메소드를 오버라이딩함으로써 앞에서 말한것처럼 메세지 스트림이 전달되는 경로상에서 그 스트림을 가로챌 수 있는 기회를 제공해준다. 익스텐션이 설치되면 ASP.NET은 ChainStream 메소드를 호출해서 SOAP 메세지를 포함하고 있는 또는 포함하게 될 스트림에 대한 레퍼런스를 넘겨준다. 다음 코드는 대부분의 경우에 ChainStream에서 구현되는 전형적인 코드이다:

Stream oldStream;

Stream newStream;


public override Stream ChainStream(Stream stream)

{

  oldStream = stream;

  newStream = new MemoryStream();

  return newStream;

}

이 코드는 이전 단계에서 넘어온 메세지에 대한 스트림과 그 스트림을 가공할 수 있는 별도 공간의 스트림에 대한 레퍼런스를 로컬 변수에 저장해두고 있다. 인자로 넘어오는 stream이 이전 단계에서 건너오는 스트림이고, newStream이 인자로 넘어온 이전 스트림을 가공(지지고 볶고)할 수 있는 공간이다.

■ 메세지 처리하는 단계

SOAP 익스텐션에서 개발자가 참여하게 되는 것은 바로 오버라이딩된 ProcessMessage 메소드에서이다. ProcessMessage 메소드는 그림에서 보듯이 각 단계에서 여러번 호출된다.

// Process the SOAP Message

public override void ProcessMessage(SoapMessage message)

{

  // Check for the various SOAP Message Stages

  switch (message.Stage)

  {


  case SoapMessageStage.BeforeSerialize:

     BeforeSerialize(message);

    break;


  case SoapMessageStage.AfterSerialize:

     AfterSerialize(message);

    break;


  case SoapMessageStage.BeforeDeserialize:

     BeforeDeserialize(message);

    break;


  case SoapMessageStage.AfterDeserialize:

     AfterDeserialize(message);

    break;


  default:

    throw new Exception("invalid stage");

  }

SOAP 메세지의 단계에 따라 적절한 메소드를 실행시킨다. BeforeSerialize단계가 서버측에서도 발생하고 클라이언트에서도 발생하는데 어떤 쪽의 단계인지는 인자로 넘어오는 SoapMessage가 구체적으로 어떤 타입인지를 보면 알 수 있다.

private void BeforeSerialize(SoapMessage message)

{

  SoapClientMessage clientMessage = message as SoapClientMessage;

  SoapServerMessage serverMessage = message as SoapServerMessage;

  if (clientMessage != null)

  {

      BeforeSerializeAtClient(clientMessage);

  }

  if (serverMessage != null)

  {

      BeforeSerializeAtServer(serverMessage);

  }

}

다른 단계에서도 같은 방법으로 해서 서버측에서 발생하는 것인지 클라이언트에서 발생하는 것인지를 구분할 수 있다. 이렇게 직렬화/역직렬화단계의 before, after순간에 SOAP 메세지에 접근해서 수정을 할 수 있다는 것이다. 달봉이도 정말 그 수준까지만 알고서는 SOAP 익스텐션 제작을 쉽게만 생각했었다. 그런데 이런 일반적인 얘기 뒤에는 또다른 진실들이 있었다. 다음은 달봉이가 SOAP 익스텐션을 제작하면서 알게 된 사실들이다. 인터넷 어디가에 있을 지도 모르겠지만, 달봉이는 다음 사실들을 알아 내기 위해서 적잖은 시행착오를 겪어야만 했다.

■ 달봉이의 오해들

달봉이가 ChainStream과 관련해서 오해한 부분이 2 가지 있었다. 하나는 ChainStream이 언제 호출되는가였다. 직렬화와 역직렬화의 단계가 변화되는 때만 호출된다. 이 그림을 보기전에는 8개의 Before/After 이벤트가 발생할때마다 호출된다고 생각했었다. 이 오해로 인해 많은 시행 착오를 겪어야만 했었다. 달봉이의 두번째 오해는 로컬 변수로 레퍼런스를 저장해 놓고 있는 oldStream과 newStream에 대한 것이다. 앞의 그림에도 나오지 않은 중요한 내용이 있었던 것이다. 달봉이가 그것을 알기까지 참으로 많은 시행착오가 있었다. 중단점이 빼곡히 찍히 프로그램을 계속 반복해서 실행시켰다 종료시켰다 해야만 했었다. 그러면서 여러가지 상상을 해야만 했고 그런 다음 여러가지 추론을 이끌어 내서 가장 알맞은 시나리오를 만들어냈다. 그 시나리오는 아직까지도 잘 적용되고 있다. 달봉이는 이전 단계에서 넘어오는 스트림이 oldStream이였다면 새로운 단계로 넘어가는 스트림은 newStream이라고 생각했었다. 그래서 oldStream의 내용을 가공해서 newStream으로 채워야 한다고 이해했었다. 그러나 그렇게 생각하고 작성된 프로그램은 계속 예외를 뱉어 냈다.  많은 에러가 여러가지형태로 난다. 하나를 수정했다고 생각하면 다른 문제가 솟아나고. 근본적인 이해가 잘못되었던 것이다. 그러고 있는 도중에 다음과 같은 코드를 만나게 되었다. 인터넷에서 찾아낸 예제 중에서 클라이언트의 AfterSerialize단계에서 다음과 같은 코드가 있었다.

//이곳에서 먼저 newStream의 내용을 가공하는 작업을 했다.

//그런 다음 최종적으로 newStream의 내용을 oldStream으로 복사를 하고 AfterSerialize단계를 마쳤다.

Copy(newStream, oldStream);

Copy 메소드의 시그너쳐를 찾아 보니까 다음과 같았다.

void Copy(Stream from, Stream to)

클라이언트의 AfterSerialize단계를 마치고 나면 서버측으로 스트림이 전달된다. 그런데 스트림 가공후의 최종 결과를 oldStream으로 복사하는 것이다. 다시 말하면 서버로 전달되어야 하는 스트림이 oldStream이라는 말이다. 이게 무슨 말인가? 달봉이는 한참이나 고민했다. 분명 다음 단계로 전달되는 스트림은 newStream이라고 생각하고 있었다. 그래서 from, to에 해당하는 스트림의 위치를 다음처럼 바꿔봤다

Copy(oldStream, newStream)

에러였다. oldStream은 읽을 수가 없다는 것이다. 그래서 다음은 이 메소드를 주석처리해봤다.

//Copy(newStrream, oldStream);

또 에러였다. 그제서야 각각의 스트림에 대한 속성을 알아보기 시작했다. 그림을 보면 알 수 있듯이 클라이언트측에서 BeforeSerialize, AfterSerializ 단계 이전에 ChainStream 메소드가 한번 호출되는데, 이때 ChainStream 메소드로 넘어오는 stream 객체는 읽을 수가 없었고(CanRead=false) 대신에 쓸수만 있는(CanWrite=true) 스트림이었다. 이 스트림이 쓰기 전용이라는 것은 결국 newStream에서 작업한 스트림 내용을 oldStream으로 복사한다는 앞의 코드와 이전 스트림을 새로운 스트림으로 복사하는 코드에서 에러가 발생하는 이유를 알 수 있었다. 그러나 문제는 또 있었다. oldStream에서 읽을 수가 없다면 newStream에는 조작할 기존 데이터는 어떻게 구하는가였다. newStream에 기존의 내용을 부어줄 수 없다면 어떻게 BeforeSerialize, AfterSerialize단계에서 이전 내용을 가공해서 다음 단계로 넘겨줄 수 있는가이다. 인터넷에 있는 코드들을 보아도 클라이언트측의 BeforeSerialize, AfterSerialize의 어떤 코드에서도 newStream에 기존의 데이터를 채워주는 코드는 없었다. 그래서 추측을 했다. 개발자가 직접 newStream에 기존 메세지 내용을 채우지 않아도 ChainStream 메소드에서 리턴해준 스트림를 이용해서 웹 서비스 프레임워크가 채워주는 것은 아닐까? 만약 그렇다면 언제 newStream이 채워지는 것일까? 테스트 결과 첫번째 추측은 정말 맞았다. 즉 newStream에는 웹 서비스 익스텐션 프레임워크에 의해 자동으로 채워졌던 것이다.!. 그리고 채워지는 시기는 테스트결과 BeforeSerialize에서는 빈 스트림으로 있었고, AfterSerialize단계에서 기존의 메세지 내용으로 채워지는 것을 확인할 수 있었다. 이 사실을 추측할 수 있게 되기까지란 -_-;; 더불어 한 가지 더 알게 되었다. 클라이언트측의 BeforeSerialize단계에서는 ProcessMessage의 인자로 넘어오는 SoapMessage 타입의 message 객체를 통해서 메세지의 헤더값들을 변경할 수 있지만, 직렬화가 되고 나서의 AfterSerialzie 단계에서는 헤더값을 변경할 수 없었다.

클아이언트측의 ChainStream, BeforeSerialzie, AfterSerialize단계에서 일어나는 일들을 요약하면 다음과 같다.

▶ChainStream의 인자로 넘어오는 oldStream 객체는 쓰기전용의 빈 스트림이다.

▶ChainStream이 반환한 스트림객체 newStream에는 AfterSerialize단계 직전에 서버로 전달될 기존 메세지 내용이 채워지게 된다.

▶기존 메세지 내용을 변경하고 싶다면 newStream의 내용을 변경하고 그리고 최종 결과는 oldStream에 복사해 놓으면 된다.

▶AfterSerialize단계가 끝나고 나면 웹 서비스 익스텐션 프레임워크는 oldStream에 채워진 메세지 내용을 서버로 전달할 것이다.

▶직렬화전인 BeforeSerialize 단계에서는 message의 헤더 속성을 변경할 수 있다. 그러나 직렬화가 일어난 후에는 message 헤더를 변경할 수 없다.

지금까지가 클라이언트측에서의 직렬화단계의 메소드들에 대한 인자들에 대해서 알아 본 것이다. 서버측의 역직렬화 단계(Deserialize)에서는 반대로 전개된다. 바로 그 특징을 정리하면 다음과 같다.

▶ ChainStream의 인자로 넘어오는 oldStream에는 클라이언트에서 올라오는 메세지 내용이 이미 채워져서 올라온다.

개발자는 oldStream의 내용을 변경해서 그 변경된 내용을 newStream으로 복사해 놓는다.

AfterDeserialize단계가 끝나고 나면 웹 서비스 익스텐션 프레임워크는 newStream에 채워진 내용을 웹 메소드를 호출하는 곳에 사용하게 된다.

다음은 서버측의 웹 메소드가 호출되고 나서 그 결과를 다시 직렬화(Serialize)하는 단계에서의 특징들이다.

▶ 서버측의 Deserialize단계, 웹 메소드 호출 단계 그리고 반환값의 Serialize단계는 모두 하나의 익스텐션 인스턴스 내에서 일어난다. 무슨 말인가하면 만약 Deserialzie단계에서 클래스 전역 변수로 어떤 값을 저장해 뒀다면 그 값은 Serialize단계에서 접근할 수 있다는 것이다. 앞에서 보여준 두번째 그림을 보면 Deserialzie단계, Serialize단계가 하나의 박스로 묶여져 있다는 것은 그같은 사실을 표현한 것이다.

▶ 그러나 웹 서비스 메소드가 호출되고 나서 그 익스텐션 인스턴스의 ChainStream이 호출된다. 이 메소드에서는 익스텐션에 캐시되어 있는 oldStream과 newStream을 다시 생성하게 된다.

▶ BeforeSerialize단계에서는 oldStream과 newStream은 비어있게 된다.

▶AfterSerialize단계 직전에 프레임워크에 의해 클라이언트로 반환될 메세지 내용이 newStream에 채워진다.

▶newStream의 내용을 조작한 후 oldStream에 복사해 놓는다.

▶프레임워크는 AfterSerialize단계가 끝나고 나면 oldStream의 내용을 네트워크로 흘려보내게 된다.

▶클라이언트에서와 마찬가지로 BeforeSerialize단계에서는 ProcessMessage 메소드의 인자로 넘어온 SoapMessage 객체를 통해서 헤더 값을 변경할 수 있다. 그러나 AfterSerialize단계에서는 변경할 수 없다.

마지막으로 클라이언트측의 Deserialize단계의 내용을 보면 다음과 같다.

▶ 그림을 보면 ChainStream이 호출되는데, 이곳에서 읽기 전용의 oldStream과 쓰기 전용의 newStream이 생성된다.

▶oldStream에는 서버에서 내려온 응답 메세지 내용이 채워지게 된다. oldStream의 내용을 읽어와서 조작한 다음 newStream으로 복사해 놓는다.

▶ AfterSerialize단계까지 끝나고 나면 응답 메세지를 웹 서비스 호출 프락시로 건네주고 최종 클라이언트 프로그램으로 전달되게 된다.

휴우~~ 힘들었다. 어떻게 보면, 알고 나니까 당연한 사실처럼 여겨지지만 이 사실을 구체적으로 알려주는 곳을 찾지 못해서 아주 힘들었었다.

구체적인 샘플 코드들은 다음 문서들을 참고하기 바란다. 특히 아래의 Using SOAP Extension in ASP.NET문서를 잘 참고하길 바란다.

아들놈이 자꾸 옆구리를 찔러서 더이상 글을 진행하지 못하겠다.

야튼 남은 문제는 Soap익스텐션을 제작했으니 이제 이것을 어떻게 웹 서비스 및 클라이언트에 설치해야 할지를 알아야 할 것이다. 결론만 얘기하면 config 파일을 이용하는 방법이 있고, SOAP 익스텐션 어트리뷰트를 이용하는 방법을 이용할 수도 있다. 이 방법에 대해서는 아래 문서들을 보면 자세히 설명하고 있다.

■ 결론

웹 서비스에 대한 Soap 익스텐션을 사용하면 개발자 코드와는 상관없이, 웹 서비스 기능을 추가하거나 확장할 수 있다. 이 방법을 이용하면 여러 단계를 거치면서 SOAP 요청/응답을 중간에 가로채서 필요한대로 전처리, 후처리 하거나 또는 메세지 내용은 변경하지 않더라도 각 단계를 이용해서 로깅작업등을 할 수 있을 것이다.  그리고 마지막으로 짚고 가고 싶은 것은 Soap익스텐션은 서버측만 또는 클라이언트측만 또는 양쪽 모두에 설치될 수 있다는 것이다.

더이상 안되겠다. 아들놈이 계속 책을 덮쳐 오고 있다.


Creating Custom SOAP Extensions - Compression Extension

- http://www.mastercsharp.com/article.aspx?ArticleID...

압축 라이브러리

- http://icsharpcode.net/OpenSource/SharpZipLib/Defa...

Using SOAP Extensions in ASP.NET

- http://msdn.microsoft.com/msdnmag/issues/04/03/ASP...

SOAP Message Modification Using SOAP Extensions

- http://msdn2.microsoft.com/en-us/library/esw638yk....