티스토리 뷰

초기의 CGI부터 IIS 기반의 ASP, PHP, JSP 등 많은 웹 애플리케이션 개발 언어들 속에서 개발자들은 어떤 선택을 해야 할까. 그중 하나인 콜드퓨전은 국내에서는 잘 알려지지 않았지만 상당히 많은 매력을 가진 웹 애플리케이션 제품이다.
특 히 콜드퓨전 MX는 J2EE 기반으로 출시되어 진정한 WAS(Web Application Server)로서 자리잡았다는 평을 받고 있다. 지금부터 콜드퓨전에 대한 모든 것을 담아내기는 힘들지만 간단하게나마 콜드퓨전의 특징을 살펴보고 매크로미디어 MX 제품군으로 개발한 펫 마켓(Pet Market) 웹 애플리케이션 샘플을 통해 콜드퓨전과 플래시 리모팅 기술의 결합을 살펴본다.

개발 시간 단축과 유지보수의 용이
우 선 콜드퓨전의 동작 원리부터 살펴보자. 콜드퓨전도 대부분의 웹 애플리케이션들이 동작하는 구조와 비슷한 원리를 갖는다. 즉 클라이언트로부터 요청을 받아 CFML(ColdFusion Markup Language) 기반으로 구성된 콜드퓨전 태그를 분석해 처리하고, 결과 값을 다시 클라이언트에 보내주는 방식이다.
콜드퓨전의 동작 원리를 보면 다른 웹 애플리케이션과 큰 차이가 없지만 콜드퓨전이 갖고 있는 기능과 코드의 간편함, 이를 통한 개발 시간의 단축과 유지보수의 용이함은 타 웹 애플리케이션과 비교될 수 없을 정도의 성능을 자랑한다. 또한 콜드퓨전 MX에 와서는 드림위버 MX, 플래시 MX와의 완벽한 연동을 지원하므로 웹 애플리케이션 개발의 효율성을 증대시킨다. 이밖에 콜드퓨전의 다양한 기능들을 타 웹 애플리케이션과 비교해 살펴보자.

편리한 웹 기반의 관리자 콘솔 지원
웹 애플리케이션 언어를 이용해 개발해 봤다면 시스템의 환경설정을 위해 고생해 본 경험이 있을 것이다. 전문적인 지식을 가진 사람만이 시스템의 환경설정을 변경할 수 있기 때문이다. 환경설정 관련 파일을 텍스트 편집기를 이용해 직접 수정하거나 별도로 지원하는 콘솔을 이용해 설정하면서 상당히 많은 어려움에 처하게 된다. 물론 이러한 것이 시스템 운용의 지침일 수도 있지만 모든 시스템은 관리자에게 좀더 쉬운 인터페이스를 제공해야 한다는 것은 분명한 진리이다. 이런 점에서 콜드퓨전은 웹 기반의 관리자 콘솔을 지원한다는 장점을 갖고 있다.

콜드퓨전에서는 데이터베이스 설정을 비롯한 캐시, 메일 서버, 웹 서비스, 검색 엔진, 디버깅, 로그 등에 관한 셋팅을 웹에서 쉽게 할 수 있다. 이렇게 설정된 시스템 환경은 이후에 실제 코드를 줄여주는 데 큰 역할을 하는데, 데이터베이스에 관련된 정보를 관리자 콘솔에서 등록하고 실제 코딩에서는 설정한 내용을 토대로 개발하기 때문에 코드에 불필요한 정보를 매번 적지 않아도 된다. 또한 로그 설정 및 로그 내용도 편리하게 웹 기반으로 볼 수 있으며 메일 서버, 스케쥴링 환경설정을 통해 다른 웹 애플리케이션에서 개발하기 힘든 항목도 아주 짧은 몇 줄의 코드로 개발할 수 있다.



<그림 1> 콜드퓨젼 MX서버의 구조



<그림 2> 콜드퓨전의 동작 원리

코드의 간결함
앞 서 언급했던 것처럼 콜드퓨전은 다른 웹 애플리케이션 언어와 달리 관리자 콘솔을 통해 직접 코드에 기록해야 할 부분을 실제 코드에 추가하지 않도록 했다. 예를 들면 CFMX라는 데이터소스를 관리자 콘솔을 통해 콜드퓨전에 등록했다면 이후에 데이터베이스와의 연동을 다음과 같이 할 수 있다.

<cfquery name="커넥션의 이름" Datasource="CFMX">
   <!--- 실행하고자 하는 SQL문--'
   SELECT *
   FROM table이름
</cfquery>

이처럼 몇 줄의 코드만으로 데이터베이스에 접속해 데이터를 가져올 수 있다. 또한 가져온 데이터들은 이나 태그를 이용해 실제로 웹 페이지에 출력할 수 있는데, 이 또한 다른 언어들보다 상당히 간단하다.

<cfoutput query="커넥션의 이름">
   #컬럼1#, #컬럼2#
</cfoutput>

동일한 프로그램을 다음과 같이 ASP, JSP, PHP의 세 언어로 작성해 봤다. 콜드퓨전이 타 언어에 비해 문법 구조와 실제로 작성하는 코드가 간결함을 비교할 수 있다.

◆ ASP
<% set cnn = server.createobject("ADODB.Connection")
   cnn.ConnectionString = "DSN=TBTest;
   database = tbwork;UID;PWD=;"
   cnn.Open
   Set rst = cnn.Execute("Select * From test")
   While Not rst.EOF
   Response.Write rst("FirstName") &_ " " & rst("LastName") & " "
   rst.MoveNext
   Wend
%>
◆ JSP
<%@ page import = "javax.sql.*, java.sql.*, java.util.*" %>
<% InitialContext context = getInitialContext();
   ConnectionSource ds = context.lookup
   ("java:comp/env/jdbc/TBTest");
      Connection conn = ds.getConnection();
   RowSet Qtest;
   try { Statement stmt = conn.prepareStatement
      ("Select * From test");
      Qtest = stmt.execute();
   }
   finally { conn.close(); }
%>
<% while (Qtest.next()) { %>
   <%= Qtest.getString("FirstName") %>
   <%= Qtest.getString("LastName") %>
<BR>
<% } %>
◆ PHP
<?php $Connection = mysql_connect("localhost", "", "")
   $db = mysql_select_db("tbwork", $connection)
   $sql = "Select * from test" ;
   $sql_result = mysql_query($sql)
   while ($now = mysql_fetch_array($sql_result)) {
      $FirstName = $row["FirstName"];
      $LastName = $row["LastName"];
         echo "$FirstName $LastName"; }
   mysql_free_result($sql_result);
   mysql_close($connection);
?>

또 하나의 예를 들어, 메일을 보내는 프로그램을 작성할 때 다른 웹 애플리케이션으로 개발한다면 상당히 복잡한 코드나 다른 컴포넌트들과 연동해 사용해야 한다. 그렇지만 콜드퓨전에서는 태그를 이용해 쉽게 메일을 보낼 수 있다.



<화면 1> 메일 서버 절정 화면

태그를 사용하기 위해서는 <화면 1>과 같이 메일 서버의 주소를 관리자 콘솔의 메일 서버 항목에서 설정해야 한다. 그리고 나서 다음과 같이 메일을 보내는 코드를 작성한다.

<cfmail from="보내는 사람의 전자메일 주소" to="받는 사람의 전자메일 주소" subject="메일 제목">
메일 내용
</cfmail>

이처럼 어렵게만 생각했던 메일 발송 프로그램을 단 세 줄로 만들 수 있다. 또한 데이터베이스에 전자주소를 가져와 단체 발송할 경우도 앞서 설명한 태그와 함께 쉽게 만들 수 있다.

<cfquery name="get" datasource="test">
   SELECT    email
   FROM        table이름
</cfquery>
<cfloop query="get">
<cfmail from="보내는 사람의 이메일 주소" to="#email#" subject="제목">
메일 내용
</cfmail>
</cfloop>

데이터베이스에 가져온 결과 값을 태그를 이용하여 루프를 돌면서 메일을 하나씩 보내게 된다. 단지 몇줄을 추가해 단체 메일을 발송하는 코드를 작성한 것이다. 콜드퓨전은 이 외에도 다양한 기능을 태그 기반으로 쉽게 작성할 수 있다.



<화면 2> 콜드퓨전의 디버깅 설정 메뉴

편리한 디버깅 환경
웹 프로그래밍을 해 본 사람이라면 작성한 프로그램에 에러가 발생했을 때 어디서 에러가 나왔는지 찾기 힘들어서 고생한 적이 있을 것이다. 필자도 불과 한 달 전 JSP 코드에서 데이터 포맷을 잘못 작성해 발생한 에러 때문에 상당히 고생을 했다. 결국 모든 코드를 한 줄씩 분석해가며 문제가 되는 부분을 찾을 수 있었다. 많은 프로그래밍 언어들이 컴파일이나 코드 실행시 프로그램의 오류를 체크해 출력해 주지만 모든 정보가 정확하게 맞는 것은 아니다.
콜드퓨전을 사용하면 관리자 콘솔을 통해서 디버깅 설정을 할 수 있다. 디버깅 설정을 하고 개발하면 시스템에 대한 정보 및 에러가 발생했을 경우 약 98∼99% 이상의 정확한 오류 정보를 출력해 준다. 또한 , 등의 디버그 태그를 이용해 쉽게 개발할 수 있다.

Array 

<화면 3> cfdump 기능 사례

다음의 의 기능을 예로 살펴보자. 데이터베이스에 쿼리문을 보내어 결과값을 가져왔다. 이때 그 결과 값들을 정상적으로 가져왔는지 보려면 어떻게 해야 할까? 콜드퓨전은 다음과 같이 한 줄의 코드를 삽입함으로써 <화면 3>처럼 결과 값을 볼 수 있다.

<cfquery name="get" datasource="test">
   SELECT  *
   FROM     table이름
</cfquery>
<cfdump var="#get#">

개발 툴과의 완벽한 연동
콜 드퓨전은 같은 회사 제품인 드림위버 MX와 플래시 MX와의 완벽한 연동을 자랑한다. 웹 개발을 하면서 디자인과 코딩은 떼어놓을 수 없는 관계이다. 위지윅(WYSIWYG) 웹 에디터로 강력한 기능을 자랑하는 드림위버 MX와 콜드퓨전 서버의 연동은 다양한 방식으로 이뤄진다.
FTP나 RDS(Remote Develo pment Service) 방식을 통해서 원격 콜드퓨전 서버와 연결해 작업할 수 있으며, 콜드퓨전 파일을 드림위버에서 직접 실행해 디스플레이해 볼 수도 있다. 또한 뒤에서 더 자세하게 볼 플래시 리모팅을 이용한 플래시 MX와의 연동도 지원한다. 큰 확장력을 지닌 플래시 인터페이스의 애플리케이션을 개발할 수 있다는 점에서 최근에 각광받고 있는 이슈 중 하나다.
WAS로서의 기능
콜 드퓨전은 WAS이다. 그러므로 WAS가 갖고 있어야 할 기본 기능을 모두 갖추고 있다. 콜드퓨전이 WAS로 자리잡기 이전부터 기본적으로 가지고 있었던 기능들을 포함해 로드밸런싱, 분산 처리, 보안 기능, 웹 서비스 기능 등을 갖고 있다. 고가로 제공되던 기존의 WAS들은 사용상에도 어려운 점이 많았지만, 저렴한 비용의 콜드퓨전을 이용한다면 보다 손쉽게 웹 애플리케이션을 개발할 수 있다.
관련 자료 지원은 어떻게?
필자가 콜드퓨전을 3년 정도 접하면서 느껴온 점은 국내에 콜드퓨전 관련 자료가 너무 부족다는 점이다. 수많은 웹 애플리케이션 언어 서적 중에서 콜드퓨전 서적은 한 권도 없으며, 한글로 된 자료 또한 구하기가 어렵다. 최근 들어 한국 매크로미디어를 통해서 조금씩 자료들이 생산되기는 하지만, 아직까지는 콜드퓨전에 대한 자료는 외국에 비해서 턱없이 부족한 편이다.
그렇지만 혼자서 콜드퓨전을 배울 수 있었던 것은 콜드퓨전 자체가 쉽게 배울 수 있는 언어이고, 회사 차원에서 생산되는 수많은 문서들이 좋은 자료가 되었기 때문이다. 수 백장의 태그와 함수에 대한 레퍼런스부터 특정 기술에 대한 기술 문서까지 수많은 예제와 함께 생산되는 다양한 자료는 콜드퓨전을 이용하여 웹 개발을 좀더 효율적이며, 강력하게 할 수 있는 초석이 된다. 아직까지 이런 자료들이 한글로 완벽하게 제공되는 것은 아니지만 웬만한 수준의 영어 실력을 갖췄다면 쉽게 이해할 수 있을 것이다.
콜드퓨전과 플래시가 만났을 때
지 금까지 간단하게 다른 웹 애플리케이션 서버와 비교해 콜드퓨전의 특징들을 살펴봤다. 모든 웹 애플리케이션 서버는 나름대로의 특징이 있으며 개발하고자 하는 웹 애플리케이션이 어떠한 것인지에 따라 가장 잘 맞는 웹 애플리케이션 서버를 선택하는 것이 가장 중요한 일일 것이지만, 그중에서도 콜드퓨전을 이용한 개발시 장점을 엿볼 수 있었다. 이번에는 플래시 리모팅 기술을 접목시킨 콜드퓨전의 활용을 살펴보겠다.
플래시 리모팅, 플래시로 통신하자
플래시의 등장은 웹을 사용하는 사람들에게 상당히 신선한 것이었다. 엽기토끼, 플래시 카드, 플래시 애니메이션 등 플래시는 다양한 분야에서 사용되었다. 플래시 플레이어만 설치돼 있으면 어디서든지 플래시로 제작된 컨텐츠를 볼 수 있다는 장점도 있지만 플래시가 갖고 있는 가장 큰 장점은 웹 페이지를 정적인(움직이지 않는) 상태에서 역동적으로 움직이도록 만들어 낼 수 있다는 점일 것이다. 이전에 자바스크립트나 DHTML 등을 이용해 몇 가지 효과를 줄 수 있었지만 플래시가 나타내는 효과와는 비교할 수 없다.
국내에 플래시가 보편적으로 보급되면서 플래시로 제작된 페이지, 컨텐츠는 이용자들에게 많은 관심과 흥미를 가져다 줬는데, 그런 플래시에도 몇 가지의 단점들이 있었다. 그것은 하나의 swf 파일로 구성돼 있는 플래시는 하나의 파일에 모든 것을 담아야 하기 때문에 파일 자체의 용량이 커진다는 것이다. 즉 긴 애니메이션이나 용량이 큰 사운드가 포함됐다면 초기 로딩 시간이 많이 소요됐다.
이 러한 문제를 풀기 위해서 파일을 분할하여 각 경계에서 다른 파일 플래시 파일을 호출한다거나, 사운드의 압축 등 용량을 줄이는 여러 가지 방식을 채택해 봤지만 근본적인 플래시의 단점을 해결하지는 못했다. 이러한 문제는 플래시 5.0에 와서 XML 데이터를 이용해 데이터를 주고받는 것이 추가되면서 해결하려 했다. 그러나 XML 데이터를 이용하기 때문에 상당히 고난도의 XML 기술이 필요했으며 코드 또한 상당히 복잡해지면서 플래시와 XML을 이용한 방식은 그리 큰 호응을 얻지 못했다.
그러나 콜드퓨전 MX와 플래시 MX가 출시되고, 플래시 리모팅 기술이 추가되면서 데이터베이스와의 직접적인 접근이 가능해졌다. 플래시 리모팅은 콜드퓨전 뿐만이 아니라 자바 클래스, 닷넷 컴포넌트 등 다른 플랫폼과의 연결 또한 가능하므로 플래시 인터페이스를 다양한 시스템과 연결해 이용할 수 있는 기술이다.
<그림 3>에서처럼 플래시는 플래시 플레이어가 설치된 어떤 미디어에서든지 실행되는데 swf 파일은 http 프로토콜을 통해 애플리케이션 서버와 통신하게 된다. 이 때 애플리케이션 서버는 플래시 리모팅을 이용하여 웹 서비스나 데이터베이스와 통신하게 되는 것이다. 즉 플래시 리모팅 기술로서 역동적인 인터페이스에 동적인 데이터의 조작까지 가능해진 것이다. 그것도 모든 미디어에서 말이다.


<그림 3> 플래시, 플래시 리모팅, 애플리케이션 서버의 관계도

플래시 리모팅 위한 준비
플 래시 리모팅을 정확하게 이야기하자면 플래시와 데이터를 주고받기 위한 일종의 게이트웨이라고 할 수 있다. 기존에는 플래시와 데이터를 주고받기 위해서는 XML 구조의 데이터를 복잡한 코드와 함께 사용해야만 했다. 그렇지만 이제 그럴 필요없이 다른 플랫폼의 언어와 플래시 리모팅을 사용하여 쉽게 주고받을 수 있다. 특히 플래시 리모팅으로 플래시 MX와 콜드퓨전 MX를 연결하면 다른 어떤 경우보다 손쉽게 플래시 애플리케이션을 개발할 수 있다. 플래시 리모팅을 이용한 플래시 MX와 콜드퓨전 MX와의 통신을 위해서는 다음과 같은 몇 가지 소프트웨어를 설치해야 한다.

◆ 플래시 MX
◆ 콜드퓨전 MX
◆ 플래시 리모팅 컴포넌트 for 플래시 MX

콜 드퓨전 MX를 설치했다면 플래시 리모팅은 자동으로 설치된다. 콜드퓨전 MX 설치에 관한 내용은 한국 매크로미디어 홈페이지(www.macromedia.com/kr)에 게재된 ‘콜드퓨전 MX와 플래시 리모팅 : 개념잡기부터 따라하기까지’ 문서를 참고하기 바란다. 앞의 스프트웨어가 모두 설치됐다면 이제 플래시 리모팅을 이용해 플래시 애플리케이션을 제작할 준비가 다 된 것이다. 지금부터 간단한 플래시 애플리케이션을 만들어 보면서 플래시 리모팅을 자세히 보도록 하겠다.
CFC를 이용한 통신
플 래시가 애플리케이션과 통신을 하기 위해서는 서버 측 액션스크립트(Server Side ActionScript, SSAS)를 사용하는 방법과 CFC를 사용한 웹 서비스 방법이 있다. 이번 예제에서는 CFC를 이용해 통신하는 방법을 살펴보겠다. 이번에 만들 플래시 애플리케이션은 서버 측에서 특정 문자열을 출력하는 프로그램을 실행하여 플래시 인터페이스에 출력하는 것이다. 먼저 서버 측 코드를 작성해 보자. 다음의 코드를 작성하고 HelloWorld.cfc로 저장한다.

<cfcomponent name="HelloWorld">
   <cffunction name="sayHello" access="remote">
      <cfreturn "안녕하세요">
   </cffunction>
</cfcomponent>

앞의 코드는 CFC 방식으로 작성된 코드이다. 보다시피 CFC는 와 , 으로 기본적인 구조를 형성한다. CFC 파일 구조를 보면 파일이 CFC라는 것을 알리기 위해서 태 그를 사용하고 컴포넌트 안에 cffunction을 이용해 함수를 추가한다. 하나의 컴포넌트에는 여러 개의 함수가 들어갈 수 있다. 함수는 고유한 이름과 몇 가지 옵션을 설정할 수 있는데 예제에서의 access는 함수를 접속하는 권한을 원격까지 지원하겠다는 내용이다. 앞에서 sayHello라는 함수는 “안녕하세요”라는 문자열을 반환해주는 함수이다.
다음과 같은 형식으로 컴포넌트를 작성해주는 것이 플래시 리모팅에서 서버 측 코드를 작성하는 전부이다. 서버 측 코드가 정확하게 동작하는지 살펴보려면 다음과 같은 코드를 작성하고 실행한 후, “안녕하세요”라는 문자열이 출력되면 정상적으로 코드가 동작되는 것이다.

<cfoutput>
   <cfinvoke component="com.macromedia.test.HelloWorld"
      method = "sayHello"
      returnVariable = "message">
   #message#
</cfoutput>

이 코드에서는 태 그를 이용해 컴포넌트 내의 함수를 호출한다. 이때 component 옵션에는 콜드퓨전의 웹 루트 경로에서부터 컴포넌트 파일이 있는 폴더까지의 구조를 적는다. 예를 들어 콜드퓨전이 설치된 폴더가 c:cfusionMX이고 Hello World.cfc 파일이 c:cfusionMXwwwrootcommacro mediatest에 있다면 이것을 앞과 같이 표현하는 것이다(콜드퓨전을 설치하면 웹 루트는 기본적으로 c:cfusionMXwwwroot가 된다).HelloWorld 컴포넌트를 호출하고 그 안의 sayHello 메쏘드(함수)를 실행해 반환되는 값을 message라는 변수에 담는 것이 바로 태그의 역할이다. 그리고 변수의 값을 출력하는 코드가 있다.
이 렇게 서버 측 코드를 작성하고 정상적으로 실행되는지 봤다면, 이제 실제로 플래시 파일을 작성하고 CFC와의 웹 서비스 연결을 통해 결과 값을 가져오는 코드를 작성해 보겠다. 플래시 MX에서 새로운 무비를 작성하고 프레임에 다음과 같은 스크립트 코드를 삽입해 보자.

1 #include "NetServices.as"
2 #include "NetDebug.as"
3 function Result()
4 {
5 //receives data returned from the method
6 this.onResult = function(result)
7 {
8 trace("Data received from server : " + result);
9 }
10
11 this.onStatus = function(error)
12 {
13 trace("Error : " + error.description);
14 }
15 }
16 NetServices.setDefaultGatewayUrl
("cfrootMX/flashservices/gateway");
17 var gw = NetServices.createGatewayConnection();
18 var server = gw.getService
("com.macromedia.test.HelloWorld", new Result());
19 server.sayHello();

앞 의 코드를 자세히 보면, 1∼2행은 액션스크립트의 라이브러리를 추가하는 코드이며, 3∼15행까지는 서버로부터 통신 결과를 받아 상황에 따라 결과 값을 출력하거나 에러 메세지를 출력하는 함수이다. 실질적으로 서버 측과 통신을 하는 코드는 16∼19행이다.
NetServices.setDefaultGatewayUrl (“cfrootMX/flash/services/gateway”);를 통해서 연결할 서버의 주소를 설정하는데, 이때 cfrootMX는 콜드퓨전 MX의 루트 경로이다. 그리고 그 다음 var gw = NetServices.createGatewayConnection();을 통해서 서버와 연결하고, var server = gw.getService(“com.ma cromedia.test.HelloWorld”, new Result());를 통해서 서버의 HelloWorld 컴포넌트와 연결하여 Service 객체를 생성한다. 그리고 sayHello 함수를 실행해 서버로부터 결과 값을 반환받게 된다.

Array

<화면 4> sayHello 함수의 실행

Array

<화면 5> 공원 타입선택 화면

파일을 저장한 후 실행하면 이전에 작성했던 HelloWorld.cfc 파일에서 설정했던 반환 값이 서버로부터 보내졌다는 메시지를 볼 수 있다.
지 금까지 본 CFC 파일과 플래시 리모팅을 이용하여 서버와 통신을 하는 애플리케이션을 작성해 봤다. 직접 작성해 보면 알겠지만, 기존의 XML 데이터를 처리하는 방식에 비해 훨씬 간단한 방식으로 서버와의 통신이 몇 줄의 코드로 이뤄지는 것을 볼 수 있다.
플래시 리모팅의 활용 예
앞 에서 우리는 CFC와 액션스크립트를 이용해 서버와의 통신이 어떻게 이뤄지는지 살펴봤다. 플래시 리모팅은 데이터베이스와의 자유로운 통신으로 화려한 플래시 인터페이스에 동적인 정보를 제공할 수 있음에 따라 그 활용의 예가 무궁무진하다. 앞에서 본 예는 상당히 간단하지만, 플래시 리모팅을 통해 콜드퓨전과 플래시간의 통신이 이뤄지도록 하는 가장 근본적인 원리를 설명한 예다.
플래시 리모팅의 방식을 이용해 작성한 하나의 예제를 살펴보도록 하자. 이 예제는 매크로미디어 웹사이트(www.macromedia.com/kr/desdev/mx/articles/remoting)에서 다운받아 실행할 수 있는데, 이 예제 또한 앞의 예제를 구현하기 위해 필요한 환경을 먼저 마련해야 한다.
이 예제에서는 미국 국립공원의 종류를 공원 타입에서 선택하면 공원 리스트가 출력된다. 그리고 <화면 5>와 같이 파크 리스트에서 공원을 클릭하면 하단에 공원에 대한 상세 정보가 출력된다. 이 모든 것은 순간순간 데이터베이스와의 통신을 통해 이뤄진다는 점에 명심하자. 이 샘플과 같은 역할을 하는 플래시 파일을 데이터베이스와의 통신없이 제작한다고 하면, 생각해야 할 부분과 복잡한 코드를 감당하지 못할 것이다.
즉 방대한 데이터를 처리해야 할 경우 플래시 리모팅 기술을 사용하면 적은 용량으로 필요한 정보들을 효과적으로 전달할 수 있어 유용하다. 이러한 플래시 리모팅의 장점은 여러 분야에 적용시킬 수 있는데, 실제로 인사 관리 시스템, 예약 시스템, E-Test 시스템 등 여러 응용 분야에 적용한 사례가 있다. <화면 6>은 실제 구축된 사례로 한 호텔의 예약 시스템이다.

Array

<화면 6> 플래시 리모팅을 이용한 예약 시스템

플 래시로 구성된 화면을 사용하여 투숙 일시부터 비용의 결재까지 모든 것을 한 화면에서 처리할 수 있다. 2∼3페이지 이상의 페이지 이동이 필요한 여타 예약 시스템에 비해 훨씬 간편한 구조를 갖고 있다. 실제로 기존 예약 시스템에서 입력을 잘못한 실수나 오류로 웹에 타이핑했던 정보들을 손실했던 경우가 종종 있을 것이다.
그러나 플래시 리모팅을 이용해 <화면 6>과 같이 구성한 예약 시스템은 기존의 시스템보다 효과적인 정보를 제공하고, 한 화면에서 일괄 처리되므로 고객들로 하여금 좋은 반응을 얻을 수 있을 것이다. 또한 필요한 정보만을 그때 그때 데이터베이스와의 통신을 통해서 가져오기 때문에 초기 로딩 시간도 줄일 수 있다. 플래시 리모팅은 이 밖에도 아이디어만 있으면 다양한 분야에서 활용될 것으로 보인다.
콜드퓨전과 플래시 리모팅 이용한 펫 마켓
앞 서 설명한 플래시 리모팅 기술을 활용한 대표적인 예로 펫 마켓(Pet Market)을 소개하겠다. 펫 마켓은 매크로미디어가 플래시 리모팅 기술을 활용한 리치 인터넷 애플리케이션 실례를 보여주기 위해 작성한 데모용 온라인 애완동물 쇼핑몰 프로그램이다. 매크로미디어는 펫 마켓에 관련된 모든 소스 코드와 제작 과정 및 산출물 등을 모두 공개해 자사의 MX 제품군, 특히 플래시 리모팅 기술을 활용해 차별화된 사용자 경험을 전달하는 웹 사이트를 만들고자 하는 개발자들에게 가이드라인을 제시하고 있다.

Array Array

<화면 7> 펫 마켓 초기화면

<화면 8> Dogs 카테고리를 선택하고 상세 정보 검색

펫 마켓이라는 용어를 처음 듣는 순간 자바 개발자라면 썬에서 배포하고 있는 자바 펫 스토어(Pet Store)를 연상하게 될 것이고, 자바에 대응하기 위해 닷넷에서 출시한 닷넷의 펫 샵(Pet Shop) 역시 떠오를 것이다. 그러면 매크로미디어의 펫 마켓은 무엇일까.
펫 마켓이란?
펫 마켓에 대한 모든 정보는 www.macromedia.com/desdev/mx/blueprint에서 찾을 수 있다. 펫 마켓의 개발 과정, 사용된 기술, 데모용 URL 등 다양한 정보가 있으며, 전체 샘플 프로그램을 다운받아 자신의 PC에서 테스트해볼 수 있도록 모든 소스 코드와 설치 파일을 제공한다. 또한 단순히 사이트가 어떻게 구성될 수 있는지 확인만을 원하는 사용자들을 위해 데모용 웹 사이트(examples.macromedia.com/pet market/flashstore.html)를 별도로 운영하고 있다
백 문이 불여일견이라는 속담이 있듯이 플래시로 구축된 역동적인 사용자 인터페이스는 직접 실행해 봐야만 확인할 수 있다. 펫 마켓 샘플을 다운받아 설치하기 전에 한 번 데모용 사이트를 방문해 그 기능을 확인해 보기를 당부한다. examples.macromedi a.com/petmarket/flashstore.html를 방문하면 <화면 7>이 나타난다.
초기 화면에서 Dogs를 선택하면 전체 페이지는 바뀌지 않은 채 화면 왼쪽에 Dogs 카테고리에 해당하는 상세 정보를 보여주는 별도의 윈도우가 역동적으로 나타난다. 이 상세 정보 윈도우에서 원하는 항목, 예를 들어 Bulldog을 선택하면 <화면 8>이 표시되는 것을 볼 수 있다. 모든 사용자 화면이 플래시로 구축돼 역동성을 느낄 수 있으며, 플래시로 구축된 사용자 화면에 표시되는 정보들이 서버에 위치한 데이터베이스와의 연동을 통해 전송된다는 점에서 기존의 단순한 플래시 웹 사이트와 차별화된다. 이제 펫 마켓 샘플을 다운받아 설치해 보고 소스 코드를 분석하는 단계로 넘어가도록 하자.
펫 마켓 설치
펫 마켓을 설치하기 위한 샘플 파일은 www.macromedia.com/desdev/mx/blueprint에서 다운받을 수 있다. 펫 마켓은 크게 세 가지 종류로 콜드퓨전에서 동작하도록 구성된 매크로미디어 펫 마켓, 자바 개발자를 위한 자바 펫 스토어에서 동작하는 자바 펫 마켓 버전, 닷넷 개발자를 위한 닷넷 펫 샵에서 동작하는 닷넷 펫 마켓 버전으로 구분된다. 즉 동일한 애완 동물 쇼핑몰 예제에 대해 전체를 콜드퓨전으로 구현한 펫 마켓 버전, 닷넷이나 자바를 선호하는 개발자를 위해 기존 코드는 그대로 재사용하고 플래시 프론트 요소만 설치할 수 있는 닷넷 버전과 자바 버전 세 가지로 구성해 놓았는데, 이 글에서는 매크로미디어 MX 펫 마켓을 다운받아 설치해 보겠다.
일단 사이트에서 zip 파일 형식의 펫 마켓 샘플 파일을 다운받는다. 압축을 풀면 몇 개의 html 파일, 이미지 파일과 car 파일이라는 콜드퓨전 배포 방식의 파일을 찾을 수 있다. car 파일은 자바에서 사용하는 ear 혹은 war 파일과 유사한 것으로 콜드퓨전 관리자 콘솔의 ‘아카이브 배포 및 배포 메뉴’에서 샘플 파일을 콜드퓨전에 설치할 수 있다.

Array

<화면 9> 콜드퓨젼 관리자 화면 - 샘플 파일 배포하기1

Array

<화면 10>콜드퓨젼 관리자 화면 - 샘플 파일 배포하기2

콜 드퓨전 관리자 콘솔 메뉴에서 아카이브 및 배포 매뉴를 클릭해 나온 화면에서 기존 아카이브 배포 항목의 브라우즈 서버 버튼을 클릭한다. <화면 10>처럼 나타난 브라우저 탐색기와 같은 창에서 압축을 푼 후 나온 car 파일을 선택한다. 그 후 배포 버튼을 누르면 관련된 파일이 자동으로 해당 폴더를 생성해 CFC, 이미지, SWF, FLA, CFM 파일이 생성되고 데이터베이스 또한 콜드퓨전 데이터 소스에 자동으로 등록된다. car 파일 배포가 완료된 후 브라우저를 열어 http://localhost:8500/petmarket/ store.html라는 주소를 입력하고 실행하면 자신의 컴퓨터에 설치된 펫 마켓을 실행할 수 있다.
펫 마켓의 아키텍처
펫 마켓 구현에는 다양한 제품군이 사용됐는데 서버 측 서비스 구현에는 콜드퓨전 MX, 풍부하고 역동적인 사용자 화면 구성에는 플래시 MX, 서버 측 서비스와 플래시와의 연동에는 플래시 리모팅 기술, 개발 도구로는 드림위버 MX, 비트맵 그래픽 이미지 편집에는 파이어웍스 MX를 이용했다. 펫 마켓의 기본적인 구조는 <그림 4>와 같다.
펫 마켓은 서비스 기반 아키텍처를 사용해 개발됐다. 플래시로 작성된 사용자 인터페이스 단에서 프리젠테이션을 모든 로직이 수행되도록 해 서버에서는 순수한 비즈니스 로직만을 수행함으로써 서버 측에 필요한 코드 라인 수를 현저하게 줄였다. 또한 서버의 비즈니스 로직은 모든 콜드퓨전의 컴포넌트 기반 개발 방법인 CFC로 개발해 자동 문서 생성, 재사용성 등을 충분히 배려했다.

Array

<그림 4> 펫마켓의 아키텍처

펫 마켓 구조를 기술적인 관점에서 구분해 보면 플래시로 구성된 사용자 인터페이스 계층과 콜드퓨전으로 작성된 비즈니스 로직 계층의 두 계층으로 구분할 수 있으며, 플래시 사용자 인터페이스와 콜드퓨전으로 구축된 서버 측 서비스를 연동하기 위한 플래시 리모팅 계층이 중간에 위치하게 된다.
서버측 서비스를 작성하자
그러면 소스 코드와 함께 좀더 자세한 기술 요소를 살펴보자. 먼저 플래시가 호출해 사용할 서버 측 서비스를 작성하는 방법을 설명하겠다. 이미 언급했듯이 서버 측 서비스를 작성하는 방법에는 두 가지 방식이 있는데 하나는 SSAS를 이용하는 것이고 다른 하나는 콜드퓨전의 CFC 방식을 이용하는 방식이다.
둘 중에서 어느 방식을 사용할 것인지는 개발자의 취향에 달려 있다. 즉 액션스크립트가 좀더 익숙하면 SSAS 방식을 이용하고 콜드퓨전을 이용한 서버 프로그래밍에 익숙한 개발자는 콜드퓨전 컴포넌트 방식을 이용하면 되는데, 여기에서는 CFC를 이용한 방식을 중점으로 살펴보겠다. 참고로 SSAS 방식을 사용한 방법은 펫 마켓 설치 디렉토리의 apicreditcardservice.asr 파일을 참조하기 바란다.
CFC 방식으로 플래시 사용자 인터페이스를 연동하는 방법을 살펴보기 위해 소스 코드를 분석해 보자. 예를 들어 나의 쇼핑 카트에 있는 데이터를 삭제하기 위해 콜드퓨전 컴포넌트를 작성한다면 다음과 같다.

<cfcomponent>
   <cffunction name="empthCart" access="remote">
      <cfargument name="carotid" require="true"
         type="numeric">
      <!--- DB에 접속해 나의 카드 항목을 모두 삭제한다는 SQL 코드--'
      <cfquery datasource="blueprintdatabase">
         DELETE from itemlist
            WHERE carotid = #arguments.cartoid"
      </cfquery>
   </cffunction>
</cfcomponent>

이 와 같은 CFC 방식의 코드를 작성해 서버에 파일을 위치시키고 플래시의 액션스크립트에서 플래시 리모팅 프로그램을 구현해 서버 측의 콜드퓨전 컴포넌트를 실행한다. 펫 마켓의 모든 방식은 이러한 방식을 통해 서버와 플래시의 연동을 구현했다. 콜드퓨전의 기본적인 문법 사용법만 알면 초보자도 쉽게 구현할 수 있는 코드들이기 때문에 자세한 설명은 하지 않겠다.
다음에는 작성된 서버 측 서비스를 사용하는 플래시 화면을 작성하고 플래시 액션스크립트를 사용해 필요한 코드를 작성하면 된다. 플래시측 코드는 이미 공원 예제를 통해 언급했으므로 생락한다. 플래시에 관심 있는 독자는 펫 마켓에서 제공하는 원본 소스 파일을 참고하기 바란다(펫 마켓의 설치 디렉토리flash-src 디렉토리에 펫 마켓의 플래시 원본 소스들이 모두 들어 있다). 플래시 리모팅 기술을 사용해 개발하는 개발자라면 필요한 대부분의 기능을 펫 마켓의 코드에서 찾아 재사용할 수 있을 것이다.
콜드퓨전을 이용한 효율적인 개발
펫 마켓에서 플래시 사용자 인터페이스와 상호 연동하기 위해 사용하는 플래시 리모팅 방식은 플래시가 서버 측과 통신하기 위해 사용했던 기존의 XML 데이터 처리 방법과는 상당히 많이 다른, 여러 면에서 개선된 방법이다. 플래시 리모팅을 사용하면 XML 처리시 요구됐던 부가적인 처리 시간을 줄이고 필요한 데이터만을 압축된 형태로 주고받음으로써 많은 양의 데이터를 빠른 시간 내에 전송할 수 있다. 만약 콜드퓨전이 아니라 다른 웹 애플리케이션 서버를 사용하면서 플래시와의 연동 기능을 사용하고 싶다면 애플리케이션 서버에 플래시 리모팅만 별도로 설치해 플래시 리모팅 방식으로 서버 측 서비스와 연동할 수 있다.
펫 스토어, 펫 샵, 펫 마켓
마 이크로소프트는 닷넷을 출시하면서 썬의 자바 펫 스토어를 닷넷 버전으로 수정한 펫 샵을 출시해 자바와 닷넷 간의 수많은 논쟁에 불을 불였다. 자바가 우월한가 닷넷이 우월한가에 대한 논쟁은 아직 끊이지 않고 있지만 필자의 소견으로는 성능에 중점을 두지 않고 J2EE 기술을 어떻게 사용하는가에 대한 가이드라인을 제시하고자 개발됐던 자바 펫 스토어와 자바와의 성능 비교를 염두에 두고 자사의 제품에 맞게 코드를 최적화한 닷넷 버전과의 성능 비교는 무의미한 것이라 생각한다.
매크로미디어가 펫 마켓을 출시한 의도는 결코 자바나 닷넷과 성능상의 비교를 하고자 함이 아니고 썬의 자바 펫 스토어와 닷넷 펫 샵은 각각 다른 기술로 인정하되 매크로미디어의 플래시 리모팅 기술을 사용하면 어떤 점이 달라질 수 있는 것인지에 대해 말하고자 함일 것이다. 그러면 또 다른 논쟁을 일으킬지 모르는 매크로미디어의 펫 마켓과 자바 펫 스토어, 닷넷 펫 샵을 비교 분석해보도록 하자.
네트워크 대역폭 사용율의 비교
앞 서 글에서 살펴본 바와 같이 펫 마켓은 웹에서의 사용자 인터페이스를 플래시로 구현해 웹에서 마치 데스크톱 환경과 같은 정교하고 복잡한 상호작용을 가능하게 하는 새로운 개념인 플래시 리모팅 기술을 중심으로 구축됐다. 별도 클라이언트 프로그램 설치없이 브라우저만 갖고 이러한 정교한 상호작용을 가능하게 한다는 것은 흥미로운 일임에 틀림없으나, 개발 경험이 풍부한 개발자라면 플래시로 사용자 인터페이스를 구축함으로써 속도나 성능이 저하되지 않을지를 가장 먼저 염려할 것이다. 그러면 이러한 대역폭 측면에 중점을 두고 자바와 닷넷, 매크로미디어 솔루션을 한 번 비교해 보자.

Array

<그림 5> 대역폭 사용량 비교 결과

플 래시로 사용자 인터페이스를 구축했을 경우 가장 먼저 의심이 되는 것은 사용자 인터페이스가 풍부해짐에 따라 사용되는 네트워크 대역폭이 늘어나리라는 것이다. 그러나 비교 결과, 플래시를 사용한 펫 마켓이 훨씬 적은 양의 대역폭을 사용하는 것으로 나타났다.
자세한 내용을 보기 전에 결과를 먼저 요약해보면 플래시 리모팅 기술을 사용하는 경우, 첫째 플래시 리모팅 기술을 사용한 사용자 인터페이스는 프리젠테이션 계층을 세분화해 클라이언트측에서 수행 가능한 코드는 클라이언트 단에 미리 전송해 놓고 서버와는 필요한 데이터만을 주고받는 것을 가능하게 한다.
따라서 매 요청마다 더 적은 양의 데이터만이 서버로 전송돼 결과적으로 적은 양의 대역폭을 사용하게 된다. 둘째 플래시가 제공하는 액션스크립트라는 언어를 사용해 클라이언트에서 처리 가능한 프로세스들은 클라이언트 단에서 수행되도록 구현함으로써 서버에 보내지는 총 요청 회수를 줄일 수 있다. 결과적으로 플래시 리모팅 기술을 사용하면 전송되는 총 데이터량은 줄어든다.
대역폭 사용량 비교를 위해 간단한 테스트를 수행해 봤다. 다음과 같은 네 가지 유형의 작업을 콜드퓨전 MX에서 수행하고 닷넷 펫 샵과 자바 펫 스토어, 매크로미디어 펫 마켓이 사용하는 대역폭을 측정했다. 대역폭 측정은 마이크로소프트 자동화 테스팅 도구인 웹 애플리케이션 스트레스(Web Application Stress, WAS나 homer라고도 함)를 사용했으며 테스트 환경은 다음과 같다. 시나리오에 대한 자세한 스크립트는 www.macromedia.com/desdev/mx/blueprint/articles/performance/scripts.txt에 서 참조한다.

윈도우 2000 어드밴스드 서버 SP2
J2RE 1.3.1 IBM 윈도우 32 빌드 cn131w-20020403 ORB130 (JIT enabled: jitc)
콜드퓨전 MX 6,0,0,40897
인텔 OCPRF100 서버
8×500MHz PIII Xeon
4GB 메모리
◆ 시나리오 1 : 상품을 구매하지 않고 단순히 상품 정보만을 검색하기
◆ 시나리오 2 : 상품정보를 검색하지 않고 바로 구매만 수행(여러 개의 상품을 카트에 추가)
◆ 시나리오 3 : 단순 구매(상품 정보를 검색한 후 상품을 카트에 추가하거나 삭제하기)
◆ 시나리오 4 : 확장된 구매(상품 정보를 검색하고 쇼핑 카트에 아이템 추가, 구매 기록 보기, 검색, 비용 처리 등 다양한 작업 수행하기)

이 러한 유형의 작업에 대해 대역폭 사용량을 측정한 결과, 플래시로 사용자 인터페이스를 구축한 펫 마켓이 펫 스토어나 펫 샵보다 적은 양의 대역폭을 사용하는 것으로 나타났다. 특히 시나리오 1, 3, 4와 같이 서버에 요청을 빈번하게 전송하고 응답을 받는 경우에는 그 차이가 현저하다. 이러한 결과가 나오는 이유는 펫 마켓의 경우 초기에 사용자 인터페이스가 이미 로드된 이후에는 클라이언트에서 데이터 포맷이나 프리젠테이션 생성을 위한 코드가 실행되고 서버에서는 단지 필요한 데이터만 클라이언트에 보내주게 되므로 네트워크를 통해 전송되는 데이터량을 현저하게 줄일 수 있기 때문이다.
반면에 펫 스토어나 펫 샵의 경우에는 매 요청마다 필요한 데이터와 프리젠테이션 코드를 모두 클라이언트에 전송해야 하므로 상대적으로 더 많은 양의 대역폭을 소모하게 되는 것이다. 실제 USABancShares.com 등의 사이트에서는 기존의 HTML로 구성된 사용자 인터페이스를 전부 플래시로 재구성하였는데 그 결과 응답 속도가 5배 이상 빨라지고 서버 부하가 40% 감소했다는 통계가 이러한 사실을 뒷받침한다.
오랫동안 실행할수록 대역폭 절약
앞의 테스트 결과를 자세히 관찰하면 펫 마켓의 또 하나의 장점을 발견할 수 있다. 펫 마켓의 경우 애플리케이션을 오랫동안 실행하면 실행할수록 대역폭 절약 측면의 장점이 더 커진다는 것이다. 즉 플래시로 사용자 인터페이스를 구축한 경우 초기에 클라이언트로 로드되는 데이터량은 많아지게 되나 일단 로드된 이후에는 클라이언트 측에 캐시로 저장된 파일을 사용하게 됨으로써 애플리케이션이 반복 수행될수록 속도나 대역폭 측면에서 장점을 가지게 되는 것이다. 그러면 펫 마켓에서 플래시로 작성된 사용자 인터페이스를 초기에 로드하기 위해 소모되는 대역폭을 한번 살펴보자.
<표 1>은 애플리케이션이 최초 수행된 이후에 브라우저 캐시에 저장되는 데이터를 측정한 것으로 최초 실행시 펫 마켓이 펫 샵이나 펫 스토어보다 많은 양의 데이터를 로드하는 것을 보여주고 있다. 그렇다면 이러한 차이가 어떤 의미가 있는지 알아보기 위해 상용 사이트의 경우를 살펴보자.

Array

<표 1> 초기 로드되는 데이터량 비교(윈도우 2000 서버에 설치된 IE 6.0에서 테스트 했음

<표 2> 같이 아마존, 반스앤노블스 등의 일반 전자 상거래 사이트와 비교해볼 때 펫 마켓의 초기 로드량은 결코 평균 범위를 벗어나지 않는 수용 가능한 범위라는 것을 확인할 수 있다.

Array

<표 2> 일반 상거래 사이트와 펫마켓의 초기 로드 데이터량 비교

< 표 3>에서 대역폭 차이를 좀더 자세하게 정리해 봤다. 한 명의 사용자에 대해서는 이러한 차이가 별 의미가 없다고 생각할지 모르지만 24시간 내내 사용되고 많은 사용자가 접속하는 상용 사이트의 경우에는 이러한 효과가 가감되어 현저한 대역폭 절약 효과를 가져오게 된다.

Array

<표 3> 대역폭 비교

사용 편의성 측면에서의 비교
이 번에는 사용 편의성을 비교하기 위해 동일한 시나리오에 대해 사용자가 단위 작업을 수행하는 데 소요되는 시간을 측정했다. 결과를 분석하면, 특히 시나리오 1과 4의 경우(구매는 하지 않고 상품 검색만 하는 경우와 상품 검색과 구매를 모두 수행하는 경우) 펫 스토어와 펫 샵에 비해 펫 마켓이 동일한 작업을 수행하는 데 소요되는 시간을 현저하게 줄일 수 있음을 알 수 있다.

Array

<그림 6> 사용편의성 측면에서의 비교

펫 스토어나 펫 샵의 경우 사용자가 여러 가지 상품에 대한 카탈로그를 보려면 여러 번 페이지를 이동해야 하며, 또한 현재까지 구매한 카트 정보를 보려면 브라우저의 ‘앞으로’, ‘뒤로’ 버튼을 사용해 페이지를 이곳 저곳으로 이동해야만 한다. 바로 이러한 페이지 이동 작업이 불필요한 오버헤드를 생성하므로 네트워크를 통해 불필요한 데이터가 전송되고 동일한 작업을 수행하는 시간이 오래 걸리는 것이다.
반면 펫 마켓의 경우 플래시로 짜임새있게 디자인된 화면을 통해 불필요한 페이지 이동 회수를 줄이고 하나의 화면을 필요에 따라 분할해 사용하도록 구성했기 때문에 단일 작업에 소요되는 시간을 단축시킬 수 있다.
효율적인 지불 단계
플 래시로 웹 응용 프로그램의 사용자 인터페이스를 구축하면 하나의 웹 페이지를 효율적으로 분할하여 사용할 수 있는 원 페이지(One Page) 모델 기법이 가능하다. 이 기법의 효과를 가장 적절하게 사용한 것은 펫 마켓의 지불 단계에서 사용되는 화면이다.

Array

Array

<화면 11> 펫 마켓의 지불 단계

<화면 12> 플래시 MX에서 제공하는 UI컴포넌트

온 라인 쇼핑몰의 지불 처리 단계에서 전형적으로 수행해야 하는 아이디 등록, 고객 상세 정보 입력, 배송 주소 입력, 배송 선택 사항 입력, 지불 방법 선택 등의 5가지 단계를 하나의 페이지 내에서 수행되도록 구성함으로써 모든 정보를 한눈에 볼 수 있도록 사용자 편의성을 제공하고 있다. 또한 5단계를 수행하는 동안 페이지 이동을 전혀 할 필요가 없기 때문에 잘못 입력된 정보가 있으면 이전 단계를 반복하지 않고 잘못된 정보만 수정할 수 있다.
개발 생산성 측면에서의 비교
마 지막으로 개발 생산성 측면에서 펫 마켓 구현에 필요한 소스 코드 라인 수를 자바나 닷넷 버전과 한 번 비교해 보고자 한다. 애완동물 쇼핑몰을 구현하기 위해 필요한 소스 코드는 크게 시스템 구성, 데이터베이스 연결, 프리젠테이션 계층으로 분류될 수 있다.
<그림 7>을 보면 매크로미디어의 펫 마켓은 플래시를 사용자 인터페이스로 사용했기 때문에 프리젠테이션 계층을 구성하기 위해 필요한 소스 코드 라인 수는 상당히 많은 편이다. 그러나 데이터베이스 연결 등의 미들 계층에서 사용되는 코드를 살펴보면 자바나 닷넷에 비해 현저하게 적은 소스 코드로 동일한 기능을 구현할 수 있음을 확인할 수 있다.

Array

<그림 7> 프로그램 소스 코드라인 수 비교

이 는 콜드퓨전에서 사용하는 바로 CFML이라는 스크립트 언어의 생산성 때문으로 이미 다른 언어와 비교해 본 바와 같이 동일한 기능을 구현할 때 코드 라인 수를 많게는 1/2 혹은 1/3까지 줄일 수 있는 장점 때문에 많은 개발자에게 애용되고 있는 언어이기도 하다.
개발 생산성 측면에서 효율성은 사용자 화면 구축시에도 그 진가를 발휘한다. 플래시로 사용자 화면을 구축할 경우 플래시 MX에는 미리 구축된 컴포넌트 라이브러리를 사용해 개발 시간을 훨씬 단축할 수 있다. 컴포넌트 라이브러리에서 사용자 화면에서 빈번하게 사용되는 체크박스, 콤보박스, 리스트박스, 다양한 버튼 등을 사전에 제작된 UI 컴포넌트로 제공하고 있으므로 플래시 개발자 역시 효율적인 시간 내에 펫 마켓과 같이 차별화된 사용자 인터페이스를 구축할 수 있다.
펫 마켓의 확장성 테스트
끝으로 콜드퓨전 MX의 안정성과 확장성에 대한 테스트를 수행했고 그 결과는 다음과 같다. <그림 8>과 같이 테스트 환경에서 펫 마켓은 동시 사용자 700명까지는 평균 응답 시간이 크게 변화하지 않고 일정한 성능을 제공한다.
펫 마켓과 플래시 리모팅, 그리고 콜드퓨전
웹 애플리케이션 개발에 대한 패러다임은 변하고 있다. 단순히 브라우저를 통해 서버 상의 정보를 보는 것으로 만족하지 못함에 따라 1세대, 2세대, 3세대를 거치면서 웹은 다양한 기능을 추가하게 됐다. 이러한 경향과 함께 등장한 플래시는 웹에서도 동적인 화면과 다양한 움직임이 가능하다는 것을 알려줬고 플래시로 작성된 웹 사이트는 화려하고 환상적인 화면으로 사람들의 시선을 끌어당겼다. 그러나 사용자들은 곧 모든 사람에게 혹은 매번 똑같은 화면을 보여주는 플래시에도 식상함을 느끼게 됐고 개발자들은 역동적인 플래시와 서버와의 연동을 통해 또 다른 차별화를 추구하게 됐다.

Array

<그림 8> 펫 마켓의 확장 가능성 테스트 결과

이제 사용자들은 웹에서 다양한 활동을 수행하기를 원하고 있다. 단지 정보를 검색하고 몇 가지 제한된 기능을 수행하는 수동적인 활동이 아닌, 사용자 중심에서 원하는 작업을 적극적으로 실행하고자 하는 욕구를 만족시켜줄 수 있는 것은 바로 플래시 리모팅 기술이 아닐까. @

플래시 MX가 이전의 버전에 비해 상당 기능이 추가돼 애플리케이션 개발 도구로 부상하고 있다. MX 제품군은 콜드퓨전(ColdFusion) MX, 리모팅(Remoting) MX 등 매크로미디어의 다양한 도구와 서버 제품의 연계성을 한층 더 발전시키고, 기업 환경의 애플리케이션 개발에 필요한 다양한 요구를 수용하고 있다(MX가 약자이거나 특별한 뜻이 있는 것은 아니다. 예전에 윈도우 CE에서 CE가 그랬듯이, 많은 사람들이 흔히 뭔가 심오한 뜻이 있을 거라 생각했지만, CE 역시 아무런 뜻 없이 지워진 이름이었다).
플래시와 엔터프라이즈 애플리케이션
90년대 중반 이후 소프트웨어 개발은 그 기반 자체가 웹으로 모두 바뀌었다. 특히, 기업 애플리케이션들이 웹으로 전환됐다. 널리 쓰일수록, 규모가 큰 서비스일수록 웹으로 전환이 급속하게 이뤄졌다. 이처럼 웹 기반으로 급격히 바뀐 가장 큰 이유는 무엇일까? 그것은 바로 ‘배포’의 문제이다. 기업 환경에서 애플리케이션 배포의 문제는 굉장한 골칫거리다. ‘웹 기반’ 즉 씬 클라이언트(thin client)는 추가적인 설치나 부가적인 조작 없이 브라우저 하나만으로 응용프로그램을 수행할 수 있다는 점 때문에 아주 널리 사용되게 됐다. 하지만, 머지않아 HTML의 한계는 사용자들의 다양한 요구를 수용하기 힘들게 됐다. 이런 시점에서 자바 애플릿은 큰 희망이 됐다.
즉, 자바 애플릿은 플랫폼 종속적이지 않고 HTML이 제공하지 못하는 다양한 기능을 보여줬다. 국내에 한정된 이야기일 수 있으나 오늘날에 와서 자바 애플릿보다 플래시의 도약이 훨씬 두드러지게 나타나고 있다. 물론 자바에 대한 마이크로소프트의 소극적인 지원도 이유가 되겠지만, 플래시가 자바 애플릿에 비해 상대적으로 생산성이 높고 훨씬 다양한 표현을 하는 컨텐츠를 쉽게 만들 수 있다는 데서 그 이유를 찾을 수 있다.
애플리케이션은 사용상의 편의뿐만 아니라 미려한 사용자 인터페이스가 요구되고 있다. 리치 클라이언트에 대한 요구는 언제나 있어 왔지만 배포의 문제와 제작상의 생산성 문제 그리고 클라이언트 기계의 속도, 호환성 등이 걸림돌이 되어 왔다. 플래시는 이러한 걸림돌들에 좋은 답을 제공한다.
플 래시는 어떤 면에서 오히려 자바보다 플랫폼에 종속적이지 않다(자바가 특정 JVM의 특성을 타는 것을 널리 알려진 사실이다). 플래시는 4억 1천 4백만 개가 배포된 가장 폭 넓은 표준 클라이언트로 사실상의 표준(de facto standard) 클라이언트 환경이라고까지 할 수 있다(문서를 왜 PDF로 배포하는가와 같은 맥락이라고 할 수 있다). 모바일이나 포켓PC용 플래시 플레이어가 이미 있으며, 앞으로 더욱 다양한 플랫폼이 지원될 것이라는 사실은 자명하다.
플래시는 엔터프라이즈 애플리케이션에서 필요로 하는 보안과 상호운용성(interoperability), 대용량 처리 (scalability) 등에 좋은 솔루션이 될 수 있다. 플래시는 브라우저는 통해 SSL 통신을 지원하며, 자바 애플릿과 수준의 보안 정책을 제공한다. HTTP 통신뿐만 아니라 XML 웹 서비스, 리모팅 등 서버와 다양한 통합 방법을 제공하고 있다. 또 대용량 처리 면에서도 오히려 플래시가 더 좋은 방법이 될 수 있다. 현재 웹 애플리케이션은 몇 가지 일을 처리하기 위해 굉장히 많은 서버 트래픽을 일으킨다.
한 페이지를 보기 위해 최소한 10회 정도의 HTTP 연결이 필요하며, 대부분 2-3페이지를 거쳐서 하나의 작업이 이뤄진다. 플래시와 같은 리치 클라이언트는 오히려 이런 트래픽을 줄일 수 있게 한다. 즉, 사용자 입력 처리, 상호 작용, 입력 데이터 확인(validation)과 같은 웬만한 작업은 클라이언트에서 이뤄지고, 서버와는 필요한 데이터만 주고받으면 되기 때문에 서버에 훨씬 적은 부담을 주도록 만들어 질 수 있다.
이 글은 플래시가 디자인이나 애니메이션 도구로서가 아닌 기업 애플리케이션 제작을 위한 클라이언트 환경으로서 사용될 수 있는 가능성을 살펴보기 위한 목적으로 씌여졌다. 그래서 애니메이션이나 미디어를 다루는 방법 등은 논외로 한다. 우리는 이 글을 통해 플래시의 통신 기능, 서버사이드(ASP.NET)와 연결 방법들, XML과 XML 웹 서비스 이용, 플래시 보안 등을 살펴보려 한다.
난로연통 문제 : 플래시 웹 서비스의 의미
비 즈니스 환경이 급속하게 변화하고 기술도 이에 따라 급격하게 변하고 있다. 클라이언트/서버, 웹 기반, 기업 애플리케이션 통합(EAI), 컴포넌트 기반 개발(CBD) 등 이런 변화 속에서 현재 가장 중요한 키워드 두 가지는 바로 ‘통합’과 ‘속도’이다. 최근에 와서는 XML 웹 서비스가 크게 부각되고 있다. XML 웹 서비스가 부각되게 된 데는 HTTP 기반의 XML 통신이라는 장점이 있기도 하지만 그 보다 중요한 이유는 바로 ‘난로연통(stovepipe)’ 문제를 해결하고, ‘서비스 중심의 아키텍처(SOA, Service Oriented Architecture)’를 가능하게 하기 때문이다.
정보 시스템에 대한 수직적(버티컬) 통합으로 안정성을 확보하는 데 치중하던 과거의 방식으로는 더 이상 IT 산업 환경의 빠른 변화에 대응할 수 없게 됐다. 기업용 애플리케이션을 ‘통합’하고 더 ‘빠르게’ 구현하는 것이 가장 중요한 일이 됐다. 조직의 내부나 외부 변화에 효과적으로 대응하기 위해서는 시스템의 기능(모듈) 간에 신속한 조정이나 협력이 요구되나 현실적으로 이것이 어렵기 때문에 문제점들이 발생하는데, 이러한 문제점들은 종종 ‘난로연통’에 비유하곤 한다.
모듈들이 효과적이고 효율적으로 운영되기 위해서는 난로연통(기능별 부서)을 가로지르는 수직적 기능간의 상호작용(cross-functional activity)이 훨씬 중요하며, 여기에 대한 해결책으로 제시되고 있는 것이 SOA이다. 또, 이것을 가능하게 하는 기술이 바로 XML 웹 서비스다. 플래시의 미디어적인 측면에서 뿐만 아니라, 바로 이 웹 서비스 클라이언트의 기능을 충분히 제공한다는 점에서 큰 가능성은 찾아 볼 수 있다.
이제, 좀더 본격적으로 플래시가 제공하는 다양한 통신 기능들을 살펴보자. 이 글에는 플래시와 SWF를 구분해 쓴다. 플래시는 저작 환경과 기술을 지칭하고, SWF는 플래시 플레이어에서 수행되는 플래시 파일을 가리키는데 사용한다.
플래시와 한글
플 래시는 내부적으로 유니코드를 사용하며 기본적으로 모든 데이터를 유니코드(UTF-8, Universal Character Set Transformation Format, 8 bit)로 다룬다. 경우에 따라 URL을 다루거나 외부와 통신을 할 때 UTF-8가 아닌 운영체제가 사용하는 인코딩을 그대로 써야 할 경우가 있다. 예를 들어, 웹 서버가 기본적으로 UTF-8을 인코딩을 사용하지 않고, KSC5601(EUC-KR)을 사용하는 경우이다. 이런 호환성을 위해 플래시 MX에 System.useCodepage이 추가됐다.
System.useCodepage는 입출력에 유니코드를 쓸 것인지(false) 아니면 플래시 플레이어가 동작하고 있는 운영체제의 기본 인코딩 언어(로케일)을 사용할 것인지(true)를 결정한다. 한글 윈도우에서는 기본적인 인코딩은 KSC5601(EUC-KR)이다. 만일, 일본어 윈도우라면 기본 인코딩은 SJIS가 된다. 즉, SWF가 수행되는 환경의 인코딩을 사용하게 된다. 운영체제의 인코딩을 그대로 사용하려면, 보통 무비 타임 라인의 첫 프레임에 다음과 같은 코드를 넣어 준다.

System.useCodepage = true;

SWF는 내부적으로 UTF-8만 다루며 이것을 바꾸는 방법은 없다. 또한 코드페이지를 지정하는 방법도 없다. 따라서 useCodepage가 true인 경우는 SWF가 수행되는 운영체제의 코드페이지를 따라가며, false인 경우는 유니코드(UTF-8)을 사용하게 된다. 플래시가 내부적으로 사용하는 UTF-8은 유니코드의 각 문자를 1~4개의 바이트로 인코딩한 형태인데 다음과 같은 액션 스크립트를 통해 쉽게 확인할 수 있다.

System.useCodePage= false; // 코드페이지 사용 안함(UTF-8)
trace(escape("abc가나다"));
System.useCodePage= true; // 코드페이지 사용(EUC-KR)
trace(escape("abc가나다"));

이 코드는 다음과 같이 출력된다. escape는 URL에 사용하기 위해 %가 붙은 형태로 인코딩하는 함수이다. 이 코드는 다음과 같이 출력된다.

abc%EA%B0%80%EB%82%98%EB%8B%A4
abc%B0%A1%B3%AA%B4%D9

UTF-8인 경우, 한글의 한 글자가 3개의 바이트로 인코딩되었음을 알 수 있다. useCodePage가 true인 경우는 한글 한 글자가 2개의 바이트가 된다.
플래시 MX 통신
플래시 MX는 다양한 통신 방법을 제공한다. 플래시에서의 통신 범위와 통신에 사용되는 함수들을 정리해보면 다음과 같다.

통신 범주 사용되는 함수, 객체
플래시와 자바 스크립트 통신 getURL(), fscommand()
플래시와 웹 서버간 HTTP 통신 loadVariables(), loadVariablesNum(), getURL(), loadVars 객체
플래시와 플래시 간 getURL(), fscommand() (SWFJavaScript SWF), LocalConnection 객체
플래시와 XML 웹 서비스, MX 리모팅 XML 객체 NetServices, NetConnection 객체

<표 1> 플래시용 통신 범주와 함수

getURL ()은 예전부터 사용되던 함수인데 주로 페이지를 이동하는데 사용됐지만 자바 스크립트를 호출하는데도 사용될 수 있다. loadVariables()는 외부로부터 데이터를 읽어내는 함수이다. getURL()과 마찬가지로 URL을 지정하고 여기 “이름=값” 쌍을 읽어내어 플래시 변수(variable)에 할당한다. 만일 URL에 파일명을 지정하면(“a.txt”), SWF이 다운로드 된 URL과 같은 위치에 있는 파일로부터 읽어 들인다. LoadVars.load()는 MX에 새로 추가된 기능이다. loadVariables()를 이용하는 것보다 간편하며 비동기로 동작하기 때문에 데이터를 읽어오는 동안 ‘로딩중입니다’를 표시하는 것 같이 다른 동작을 하는 것을 손쉽게 구현할 수 있다.
XML 객체 역시 LoadVars와 사용하는 방법이 거의 같다. 다른 점은 XML 문서를 가져와서 파싱을 한다는 점이다. 플래시 MX에서는 XML DOM을 다루는 메쏘드들이 제공된다. XML을 이용하면 잘 구조화된 데이터를 처리할 수 있다는 장점이 있으며, 훨씬 유연한 애플리케이션을 제작할 수 있다. 실제로 애니메이션 방법을 XML로 기술해 SWF 파일은 하나인데 상황에 따라 다른 애니메이션을 보여주는 방법도 사용할 수 있다.
플래시와 자바 스크립트간의 통신 : getURL()을 이용한 간단한 예제
자바 스크립트와 액션 스크립트, 두 언어 모두 ECMA-262(ECMAScript) 기반 스크립트 언어로 매우 비슷한 점이 많다(똑같은 것은 아니다). 액션 스크립트에서 브라우저에 있는 자바 스크립트를 호출하거나 혹은 거꾸로 브라우저의 자바 스크립트에서 플래시의 액션 스크립트를 호출하는 것이 가능하다.
getURL() 특정 URL로 이동하는 함수이지만 “javascript”를 이용하여 브라우저가 가지고 있는 자바 스크립트를 호출할 수 있다. 다음의 액션 스크립트는 간단한 예를 보여주고 있다.

var hello = "Hello, World";
getURL("javascript:alert(""+hello+"")");

간단한 예제를 만들어보자. 플래시 MX를 기동하고 컴포넌트에서 PushButton을 무비에 끌어다 놓는다. 버튼의 레이블을 ‘Hello, World’로 수정하고, 다음과 같이 버튼에 대해 다음과 같이 액션 스크립트를 입력한다(<화면 1>).

on(release) {
getURL("javascript:alert('Hello,World')");
}

Array

<화면 1> getURL()을 이용하는 ‘Hello, World’ 만들기

혹은 SWF를 클릭하여 수행해 보자. 자바 스크립트의 alert 창이 나타나는 것을 확인해 볼 수 있다(<화면 2>). 원래 getURL()은 SWF가 포함된 페이지를 다른 URL로 이동하는 기능을 수행하는 것이 목적이지만, 프로토콜을 기술하는 부분(보통 “http://”)에 javascript 등을 써서 브라우저와 다양한 통신을 할 수 있다.

Array

<화면 2> 예제 02-HelloWorld 수행

예를 들어, “mailto:” 등도 사용할 수 있다(“mailto:”를 사용하면 기본 메일프로그램이 뜨게 된다).

getURL(url [, window [, "variables"]])

다음 예는 SWF가 포함된 페이지를 닫는 기능을 수행한다. 결국 getURL()을 사용해 페이지에 포함된 자바 스크립트를 수행할 수 있고, 다양한 방법으로 자바 스크립트와 연결할 수 있다.

on (press) {
getURL ("javascript:window.self.close()");
}

getURL()을 사용하는 다른 예를 보자. 다음과 같이 HTML 페이지에 스크립트를 만든다. 이 페이지의 스크립트를 액션 스크립트에서 호출한다.

◆ HTML 페이지
<SCRIPT LANGUAGE="JavaScript">
var newWin;
function OpenNewWindow(url, name, features) {
   newWin = window.open( url, name, features );
}
</SCRIPT>
◆ 액션 스크립트 : 버튼 액션
on (release) {
getURL("javascript:OpenNewWindow('popup.html','팝업창','height=150,width=300')");
}

이 내용은 액션 스크립트의 getURL()을 이용하여 페이지 내의 자바 스크립트 함수 OpenNewWindow()를 호출하는 예를 보여준다.

Array

<화면 3> 예제 03-openwin 수행 결과

SWF과 플래시 플레이어와 통신을 할 수 있는 함수로 fscommand()가 있다. 이 함수는 원래 SWF가 자신의 환경(플레이어)과 통신하기 위한 기능을 하지만, 스크립트를 호출하거나 외부 명령을 수행하는데도 사용할 수 있다. [File|Publish Settings] 메뉴를 선택하면, <화면 4>와 같은 화면이 나타나는데, HTML 탭에서 템플릿을 지정할 수 있다.

Array

<화면 4> FSCommand 템플릿 생성

플래시가 FSCommand 템플릿과 함께 생성되도록 지정하면 FSCommand를 사용하는 페이지를 만들어 준다. 기본적인 원리는 페이지 내의 자바 스크립트를 호출하는 것과 같은데 표준적으로 제공되는 것이므로 자바 스크립트를 호출할 때는 getURL(), loadVariables()를 쓰기보다 FSCommand를 사용할 것을 추천한다. fscommand()는 다음과 같이 명령과 파라이미터를 지정해준다(파라미터는 옵션).

fscommand("command", "parameters")

“command”는 <표 2>와 같은 기본 명령들이 있다. 외부 프로그램을 수행할 수도 있다. 이들 기본 명령들은 보통 실행파일 형태로 제작된 플래시에서 사용하며, 웹 페이지에서 보여지는 SWF에서는 많이 사용하지 않는다.

"allowscale", true/false 화면 크기에 따라갈 것인지 지정
"showmenu", true/false 플래시 메뉴를 보일 것인지 지정
"exec", "path\a.exe" 외부 프로그램 수행
"quit"  플레이어 종료

<표 2> “commnad”의 기본 명령

fscommand를 사용하는 예를 살펴보자. 다음과 같은 액션 스크립트가 있다면, “close”를 인자로 넘겨 자바 스크립트를 호출한다.

fscommand ("close");

브라우저 스크립트에서 인자를 받아 수행된다. 브라우저의 fs_DoFSCommand() 스크립트가 호출된다. 다음에 나오는 소스 코드는 자동으로 생성된 FSCommand 템플릿(HTML 파일)의 스크립트를 수정한 것이다. 전달받은 command, args에 따라 적절한 작업을 하도록 자바 스크립트를 수정하면 된다.

function fs_DoFSCommand(command, args) {
var fsObj = InternetExplorer ? fs : document.fs;
//
// Place your code here...
//
if(command = "close") {
window.self.close();
}
}

자바 스크립트에서 플래시로 값을 전달
플래시에 특정한 값을 전달하려면 플래시 플레이어 <OBJECT>의 ID를 이용하고 SetVariable() 함수를 이용한다. 이 함수는 SWF의 변수에 지정된 값을 셋팅하는 함수로 플레이어가 제공한다.

Array

<화면 5> JsToFlash 수행 결과

<화면 5>는 이 페이지를 수행한 결과이다. HTML 페이지에 입력된 값을 SWF로 전달해 입력된 문자열이 그대로 SWF에 표시되는 것을 확인할 수 있다.

movie.SetVariable("inputText", form1.inputVal.value) ;

SWF에 있는 텍스트박스의 변수명(vars)가 “inputText”이다. form1에 있는 값을 SWF가 가지고 있는 “inputText” 변수에 할당하는 것이다.

<HTML>
<HEAD>
<meta http-equiv=Content-Type content="text/html;  charset=">
<TITLE>JsToFlash</TITLE>
<SCRIPT LANGUAGE="JavaScript">
<!--
function SetFlashVariable() {
var movie = window.document.JsToFlash;
movie.SetVariable("inputText", form1.inputVal.value) ;
}
//-->
</SCRIPT>
</HEAD>
<BODY bgcolor="#999999">
<!-- URL's used in the movie-->
<!-- text used in the movie-->
<OBJECT classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"
WIDTH="300" HEIGHT="100" id="JsToFlash" ALIGN="">
<PARAM NAME=movie VALUE="JsToFlash.swf">
<EMBED src="JsToFlash.swf" WIDTH="300" HEIGHT="200"
NAME="JsToFlash" ALIGN=""
</EMBED>
</OBJECT>
<br/>
<form id="form1">
<input name="inputVal" type ="input" value="여기에
입력하세요"><br/>
<input type ="button" value="플래시에설정" onClick="SetFlashVariable
()">
</form>
</BODY>
</HTML>

플래시와 플래시간 통신
하나의 페이지에 두 개 이상의 플래시가 있고 이들 사이에 통신을 하려면 어떻게 할까? 플래시 MX 이전에는 SWF간에 통신을 하기 위해 앞서 살펴본 자바 스크립트를 중간에 이용해야 했다. 즉, SWF 자바 스크립트 SWF 이런 방법을 사용했다. 이 방법은 브라우저에 의존하는 면이 강해서 익스플로러나 넷스케이프에 따라 다르게 처리를 해줘야 하므로 매우 불편했다.
플래시 MX에서는 SWF간 통신을 지원하기 위해 LocalConnection라는 객체를 제공한다. 같은 브라우저 내에서는 간편하게 SWF간에 통신을 할 수 있다. 여러 개의 윈도우를 가진 플래시 컨텐츠를 비교적 간단하게 만들 수 있다. 예를 들어, 메뉴를 담고 있는 SWF과 내용을 보여주는 SWF를 따로 제작해 훨씬 유연한 컨텐츠를 제작할 수 있을 것이다. 간단히 사용법을 살펴보자. 먼저 명령을 수신하는 쪽은 다음과 같이 작성한다.

// LocalConnection 객체를 생성
lcObj = new LocalConnection();
// 객체에 onContentSelect 메쏘드를 정의
lcObj.onContentSelect = function(num) {
// num 값에 따라 처리
}
// lcObj를 오브젝트를 아이디로 "contents"를 지정
lcObj.connect("contents");
상대편, 즉 명령을 보내는 쪽은 다음과 같이 작성한다.
lcObj = new LocalConnection();
// 메뉴 선택시 "contents"로 전송하는 데이터 정의
_root.menu1.onRelease = function() {
lcObj.send("contents", "onContentSelect", 100);
}
_root.menu2.onRelease = function() {
lcObj.send("contents", "onContentSelect", 200);
}

이처럼 LocalConnection을 사용하면 자바 스크립트를 작성하지 않고도 훨씬 간편하게 SWF간에 데이터를 송수신 할 수 있다.
플래시 HTTP 통신
getURL ()은 특정 URL을 호출하는 기능을 가지고 있다. 따라서 URL을 다음과 같이 지정한다면 특정 URL에 값을 전달하도록 할 수 있다. 예를 들어, SWF에서 입력받은 결과를 특정 페이지에 다음과 같이 전달할 수 있다.

getURL( 'http://localhost/memo.aspx?n="홍길동"')

getURL()보다 훨씬 다양하게 사용될 수 있는 것이 loadVariables() 함수이다. 이 함수는 URL에서 값들을 읽어 올 수 있다. URL은 파일일 수도 있고 http로 시작하는 페이지일 수도 있다. loadVariables()는 다음과 같은 형태로 사용한다.

loadVariables ("url" , level/"target"[, variables])
loadVariablesNum ("url" ,level [, variables])

만약에 loadVariables()가 “이름1=값1&이름2=값2&이름3&값3”과 같은 값을 수신했다면, SWF 내의 변수 중에서 “이름1”에 “값1”을 할당하게 된다. 여러 개의 값을 한꺼번에 지정할 때 각기 다른 이름을 줘야하므로 보통 변수명에 번호를 붙여서 사용한다.

◆ loadVariablesNum 샘플
on(release) {
loadVariablesNum("phonebook.txt", 0);
loadVariablesNum("data.txt", 0);
}
◆ phonebook.txt의 내용
name=홍길동&phone=013-443-2331
◆ data.txt의 내용
memo=안녕하세요? 길동님.. 메모를 남깁니다.

여기서는 phonebook.txt와 data.txt 파일에서 값들을 읽어서 변수에 할당하는 예이다. SWF에는 name, phone, memo를 변수 이름으로 가지는 객체가 각각 있다.
phonebook.txt, data.txt와 작성된 html, swf 파일을 같은 폴더에 웹 서버에 올려놓고 테스트를 해 보라. 수정해야 할 것은 전혀 없다. 즉, 로컬에서 수행되는 것과 마찬가지로 웹상에서도 똑같이 수행된다. 물론 다음과 같이 특정 URL을 지정할 수 도 있겠지만, 파일명만 지정하면 기본적으로 SWF가 다운로드된 URL과 같은 위치를 요청하게 된다.

loadVariablesNum("http://localhost/test/phonebook.txt", 0);

이제 ASP.NET 페이지에 데이터를 보내고 보낸 내용에 따라 다르게 반응하는 예제를 만들어 보자.

Array

<화면 6> 예제 06-loadVariables2 수행 결과

<화면 6>과 같이 화면을 구성하고, 메모로 되어 있는 회색 영역의 변수 이름은(vars)는 “memo”로 되어 있다. GET 버튼에는 다음과 같은 액션 스크립트가 작성되어 있다. 이 내용은 localhost의 test 폴더에 memo.aspx를 호출하는 내용이다.

on(release) {
loadVariablesNum("http://localhost/test/memo.aspx?name=" + name, 0);
}

서버측의 페이지는 아주 간단한 ASPX 페이지로, “홍길동”이 인자로 전달되면, “memo=홍길동님 12시까지 오세요”란 문자열이 리턴되고 아닌 경우는 “memo= 메모가 없읍니다”가 리턴된다(<리스트 1>).

<리스트 1> memo aspxArray
<%@Page Language="C#" Codepage="949"%>
<%
   if(Request.Params["name"] == "홍길동")
   Response.Write("memo=" + "홍길동님 12시까지 오세요");
   else
   Response.Write("memo=" + "메모가 없읍니다");
%>
Array

여 기서 한 가지 주의할 것이 있다. 앞서 이야기한대로 System.useCodepage = true로 하고 <리스트 1>의 memo.aspx 파일이 있는 곳에 web.config를 다음과 같이 수정한다. 요청(request) 인코딩과 응답(response) 인코딩을 다음과 같이 모두 “ecu-kr”로 맞춰준다. 이렇게 하는 것이 테스트하기도 좋고, 실제 대부분의 사이트들이 EUC-KR(KSC5601)을 사용하기 때문에 글자가 깨지는 문제를 일으키지 않는다.

◆ web.config 파일의 일부
<globalization
   requestEncoding="euc-kr"
   responseEncoding="euc-kr"
/>

loadVariables는 배경색과 같은 SWF의 설정 등을 읽어오는데도 사용할 수 있다. 가능한 데이터를 SWF 안에 두지 말고 파일이나 혹은 특정 URL(apsx 페이지)에서 생성된 데이터를 읽어 들여 처리하는 것이 훨씬 좋다. 내용이 변경되어도 SWF를 다시 제작하는 수고를 덜어 줄 것이기 때문이다. 훨씬 더 유연하고 동적인 SWF를 구성할 수 있다.
LoadVars 객체
LoadVars 객체는 앞서 이야기한 loadVariables()를 대신해 사용할 수 있으며, 훨씬 다양한 기능을 제공한다. 특히 다양한 데이터를 읽어 들어 처리할 때 매우 유용하다. loadVars()는 다음과 같이 객체를 생성하여 사용한다.

lv = new loadVars();

loadVars 객체는 다음과 같이 다양한 메쏘드와 속성을 제공한다(<표 3>).

메쏘드 load() 지정된 URL에서 변수를 가져온다
메쏘드 getBytesTotal() load()/sendAndLoad()에서 로드한 바이트 수를 리턴
메쏘드 send() 변수를 지정된 URL로 전송
메쏘드 sendAndLoad() 변수를 지정된 URL로 전송하고 응답을 수신
메쏘드 toString() 열거 가능한 변수를 포함하는 URL 문자열을 반환
속성 contentType 데이터의 MIME 타입
속성 loaded sendAndLoad()가 완료되었는지 여부
이벤트 onLoad load(), sendAndLoad()가 완료되었을 때 발생
이벤트 onData onLoad와 비슷(raw 데이터 처리)

<표 3> loadVars 메쏘드와 속성

<리스트 2>는 액션 스크립트 예제이다. 액션 스크립트의 Array를 이용했고 onLoad를 이용해 load가 다 끝난 뒤에 데이터를 처리하도록 했다. 데이터가 로드되면 loaded가 함수가 수행된다.

<리스트 2> 주소록 액션 스크립트 Array

System.useCodePage = true;
index = 0;
maxIndex = 0;
addressbook = new Array();
function loaded() {
   maxIndex = Number(this.loop);
   rec = "" + (index + 1) + "/" + maxIndex;
   for (i = 0; i < maxIndex; i++) {
      card = new Object()
      card.name = this["name"+i];
      card.phone = this["phone"+i];
      card.memo = this["memo"+i];
      addressbook.push(card);
   }
}
var lv = new loadVars();
lv.onLoad = loaded;
lv.load("https://plusjune/test/addressbook.aspx");

Array

< 리스트 3>는 이전(prev) 버튼을 눌렀을 때 동작하는 액션 스크립트이다. <리스트 4>는 서버측 ASPX 페이지이며, 여기서 이 페이지에서 데이터를 보기 좋게 하기 위해 ‘&’ 문자를 두 번에 나눠 쓴 것을 눈여겨보자. ‘&’을 연속해서 사용해 개행 문자 사이를 이어 줄 수 있다.

<리스트 3> Prev 버튼에 대한 이벤트 핸들러 Array
on(release) {
   if(index > 0)
      index--;
      name = addressbook[index].name;
      phone = addressbook[index].phone;
      memo = addressbook[index].memo;
      rec = "" + (index + 1) + "/"
+ maxIndex;
}
Array

 

<리스트 4> addressbook.aspx Array
<%@Page Language="C#" Codepage="949"%>
<%
   Response.Write(@"&name0=김철수&phone0=02-233-2321&memo0=사용자1메모&
   &name1=홍길동&phone1=02-343-3232&memo1=홍 길동 사용자2메모&
   &name2=김란아&phone2=02-3322-4544&memo2=김란아 전화 메모&
   &name3=user2&phone3=02-7533-3344&memo3=사용자 2 메모&
   &loop=4&
   ");
%>
Array

 

< 화면 7>은 이 주소록의 수행되는 화면이다. 총 4개의 주소를 ASPX 페이지로부터 로드하여 이전 이후를 볼 수 있도록 되어 있다. 서버측이 무척 간단하게 되어 있지만 요청을 받았을 때 이 내용을 데이터베이스에서 읽어오도록 할 수도 있을 것이다. 결과적으로 플래스 MX가 HTTP 통신을 위해 다양한 방법들이 제공되지만 LoadVars 객체를 이용하는 것이 가장 유연하고 좋은 방법이다.

Array

<화면 7> loadVars 예제 수행결과

플래시의 경쟁자들
SVG 포맷은 벡터그래픽을 위한 표준 XML 문서로 1998년 어도비, 썬마이크로시스템즈, 넷스케이프가 제안한 PGML(Precisson Graphics Markup Language)과 매크로미디어와 마이크로소프트가 제안한 VML(Vector Markup Language)을 기반으로 만들어졌다. SVG는 W3C의 표준 권고안으로 채택되어 어도비를 포함하여 여러 회사에서 지원하고 있다.
플래시의 SWF 파일 포맷은 공개되어 있지만 FLA 파일 포맷은 공개되어 있지 않다. 반면 SVG는 오픈 표준으로 순수하게 XML로만 제작이 가능하다는 장점 때문에 오픈 진영에서도 다양한 도구들이 많이 만들어 지고 있다. 반면에 플래시는 이미 광범위하게 사용되고 있어 SVG와 플래시가 향후 어떻게 발전할지 그 귀추가 주목된다.
사실상 플래시는 클라이언트 환경에서 자바 애플릿을 제치고 좀더 다양한 기능을 제공하고 있다(이 배경에는 마이크로소프트가 JVM을 더 이상 지원하지 않는 이유도 크게 작용했다). 플래시는 자바 애플릿과 달리 프로그래밍 도구가 아닌 디자인 도구에서 출발했다는 점이 풍부하고 다양한 컨텐츠를 가능하게 했고, 이 때문에 플래시가 더욱 많이 사용되게 됐다. 아직 CLR이 설치된 클라이언트가 그리 많지는 않지만 장기적으로 볼 때, 닷넷의 XML 웹 서비스, 리모팅 기술을 포함한 스마트 클라이언트 기술도 플래시의 경쟁 기술 중의 하나가 될 수 있다.
이제 플래시 MX는 개발 도구로서의 면모를 갖추기 시작했고, 기업 환경에 적용될 채비를 모두 갖추었다. 닷넷이나 웹 서비스 등의 분야들과 마찬가지로 플래시 또한 하나의 새로운 개발 영역이 되었으며 사용상의 편의와 풍부한 인터페이스를 기반으로 계속 확장될 것이다.
이 번 글에서는 플래시 MX의 다양한 통신 방법과 기본적인 HTTP 통신에 대해 살펴봤다. 예제들을 실행해 FLA 파일을 살펴보면 충분히 소화할 수 있을 것이다. 플래시를 처음 접하는 독자라면 인터넷에 많은 플래시 강좌들이 있으니 이들을 먼저 접해보기 바란다. 다음에는 플래시에서 XML 문서를 다루는 방법들과 XML 웹 서비스 그리고 리모팅에 대한 주제를 다뤄 본다. @

지난 글에서 우리는 플래시의 통신 방법에 대해 살펴봤다. 지난 글에 살펴본 통신 방법 외에 중요한 통신 방법이 있는데 바로 XML이다. XML에 대한 중요성은 더 얘기할 필요가 없을 것이다. 더구나 다음과 같은 플래시의 이점과 함께 XML의 유연성을 함께 사용할 수 있다면, 금상첨화라고 해야 할 것이다.

◆ 리치 인터넷 애플리케이션을 구현
◆ 동적 업데이트
◆ 백그라운 다운로드, 트래픽 감소

XML 자체가 많은 이슈를 가지고 있지만, 가장 중요한 부분은 물론 XML 웹 서비스(SOAP)이다. 많은 통신 규약들도 XML로 만들어지고 있다. 예를 들어, 오픈 소스 메신저인 Jabber 같은 경우가 그렇고, B2B와 EAI 등의 이슈와 함께 항상 빠지지 않고 등장하는 게 바로 XML이다. 이제 플래시가 지원하는 XML과 ASP.NET의 웹 서비스 등이 어떻게 서로 연동될 수 있는지 그 넓은 가능성을 하나하나 짚어 보자(이 글에서는 플래시의 화면 출력 방법이나 애니메이션 등에 대해서는 설명하지 않는다).
플래시 5 이상의 버전에서 두드러진 변화 중 하나가 바로 XML에 대한 지원 기능이다. 플래시는 기본적으로 XML 문서를 파싱하고, DOM을 다루기 위한 다양한 기능들을 제공한다. XML 문서 구조를 만들거나 XML을 읽어와서 DOM으로 구성하거나 내부에서 DOM을 생성할 수 있다. 다만, XPath와 같은 기능이 없어 XML 내용을 참조하기 위해 hasChildNodes(), firstChild, nextSibling 등을 반복적으로 사용해야 한다는 점이 아쉽지만, 플래시 애플리케이션에서 필요한 XML과 XML 웹 서비스 관련 기능을 대부분 제공하고 있다. XML을 사용하기 위해서는 다음과 같이 XML 클래스를 이용해 인스턴스를 만든다.

xmldoc = new XML(); // XML 객체 생성

혹은, 다음과 같이 XML를 생성자에 직접 지정할 수도 있다.

xmldoc = new XML("<book><title>어떻게 문제를 풀 것인가</title></book>");

하지만, 문자 배열을 사용하는 것이 훨씬 간편하기 때문에 이렇게 내부에서 DOM을 생성하는 경우보다는 외부에서 XML 문서를 읽어서 처리하는 경우가 훨씬 많다. 플래시에서 XML 조작과 관련해 자주 사용하는 메쏘드를 정리하면 <표 1>과 같다.

getByteLoaded() 로딩된 XML 문서의 크기를 얻는다.
hasChildNodes() 자식 노드가 있는지 확인한다.
load() XML 문서를 로드한다.
parseXML() 문자열로 된 XML 소스를 파싱한다.
send() XML 문서를 외부로 보낸다.
sendAndLoad() XML 문서를 외부로 보내고 리턴되는 XML 문서를 로드한다.
toString() XML 객체를 문자열로 리턴한다.
firstChild 첫 번째 자식 노드에 대한 참조
ignoreWhite XML 소스에서 여백을 무시할 것인지 여부
loaded load(), sendAndLoad() 작업 완료 여부
nextSibling 현재 노드와 같은 레벨에 있는 다음 형제 노드
previousSibling 현재 노드와 같은 레벨에 있는 이전 형제 노드
nodeName 현재 노드의 이름
nodeValue 현재 노드의 값
onData 데이터를 수신했을 때 발생하는 이벤트
onLoad 데이터를 수신한 후 XML 문서를 파싱했을 때 발생하는 이벤트

<표 1> XML에 쓰이는 메쏘드

XML 파일 읽어오기
loadVars 객체를 사용하는 것보다 약간 번거롭지만, XML을 읽고 조작하는 것은 매우 중요하다. XML 자체가 가지는 장점 외에도 XML 웹 서비스를 이해하고 활용하기 위한 필수적인 단계이기 때문이다. 우선 XML 문서를 로드하고, 내용을 어떻게 읽어내는지 살펴보자. 노트패드와 같은 텍스트 편집기로 <화면 1>과 같은 내용을 가진 “sample.xml”이란 XML 파일을 다음과 같이 만든다.

Array

<화면 1> sample.xml

이 파일을 플래시에서 어떻게 읽어내는지 살펴보자. 새로운 플래시 프로젝트를 만든다. 첫 번째 프레임에 <리스트 1>과 같은 액션스크립트를 작성한다(이달의 디스켓 : loadxml_sample1.fla 파일에 포함된 액션스크립트이다).

<리스트 1> XML 문서를 읽는 간단한 예제Array
System.useCodepage = true;
xmldoc = new XML();
xmldoc.ignoreWhite = true;
xmldoc.load("sample.xml");
Array

 

지난 글에 소개한 대로 useCodepage는 코드 페이지를 사용하도록 한다. 플래시 내부적으로는 기본적으로 모두 유니코드 문자를 사용하는데, 외부 파일이나 통신할 때 유니코드(UTF-8)로 되어 있는 경우는 그렇게 많지 않다. 따라서 거의 대부분 첫 프레임에 다음과 같이 기술해 유니코드를 사용하지 않고, 현 시스템의 코드 페이지를 사용한다(이렇게 하면 한글 윈도우의 경우 기본적으로 ‘euc-kr’을 사용하게 된다).

System.useCodepage = true;

XML.ignoreWhite 프로퍼티는 원문 XML에서 공백문자를 무시하도록 한다. 만일, 이것을 지정하지 않으면 공백 문자 부분도 하나의 노드가 되어 다루기가 매우 힘들어진다.

xmldoc.ignoreWhite = true;

타임라인의 5번 키 프레임을 추가하고, <리스트 1.1>의 액션스크립트를 입력한다. <Ctrl+Enter>를 눌러 수행시키면 다음과 같은 수행 결과를 볼 수 있다.

isbn = 8981723427
title = 어떻게 문제를 풀것인가
author = G.폴리아
review = 이책은 수학적 사고 방법에 대해서 설명하고 있다.

<리스트 1.1> 로드 상태를 확인 : <리스트 1>에서 이어짐(loadxml_sample1.fla)

if(xmldoc.loaded = true )
{
   var ref = xmldoc.firstChild.firstChild.firstChild;
   for( var ch = ref; ch != null; ch = ch.nextSibling ) {
      trace ( ch.nodeName + " = "
+ ch.firstChild.nodeValue );
   }
   this.stop();
} else {
   gotoAndPlay(2);
}

예제에서 사용한 gotoAndPlay()는 플래시에서 흔히 사용하는 기법으로, 데이터 로딩이나 복잡한 계산처럼 시간이 많이 걸리는 일이 끝났는지를 확인한다. 즉, 다음과 같이 끝나지 않았으면 이전의 다른 프레임으로 이동해 처리중임을 표시하는 용도로 종종 사용한다.

if( 작업이 끝났는지 확인 )
{
} else {
   gotoAndPlay(2);
}

이런 방법보다 더 명시적이고 효과적인 것은 XML.onLoad()를 사용하는 것이다(<리스트 2>). onLoad()는 로드가 끝났을 때 즉, load() 혹은 sendAndLoad() 메쏘드 수행이 끝났을 때 불려지는 이벤트이다. <리스트 2>는 onLoad()를 사용해 앞의 예제와 비슷한 기능을 수행하도록 한 코드이다. 수행 결과는 똑같다.

<리스트 2> onLoad()을 이용한 XML 처리 Array
System.useCodepage = true;
xmldoc = new XML();
xmldoc.onLoad = loaded;
xmldoc.ignoreWhite = true;
xmldoc.load("sample.xm");
function loaded(success) {
   if (success == true) {
      var ref = xmldoc.firstChild.firstChild.firstChild;
      for (var ch = ref; ch != null; ch=ch.nextSibling)
{
         trace(ch.nodeName+"
= "+ch.firstChild.nodeValue);
      }
      trace(xmldoc.getBytesTotal());
      this.stop();
   } else {
      trace("xml load error");
   }
}
Array
XMLSQL의 역할
오 라클 역시 XML에 대한 지원이 많지만 SQL 서버 2000은 XML에 대한 다양하고 많은 기능을 제공한다. SQL 서버가 제공하는 XML 기능 가운데 XMLSQL이 있는데, 이것의 큰 특징은 애플리케이션을 거치지 않고 웹 서버(IIS)를 통해 DBMS에 바로 쿼리할 수 있다는 점이다. ASP.NET 코드 없이도 바로 DBMS만으로 웹 애플리케이션을 작성할 수 있게 해준다.
따라서 개발자는 XML과 관계형 데이터의 차이를 극복할 수 있다. 쿼리 결과를 XML로 생성한다든가 혹은 XML 문서를 바로 관계형 데이터베이스에 저장하는 것은 관계형 데이터를 일종의 XML 파일처럼 작업할 수 있게 해준다. 간단한 예를 통해 구현해 보자. SQL 서버에 다음과 같은 스키마의 테이블을 작성하고 데이터를 입력한다.

CREATE TABLE article (
id int IDENTITY (1, 1) NOT NULL ,
c_time datetime NULL ,
u_time datetime NULL ,
title varchar (128) NULL ,
content varchar (2000) NULL
)

SQL 서버 프로그램 그룹에서 “Configure SQL XML Support in IIS”을 선택해 수행하고 다음과 같은 절차로 가상 디렉토리를 만든다(<화면 2>, <화면 3>).

1 General Virtual Directory Name을 입력(여기서는 "board"), 물리적인 경로를 설정한다("C:Inetpubwwwrootboard").
2 Security 탭에서 Windows 통합 인증을 선택
3 DataSource 탭 SQL Server("(local)")와 Database 이름("board")을 각각 선택
4 Virtual Names 탭 New를 선택하고, type에서 dbobject를 선택한 후 적절한 가상 이름을 입력("dbo_board")

Array

<화면 2> SQL 서버 IIS 가상 디렉토리 관리자

Array

<화면 3> SQLXML 등록정보

XMLSQL은 별도 설치가 가능하며, SQL 서버 2000 SP3에 추가된 내용이다. SQL 서버의 도움말에서 “SQL 서버용 IIS 가상 디렉터리 관리 유틸리티 사용”, “nwind 가상 디렉토리 만들기” 항목을 참조하라. 다음과 같은 URL을 브라우저에 입력해 보자.

http://plusjune/board?sql=SELECT * FROM article FOR XML AUTO, ELEMENTS&root=Articles

앞에 입력한 URL은 자동으로 인코딩돼 다음과 같은 형태가 된다.

http://plusjune/board?sql=SELECT%20*%20FROM%20article%20FOR%20XML%20AUTO,%20ELEMENTS&root=Articles

이러한 내용을 입력했을 때 쿼리의 결과가 표시된다. <화면 4>는 수행 결과를 보여준다.

Array

<화면 4> SQL 서버의 HTPP 쿼리

SQL 서버의 HTTP-XML 쿼리는 티어를 분리하지 않기 때문에, 다소 유연성은 떨어지지만 간편하게 쿼리하고 그 결과를 XML로 가져오거나 XML 문서를 관계형 데이터베이스에 저장할 수 있다. 때문에 관리자 페이지나 데이터베이스의 내용을 모니터링하는 - 중요한 비즈니스 로직과 크게 관련이 없는 - 내용이라면 충분히 도입을 생각해 볼만하다. <화면 4>와 같이 XML로 결과가 나오면 XSL을 적용해 바로 게시판을 만들 수도 있다. 앞서 살펴본 플래시의 XML 로딩을 여기에 직접 사용할 수도 있다. XML.load() 메쏘드는 기본적으로 URL을 로드하기 때문에, XMLSQL을 이용하면 데이터베이스에 직접 접근하는 애플리케이션을 제작할 수 있다.
XMLSocket를 이용한 다중 사용자 채팅 프로그램 개발
플래시의 getURL(), LoadVars, XML 등의 메쏘드는 모두 URL에서 지정된 리소스를 읽어오거나 HTTP 연결을 사용하는 비 연결형(non-connection oriented) 통신이다.
XMLSocket 은 플래시 5에 추가된 클래스로 SWF와 서버 사이에 TCP 연결을 맺을 수 있기 때문에. 채팅이나 게임과 같이 연결이 필요한 서비스를 위한 애플리케이션 개발에 사용할 수 있다. 연결형(connection oriented) 서비스는 서버에서 클라이언트로 알려줄 수 있기 때문에 훨씬 다양한 응용이 가능하다. 즉, 클라이언트가 주기적으로 변동 사항을 조회하는 것이 아니라 변화가 생겼을 때 서버가 클라이언트에 정보를 줄 수 있기 때문에 훨씬 다양하고 대화적인 애플리케이션이 가능하다. XMLSocket 객체는 다음과 같이 생성한다.

socket= new XMLSocket();

서버에 연결은 다음과 같이 한다. 서버를 지정하고 포트를 지정하는데, 포트 번호는 1024번 이상이어야 한다.

sock.connect( "192.168.0.55", 65000);

<표 2>는 XMLSocket 클래스에서 자주 사용되는 메쏘드와 이벤트를 정리한 것이다. 대략 살펴봐도 사용 방법은 다른 통신 객체들과 크게 다르지 않지만 close(), onClose처럼 연결에 관련된 메쏘드와 이벤트가 있다.

메쏘드 connect 서버에 TCP 연결한다.
메쏘드 send 서버에 문자열 전송한다.
메쏘드 close 연결을 닫는다.
이벤트   서버와 연결이 끊겼을 때 발생
이벤트 onConnect 서버로 연결됐을 때(혹은 연결 시도가 실패했을 때)
이벤트 onData 서버에서 데이터가 도착했을 때
이벤트 onXML 서버에서 도착한 XML 문자열 파싱이 끝났을 때

<표 2> XMLSocket 클래스용 메쏘드와 이벤트

onConnect (success)는 XMLSocket.connect 메쏘드를 통해 초기화된 연결 요청이 성공하거나 실패했을 때 불려지는 이벤트이다. onData()와 onXML()는 둘 다 서버로부터 데이터가 도착했을 때 불리는데 용도가 다르다. 순서상으로는 onData()가 먼저 불린다. onData()는 데이터가 도착했을 때, onXML()는 도착한 데이터가 XML로 파싱됐을 때 불린다.
<리스트 3>은 XMLSocket을 생성해 연결하고 onXML()을 사용해 수신된 데이터를 처리하는 코드이다. OnConnect()를 사용해 연결의 성공여부를 확인하는 내용도 잘 살펴보자. <리스트 3>을 수행하면 다음과 같은 결과가 출력된다.

>>서버에 연결되었읍니다
PeerAddress = 192.168.0.55:2231
DateTime = 오후 1:53:34
Context = 안녕하세요?

<리스트 3> XMLSocket 객체를 이용한 연결 Array  
System.useCodePage = true;  // 코드페이지 사용
sock = new XMLSocket();
sock.onConnect = OnConnect;
sock.onClose = OnClose;
sock.onXML = OnXml;
sock.connect( "192.168.0.55", 65000);
function OnXML( xmldoc )
{
   trace(xmldoc.toString());
}
function OnConnect(success)
{
   if (success == true) {
      trace(">>서버에 연결되었읍니다")
      sock.send("안녕하세요?");
   } else {
      trace(">>연결실패")
   }
}
function OnClose()
{
   trace(">>연결이 끊겼습니다")
}
Array

 

서버는 C#으로 작성됐으며, 서버로 전달된 내용을 XML로 만들어 연결된 모든 클라이언트에 보내주는 간단한 채팅 서버이다. 이제 C# 채팅 서버를 제작해 보자. 채팅 서버에서 여러 클라이언트의 연결을 처리하기 위해서는 소켓 연결 객체의 목록을 관리할 필요가 있다. <리스트 4>는 XmlChatServer.cs의 주요 부분이다. 전체적인 흐름을 살펴보자. 이 코드에는 대략 다음과 같은 주요한 이슈들이 구현되어 있다.

◆ 다중 사용자 연결 위해 ClientSocekt 목록 관리
◆ 비동기 I/O
◆ 소켓 입출력에 ‘euc-kr’ 인코딩

이 중 비동기 프로그래밍과 다중 사용자 채팅을 위한 목록 관리 부분은 이 글의 범위를 벗어나므로 논외로 한다.

<리스트 4> XmlChatServer.cs의 주요 부분 Array  
// XmlChatServer
// 1. 다중 사용자 연결 위해 ClientSocekt 목록 관리
// 2. 비동기 I/O
// 3. 소켓 입출력에 "euc-kr" 인코딩
// (c) 2003 eLasticWare, plusjune
using System
using System.IO;
using System.Net.Sockets;
using System.Collections;
using System.Text;
using System.Xml;
namespace XmlServer
{
    class XmlServer
    {
        private ArrayList clientList;
        public XmlServer()
        {
            clientList = newArrayList();  // 클라이언트 소켓 목록 관리
        }
// 중략
        private void OnReceive(IAsyncResult ar)  // 비동기 데이터 수신
        {
            ClientSocket client = (ClientSocket)ar.AsyncState;
            ClientSocket socketReceiveData = client;
            try
            {
                byte[] recvBytes= client.GetReceivedData(ar);
                if ( recvBytes.Length < 1) throw new Exception();
                // 수신 데이터를 인코딩한다.
                Encoding e = Encoding.GetEncoding("euc-kr");
                stringrecvXml = e.GetString( recvBytes );
                Console.WriteLine( "recv ({0}) : '" + recvXml + "'",
                client.PeerAddress );
                // TOD: 수신된 XML을 여기서 처리한다.
                // 보낼 문자열을 만든다.
                string strSend = @"<?xml version=""1.0"" encoding=""euc-kr"" ?>"
                + "n" + "<Message>n" + "<PeerAddress>" +
                client.PeerAddress + "</PeerAddress>n" + " <DateTime>" +
                DateTime.Now.ToLongTimeString() + "</DateTime>n" +
                "<Context>" + recvXml + "</Context>n" + "</Message>n";
                // 연결된 모든 클라이언트에 메시지를 보낸다.
                for ( int index = 0; index < clientList.Count; index++ )
                {
                    client = ((ClientSocket)clientList[index]);
                    client.SendMessage(strSend );
                    Console.WriteLine("send ({0}) : '" + strSend + "'",
                    client.PeerAddress );
                }
                // 비동기 수신 다시 시작
                socketReceiveData.Start( new AsyncCallback(OnReceive) );
            }
            catch
            {
                clientList.Remove(client);
                client.Release();
            }
        }
        static void Main(string[] args)
        {
            XmlServer tcpServer = new XmlServer();
            tcpServer.Run();
        }
    }   
    class ClientSocket
    {
        private Socket socket;
        private byte[] receiveBuffer;
// 중략
        public void Start(AsyncCallback receiveCallback)
        {
            socket.BeginReceive(receiveBuffer, 0, receiveBuffer.Length,
                SocketFlags.None, receiveCallback, this );
        }
        public byte[] GetReceivedData(IAsyncResult ar)
        {
            int nReceived = socket.EndReceive(ar);
            byte[] buff = new byte[nReceived];
            Array.Copy(receiveBuffer, buff, nReceived);
            return buff;
        }
        public void SendMessage(string str)
        {
            Encoding e = Encoding.GetEncoding("euc-kr");
            byte[] byteBuffer = e.GetBytes(str);
            socket.Send(byteBuffer);
        }
//  중략
    }
}
Array
인코딩 문제를 잠깐 살펴보자. XML 문서를 처리할 때 몇 가지 이슈가 있는데, 가장 먼저 극복해야 할 것이 바로 인코딩 문제이다. 이전에도 이야기했듯 애플리케이션 외부의 데이터는 대부분의 KSC5601(“euc-kr”) 인코딩이다. 물론 유니코드로 통신할 수도 있겠으나 환경을 생각해 보면 반드시 인코딩을 고려해야 한다. 닷넷에서 인코딩은 System.Text.Encoding 네임스페이스를 사용한다. 우리에게는 유니코드나 아스키 인코딩보다 한글에 대한 지원이 우선이므로 주로 다음을 사용하게 된다.

System.Text.Encoding.GetEncoding(int codepage)

여기서 코드페이지란 코드 체계에 부여되는 번호이다. 한국어(KSC5601) 코드 페이지는 949번이며, 문자열로는 “euc-kr”로 표시한다. 소켓을 통해 송수신되는 데이터는 바이트 배열인데, 이것을 유니코드 String으로 바꾸려면 인코딩을 해야 한다. <리스트 5>는 KSC5601과 유니코드 사이에 인코딩하는 간단한 예를 보여준다.

<리스트 5> KSC5601과 유니코드 인코딩 예제 Array  
// Unicode 인코딩 : 유니코드 ==> KSC5601
string str = "abc가나다";
Encoding e = Encoding.GetEncoding(949);
byte[] byteBuffer = e.GetBytes(str);
for(int i = 0; i < byteBuffer.Length; i++)
        Console.Write("0x{0:X}
", byteBuffer[i]);
Console.WriteLine();
// 0x61 0x62 0x63 0xB0 0xA1 0xB3 0xAA 0xB4 0xD9
Array
onData 활용
XMLSocket 객체의 onData는 데이터가 도착했을 때 불린다. 특이할 만한 점은 onData를 사용하면 XML이 아니어도 처리가 가능하다는 것이다. 따라서 XMLSocket으로 반드시 XML만을 다뤄야 하는 것은 아니다. 즉, XMLSocket은 XML뿐만 아니라 일반적인 TCP 통신을 위한 소켓으로도 사용이 가능하다. 내부적으로 원래 onData는 다음과 같이 동작한다.

XMLSocket.onData = function (src) {
  this.onXML(new XML(src));
}

프로그래머가 onData을 오버라이딩하면, 즉 코드에서 onData를 재정의하면 onXML이 불리지 않는다(만일, onData와 onXML을 둘 다 사용하고 싶다면 onData의 핸들러 함수 끝에서 onXML 핸들러를 불러주면 된다). <리스트 6>은 완전한 예제이다. onData를 사용해 XML을 파싱하도록 했다. 이렇게 하는 이유는 ignoreWhite를 사용하기 위해서이다. 이렇게 하면 다양한 처리가 가능하다.

<리스트 6> onData(), onXML()을 이용한 파싱 Array  
System.useCodePage = true;  // 코드페이지 사용
sock = new XMLSocket();
sock.onConnect = OnConnect;
sock.onClose = OnClose;
sock.onData = onData;
sock.onXML = onXml;
sock.connect( "192.168.0.55", 65000);
function OnData ( src )
{
   var xmldoc = new XML();
   xmldoc.ignoreWhite = true;
   xmldoc.parseXML( src );
   OnXml( xmldoc )
}
function OnXml( xmldoc )
{
    xmldoc.ignoreWhite = true;
    var ref = xmldoc.firstChild.firstChild,firstChild;
        for (var ch = ref; ch != null; ch=ch.nextSibling) {
            trace(ch.nodeName+"
= "+ch.firstChild.nodeValue);
    }
}
function OnConnect(success)
{
   if (success == true) {
      trace(">>서버에 연결되었읍니다")
      sock.send("안녕하세요?");
   } else {
      trace(">>연결실패")
   }
}
function OnClose()
{
   trace(">>연결이 끊겼읍니다")
}
Array
< 화면 5>는 <리스트 6>에서 만들어진 SWF를 여러 개 수행하고 XmlChatServer를 수행한 결과이다. 여러 개의 SWF 연결이 관리되고, 메시지가 각 SWF에 전달되는 것을 확인할 수 있다. UI는 없지만 여러 클라이언트가 연결된 플래시 채팅이 가능하다는 것을 보여주고 있다.

Array

<화면 5> XmlChatServer 수행 결과

XML 웹 서비스 주가 조회 프로그램
다음 URL을 브라우저 창에 입력해 보자.

http://quote.yahoo.com/d/quotes.cvs?s="+SUNW+"&d=t&f=sl1d1t1c1ohgvj1pp2wern
http://quote.yahoo.com/d/quotes.csv?s="+MSFT+"&d=t&f=sl1d1t1c1ohgvj1pp2wern

야후에서 제공하는 주가 조회 서비스이다. 각각 썬과 MS의 주가 현황(20분 단위 갱신)을 보여준다. 약간 암호 같아 보이지만, 다음과 같은 결과가 브라우저 창에 표시된다.

"MSFT",26.1801,"6/20/2003","3:45pm",+0.1101,26.344,26.38,26.01,71121984,
281.1B,26.07,"+0.42%","20.705 - 29.48",0.88,29.62,"MICROSOFT CP"

<리스트 7>은 이 내용을 웹 서비스로 구현한 것이다. <리스트 7>의 내용은 quote.yahoo.com에서 리턴된 문자열을 ‘,’를 분리하여 객체로 만들고, 이 객체를 리턴하는 웹 서비스로 되어 있다(Stock 클래스의 속성이 [Serializable]로 되어 있다는 점을 주목하자). 객체를 리턴하면 XML 형태로 만들어진다. 즉, 객체를 XML로 시리얼라이즈한다).

Array

<화면 6> 주가 조회 웹 서비스 수행 화면

<리스트 7> 야후 주가 조회 웹 서비스Array  
// 야후 주가 조회 XML 웹 서비스
// 1. HttpWebRequest
// 2. 객체 시리얼라이제이션
// 3. Split()을 이용한 문자열 tokenize
// (c) 2003 eLasticWare, plusjune
using System;
using System.Collections;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.Web;
using System.Web.Services;
using System.Net;
using System.IO;
using System.Text;
namespace StockQuote
{
    [Serializable]
    public class Stock
    {
        public string Symbol;
        public string Last;
        public string Date;
        public string Time;
        public string Change;
        public string Open;
        public string High;
        public string Low;
        public string Volume;
        public string MktCap;
        public string PrevClose;
        public string PctChange;
        public string AnnRange;
        public string Earns;
        public string PE;
        public string Name;
    }
    [WebService(Namespace="http://elasticware.com/stockwebservices/")]
    public class StockService : System.Web.Services.WebService
    {
        public StockService()
        {
            InitializeComponent();
        }
#region Component Designer generated code
// 중략
#endregion
        [WebMethod]
        public Stock GetQuote(string symbol)
        {
            string buffer;
            string[] tokens = null
            stringurl=@"http://quote.yahoo.com/d/quotes.csv?s="+symbol+"&f=sl1d1t1c1ohgvj1pp2wern"
            WebRequest req;
            WebResponse res;
            Stock stock = new Stock();
            try
            {
                req = (HttpWebRequest)WebRequest.Create(url);
                res = (HttpWebResponse)req.GetResponse();
                StreamReader strm =
                   newStreamReader( res.GetResponseStream(), System.Text.Encoding.ASCII);
                buffer = strm.ReadToEnd();
                strm.Close();
                buffer = buffer.Replace( """, "" );
                tokens = buffer.Split( new char[] {','} );
                stock.Symbol = tokens[0];
                stock.Last = tokens[1];
                stock.Date = tokens[2];
                stock.Time = tokens[3];
                stock.Change = tokens[4];
                stock.Open = tokens[5];
                stock.High = tokens[6];
                stock.Low = tokens[7];
                stock.Volume = tokens[8];
                stock.MktCap = tokens[9];
                stock.PrevClose = tokens[10];
                stock.PctChange = tokens[11];
                stock.AnnRange = tokens[12];
                stock.Earns = tokens[13];
                stock.PE = tokens[14];
                stock.Name = tokens[15];
            }
            catch(Exception)
            {
                stock = null
            }
            return stock ;
        }
    }
}
Array

< 화면 6>은 주가 조회 웹 서비스를 브라우저에서 수행한 결과이다. ASP.NET은 SOAP을 이용하지 않고도 HTTP GET이나 POST로 웹 서비스를 사용할 수 있게 해준다. 따라서 이 웹 서비스를 이용하고 싶다면, 단지 다음과 같이 HTTP 요청을 보내면 된다.

GET /StockQuote/StockQuote.asmx/GetQuote?symbol=string HTTP/1.1
Host: localhost

예를 들어, MS의 주식을 조회하고 싶다면(MSFT는 MS의 주식 시장 코드이다),

http://localhost/StockQuote/StockQuote.asmx/GetQuote?symbol=MSFT

과 같이 HTTP 요청을 하면 된다. 플래시의 XML 객체를 이용하면, ASP.NET 웹 서비스를 간단한 HTTP로 요청해 수신할 수 있다. 앞 URL에서 XML 결과를 리턴받아 처리하는 액션스크립트를 작성해 보자.

<리스트 8> 주가 조회 웹 서비스Array  
System.useCodepage = true;
xmldoc = new XML();
xmldoc.onLoad = loaded;
xmldoc.ignoreWhite = true;
xmldoc.load( "http://localhost/StockQuote/StockQuote.asmx/GetQuote?symbol=MSFT"
)
function loaded(success) {
   if (success == true) {
      var ref = xmldoc.firstChild.firstChild;
      for (var ch = ref; ch != null; ch=ch.nextSibling)
{
         trace(ch.nodeName+"
= "+ ch.firstChild.nodeValue);
      }
      trace(xmldoc.getBytesTotal());
      this.stop();
   } else {
      trace("xml load error");
   }
}
Array
< 리스트 8>은 <리스트 7>, <화면 6>의 주가 조회 웹 서비스를 조회하는 액션스크립트이다. 간단하게 나열하고 있다(이 조회 내용을 ‘어떻게 예쁘게 보여줄 것인가’하는 것은 이 글에서 다루지 않는다). 다음은 <리스트 8>의 액션 스크립트를 수행한 결과이다.

Symbol = MSFT
Last = 26.62
Date = 8/19/2003
Time = 4:01pm
Change = +0.92
Open = 25.85
High = 26.65
Low = 25.77
Volume = 73001488
MktCap = 286.7B
PrevClose = 25.70
PctChange = +3.58%
AnnRange = 21.5585 - 29.48
Earns = 0.92
PE = 27.93
Name = MICROSOFT CP
624 byte(s) read

플래시와 SOAP
HTTP GET을 이용해도 ASP.NET의 웹 서비스를 사용할 수 있지만, 웹 서비스를 제대로 쓰려면 UDDI와 SOAP을 사용해야 한다. 다음은 주가 조회 웹 서비스를 이용하는 SOAP 요청이다.

POST /StockQuote/StockQuote.asmx HTTP/1.1
Host: localhost
Content-Type: text/xml; charset=utf-8
Content-Length: length
SOAPAction: "http://elasticware.com/stockwebservices/GetQuote"
<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xmlns:xsd="http://www.w3.org/2001/XMLSchema"
     xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
  <soap:Body>
    <GetQuote xmlns="http://elasticware.com/stockwebservices/">
      <symbol>string</symbol>
    </GetQuote>
  </soap:Body>
</soap:Envelope>

앞의 SOAP 요청은 XML 객체로 만들어 XML.sendAndLoad()를 사용해 호출할 수 있다. sendAndLoad()는 기본적으로 POST로 요청을 보낸다. 이런 SOAP 요청을 만들기 위해 HTTP 헤더에 “SOAPAction”을 넣어야 하는데, 플래시 5.0 이하의 버전에는 HTTP 헤더를 프로그래머가 추가할 방법이 없다(5.0 이하 버전에는 HTTP 헤더를 추가하거나 조작하는 기능이 없다). 플래시 5를 써야 한다면, GET으로 요청을 받아 SOAP 요청을 만들고 처리된 결과를 리턴하는 페이지를 만들어야 한다. 예를 들어, soapcall.aspx 페이지를 만들고 다음과 같이 필요한 정보를 GET 요청으로 페이지에 넘긴다.

http://server/soapcall.aspx?url=http://ws_server/StockQuote.asmx&SOAPAction=SoapAction

soapcall.aspx 페이지에서 SOAP 요청을 만들어 URL 인자와 SOAPAction 인자에 지정된 내용으로 SOAP 메시지를 만들어 요청한 다음 결과 XML을 클라이언트에 리턴하는 방법이다. 플래시 플레이어 6.0r65 이상의 버전에서는 HTTP 헤더를 정의하기 위해 XML 및 LoadVars 객체가 사용하는 HTTP 헤더에 사용자 정의 값을 지정할 수 있는 다음 두 가지 새로운 메쏘드가 있다.

◆ addRequestHeader(<header name>, <header value>)
◆ addRequestHeader(<headers>)

첫 번째 addRequestHeader는 여러 번 호출이 가능하다. 같은 헤더 이름을 지정하면, 나중에 설정한 값이 지정된다. 두 번째 addRequestHeader는 문자 배열을 사용해 한꺼번에 지정할 수 있는 메쏘드이다. 예를 들어, 다음과 같이 HTTP 헤더에 SOAPAction을 추가할 수 있다.

xmldoc.addRequestHeader("SOAPAction", ""GetQuote"");

addRequestHeader()은 6.0r65 버전 이상에만 적용되므로, 버전 호환성을 생각한다면 별로 좋은 방법이 아니다. 이보다는 간단하게 이용하는 경우에는 loadVars, XML 클래스를 이용하고, 본격적으로 SOAP을 사용하고자 한다면 플래시 리모팅을 이용하는 것이 좋다. 플래시 리모팅은 훨씬 간편한 방법으로 SOAP을 이용할 수 있게 할 뿐만 아니라 DB에 대한 접근 등 다양한 기능을 제공한다. 플래시 리모팅에 대해서는 다음 호에서 자세하게 살펴볼 것이다.
플래시 프로그래머
요 즘은 ‘플래시 디자이너’란 말은 익숙하지만 ‘플래시 프로그래머’라는 용어는 그리 널리 알려져 있지 않다. 플래시 클라이언트가 가진 다양한 기능을 최대한 활용하려면, ‘디자이너’로서가 아닌 ‘프로그래머’로의 접근도 중요한 요소가 될 것이다. 특히, 닷넷처럼 웹 서비스를 손쉽게 구축할 수 있는 환경과 맞물린다면 ‘플래시 프로그래머’는 더 큰 힘을 얻을 수 있다.

Array

<화면 8> 닷넷 PetShop 플래시 프론트엔드 수행 화면

매크로미디어 홈 페이지에 보면 PetMarket이란 블루프린트 프로그램이 있다. J2EE의 청사진 프로그램이었던 ‘자바 Pet Store’를 MS가 ‘'닷넷 Pet Shop’이란 이름으로 닷넷 버전을 개발해 이를 놓고 한 동안 자바와 닷넷 진영이 신경전을 벌였었다. 매크로미디어의 Pet Market은 ‘자바 Pet Store’와 ‘닷넷 Pet Shop’과 비슷한 맥락으로 만들어진 프로그램이다.
‘플래시 클라이언트’ + ‘닷넷 Pet Shop’ 버전(<화면 8>)은 닷넷 개발자든 플래시 개발자든 상당히 연구해 볼만한 가치가 있다. 플래시 클라이언트가 ASP.NET과 어떻게 잘 연동될 수 있는지에 대한 좋은 예제이다. 다음 글에서 플래시와 관련한 보안 문제를 다뤄보자. @

엔터프라이즈 애플리케이션에서 보안은 필수적인 요소이다. 이번 호에서는 플래시의 보안 이슈와 XML 웹 서비스를 이용하는 또 다른 강력한 방법인 플래시 리모팅에 대해 살펴본다.
지난 호에서 우리는 플래시에서 XML을 어떻게 다루고 XML 통신을 어떻게 하는지에 대해 살펴봤다. 하지만 간편하면서도 훨씬 강력한 방법이 제공되는데 그것이 바로 ‘플래시 리모팅(Flash Remoting)’이다.
플 래시 리모팅은 주로 XML 웹 서비스를 이용하기 위해 玲淪舊嗤? 닷넷 리모팅(.NET Remoting)이나 자바 RMI와 비슷하다. 즉, 리모팅을 통해 원격(서버 사이드)에 있는 메쏘드를 호출할 수 있다. 내부적으로는 SWF(플래시 플레이어에서 수행되는 플래시 파일을 가리킴)의 통신 기능을 플래시 리모팅 컴포넌트를 통해 확장한 것으로, 통신 프로토콜 자체는 플래시의 고유한 방법으로 구현되어 있다.

Array

<그림 1> 플래시 리모팅 개념

플래시 리모팅
플래시 리모팅은 SOAP 기반의 웹 서비스를 통합하기 위한 서버측 도구(실제로 웹 서버용 필터로 동작한다)이다(<그림 1> 참조). 콜드퓨전 MX, 닷넷, 자바 버전이 각각 따로 있으며, EJB 구축된 서비스, 닷넷 서버, 콜드퓨전 등으로 구축된 웹 서비스를 플래시에서 그대로 이용할 수 있도록 하기 때문에 웹 서비스를 이용하는 플래시 애플리케이션을 쉽게 구축할 수 있다. 즉, 비즈니스 로직은 서버측에 웹 서비스로 구축하고 클라이언트측 UI 플래시로 개발하려는 경우에 가장 좋다고 할 수 있다. 서버측 개발자는 ‘Flash Remoting MX’를 설치하는 것 외에 추가적인 코드를 작성해야 하는 일은 없다.
ASP.NET 웹 서비스를 만들고 플래시에서 이 웹 서비스를 호출하는 과정을 간단하게 살펴보자. ASP.NET 서버측에 ‘Flash Remoting MX for Microsoft .NET’이 설치되어 있어야 한다(Flash Remoting MX는 상용 제품이지만, 매크로미디어 사이트에서 평가판을 받아 충분히 테스트해 볼 수 있다). 일반적으로 다음 폴더에 설치된다.

C:Inetpubwwwrootflashremoting

플래시 리모팅을 사용하는 클라이언트 SWF를 개발하기 위해서는 플래시에 리모팅 컴포넌트(Flash Remoting Components)를 설치해야 한다(혼돈하지 말아야 한다. Remoting MX는 서버측에 설치되는 요소이며, 자바, 닷넷, 콜드퓨전 등의 버전이 따로 있다. Remoting 컴포넌트는 플래시에 추가로 설치되는 플래시 클라이언트 개발용 컴포넌트이다). 리모팅 컴포넌트가 설치되면 <화면 1>처럼 ‘Remoting’이란 컴포넌트가 설치되고, 이하에 ‘NetServices’등의 객체가 생기는 것을 볼 수 있다.

Array

<화면 1> 플래시 리모팅 컴포넌트 설치 확인

먼저 ASP.NET으로 간단한 웹 서비스를 만들어 보자. 비주얼 스튜디오 닷넷을 시작하고 ‘HelloFlash’란 이름으로 웹 서비스 프로젝트를 만든다(<화면 2> 참조). 그리고, HelloWorld 메쏘드의 주석을 제거해 빌드한다. <리스트 1>처럼 가장 간단한 웹 서비스를 만들고 다음과 같이 WSDL을 확인해 보자.

http://localhost/HelloFlash/Service1.asmx?WSDL

이 WSDL은 HelloFlash 프로젝트에서 노출되는 웹 서비스에 대한 설명을 담고 있다(플래시측에서 이 WSDL이 사용된다).

Array

<화면 2> 웹 서비스 프로젝트 만들기

<리스트 1> Hello World 웹 서비스 Array
using System;
using System.Collections;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.Web;
using System.Web.Services;
namespace HelloFlash
{
   public class Service1 : System.Web.Services.WebService
   {
      public Service1()
      {
         InitializeComponent();
      }
      [WebMethod]
      public string HelloWorld()
      {
         return "Hello World";
      }
   }
}
Array

이제 다음과 같은 수순으로 작업해 HelloFlash 폴더를 플래시 리모팅이 가능하도록 준비를 한다.
1 플래시 리모팅 폴더(flashremoting)에 있는 flashgateway.dll 파일을 HelloFlash/bin 폴더에 복사한다.
2 HelloFlash 폴더 등록정보에서 보안 탭을 누르고, ASPNET 계정을 추가하고 쓰기(Write) 권한을 추가한다.
3 빈 apsx 페이지를 만든다(여기서는 default.aspx 페이지를 만들었다).
4 web.config에 다음의 내용을 추가한다.

<httpModules>
<add name="GatewayController"
type="FlashGateway.Controller.GatewayController,flashgateway"/>
</httpModules>

HelloFlash 폴더가 플래시 리모팅에 사용될 수 있게 준비됐다. 이제 플래시를 시작하고, 첫 프레임에 <리스트 2>와 같이 액션스크립트를 작성한다. getService() 메쏘드는 ‘http://localhost/HelloFlash/Service1.asmx?wsdl’에 기술된 WSDL 내용을 가지고 프록시 객체를 생성한다. 생성된 serv 객체를 통해 다음과 같이 메쏘드를 호출할 수 있다.

serv.HelloWorld();

웹 메쏘드를 호출하면 “메쏘드 이름_Result()”, “메쏘드 이름_Status()”의 콜백이 호출된다. 이 콜백을 통해 리턴 값을 얻을 수 있다.

function HelloWorld_Result(result)
{
   trace( result );
}

수행 결과는 “Hello, World”라고 출력되지만 XML 웹 서비스를 간단한 액션스크립트 코드로 이용할 수 있다는 점에서 리모팅은 무척 강력한 방법이 된다(ATL/COM으로 어렵게 액티브X 컨트롤을 구현하는 것에 비해 훨씬 적은 비용으로 빠르게 클라이언트를 개발할 수 있다).

<리스트 2> XML 웹 서비스를 호출하는 액션스크립트Array
#include "NetServices.as"
#include "NetDebug.as"
if (inited == null)
{
   inited = true;
   NetServices.setDefaultGatewayURL("http://localhost/HelloFlash/default.aspx")
   gateway  = NetServices.createGatewayConnection();
   serv = gateway.getService("http://localhost/HelloFlash/Service1.asmx?wsdl", this);
}
serv.HelloWorld();
function HelloWorld_Result(result)
{
   trace( result );
}
function HelloWorld_Status(result)
{
   trace( error.description );
   trace( result );
}
Array

날 씨, 증권, 상품 조회 등 변화하는 데이터를 조회하거나 서버와 상호작용을 하는 서비스를 구성할 때 플래시 리모팅은 매우 유연하면서도 강력한 방법이 된다. 플래시 리모팅은 ADO와 연결되어 서버 측의 데이터를 처리하거나 서버 측의 어셈블리를 호출할 수 있다. 이런 기능도 웹 서비스를 이용하는 것 못지않게 다양한 응용이 가능하게 한다.
플래시 보안 : SWF는 HTML이나 자바스크립트와 다르지 않다?
엔 터프라이즈 애플리케이션에서 보안은 빼놓을 수 없는 이야기이다. 애플리케이션이 공개적으로 배포되는 인터넷 환경에서는 더더욱 그렇다. 플래시 관련 보안 이슈 중 가장 먼저 떠오르는 것이 바로 SWF 디컴파일러(decompiler) - SWF 파일에서 리소스와 액션스크립트를 추출하는 도구에 대한 것이다.
<화면 3>은 대표적인 SWF 디컴파일러(Sothink)의 수행 화면이다. 리소스는 물론 액션스크립트를 완벽하게 디컴파일한다. 보안의 관점에서 볼 때 SWF는 HTML이나 자바스크립트와 다르지 않다. 액션스크립트를 포함해 SWF가 가지고 있는 내용은 모두 분해되고 해체될 수 있다. SWF 파일 포맷이 공개되어 있기 때문인데, 원천적으로 이를 방지할 방법은 없다(반면에 FLA 파일은 포맷이 공개되어 있지 않기 때문에 SWF를 FLA로 디컴파일해 주는 도구는 없다).

Array

<화면 3> SWF 디컴파일러 수행 화면

일단, 플래시 개발자 입장에서 볼 때 애써 작성해 놓은 코드나 리소스가 노출된다는 측면에서 매우 안타까울 수 있다. 플래시 전문가들은 자신의 코드를 보호하고자 할 경우 무비클립을 나누어 동적으로 로드하도록 권고하고 있지만, 이 방법 또한 디컴파일을 어렵게 할 뿐 디컴파일 자체를 원천적으로 막을 수는 없다.
이보다 더 문제가 되는 것은 액션스크립트에 포함되어 있는 로직이나 데이터가 노출된다는 점이다. 서버에서 인증을 받는 SWF나 서버와 상호 작용을 하는 게임을 플래시로 제작할 때는 반드시 이런 점을 고려해야 한다(서버와 통신하는 프로토콜이 모두 노출된다). SWF 안에 중요한 데이터나 패스워드 같은 내용을 넣어서는 안 된다. 이 글에서 설명하고 있는 플래시 통신의 다양한 방법들이 중요한 이유 중 하나가 여기에 있다. 즉, 중요한 알고리즘은 서버에 구현함으로서 악의적인 공격에 대비해야 하며, 프로토콜 노출을 고려해 서버를 안전하게 보호할 방법을 마련해야 한다.
플래시 통신 보안과 SSL
우리가 사용하는 인터넷 상의 통신은 엽서와 같다. 즉, 엽서가 지나는 길에 있다면 누구나 엽서에 쓰여진 내용을 볼 수 있다. HTML이나 SWF, 메일 모두 마찬가지이다(심지어 대부분의 인터넷 사이트에서 입력하는 아이디와 패스워드도 전혀 암호화되지 않은 채 돌아다닌다). 서버와 클라이언트 사이에서 누군가 마음만 먹으면 손쉽게 데이터를 볼 수 있다. 이 문제를 해결하는 것이 SSL(Secure Socket Layer)이다. SSL은 별도의 추가적인 개발 없이 완전한 보안을 구현한다(즉, 보내는 측과 받는 측 사이에서 데이터를 중간에 볼 수 있다 하더라도 전혀 해독할 수 없다).
플래시는 HTTPS(HTTP + SSL)를 지원한다. 사실 플래시 자체가 SSL을 지원한다기보다는 브라우저가 제공하는 SSL을 이용한다고 하는 편이 맞을 것이다. 여러분이 SSL이 적용된 웹 페이지에서 SWF를 받았다면 그것은 SWF와 데이터 전체가 암호화돼 전송됐음을 의미한다. 주의할 것은 SWF가 전송 중에는 암호화된다는 점이다. 즉, SWF 디컴파일을 막는 것과 SSL은 아무런 상관이 없다.
SSL은 가장 안전한 통신 보안 방법이다. 대부분의 브라우저가 128비트 SSL 암호화를 제공하고 있어 충분히 안전하다고 할 수 있다. 웹 서버인 IIS(Internet Information Server)가 HTTPS를 사용하게 하려면 <화면 4>와 같이 공인 인증기관으로부터 공인 인증서를 받아서 IIS에 설치하고, <화면 5>처럼 SSL을 사용하도록 설정하면 된다(SSL 설정은 이 글의 범위를 벗어나므로 자세한 방법은 베리사인과 같은 공인 인증기관 사이트를 참고하라).

Array

<화면 4> IIS에 서버 인증서 설치

Array

<화면 5> IIS에 128비트 SSL 설정

플래시 암호화 통신
SSL을 사용할 수 없을 때 최소한 데이터를 암호화해 보내야 한다. 인증(authentication) 과정을 예로 들어 살펴보자. 패스워드와 같이 중요한 데이터를 그대로 전송해서는 안 된다. <화면 6>은 MD5를 구현한 예이다. 해시는 데이터 전체를 암호화하는 것은 아니지만 암호화된 결과를 가지고 원본을 알 수 없다는 단방향성을 가지고 있어 인증에 많이 사용된다(소스는 ‘이달의 디스켓’에 포함되어 있다).

Array

<화면 6> MD5 해시 구현 예

해시 방법을 이용해 다음과 같이 인증을 할 수 있다. 이 방법의 핵심은 실제 패스워드를 보내지 않는다는 것이다.

1 SWF는 서버에 임의의 문자열 S을 요청한다.
2 SWF는 수신된 S에 사용자가 입력한 패스워드를 붙여서 해시 암호화해 문자열 A를 생성한다.
3 A를 서버로 전송한다.
4 서버는 SWF로 보낸 S와 데이터베이스에서 얻은 사용자 패스워드를 붙여서 해시화해 B를 생성한다.
5 서버는 A와 B가 일치하는지 여부를 확인한다.

플래시로 제작된 네트워크 게임이 있다고 하자. SWF가 디컴파일된다면 서버와 통신하는 프로토콜이 모두 공개될 것이다. 이 때문에 게임 서버가 해킹된다면 이것은 엄청난 일을 야기한다. 따라서 플래시 암호화가 필요하다. 다시 말해, 프로토콜이 노출되더라도 안전하게 통신할 방법을 마련해야 하는데 바로 해시 암호화와 같은 방법이다. 예를 들어, 서버로부터 특정한 키를 발급받아 항상 해당 키로 암호화된 데이터를 주고받는다면, 클라이언트 암호와 알고리즘과 로직을 알더라도 서버측에 있는 로직을 알아내지 못하게 되어 게임을 해킹하기 힘들어진다.
다음은 SHA1 알고리즘을 테스트하는 액션스크립트이다. MD5 해시보다 SHA1 알고리즘이 훨씬 안전한 것으로 알려져 있다(MD5 알고리즘과 SHA1 알고리즘을 구현한 액션스크립트는 ‘이달의 디스켓’에 포함되어 있으며, 참고자료를 통해서도 얻을 수 있다).

#include "sha1.as"
trace(b64_sha1("Hello, World"));

앞의 수행 결과는 다음과 같다.

kH0U+zrysNTxjC1Gq+iu3OFzZ70

앞의 액션스크립트와 <리스트 3>의 C# 코드의 수행 결과는 똑같다. 이 결과를 이용해 앞서 설명한 인증을 구현할 수 있다. 사실 여기에는 좀더 개선된 방법이 필요하다. 예를 들어, 사전(dictionary)에 의한 공격을 막기 위해 암호화 데이터에 salt를 추가하거나 해시를 두 번 통과하게 하는 방법도 좋은 개선 방법이다.

<리스트 3> C# SHA1 알고리즘 구현 코드Array
using System;
using System.Text;
using System.Security.Cryptography;
class TestSha1
{
    static void Main(string[] args)
    {
        string plainText = "Hello, World";
        // SHA1 해시
        UTF8Encoding u8e = new UTF8Encoding();
        byte[] byteBuffer = u8e.GetBytes(plainText);
        SHA1 sha = new SHA1CryptoServiceProvider();
        byte[] result = sha.ComputeHash(byteBuffer);
        // 해시 값을 base64 문자열로 변환해 출력
        Console.WriteLine( Convert.ToBase64String( result ) );
    }
}
Array

다른 보안 이슈
다 운로드된 SWF가 다른 서버(도메인)와 통신을 하는 것은 악의적으로 사용될 가능성이 있어서 이를 못하게 막고 있다(이것은 자바 애플릿도 마찬가지다). 이것을 교차 사이트 공격(Cross-Site Attack) 방지라고 하는데, SWF는 다운로드된 서버의 도메인 안에 있는 서버와 통신이 가능하다. 예를 들어, www.elasticware.com에서 SWF가 다운로드됐다면 SWF는 content.elasticware.com과 통신이 가능하다(즉, 자신이 다운로드된 서버 및 같은 도메인에 있는 서버와 통신이 가능하다).
플래시는 사용자의 파일 시스템, 카메라, 마이크로 폰에 접근이 가능하다. 접근 가능 여부는 사용자 설정에 따른다. 사용자의 파일 시스템에 접근은 독립적인 SWF 단위로 제한된다. 일반적으로 설정 정보나 상태 정보를 저장하는 목적으로 사용되며, 기본 저장 공간은 100K이다. 사용자가 크기를 설정할 수 있고(<화면 7> 참조), 저장소의 크기와 쿠키 저장 여부에 대한 것은 브라우저 설정과 무관하다.

Array

<화면 7> 플래시 설정 저장 공간

클라이언트에 데이터를 저장하는 방법으로 액션스크립트에서 SharedObject 객체를 사용한다. 다음은 SharedObject를 사용해 클라이언트에 카운터를 저장하고 로드될 때마다 증가시키는 예이다.

sObject = SharedObject.getLocal("counter"); // 읽기
if( sObject != null )
{
   sObject.data.counter++; // 연산
   _root.savedata = sObject.data.counter;
   sObject.flush(); // 저장
   trace(sObject.data.counter);
}

SWF에서 저장한 내용은 다음 폴더에 저장되는데, 각 SWF마다 그리고 SharedObject에서 저장하는 객체 이름으로 내용을 담은 파일이 생성된다.

C:Documents and SettingsAdministratorApplication DataMacromediaFlash Player

플래시 보안의 핵심은 SSL과 동적인 서버 사이드 연결로 요약할 수 있다. 즉, 통신 보안을 위해 가능한 SSL을 사용하고, 액션스크립트가 완전히 노출된다는 가정하에 중요한 로직은 서버측에 구현하고 데이터를 암호화하는 별도의 로직을 클라이언트와 서버에 따로 구현하는 것을 추천한다.
‘Flash.NET’의 가능성
지금까지 3회에 거쳐 서버측(ASP.NET)과 플래시(SWF) 사이의 다양한 통신 방법과 개발 이슈들에 대해 살펴봤다. 플래시는 리치 클라이언트의 가능성뿐만 아니라 사실상의 플랫폼 독립성을 갖추고 있고, 높은 생산성을 가지고 있는 훌륭한 클라이언트 개발 도구이다. 플래시와 닷넷의 만남은 유연하고 고 가용성의 시스템을 구축하는 데 손색이 없으며, 특히 XML 웹 서비스의 활용은 생산성을 높이는 아주 좋은 방법이다.
다만, 그 활용의 장벽은 ASP.NET 개발자는 플래시를 모르고, 플래시 디자이너는 ASP.NET에 익숙하지 않은 기술간의 문턱이라고 할 수 있다. 예를 들어, 데이터와 비즈니스 로직을 SWF에 두지 않는 것만으로도 훨씬 유연한 개발을 할 수 있다. 비주얼 베이직과 C++, J2EE와 닷넷, 호스트와 유닉스 그리고 유닉스와 NT처럼 기술과 기술 사이의 다리가 되는 기술이 훨씬 중요할 때가 많다. 1+1이 2 이상이 될 수 있듯이 플래시(Flash)+닷넷(.NET) 역시 Flash.NET 그 이상이 될 것이다. @

올해 3월 닷넷 정식 버전이 발표되면서 C#이 새로운 언어로 떠오르고 있는데, 특히 네트워크 부분에서 기존의 IOCP(IO Completion Port) 기능을 손쉽게 사용할 수 있도록 만들어 놓았다는 점에서 주목할만 하다.
기 존에는 이 기능을 이용하려면 Win sock 2 API를 직접 호출해야 했지만, C#에서는 이 기능이 BCL (Base Class Library) 안에 포함되어 있어 손쉽게 사용할 수 있다. C#에서는 기본적으로 비동기 통신을 하면 자동으로 IOCP를 이용한다. 이는 C# 뿐만 아니라 닷넷의 기본 기능인 것이다. 또한 플래시는 이번에 MX 버전이 출시되면서 많은 기능의 개선이 있었다. 플래시 5부터 XML 소켓을 지원해 지속적으로 연결된 상태에서 네트워크 통신이 가능해졌으며 온라인 게임으로까지 영역을 넓힐 수 있게 됐다.
앞으로 총 4회의 연재를 통하여 온라인 게임 서버로서의 C#의 가능성을 알아보고, 게임 클라이언트로서 플래시의 가능성에 대해 알아볼 것이다. 기존 온라인 게임의 경우 프로그램을 다운받아 플레이해야 했으나 플래시로 온라인 게임을 만들 경우, 스트리밍 방식을 이용하여 별도의 다운로드없이 실시간으로 데이터를 주고받음으로써 즉시 플레이가 가능하다. 초보자도 해당 홈페이지에 접속하기만 하면 바로 플레이할 수 있기 때문에 누구나 쉽게 게임을 시작할 수 있다.
필자는 이러한 플래시와 C#의 특징에 주목하여 그 가능성을 테스트한다는 의미에서 포트리스와 비슷한 게임인 ‘심플 포트리스(Simple Fortress)’를 만들어 보았다. 별도의 다운로드 없이 URL 주소만 입력하면 플레이할 수 있으며, 웹 브라우저 내에서 실행되므로 게임을 하면서도 다른 작업창을 실행할 수 있다는 이점이 있다. 본격적인 설명에 들어가기 전에, 이 게임은 필자가 닷넷과 플래시에 대한 테스트용으로 만든 것으로 상업적으로 사용할 의도가 없으며, 이 게임의 거의 모든 이미지와 사운드 파일은 포트리스 2 공식 홈페이지에서 다운받아 사용한 것임을 미리 밝혀둔다.
심플 포트리스 미리보기
앞 으로 우리가 만들 게임이 어떤 게임인지 한 번 보도록 하자. ‘이달의 디스켓’의 압축을 풀면 Server와 Client 두 개의 폴더가 있을 것이다. 이중 Server 폴더에서 FortressServer.exe를 실행하면 서버가 작동한다(이 서버 프로그램은 닷넷 기반 하에서만 작동하기 때문에 최소한 닷넷 프레임워크는 설치되어 있어야 한다). 그 다음 Client 폴더의 fortress.html 파일을 실행시킨다. <화면 1>과 <화면 2>는 서버와 클라이언트의 작동 화면이다.

Array

<화면 1> FortessServer.exe 실행화면

Array

<화면 2> Fortess.html 실행화면

웹 브라우저 화면에서 원하는 ID를 입력하고 들어간가면 <화면 3>과 같은 대기실 화면이 나온다. 이 곳에서 탱크 종류와 팀을 선택할 수 있다. 이 상태에서 또 한 번 Fortress.html 파일을 실행해서 새로운 ID를 입력하고 들어오면 두 명의 게이머가 대기실에 들어온 상태가 된다.

Array

<화면 3> 대기실 화면

Array

<화면 4> 게임 시작 화면

이때 두 개의 웹 브라우저에서 동시에 배경음악이 나오므로 약간 혼란스러울 수도 있다. 서로 다른 팀을 고른 후, 처음에 들어왔던 사람이 START 버튼을 누르면 게임이 시작된다(<화면 4>). 게임 방법은 <표 1>과 같다.

기능
탱크의 이동 화살표 좌우 키
탱크의 각도 조정 화살표 상하 키
대포 발사 스페이스 바를 눌러서 파워를 조절 후 발사한다.

<표 1> 게임방법

먼 저 자신의 차례가 되면 자신의 탱크 위에 ‘READY!’라는 글자가 깜빡거린다. 그 상태에서 각도 조정이나 이동을 하면서 조절한 후 스페이스 바를 길게 눌렀다가 떼면 대포가 발사된다(<화면 5>). 서로 번갈아 가면서 대포를 발사하는 방식으로 게임을 쉽게 하기 위하여 폭발의 파편만 닿아도 생명치를 줄게 해 놓았다. 채팅도 지원하므로 대화를 입력해도 된다(<화면 6>). 이제 어떤 게임인지 살펴봤으니 본격적으로 게임 제작에 착수해 보자. 먼저 서버부터 만들어 볼 것이다.

Array

<화면 5> 대포를 발사한 장면

Array

<화면 6> 대화를 나누는 장면

비동기 호출의 기본은 델리게이트
서버 제작에서 중요한 점은 다수의 사용자를 처리해야 하는 데 있다. 한 명이 아니라 여러 명이 동시에 접속하므로 그들의 요구를 동시에 처리해 줘야만 한다. 그런데 일반적으로 프로그래밍하다 보면 한 명을 처리하기 위해 그 대답을 기다리다가 다른 사람의 요구를 못 들어주게 된다.
즉 블럭이 돼 버려서 다수의 사용자를 처리할 수 없게 된다. 이 때의 해결책이 바로 쓰레드이다. 닷넷에서는 이러한 쓰레드를 이용하여 비동기 호출을 지원하는데, 이를 이용하여 많은 사용자들의 요구를 처리할 수 있다. 이는 함수를 호출할 때 동기적으로 그 함수의 호출이 끝날 때까지 기다리는 것이 아니라 함수를 호출하면 그 함수는 새로운 쓰레드 안에서 돌아가고 호출자 또한 기존 쓰레드 내에서 돌아가므로 동시에 일을 처리할 수 있게 된다.

Array

<그림 1> 동기 호출과 비동기 호출

<그림 1>은 소켓의 Accept문을 예로 들어 동기 호출과 비동기 호출의 차이점을 설명한 그림이다. 이 비동기 호출의 핵심 개념에는 바로 델리게이트(delegate)라는 것이 자리잡고 있다. 그러므로 델리게이트의 본질부터 파악하는 것이 비동기 호출의 원리를 이해하는 방법일 것이다.
C# 세계의 브로커, 델리게이트
C#을 배우는 사람들에게 있어 델리게이트는 생소한 개념이 아닐 수 없다. 델리게이트는 C 언어의 함수 포인터에서부터 유래되었다. C 언어에서는 함수 포인터를 잘 안 썼으므로 생소할 수도 있다. 먼저 델리게이트의 사전적인 의미를 살펴보면 ‘대리자’ 또는 ‘위임형’ 등으로 정의하고 있다. 델리게이트라는 것이 어떤 함수를 대신해서 호출되기 때문에 그렇게 이름을 붙인 듯하다. 이해를 돕기 위해 다음과 같은 기상청 시나리오를 살펴보자.
기상청에서는 기상정보를 수집해 그 정보를 필요로 하는 곳에 전달한다. 그런데 누가 언제 그러한 정보를 필요로 할지 미리 알수 없기 때문에 그런 보고 시스템을 미리 구축해 놓을 수 없었다. 그래서 대신 기상 정보 브로커를 고용해 그에게 정보를 주면, 그가 자신에게 연결된 기상 정보를 필요로 하는 사람들에게 그 정보를 주기로 했다. 그렇게 해서 보고 시스템을 완성하게 됐다. 이에 신문사가 제일 처음 그 정보를 달라고 브로커에게 요청을 했다. 브로커는 그 요청을 받아들이고 기상청으로부터 기상 정보를 받는대로 신문사에게 전해 주기로 했다.

Array

<그림 2> 기상 브로커 시나리오

이를 그림으로 나타내면 <그림 2>와 같다. 그러면 이를 코딩으로 나타내 보자. 미래의 일은 예측할 수 없기 때문에 이미 모든 계획은 다 세웠지만 누가 그 계획에 참여할지는 모를 때가 있다. 프로그래밍의 세계에서도 누가 그 일에 참여하게 될지 모르는 상황이 종종 생긴다. 이럴 때 델리게이트를 사용하는 것이다. 차후에 델리게이트를 통해서 그 일을 할 메쏘드만 연결시켜주면 된다.

<리스트 1> 기상 보고 시스템Array
namespace MeteorologicalSystem
{
    // 기상 정보 브로커
    public delegate void Information(int temparature, int humidity,
        string nephanalysis);
    // 기상청
    class MeteorologicalOffice
    {
        // 기상 예보
        public static void Report(Information broker)
        {
            broker(25,60,”구름 없음”);
        }
    }
    // 신문사
    class NewspaperCompany
    {
        // 신문사에서 신문을 발간
        public static void Publish(int temparature, int humidity,
            string nephanalysis)
        {
            Console.WriteLine(“[신문사 출판] 온도:{0}, 습도:{1}, 구름분포:{2}”,
                temparature, humidity, nephanalysis);
        }
        }
        // <summary>
        // Class1에 대한 요약 설명
        // </summary>
        class Class1
        {
            // 해당 응용 프로그램의 주 진입점
            [STAThread]
            static void Main(string[] args)
            {
                // TODO: 여기에 응용 프로그램을 시작하는 코드를 추가
                // 첫번째 예제
                Console.WriteLine(“=== 첫번째 예제 ===”);
                 Information broker = new Information
                 (NewspaperCompany.Publish);
                // 신문사 등록
                MeteorologicalOffice.Report(broker); // 기상정보 보고 시스템 가동
            }
        }
    }
Array

델리게이트의 비밀을 파헤치자
<리스트 1>을 보면 한 가지 궁금증이 떠오를 수도 있다. 마지막 줄을 다시 보자.

Information broker =
    new Information(NewspaperCompany.Publish);

여기서 왜 new라는 키워드를 썼는지 궁금증이 일어날 것이다. new라는 것은 새로운 Object를 할당할 때에만 쓰는 키워드인데, 여기서 사용했다는 것은 마치 클래스를 할당하는 것과 비슷하다고 생각할 수 있다. 만약 그렇게 생각했다면 맞다. 델리게이트라는 것이 바로 클래스이기 때문이다. 델리게이트를 ‘위임[형]’이라고 번역하듯이 델리게이트는 클래스 타입이다. 그러면 클래스 바디는 어디에 있는 걸까? 클래스라면 다음과 같이 되어 있어야 한다.

Class Information
{
}

하지만 클래스의 정의가 다음과 같이 한 줄로 되어 있다.

delegate void Information(int temparature, int humidity, string nephanalysis);

도대체 바디는 어디에 있는가? 사실 이 한 줄에는 바디를 포함하고 있다(<그림 3>). 즉 리턴형과 인자형에 대한 정보가 클래스 바디가 되는 것이다.

Array

<그림 3> DeleBang의 클래스 바디

델리게이트가 정말 클래스인지 확인해 보기 위하여 중간 코드로 확인해 보자. 닷넷을 설치한 폴더에 ILDA SM.exe 파일이 있다. 이는 IL Disassembler의 약자로 말 그대로 중간 코드를 disassemble해 준다. 이를 통해 앞에서 컴파일한 Meteorological System.exe 파일을 열어 보면 <화면 7>이 나타난다. <화면 7>을 보면 글자 옆에 아이콘들이 있는데, 이들이 무엇을 의미하는지는 <화면 8>을 보면 알 수 있다.

Array

<화면 7> MeteorologicalSystem.exe를 Disassemble한 화면

Array

<화면 8> 아이콘 도움말

이를 통해서 보면 Information은 클래스라는 것과 .ctor, BeginInvoke, EndInvoke, Invoke라는 네 개의 메쏘드를 가지고 있음을 알 수 있다. 또한 Information은 System.Muticastdele gate에서 상속받았다는 정보까지 갖고 있다. 여기에 나오는 4개의 메쏘드중 .ctor은 생성자를 의미한다. BeginInvoke와 EndInvoke는 비동기 호출에 쓰이며, Invoke는 동기 호출에 쓰이는 메쏘드이다. 이들에 대한 코드는 <리스트 2>와 같다.

<리스트 2> Information 클래스의 코드 Array
public class Information : System.Multicastdelegate
{
    // 생성자
    public Information (object target, int32 methodPtr);
    public void virtual Invoke( int temparature, int humidity,
        string nephanalysis );
    public virtual IAsyncResult BeginInvoke( int temparature,
        int humidity, string nephanalysis,
        AsyncCallback callback, Object object);
    public virtual void EndInvoke( IAsyncResult result);
}
Array

필드 타입 설 명
_target System.Object 인스턴스 메쏘드에 쓰이는 것으로, 콜백메쏘드가 호출될때 참조하는 Object이다.
_methodPtr System.Int32 CLR에서 사용되는데, 콜백될 메쏘드를 가리키는 integer 값
_prev

System.Multicastdelegate

다른 델리게이트를 가리키는 값

<표 2> 델리게이트의 Private 필드

< 리스트 2>를 보면 한 가지 이상한 점을 발견할 수 있을 것이다. 바로 Information의 생성자인데, 우리는 <리스트 1>에서 생성자로 NespaperCompany.Publish를 넘겨줬다. 그런데 <리스트 2>를 보면 생성자는 두 개의 인자가 필요하다. 분명 에러를 발생해야 하는데 잘 되는 것을 보면 이상이 없는 것이다. 여기서 컴파일러는 원본 소스를 컴파일할 때, 앞의 생성자에 맞도록 파싱을 해주기 때문에 에러가 안 나는 것이다.
앞의 두 인자중 target은 메쏘드가 있는 오브젝트를 가리키는데 만약 메쏘드가 static이면 null 값을 넘겨준다. methodPtr은 callback 메쏘드를 가리키는 CLR 내부에서 쓰이는 레퍼런스 값이다. 이들 생성자에서 받은 두 개의 값을 Information 클래스는 따로 Private 필드에 저장해 두는데 그 Private 필드는 <표 2>와 같다.
이중 _prev 값은 나중에 Muticatedelegate에서 설명할 것이다. 그럼 이제 우리가 생성자에게 넘겨준 그 값들을 직접 눈으로 확인해 보자. <리스트 1>에서 main 메쏘드에 다음과 같은 코드를 추가하자.

// 생성자에 넘겨준 값을 보자.
if ( broker.Target == null )
{
   Console.WriteLine(“null”);
}
else
{
   Console.WriteLine(broker.Target);
}
Console.WriteLine(broker.Method);

이를 실행하면 다음과 같은 결과를 볼 수 있다.

=== 첫번째 예제 ===
[신문사 출판] 온도 : 25, 습도 : 60, 구름분포 : 구름 없음
=== 두번째 예제 ===
null
Void Publish(Int32, Int32, System.String)

우리가 넘겨준 메쏘드가 Static이기 때문에 Target에는 null 값이 들어갔고, 메쏘드에는 대리자에 등록된 메쏘드의 형식이 나왔다. 만약 여기에서 instance 메쏘드를 넘겨주면 어떤 값이 나올까? 앞에서 Kill 메쏘드에서 static을 빼고, NewspaperCompany 클래스를 새로 생성해서 실행해 보면 다음과 같은 결과가 나온다. 즉 메쏘드의 Object를 넘겨주는 것이다.

=== 첫번째 예제 ===
[신문사 출판] 온도 : 25, 습도 : 60, 구름분포 : 구름 없음
=== 두번째 예제 ===
MeteorologicalSystem.NewspaperCompany
Void Publish(Int32, Int32, System.String)

이제 생성자에 대한 비밀은 풀었으나 아직 Information 클래스의 메쏘드에 대한 비밀이 남아 있다. <리스트 2>에서 Invoke 메쏘드가 있는데 이것이 실제 실행하는 메쏘드이다. 그런데 우리는 그 메쏘드를 호출한 적이 없다. 그러면 컴파일러가 알아서 호출해 주는 것일까? 그렇게 생각했다면 정답이다. 우리는 <리스트 1>에서 다음과 같이 호출했다.

broker(25,60,”구름 없음”);

컴파일러는 이 코드를 보고 다음과 같이 번역한다.

broker.Invoke(25,60,”구름 없음”);

그런데 정말 이렇게 번역하는 것일까? 이것 역시 ILDASM을 이용해서 확인해 보자. <화면 9>를 보면 컴파일러가 만들어 준 Invoke 메쏘드를 볼 수 있을 것이다.

Array

<화면 9> Invoke가 호출된 부분

Array

<화면 10> += 연산자가 나타내는 메쏘드

너에게 임무를 추가한다!
이제 델리게이트에 대해 어느 정도 비밀을 풀었다. 그런데 여기서 한 가지 의문이 남아 있다. <표 2>를 보면 _prev라는 필드가 있는데 이 필드의 용도가 무엇이냐 하는 것이다. 이를 위해 다음과 같은 시나리오를 보자.
어느 날 방송사에서도 그 기상 정보를 달라는 요청이 들어왔다. 이미 기상청에서는 브로커에게 그 일을 일임했으므로 방송사는 브로커와 거래를 해 등록함으로써 브로커를 통해 기상청의 정보를 제공받게 된다. 이를 코드로 나타내면 다음과 같다. 먼저 Broadcasting Company 클래스를 다음과 같이 새로 만든다.

// 방송사
class BroadcastingCompany
{
   // 방송사에서 방송 보도
   public static void Broadcast( int temparature,
      int humidity, string nephanalysis)
   {
      Console.WriteLine(“[방송 보도] 온도:{0}, 습도:{1}, 구름분포:{2}”,
      temparature, humidity, nephanalysis);
   }
}

그 다음 브로커에 다음과 같이 추가하면 된다.

broker += new Information(BroadcastingCompany.Broadcast);

결과는 다음과 같다.

=== 세번째 예제 ===
[신문사 출판] 온도 : 25, 습도 : 60, 구름분포 : 구름 없음
[방송 보도] 온도 : 25, 습도 : 60, 구름분포 : 구름 없음

여기서는 단순히 += 연산자를 이용했다. C#에서는 연산자 오버로딩을 지원하기 때문에 += 연산자는 실질적으로 메쏘드를 호출하는 것이다. 그러면 그 메쏘드가 무엇인지 ILDASM을 통해 확인해 보자. <화면 10>을 보면 Combine이라는 메쏘드가 호출됨을 볼 수 있다.
기본적으로 델리게이트 타입은 Muticastdelegate를 상속받으므로 하나의 callback 메쏘드가 아닌 다수의 callback 메쏘드를 가질 수 있다. 앞의 시나리오에서 보듯이 어떤 사건에 의해 다수가 그 영향을 받는 경우가 있기 때문에 이러한 기능을 지원하는 것이다. 델리게이트 내부적으로는 이것을 linked-list로 유지를 한다. linked-list로 유지하기 때문에 앞의 링크를 가리키는 _prev 필드가 필요한 것이다. 이를 그림으로 나타내면 <그림 4>와 같다.

Array

<그림 4> 브로커의 linked-list

이 그림을 보면 한 가지 의문점이 들 것이다. 우선 브로커가 처음에 등록했던 신문사를 가리키는 것이 아니고 나중에 등록한 방송사를 가리킨다는 것과 일반적인 linked-list에서는 _next 필드로 다음 오브젝트를 가리키는 데 비해 여기는 _prev필드를 써서 앞의 오브젝트를 가리킨다는 것이다. 그 이유는 리턴 값 때문에 그렇다. 만약 callback 메쏘드에 리턴 값이 있다면 어떻게 할 것인가? 예를 들어 다음과 같은 경우이다.

delegate int Information(int temparature, int humidity,
   string nephanalysis);

이와 같이 리턴 값이 있는 경우 브로커에 등록된 callback 메쏘드가 한 개라면 별 문제가 없지만, 여러 개라면 어떻게 할까? 먼저 C#에서는 여러 개의 callback 메쏘드중 단 한개의 리턴 값만 받아 올 수 있다. 그 여러 개의 리턴 값을 다 받아오려면 다른 방법을 취해야 하는데 그 방법은 나중에 소개할 것이다. 일단 일반적인 상황에서는 리턴 값을 하나만 취한다.
그러면 어떤 리턴 값을 취할 것인가? 상식적으로 생각해 보면 가장 최근에 호출된 callback 메쏘드의 리턴 값이 가장 가치있다고 생각될 것이다. 그래서 가장 나중에 호출된 리턴 값이 필요하기 때문에 _next 필드를 안 쓰고 위로 거슬러 올라가서 호출하는 것이다. _next를 쓴 경우와 _prev를 쓴 경우를 그림으로 비교해 보자. 예를 들어 f1, f2, f3를 브로커에 등록했다고 하면 <그림 5>와 같은 호출 과정을 볼 수 있다.

Array

<그림 5> _prev와 _next의 차이점

그러면 직접 호출을 담당하는 Invoke 메쏘드에 대한 가상 코드를 만들어 보자.

class Information : Multicastdelegate
{
   public int virtual Invoke(int temparature, int humidity,
      string nephanalysis)
   {
      // 앞으로 거슬러 올라간다.
      if ( _prev != null ) _prev.Invoke(temparature,
      humidity,
      nephanalysis);
      // 결국 맨 나중에 호출된 callback 메쏘드의 리턴 값이 리턴된다.
      return _target.methodPtr ( temparature, humidity,
      nephanalysis);
   }
}

그럼 이제 _prev를 살펴봤으니, Combine 메쏘드가 내부적으로 어떻게 이들 연결을 만드는지 보자. <리스트 3>은 델리게이트 클래스의 Combine 메쏘드들이다. Combine 메쏘드는 두 개의 델리게이트를 인자로 받는데, 먼저 delegate는 한 번 생성되면 immutable하기 때문에 _prev 필드를 마음대로 변경할 수가 없다. 그래서 Combine을 할 때에는 second와 같은 새로운 델리게이트 오브젝트를 생성하고, 이때 _prev 필드 값을 first로 설정해 준다.

<리스트 3> 델리게이트의 메쏘드 Array
class System.delegate
{
    // first와 second를 연결한 후에 second를 리턴한다.
    public static delegate Combine(delegate first, delegate second);
    // 배열에 의한 델리게이트를 연결시켜 준다.
    public static delegate Combine(delegate[] delegateArray);
    // 델리게이트를 chain에서 제거
    public static Remove(delegate source, delegate value);
}
Array

그렇다면 이를 이용해서 Combine 메쏘드가 정말 새로운 오브젝트를 생성해서 리턴하는지 다음과 같은 코드를 보자. 다음 결과를 보면 False가 나온다. Combine 메쏘드가 새로운 델리게이트를 새로 생성해서 리턴하기 때문이다.

Information broker1 =
   new Information(NewspaperCompany.Publish);
Information broker2 = (Information) delegate.Combine(broker1,
   broker1);
Console.WriteLine( (object) broker1 == (object) broker2 );

델리게이트에 Combine시키는 방법이 있으니 Remove시키는 방법도 있을 것이다. 다음과 같은 경우를 보자.

broker -= new Information(NewspaperCompany.Publish);
MeteorologicalOffice.Report(broker);

앞에서 브로커에게 신문사와 방송사를 다 등록시켰는데 이번에는 신문사를 제거해 봤다. 이 -= 연산자 또한 실제로는 Remove 메쏘드를 나타낸다. 이는 <리스트 3>에서 보듯이 두 개의 인자를 취하는데, 첫 번째는 linked-list를 이루고 있는 델리게이트의 헤드를 가리키며, 두 번째는 삭제할 델리게이트를 가리킨다. 그런데 왜 지우는데 오브젝트를 새로 생성할까? linked-list에서 원하는 것을 찾아서 지워야 하는데, 이를 비교하는 방법에 그 원인이 있다.
우 리가 정확히 찾고자 하는 것은 NewspaperCompany. Publish로 이를 비교해야만 하는 것이다. 그런데 앞서서 델리게이트 클래스에서 생성자로 넘겨주는 것으로 _target과 _method Ptr이 있었다. 즉 instance/static이냐 하는 것과, 리턴 값과 인자형에 따라 클래스를 구분할 수 있는 것이다. 그렇기 때문에 델리게이트에서는 동등 비교를 하는 데 있어 _target과 _methodPtr을 이용한다. 다을 실행하면 TRUE를 리턴하는 것을 볼 수 있다.

Information broker3 =
   new Information(NewspaperCompany.Publish);
Information broker4 =
   new Information(NewspaperCompany.Publish);
Console.WriteLine(broker3.Equals(broker4));

이제 델리게이트에 다른 델리게이트를 쉽게 추가/삭제할 수 있게 됐다. 그러나 앞서 얘기했듯이 델리게이트의 linked-list 호출 구조는 한 가지 단점을 지니고 있다. 중간의 리턴 값들을 무시한다는 것이다. 게다가 만에 하나 리스트들 중에서 exception이 일어나든가 블러킹(blocking)되기라도 하면, 뒤에 딸려 있는 리스트들은 모두 호출되지 못하고 멈춰버린다.
이럴 때에는 알아서 호출하게 놔두지 말고 사용자가 직접 하나하나 체크해 가면서 호출하면 된다. C#에서는 이와 같은 문제를 해결하기 위해 GetInvoca tionList()라는 함수를 제공하고 있다. 이를 이용하면 linked-list의 각 구성원을 똑같이 복사한 배열로 리턴받을 수 있다. 단 이때 _prev 필드는 필요없기 때문에 null로 셋팅이 된다.

delegate[] arraydelegates = broker.GetInvocationList();
foreach(Information agent in arraydelegates)
{
   Console.WriteLine(agent.Method);
}

이벤트 핸들러로 임명합니다~
이제 마지막으로 이벤트에 대해 알아 보자. 이벤트는 한 오브젝트에서 어떤 일이 일어나서 그 일을 다른 오브젝트에게 알려줄 때 이용한다. 이는 델리게이트와도 많이 유사한데, 실제로도 델리게이트를 이용하므로 이벤트는 델리게이트의 특별한 용도라고 생각하면 된다. 예를 들어 앞서 정의한 기상정보 시스템을 이벤트로 만들어 보자. 이때 이벤트라는 의미에 좀더 충실하기 위해 기상 특보를 기상청에서 발령한다고 가상해 보았다. 이때 기상청에서는 자신의 이벤트에 등록된 신문사에게 통지해 준다. <리스트 4>를 보자.

<리스트 4> 이벤트 예제 Array
namespace MeteorologicalSystem2
{
    // 기상청
    class MeteorologicalOffice
    {
    // 이벤트 인자 정의
    public class SpecialReportEventArgs : EventArgs
    {
        public SpecialReportEventArgs(string nephanalysis)
        {
            this.nephanalysis = nephanalysis;
        }
        // 이벤트 인자 내에서 쓸 목록
        public readonly string nephanalysis;
    }
    // 위임형 선언
    public delegate void SpecialReportEventHandler( object sender ,
        SpecialReportEventArgs args);
        // 이벤트 정의
        public event SpecialReportEventHandler SpecialReport;
        // 이벤트를 발생시키는 함수
        protected virtual void OnSpecialReport(SpecialReportEventArgs e)
        {
            if ( SpecialReport != null )
            {
                SpecialReport(this,e);
        }
    }
        // 이벤트 발생을 위해 테스트용으로 만든 함수
        public void SimulateEvent(string nephanalysis)
        {
            SpecialReportEventArgs e = new SpecialReportEventArgs
            (nephanalysis);
            OnSpecialReport(e);
        }
    }
    // 신문사
    class NewspaperCompany
    {
        public NewspaperCompany( MeteorologicalOffice mm)
        {
            mm.SpecialReport += new MeteorologicalOffice.
                SpecialReportEventHandler(Publish);
        }
        // 신문사에서 신문을 발간
        public static void Publish( object sender , MeteorologicalOffice.
            SpecialReportEventArgs e)
        {
            Console.WriteLine(“[신문사 특보] 구름분포:{0}”, e.nephanalysis);
        }
    }
    // Class2에 대한 요약 설명
    class Class2
    {
        // 해당 응용 프로그램의 주 진입점
        [STAThread]
        static void Main(string[] args)
        {
            // TODO: 여기에 응용 프로그램을 시작하는 코드를 추가
            MeteorologicalOffice office = new MeteorologicalOffice();
            NewspaperCompany company = new NewspaperCompany(office);
            office.SimulateEvent(“태풍 북상”); // 이벤트를 넣어줌
        }
    }
}
Array

이 벤트 이용에는 몇 가지 관례가 있다. 먼저 일반적인 델리게이트에서는 인자에 제한이 없지만 이벤트에서는 두 개의 인자를 사용한다. 그 두 개는 보내는 이가 누구인지 나타내는 object형과 System.EventArgs에서 상속받은 클래스를 인자로 받는 것이 있다. 먼저 받는 사람이 여러 사람의 이벤트에 등록해 두면 누가 보냈는지 알 수 없으므로 누가 보냈는지 알기 위해 첫 번째 인자로 object형 인자를 받는다.
예를 들면 신문사는 정보를 기상청으로부터 들을 수도 있지만 소식이 들어오는 경로는 여러 군데일 것이다. 누가 그 정보를 보냈는지 알아야 할 때가 있기 때문에 이런 방법을 사용하는 것이다. 두 번째 인자는 EventArgs를 상속받은 클래스인데 이를 사용면 좀더 깔끔하게 인자 관리를 할 수 있다(그러나 여기서는 편의를 위해서 단순하게 하나의 인자만 썼다).
이벤트에서는 이름을 정하는 데 있어서도 몇 가지 관례가 있다. 먼저 System.EventArgs를 상속받는 클래스는 그 이름 끝에 EventArgs를 붙여준다. 또한 델리게이트를 선언시에도 이름 뒤에 EventHandler를 붙여준다. 마지막으로 이벤트를 발생시키는 메쏘드는 이름 앞에 On을 붙여준다. <리스트 4>를 보면 그냥 이벤트 키워드없이 관례만 따라주면 이벤트가 되지 않느냐고 물을 수 있는데 사실 이벤트는 내부적으로 또 다른 일을 하고 있다. 무슨 일을 내부적으로 꾸미는지 알기 위해 ILDASM으로 확인해 보자.

Array

<화면 11> 이벤트가 들어간 클래스

<화면 11>을 보면 생성하지 않았던 두 개의 함수가 추가되어 있는 것을 볼 수 있다. 게다가 우리는 SpecialReport를 public으로 선언을 했는데 private으로 되어 있다. 이를 가상 코드로 나타내 보자.

private SpecialReportEventHandler SpecialReport = null;
// 이벤트 등록 메쏘드
[MethodImpAttribute(MethodImplOption.Synchronized)]
public void add_SpecialReport(SpecialReportEventHandler handler)
{
   SpecialReport = (SpecialReportEventHandler)
      delegate.Combine
      (SpecialReport, handler);
}
// 이벤트 등록해제 메쏘드
[MethodImpAttribute(MethodImplOption.Synchronized)]
public void remove_SpecialReport(SpecialReportEventHandler handler)
{
   SpecialReport = (SpecialReportEventHandler)
      delegate.Remove
      (SpecialReport, handler);
}

먼저 우리가 public으로 선언한 SpecialReport가 private으로 되어 있으면서 null로 초기화되어 있다. 이는 이벤트라는 것을 외부에서 함부로 접근하지 못하게 막기 위함이다. 예를 들어 태풍이 오는 그런 급박한 상황에서만 이벤트가 발생해야 하는데 이를 public으로 둘 경우 외부에서 마음대로 통제하거나, 바람이 불어도 기상 특보를 발령하는 우를 범할 수가 있기 때문이다. 그래서 이벤트는 그 이벤트를 소유한 클래스 내에서만 발생시킬 수 있게 하기 위해 private 필드로 두는 것이다.
또한 add_*와 remove_*라는 두개의 메쏘드가 추가됐는데, 이는 메쏘드명에서 알 수 있듯이 이벤트에 등록자(listener)들을 등록/해제하는 역할을 한다. +=와 -=연산자를 쓰면 add_*나 remove_*로 컴파일러가 바꿔 준다. Combine이나 Remove를 쓴 경우와 다르지 않게 보일 수 있으나, 자세히 보면 메쏘드 위에 애트리뷰트가 있다.
이는 메쏘드를 동기화시켜서 쓰레드에 대한 안정성을 보장해 준다. 예를 들면 두 개의 리스너가 동시에 이벤트에 등록/해제해도 linked-list가 깨지지 않고 올바로 유지된다(<화면 12>). 결론적으로 이벤트는 결국 델리게이트의 특별한 케이스인데 보다 보안과 안정성에 중점을 둔 케이스라고 할 수 있다.

Array

<화면 12> 동기화된 메쏘드

다음 글에서는 비동기 프로그래밍을
이번 글에서는 서버 구축하는 데 있어 필수인 비동기 소켓 프로그래밍에 앞서, 비동기 프로그래밍의 기본인 델리게이트에 대하 알아봤고, 델리게이트의 특별한 케이스인 이벤트에 대해서도 알아봤다. 다음 연재에서는 비동기 프로그래밍의 원리와 사용 방법에 대해 알아 볼 것이다. @

비동기 프로그래밍이라는 것은 한 가지 일을 할 때 그 일이 끝날 때까지 기다리는 것이 아니라 그 일은 그 일 나름대로 진행하면서 동시에 자신의 일을 계속 할 수 있는 것을 말한다. 이러한 기능이 가능하게 하려면 결국 쓰레드를 써야만 한다. 이 쓰레드를 이용해 비동기 프로그래밍 기법을 흉내내 보자.
쓰레드를 이용한 비동기 프로그래밍
<리스트 1>은 쓰레드를 이용해 다른 작업을 동시에 하는 것을 보여준 예이다. 0부터 9까지 출력하는 프로그램으로 별개의 쓰레드를 하나 더 돌려서 이들을 동시에 처리하고 있다.

<리스트 1> 0부터 9까지 출력하는 프로그램Array
class Class1
{
                public void DoSubWork()
                {
                                // 서브 작업
                                for(int i =0 ; i < 10 ; i++)
                                {
                                                // 많은 계산을 요구하는 작업
                                                for(int j=0; j < 10000000 ; j++) {}
                                                Console.WriteLine(“SubWork :{0} “,i);
                                }
                                                Console.WriteLine(“부 작업 완료”);
                }
                // 해당 응용 프로그램의 주 진입점이다.
                [STAThread]
                static void Main(string[] args)
                {
                                // TODO: 여기에 응용 프로그램 시작 코드를 추가
                                Thread t = new Thread
                                (new ThreadStart(Class1.DoSubWork)); t.Start();
                                // 메인 작업
                                for(int i =0 ; i < 10 ; i++)
                                {
                                                for(int j=0; j < 10000000 ; j++) {}
                                                // 많은 계산을 요구하는 작업
                                                Console.WriteLine(“MainWork:{0} “,i);
                                }
                                Console.WriteLine(“메인 작업 완료”);
                }
}
Array

이 프로그램의 결과는 컴퓨터 사양에 따라 차이가 나고, 할 때마다 다른 결과가 나올 수 있으나 대략 다음의 결과와 비슷할 것이다.

MainWork : 0
SubWork : 0
SubWork : 1
MainWork : 1
SubWork : 2
MainWork : 2
MainWork : 3
SubWork : 3
MainWork : 4
SubWork : 4
MainWork : 5
SubWork : 5
MainWork : 6
SubWork : 6
SubWork : 7
MainWork : 7
SubWork : 8
MainWork : 8
MainWork : 9
메인 작업 완료
SubWork : 9
부 작업 완료

즉 쓰레드를 이용하면 이처럼 두 가지 작업을 동시에 처리할 수 있다. 그런데 쓰레드의 생성자를 보면 ThreadStart라는 델리게이트를 취하고 있음을 볼 수 있을 것이다. 이 ThreadStart 델리게이트의 형식을 보면 다음과 같다.

public delegate void ThreadStart();

리턴형은 void이고, 인자로는 아무런 값을 받지 않는 델리게이트이다. 따라서 우리가 쓰레드를 사용해 다른 함수를 가동할 때는 인자도 없고 리턴 값도 없는 함수만을 사용해야 된다는 얘기다. 그런데 세상사라는 것이 그리 간단하지 않은 것이, 앞의 경우만 보더라도 0부터가 아닌 임의의 숫자로 시작하고 싶어서 그 숫자를 인자로 넘기고 싶을 때가 있을 것이다. 이럴 때에는 어떻게 해야 할 것인가? <리스트 2>의 예제를 보자.

<리스트 2> 원하는 수부터 9까지 출력하는 프로그램 Array
class CSubWork
{
                private int start;
                public CSubWork(int i) { start = i; }
                public void DoSubWork2()
                {
                                // 서브 작업
                                for(int i =start ; i < 10 ; i++)
                                {
                                                for(int j=0; j < 10000000 ; j++) {}
                                                // 많은 계산을 요구하는 작업
                                                Console.WriteLine(“SubWork :{0} “,i);
                                }
                                Console.WriteLine(“부 작업 완료”);
                }
}
class Class1
{
                [STAThread]
                static void Main(string[] args)
                {
                                CSubWork = new CSubWork(5);
                                Thread t = new Thread ( new ThreadStart
                                (c.DoSubWork2) ); t.Start();
                                // 메인 작업
                                for(int i =0 ; i < 10 ; i++)
                                {
                                                for(int j=0; j < 10000000 ; j++) {}
                                                // 많은 계산을 요구하는 작업
                                                Console.WriteLine(“MainWork:{0} “,i);
                                }
                                Console.WriteLine(“메인 작업 완료”);
                }
}
Array

즉 비동기적으로 함수를 랩핑할 수 있는 클래스를 새로 만들어서 데이터 관리를 하면 되는 것이다. 그러므로 따로 인자를 넘기지 않더라도 클래스에 그 값을 줘서 해결할 수 있다. 다음은 <리스트 2>의 결과이다.

MainWork : 0
SubWork : 5
MainWork : 1
SubWork : 6
MainWork : 2
SubWork : 7
MainWork : 3
SubWork : 8
MainWork : 4
SubWork : 9
부 작업 완료
MainWork : 5
MainWork : 6
MainWork : 7
MainWork : 8
MainWork : 9
메인 작업 완료

인자가 필요할 때에는 이렇게 해서 문제를 해결했는데, 이제 남은 또 다른 가능성은 리턴 값이 필요할 때이다. 되돌려 받는 값이 없다면 caller 측에서는 호출하고 잊어버리면 그만이다. 이를 ‘fire-and-forget style programming’이라고 한다. 그런데 되돌려 받는 값이 있다면 문제가 되기 시작한다. 상대방에게 비동기적으로 수행하라고 일을 시켜 놓았으니, 언제 끝날지 모르기 때문이다.
이를 확인하는 방법은 크게 두 가지가 있는데, caller 측에서 비동기 호출이 끝났는지 확인해서 끝났다면 리턴 값을 받아오는 방법과 caller 측에서 델리게이트를 넘겨주고 호출된 쪽에서 연산이 다 끝나면 그 델리게이트를 호출해줘서 리턴 값을 받아오는 방법이 있다. 이를 그림으로 나타내면 <그림 1>과 같다.

Array

<그림 1> 비동기 호출로부터 결과값을 받는 방법

이번에는 비동기 부분에서 넘겨온 리턴 값을 가지고, 메인 부분에서 그 부분을 시작 값으로 하여 출력하는 프로그램을 만들 것이다. 지금까지 쓰레드 부분을 메인 부분에서 만들어 줬는데, 이번에는 메인 부분의 코드를 간결하게 하기 위하여 새로운 랩핑 클래스를 만들고 그곳에서 쓰레드를 담당하게 할 것이다. 쓰레드를 시작하는 함수는 Begin×××라는 이름을 붙여주고 결과 값을 받아 오는 함수는 End×××라고 붙여주자. <리스트 3>을 보자.

<리스트 3> 리턴 값을 위한 비동기 클래스Array
// 콜백 함수 델리게이트
public delegate void CallBack(int result);
// 리턴 값을 처리하기 위한 클래스
class CSubWork2
{
                private int start; // 시작 값
                private CallBack callback; // 콜백 함수 델리게이트
                public bool isCompleted;
                // 연산이 끝났는가? caller 측에서 물어볼 때 대답해 주기 위해서
                public int ret; // 결과 값
                public CSubWork2(int i, CallBack d)
                {
                                start = i;
                                callback = d;
                                isCompleted = false
                }
                public void DoSubWork2()
                {
                                ret = Calc(start);
                                isCompleted = true
                                if ( callback != null ) callback(ret );
                }
                protected int Calc(int s)
                {
                                // 서브 작업
                                for(int i =s ; i < 10 ; i++)
                                {
                                                for(int j=0; j < 10000000 ; j++) {}
                                                // 많은 계산을 요구하는 작업
                                                Console.WriteLine(“SubWork :{0} “,i);
                                }
                                Console.WriteLine(“부 작업 완료”);
                                return s+1;
                }
}
class CAsync
{
                CSubWork2 w;
                public void BeginWork(int i, CallBack d)
                {
                                w = new CSubWork2(i, d);
                                Thread t = new Thread(new ThreadStart
                                ( w.DoSubWork2 )); t.Start();
                }
                public int EndWork()
                {
                                // 결과 값이 나올 때까지 블럭
                                do
                                {
                                                if ( w.isCompleted == true ) break
                                }while(true);
                                return w.ret;
                }
                public bool IsCompleted()
                {
                                return w.isCompleted;
                }
}
Array

리 턴 값이 준비돼 있는지 안 돼 있는지 확인하기 위해 IsCim pleted하는 메쏘드를 준비했다. 또한 콜백(callback) 형식으로 델리게이트를 넘길 때를 위해 그에 대한 델리게이트도 마련해뒀다. CAsync 클래스의 BeginWork 함수에서 인자와 콜백 함수를 넘겨주게 되는데 이때 만약 콜백 함수가 필요없다면 null을 넘겨주면 된다. 그러면 대신 IsCompleted하는 함수로 비동기 연산의 종료 여부를 확인할 수 있다.
한편 EndWork 함수에서는 비동기 연산이 종료되지도 않았는데 이 함수를 호출하면 준비되지 않는 결과 값을 가져가는 오류를 미연에 방지하기 위해 결과 값이 나올 때까지 블럭되게 한 후, 결과 값이 나오면 리턴하도록 했다. 그럼 먼저 caller 측에서 비동기 연산의 종료를 확인하는 예제를 보자(<리스트 4>).

<리스트 4> 비동기 연산 종료 여부 확인하기Array
CAsync a = new CAsync();
a.BeginWork(4,null);
while(!a.IsCompleted())
{
                // 메인 작업
                Console.Write(“.”);
}
for(int i = a.EndWork() ; i < 10 ; i++)
{
                for(int j=0; j < 10000000 ; j++) {} // 많은 계산을 요구하는 작업
                Console.WriteLine(“MainWork:{0} “,i);
}
Console.WriteLine(“메인 작업 완료”);
Array

이 번 예제에서는 콜백 함수가 필요없으므로 BeginWork 함수에서 null을 넘겨줬다. 그리고 대신 IsCompleted 함수를 이용해 종료 여부를 확인하는 동안 메인에서는 자신의 일을 할 수 있도록 했다. 종료된 후에는 EndWork 함수를 이용하여 결과 값을 가져와서 메인 부분의 일을 처리했다. 이 프로그램의 결과는 다음과 같다.

................................................................
................................................................
................................................SubWork : 4
SubWork : 5
SubWork : 6
SubWork : 7
.................................................................
.................................................................
..................................SubWork : 8
SubWork : 9
부 작업 완료
MainWork : 5
MainWork : 6
MainWork : 7
MainWork : 8
MainWork : 9
메인 작업 완료

이번에는 콜백 함수를 이용해 결과 값을 받아 오는 예제를 살펴보자(<리스트 5>). 콜백 함수에서는 비동기 함수의 리턴 값을 인자로 받아 와서 그 일을 하고 수행하고 있다.

<리스트 5> 델리게이트로 결과값 받아오기 Array
class Class1
{
                // 비동기 함수가 대신 호출해 줄 콜백 함수
                public static void CallMe(int ret)
                {
                                // 메인 작업
                                for(int i = ret; i < 10 ; i++)
                                {
                                                for(int j=0; j < 10000000 ; j++) {}
                                                // 많은 계산을 요구하는 작업
                                                Console.WriteLine(“MainWork:{0} “,i);
                                }
                Console.WriteLine(“메인 작업 완료”);
                }
                [STAThread]
                static void Main(string[] args)
                {
                                CAsync b = new CAsync();
                                b.BeginWork(4,new CallBack(CallMe));
                                for(int i=0; i<400; i++)
                                {
                                                // 아무 작업 수행
                                                Console.Write(“.”);
                                }
                }
}
Array

이 방법을 쓰면 caller 측에서 일일이 확인하지 않아도, 호출된 비동기 함수 부분에서 다 끝났다고 알려주는 격이 된다. 결과는 다음과 같다.

................................................................................
................................................................................
....................................................SubWork : 4
SubWork : 5
SubWork : 6
SubWork : 7
................................................................................
................................................................................
............................SubWork : 8
SubWork : 9
부 작업 완료
MainWork : 5
MainWork : 6
MainWork : 7
MainWork : 8
MainWork : 9
메인 작업 완료

이상이 비동기 프로그래밍의 대략적인 내부 구현이다. 실제 닷넷 프레임워크 내부적으로는 이보다 훨씬 더 복잡하게 진행이 되지만 대략적인 것은 이와 비슷하다. 그럼 이제는 실제 델리게이트를 이용해 비동기 프로그래밍을 해 보자.
델리게이트를 이용한 비동기 프로그래밍의 실제
이번 예제는 앞서 우리가 만든 프로그램과 비슷하다. 시작 숫자를 인자로 넘겨주면 그 숫자부터 프린트하는 프로그램이다. 이를 비동기 호출로 하기 위해 그에 대한 델리게이트를 다음과 같이 선언했다.

public delegate void SubWork(int i);

반환형을 다루는 예제는 조금 뒤에 다룰 것이므로, 지금은 반환형이 없는 델리게이트를 이용하자. 인자로는 시작 숫자를 넘겨줬다. 우리가 이렇게 델리게이트를 만들면 지난 시간에 소개했듯이 컴파일러는 이를 바탕으로 하여 다음과 같은 클래스를 만들게 된다.

public class SubWork : System.MulticastDelegate
{
   // 생성자
   public SubWork (object target, int32 methodPtr);
   public void virtual Invoke( int i );
   public virtual IAsyncResult BeginInvoke( int i,
      AsyncCallback callback, Object object);
   public virtual void EndInvoke( IAsyncResult result);
}

BeginInvoke는 앞에서 BeginWork와 비슷한 역할을 한다. 먼저 델리게이트가 받을 인자가 오고, 그 다음에 콜백 함수가 오고, 추가로 상태를 지정할 수 있는 인자를 쓸 수 있다. 이는 추가 정보를 넘겨줄 필요가 있을 때에만 쓰는 것이므로, 필요없다면 안 써도 된다. 그럼 이를 실제로 테스트해 보자(<리스트 6>).

<리스트 6> 델리게이트를 이용한 비동기 호출 Array
public delegate void SubWork(int i);
class Class1
{
                public static void DoSubWork(int start)
                {
                                for(int i=start; i < 10 ; i++)
                                {
                                                for(int j=0; j < 10000000 ; j++) {}
                                                // 많은 계산을 요구하는 작업
                                                Console.WriteLine(“SubWork :{0}”,i);
                                }
                }
                [STAThread]
                static void Main(string[] args)
                {
                                SubWork d = new SubWork(DoSubWork);
                                d.BeginInvoke(3,null,null);
                                for(int i=0; i < 10 ; i++)
                                {
                                                for(int j=0; j < 10000000 ; j++) {}
                                                // 많은 계산을 요구하는 작업
                                                Console.WriteLine(“MainWork:{0}”,i);
                                }
                }
}
Array

앞 서 우리가 한 예제와 동일한 기능을 하는 예제이다. 델리게이트에는 기본적으로 BeginInvoke라는 메쏘드가 있어 프로그래머가 손수 쓰레드 관련 코딩을 하지 않고도 손쉽게 비동기 호출을 할 수 있게 해 준다. 결과는 다음과 같다.

MainWork : 0
SubWork : 3
MainWork : 1
SubWork : 4
SubWork : 5
MainWork : 2
MainWork : 3
SubWork : 6
MainWork : 4
SubWork : 7
MainWork : 5
SubWork : 8
MainWork : 6
SubWork : 9
MainWork : 7
MainWork : 8
MainWork : 9

이와 같이 fire-and-forget형 프로그래밍의 경우 간단하지만 만약 반환 값을 다뤄야 할 경우는 약간 복잡해진다. 닷넷 플랫폼에서는 반환 값을 다루기 위한 4가지 스타일의 프로그래밍 기법을 제공한다.

1. Use Callbacks : 콜백 델리게이트를 이용해 비동기 부분에서 연산이 다 끝나면 델리게이트를 호출해 주는 방식
2. Poll Completed : caller 부분에서 연산이 다 끝났는지 IsCompleted라는 속성을 이용하여 계속 확인해 보는 방식
3. Begin Invoke, End Invoke : caller 측에서 결과 값을 받기 위하여 블러킹돼 기다리는 방식
4. Begin Invoke, Wait Handle, End Invoke : 앞의 방식과 비슷하나 wait handle에서 제한 시간을 설정해 줌으로써 계속 블러킹되는 것을 방지할 수 있다.

그럼 이들에 대한 예제를 하나씩 살펴보도록 하자. 이 예제는 SubWork에서 반환 값을 주는데, 역시 앞의 예제와 비슷하게 main 부분에서는 시작 값으로 사용될 값을 넘겨주게 된다. 먼저 <리스트 7>을 통해 1번의 경우부터 보도록 하자.

<리스트 7> Use Callbacks 방식 Array
public delegate int SubWork2(int i);
class Class1
{
                public static int DoSubWork2(int start)
                {
                                for(int i=start; i < 10 ; i++)
                                {
                                                for(int j=0; j < 10000000 ; j++) {}
                                                // 많은 계산을 요구하는 작업
                                                Console.WriteLine(“SubWork :{0}”,i);
                                }
                                return start + 1;
                }
                public static void CallMe(IAsyncResult ar)
                {
                                SubWork2 d = (SubWork2) ((AsyncResult)ar)
                                .AsyncDelegate;
                                int result = d.EndInvoke(ar);
                                for(int i=result; i < 10 ; i++)
                                {
                                                for(int j=0; j < 10000000 ; j++) {}
                                                // 많은 계산을 요구하는 작업
                                                Console.WriteLine(“MainWork:{0}”,i);
                                }
                }
                [STAThread]
                static void Main(string[] args)
                {
                                // 여섯번째 예제
                                Console.WriteLine(“*** 여섯번째 예제 ***”);
                                SubWork2 d2 = new SubWork2(DoSubWork2);
                                d2.BeginInvoke(3,new AsyncCallback(CallMe),null);
                                // 아무런 작업
                                for(int i =0; i<300;i++) Console.Write(“.”);
                                // 그냥 끝나면 안되므로 키입력 까지 대기
                                Console.Read ();
                }
}
Array

이번에는 콜백 함수를 이용해서 함께 넘겨주고 있다. 이 콜백 델리게이트의 형식을 보면 다음과 같다.

public delegate void AsyncCallback(IAsyncResult ar)

즉 리턴 값은 없고, 인자로 IAsyncResult라는 것을 받고 있다. 이는 닷넷 프레임워크에서 콜백 함수를 호출할 때 그 인자를 자동으로 넘겨주므로 걱정하지 않아도 된다. 이때 넘어오는 인자에서 AsyncDelegate라는 속성을 이용하면 해당 델리게이트를 받아올 수 있다. 따라서 이를 이용해 EndInvoke를 호출하여 결과 값을 받아오는 것이다. 결과는 다음과 같다.

SubWork : 3
SubWork : 4
SubWork : 5
................................................................................
................................................................................
.............................SubWork : 6
SubWork : 7
SubWork : 8
SubWork : 9
................................................................................
...............................MainWork : 4
MainWork : 5
MainWork : 6
MainWork : 7
MainWork : 8
MainWork : 9

<리스트 8>은 2의 경우로서 caller 측에서 계속 폴링(polling)하면서 연산이 다 끝났는지 확인하는 방법이다.

<리스트 8> Poll Completed Array
SubWork2 d3 = new SubWork2(DoSubWork2);
IAsyncResult ar = d3.BeginInvoke(3,null,null);
while( !ar.IsCompleted )
{
                Console.Write(“.”);
}
int ret = d3.EndInvoke(ar);
for(int i= ret; i < 10 ; i++)
{
                for(int j=0; j < 10000000 ; j++) {} // 많은 계산을 요구하는 작업
                Console.WriteLine(“MainWork:{0}”,i);
}
Array

caller 측에서 IAsyncResult의 IsCompleted 속성을 이용하여 계속적으로 연산이 끝났는지 안 끝났는지 확인하고 있다. 확인하는 동안 caller 측에서는 계속 다른 작업을 할 수 있다. 결과는 다음과 같다.

................................................................................
................................................................................
...........................................MainWork : 5
MainWork : 6
MainWork : 7
MainWork : 8
................................................................................
................................................................................
......................................MainWork : 9
SubWork : 3
SubWork : 4
SubWork : 5
................................................................................
................................................................................
......................................SubWork : 6
SubWork : 7
SubWork : 8
SubWork : 9
MainWork : 4
MainWork : 5
MainWork : 6
MainWork : 7
MainWork : 8
MainWork : 9

<리스트 9>는 3의 경우인데, 이는 결국 caller 측에서 비동기 호출된 부분이 끝날 때까지 블러킹되고 있으므로, 별로 권장하는 방법은 아니다. 이렇게 되면 비동기 호출을 할 의미가 없기 때문이다. 그러나 이도 결과 값을 받는 방법의 하나이므로 알아두자. 이번 예제는 콘솔 프로그램이 아닌 윈도우용 프로그램으로 만들었다. EndInvoke를 실행했을 때 caller 측이 블러킹된다는 것을 보여주기 위해서 윈도우용 프로그램으로 만들었다.

<리스트 9> Begin Invoke, End Invoke Array
public class Form1 : System.Windows.Forms.Form
{
                private System.Windows.Forms.TextBox textBox1;
                private System.Windows.Forms.Button button1;
                public delegate void SubWork();
                public void DoSubWork()
                {
                                // 서브 작업
                                string str;
                                for(int i =0 ; i < 100 ; i++)
                                {
                                                for(int j=0; j < 10000000 ; j++) {}
                                                // 많은 계산을 요구하는 작업
                                                str = String.Format(“SubWork :{0}rn”,i);
                                                textBox1.Text += str;
                }
}
[STAThread]
static void Main()
                {
                                Application.Run(new Form1());
                }
                private void button1_Click(object sender, System.EventArgs e)
                {
                                SubWork d = new SubWork(DoSubWork);
                                IAsyncResult r = d.BeginInvoke(null,null);
                                d.EndInvoke(r);
                }
}
Array

이 프로그램을 실행하고 ‘비동기 연산 시작’ 버튼을 누르면, 비동기 연산을 시작한다. 그러나 연산 결과가 바로 나오지도 않을 것이며 윈도우가 움직이지도 않을 것이다. 모든 연산이 끝난 후에 한꺼번에 나올 것이다. 이는 EndInvoke라는 함수 때문에 블러킹돼버려서 그런 것이다. 그래서 연산 결과를 표시하기 위해 윈도우 화면 갱신도 못하고 마치 다운된 것처럼 멈췄다가 결과가 나오게 된다. <화면 1>이 그 결과 화면이다.

Array

<화면 1> 윈도우에서 비동기 연산을 한 결과 화면

이번에는 마지막 방법인 4에 대해 알아보자. 이 방법은 3번 방법과 비슷한데 한 가지 다른 점은 time out을 정해줄 수 있어서 마냥 블러킹되는 것이 아니라 일정 시간이 넘어버리면 끊어버릴 수 있는 기능을 제공한다. 이번에는 비동기 연산을 0.1초 안에 해내지 못하면 블러킹을 해제하고 main 부분을 실행하는 프로그램을 만들어 본다.

<리스트 10> Begin Invoke, Wait Handle, End InvokeArray
SubWork2 d4 = new SubWork2(DoSubWork2);
IAsyncResult ar2 = d4.BeginInvoke(3,null,null);
if ( ar2.AsyncWaitHandle.WaitOne(100,false) == false )
                {
                                ret = 4;
                                Console.WriteLine(“도중 차단”);
                }
                else
                {
                                ret = d4.EndInvoke(ar2);
                }
for(int i= ret; i < 10 ; i++)
                {
                                for(int j=0; j < 10000000 ; j++) {}
                                // 많은 계산을 요구하는 작업
                                Console.WriteLine(“MainWork:{0}”,i);
                }
Array

< 리스트 10>은 caller 측에서 결과 값을 받기 위해서 0.1초간만 기다리고 그 안에 결과 값을 받지 못하면 블러킹을 중지하고 자신의 코드를 수행하는 예제이다. 결과는 다음과 같다. 결과에서 확인할 수 있는 것처럼 비동기 호출이 0.1초 안에 연산을 끝내지 못했기 때문에 main은 블러킹을 멈추고 자신이 할 일을 하고 있다.

SubWork : 3
SubWork : 4
SubWork : 5
SubWork : 6
SubWork : 7
도중 차단
MainWork : 4
MainWork : 5
MainWork : 6
SubWork : 8
SubWork : 9
MainWork : 7
MainWork : 8
MainWork : 9

리턴 값을 이용하지 않을 경우
이상으로 비동기 호출에서 결과 값을 얻기 위한 4가지 방법을 소개했다. 그러나 비동기 호출에서 결과 값을 받기 위해 꼭 리턴 값을 이용해야 하는 것은 아니다. 우리가 인자로 넘겨줄 때 ref형이나, out형으로 넘겨주면 꼭 리턴 값을 이용하지 않더라도 그 결과 값을 받는 방법이 된다. 그러나 여기서 한 가지 주의할 것은 인자(parameter) 값의 업데이트도 비동기적으로 일어난다는 것이다. <리스트 11>을 보자.

<리스트 11> Ref 파라미터를 사용한 예Array
public delegate void SubWork3(ref int i);
public static void DoSubWork3(ref int i)
{
                i *= 2;
}
[STAThread]
static void Main(string[] args)
{
                Console.WriteLine(“*** 열번째 예제***”);
                int v = 42;
                SubWork3 d5 = new SubWork3(DoSubWork3);
                IAsyncResult ar3 = d5.BeginInvoke(ref v,null,null);
                ar3.AsyncWaitHandle.WaitOne();
                Console.WriteLine(“EndInvoke를 호출하기 이전의 값 :{0}”,v);
                d5.EndInvoke(ref v,ar3);
                Console.WriteLine(“EndInvoke를 호출하고 난 후의 값:{0}”,v);
}
Array

이 번에는 값을 ref형으로 넘기고 있다. 만약 비동기 호출이 아닌 동기 호출이라면, 처음에 결과 값이 84가 나와야 할 것이다. 그러나 비동기 호출이기 때문에, 파라미터값의 업데이트도 비동기적으로 하므로 EndInvoke를 호출해야만 값의 업데이트가 일어난다. 그 결과는 다음과 같다.

EndInvoke를 호출하기 이전의 값 : 42
EndInvoke를 호출하고 난 후의 값 : 84

그런데 여기서 만약 reference type을 파라미터로 넘기게 되면 비동기 호출 부분에서는 이 값을 실시간적으로 업데이트를 한다. 즉 EndInvoke를 호출할 필요없이, 자신이 연산을 하면서 값을 지속적으로 갱신을 하는 것이다. 이번에는 reference type인 byte형 배열을 파라미터로 넘겨서 그 값을 확인해 보겠다. <리스트 12>를 보자.

<리스트 12> reference type을 파라미터로 넘긴 예Array
public delegate void SubWork4(byte[] b);
public static void DoSubWork4(byte[] b)
{
                for(byte i=0; i < b.Length; i++)
                {
                                for(int j=0; j < 10000000 ; j++) {}
                                // 많은 계산을 요구하는 작업
                                b[i] = (byte)(i*i);
                }
}
[STAThread]
static void Main(string[] args)
{
                Console.WriteLine(“*** 열한번째 예제***”);
                byte[] b = new byte[10];
                SubWork4 d6 = new SubWork4(DoSubWork4);
                IAsyncResult ar4 = d6.BeginInvoke(b, null, null);
                // ar4.AsyncWaitHandle.WaitOne();
                for(int i=0; i< b.Length; i++)
                                Console.WriteLine(“b[{0}]={1}”,i,b[i]);
}
Array

이 예제는 byte형 배열을 만든 후(이때 모든 배열의 값은 0으로 자동 초기화된다) 이를 비동기 호출로 넘겨주면, 비동기 연산 부분에서는 이 배열의 값을 넣어주는 작업을 하는 예제이다. <리스트 12>에서 결과 값이 다 나오도록 기다리는 부분을 주석 처리했는데, 그렇게 한 이유는 비동기 호출이 값을 실시간으로 고치는지 확인해 보기 위해서이다.
그 주석 처리한 부분의 주석을 없애 버리면 연산이 다 끝날 때까지 기다리기 때문에 언제나 정확한 값을 얻을 수 있을 것이다. 그러나 우리는 과연 비동기 호출이 값을 실시간으로 바꾸는지 확인하기 위한 것이므로 주석 처리를 했다. 결과 값은 컴퓨터 사양에 따라 다르고 할 때마다 다른 값이 나온다. 다음은 필자 컴퓨터에서 실행한 결과이다.
결과를 보면 5번 방까지는 제대로 들어갔는데, 그 이후로 값이 안 들어와 있다. 이는 caller 측에서 비동기 연산이 아직 다 끝나지 않았는데, 이는 그 값을 꺼내봤기 때문이다.

b[0]=0
b[1]=1
b[2]=4
b[3]=9
b[4]=16
b[5]=25
b[6]=0
b[7]=0
b[8]=0
b[9]=0

쓰레드 풀을 이용하자
이번 호에서는 쓰레드를 이용한 비동기 프로그래밍의 원리와 4가지 구현 방법에 대해 알아봤다. 그런데 여기서 아직 해결하지 못한 문제점이 남아 있다. 닷넷 프레임워크는 우리가 매번 비동기 호출을 할 때마다 쓰레드를 새로 만들어서 하는 것일까?
만약 그렇다면, 쓰레드라는 것이 적당한 수가 유지된다면 문제가 안 되지만 과도한 쓰레드의 생성은 오히려 쓰레드를 교체하면서 생기는 컨텍스트 체인지 오버헤드(context change overhead)가 있을 것이다. 닷넷 프레임워크에서는 이 문제를 쓰레드 풀(pool)을 이용해서 해결하고 있다. 다음 연재에서는 이 쓰레드 풀을 이용한 비동기 호출에 대해 알아보고, 최종적으로 게임 서버를 완성할 것이다. @

지난 시간에는 쓰레드를 통한 비동기 프로그래밍의 원리와 구현에 대해 알아봤다. 이번 시간에는 쓰레드를 효과적으로 관리하기 위한 쓰레드 풀과 함께 서버 제작에 필요한 네트워크 기술을 설명한다. 쓰레드 풀을 이용한 비동기 프로그래밍과 소켓의 개념에 대해 알아 보고 최종적으로 게임 서버를 완성해 보자.
비동기 프로그래밍은 내부적으로 쓰레드를 이용한다. 그런데 비동기 호출을 할 때마다 새로운 쓰레드를 생성해서 작업을 하게 되면, 많은 비동기 호출이 일어날 때에는 쓰레드의 수가 너무 많아져서 오히려 컨텍스트 스위칭(context switching)하는 데 시간이 더 걸리는 현상이 일어난다. 이러한 현상을 해결하기 위해서는 적절한 쓰레드 수를 유지하는 것이 해결 방법이라 할 수 있을 것이다. 닷넷에서는 그러한 관리 방법으로 쓰레드 풀이라는 것을 이용한다. 이를 이용해 시스템의 CPU 사용량에 따라 항상 적절한 쓰레드 수를 유지시켜 준다.
쓰레드 풀이란
먼 저 풀(pool)의 사전적인 의미는 스위밍 풀(swimming pool)처럼 물 웅덩이, 저수지라는 뜻이 있다. 다른 뜻으로는 카 풀(car pool)처럼 공동으로 이용하는 것이라는 뜻이 있다. 여기서는 두 번째의 공동으로 이용한다는 의미이다. 카 풀이라는 것이 에너지 절약을 위해서 이웃끼리 통근 시간 같은 때에 차를 같이 이용하는 것을 말한다. 쓰레드 풀도 이와 비슷한 것으로 쓰레드들이 시스템의 효율성을 높이기 위하여 집합적으로 모여 있는 것을 쓰레드 풀이라고 부른다.
쓰레드 풀은 쓰레드 생성 요청이 있을 때마다 그 쓰레드를 바로 생성하는 것이라 일단 큐에 그 쓰레드를 넣어 두었다가 쓰레드 풀이 그 요청을 처리할 수 있는 여유가 있을 때 큐에서 하나씩 꺼내서 처리를 한다. 닷넷 환경에서는 기본적으로 쓰레드 풀 안에서의 최대 25개의 쓰레드를 넣어 둘 수 있다. 이를 그림으로 나타내면 <그림 1>과 같다.

Array

<그림 1> 쓰레드 풀

쓰레드 풀의 사용 방법
닷넷에서 쓰레드 풀을 이용하기 위해서는 쓰레드 풀 클래스를 이용하면 된다. <리스트 1>은 쓰레드 풀 클래스의 메쏘드들이다.

<리스트 1> 쓰레드 풀 클래스 Array
{
     // Constructors
     // Methods
     public static bool BindHandle(IntPtr osHandle);
     public virtual bool Equals(object obj);
     public static void GetAvailableThreads(ref Int32 workerThreads,
          ref Int32 completionPortThreads);
     public virtual int GetHashCode();
     public static void GetMaxThreads(ref Int32 workerThreads,
          ref Int32 completionPortThreads);
     public Type GetType();
     public static bool QueueUserWorkItem(
          System.Threading.WaitCallback callBack);
     public static bool QueueUserWorkItem(
          System.Threading.WaitCallback callBack, object state);
     public static System.Threading.RegisteredWaitHandle
          RegisterWaitForSingleObject(
          System.Threading.WaitHandle waitObject,
          System.Threading.WaitOrTimerCallback callBack,
          object state, int millisecondsTimeOutInterval,
          bool executeOnlyOnce);
     public static System.Threading.RegisteredWaitHandle
          RegisterWaitForSingleObject(
          System.Threading.WaitHandle waitObject,
          System.Threading.WaitOrTimerCallback callBack,
          object state, UInt32 millisecondsTimeOutInterval,
          bool executeOnlyOnce);
     public static System.Threading.RegisteredWaitHandle
          RegisterWaitForSingleObject(
          System.Threading.WaitHandle waitObject,
          System.Threading.WaitOrTimerCallback callBack,
          object state, long millisecondsTimeOutInterval,
          bool executeOnlyOnce);
          public static System.Threading.RegisteredWaitHandle
          RegisterWaitForSingleObject(
          System.Threading.WaitHandle waitObject,
          System.Threading.WaitOrTimerCallback callBack,
          object state, TimeSpan timeout, bool executeOnlyOnce);
     public virtual string ToString();
     public static bool UnsafeQueueUserWorkItem(
          System.Threading.WaitCallback callBack, object state);
     public static System.Threading.RegisteredWaitHandle
          UnsafeRegisterWaitForSingleObject(
          System.Threading.WaitHandle waitObject,
          System.Threading.WaitOrTimerCallback callBack,
          object state, int millisecondsTimeOutInterval,
          bool executeOnlyOnce);
     public static System.Threading.RegisteredWaitHandle
          UnsafeRegisterWaitForSingleObject(
          System.Threading.WaitHandle waitObject,
          System.Threading.WaitOrTimerCallback callBack,
          object state, UInt32 millisecondsTimeOutInterval,
          bool executeOnlyOnce);
     public static System.Threading.RegisteredWaitHandle
          UnsafeRegisterWaitForSingleObject(
          System.Threading.WaitHandle waitObject,
          System.Threading.WaitOrTimerCallback callBack,
          object state, long millisecondsTimeOutInterval,
          bool executeOnlyOnce);
     public static System.Threading.RegisteredWaitHandle
          UnsafeRegisterWaitForSingleObject(
          System.Threading.WaitHandle waitObject,
          System.Threading.WaitOrTimerCallback callBack,
          object state, TimeSpan timeout, bool executeOnlyOnce);
     } // end of System.Threading.ThreadPool
Array

< 리스트 1>을 보면 거의 모든 멤버가 static이고 public constructor가 없음을 볼 수 있을 것이다. 이는 닷넷에서는 하나의 프로세스당 한 개의 풀만을 허용하기 때문이다. 즉 모든 비동기 호출은 같은 하나의 풀을 통해 이뤄진다. 따라서 제 3자의 컴포넌트가 새로운 풀을 만들어서 기존의 풀과 함께 돌아감으로써 생기는 오버헤드를 줄일 수 있는 것이다. 쓰레드 풀의 큐에 새로운 쓰레드를 추가시키려면 다음과 같은 메쏘드를 이용한다.

public static bool QueueUserWotkItem ( WaitCallBack callBack ,object state);

우선 WaitCallBack이라는 대리자를 이용하여 처리할 함수를 등록하고, state를 이용하여 함께 넘길 파라미터를 지정해 준다.

public delegate void WaitCallBack( object state );

WaitCallBack 대리자의 형식이 반환 값은 없고, 인자로는 state 하나만을 받는 형식이다. 따라서 쓰레드를 사용할 함수는 이와 같은 signature를 가져야 한다. 이를 이용하여 0부터 3까지 출력하는 3개의 작업을 만들어 보자. 이를 실행하면 다음과 같이 보통 일반 쓰레드를 이용하는 것과 비슷한 결과 화면을 볼 수 있다. 3개의 작업이 동시에 이뤄지고 있다.

1번 작업 : 0
2번 작업 : 0
1번 작업 : 1
3번 작업 : 0
2번 작업 : 1
1번 작업 : 2
3번 작업 : 1
2번 작업 : 2
1번 작업 : 3
3번 작업 : 2
2번 작업 : 3
1번 작업 끝
3번 작업 : 3
2번 작업 끝
3번 작업 끝

이번에는 Thread.IsThreadPoolThread이라는 속성을 이용하여 정말 쓰레드 풀을 이용하고 있는지와, 현재 쓰레드의 고유 번호를 나타내주는 메쏘드인 GetHashCode를 이용하여 그 값을 확인해 보자.

<리스트 2> 0부터 3까지 출력하는 4개의 작업Array
class Class1
{
     [STAThread]
     static void Main(string[] args)
     {
          WaitCallback callBack;
          callBack = new WaitCallback(Calc);
          ThreadPool.QueueUserWorkItem(callBack,1);
          ThreadPool.QueueUserWorkItem(callBack,2);
          ThreadPool.QueueUserWorkItem(callBack,3);
          Console.ReadLine();
     }
     static void Calc(object state)
     {
          for(int i= 0; i < 4; i++)
     {
          Console.WriteLine(“{0}번 작업: {1}”,state,i);
          Thread.Sleep(1000);
     }
          Console.WriteLine(“{0}번 작업 끝”,state);
     }
}
Array

<리스트 2>에 <리스트 3>과 같은 코드를 추가한다. 결과는 다음과 같다.

Main thread. Is Pool thread:False, Hash : 2
1번 작업 thread. Is Pool thread:True, Hash : 7
1번 작업 : 0
2번 작업 thread. Is Pool thread:True, Hash : 8
2번 작업 : 0
1번 작업 : 1
3번 작업 thread. Is Pool thread:True, Hash : 9
3번 작업 : 0
2번 작업 : 1
1번 작업 : 2
3번 작업 : 1
2번 작업 : 2
1번 작업 : 3
3번 작업 : 2
2번 작업 : 3
1번 작업 끝
3번 작업 : 3
2번 작업 끝
3번 작업 끝

즉 메인 쓰레드는 쓰레드 풀에서 하는 작업이 아니며, 나머지는 쓰레드 풀 내에서 작업하고 있음을 볼 수 있을 것이다. 그리고 각자 다른 해시코드를 가지고 있으므로 각자 새로운 쓰레드를 생성해서 작업하고 있는 것이다. 이는 현재 CPU 사용량에 여유가 있었기 때문에 각자 하나씩의 쓰레드를 생성해서 작업을 한 것이다.

<리스트 3> 쓰레드 풀 확인 방법 Array
// 메인 부분에 추가
Console.WriteLine(“Main thread. Is Pool thread:{0}, Hash: {1}”,
Thread.CurrentThread.IsThreadPoolThread,
Thread.CurrentThread.GetHashCode());
// 쓰레드 작업 부분에 추가
Console.WriteLine(“{0}번 작업 thread. Is Pool thread:{1}, Hash: {2}”,
state,
Thread.CurrentThread.IsThreadPoolThread,
Thread.CurrentThread.GetHashCode());
Array

만 약 CPU 사용량이 많아져서 컨텍스트 스위칭 시간이 더 걸릴거라 판단되면, 다른 쓰레드들은 큐에서 대기하다가 기존 작업이 끝나고 그 쓰레드를 재사용해서 작업을 하게 된다. 이에 대한 예를 보자. CPU 사용량을 높이려면 다음과 같은 함수를 추가한다.

int ticks = Environment.TickCount;
while( Environment.TickCount - ticks < 500 );

Environment.TickCount 속성은 마지막 리부팅한 후부터의 시간을 millisecond 단위로 리턴해 준다. Thread.Sleep(1000)이라는 부분 대신 이 함수를 넣고 실행해 보면 <화면 1>과 비슷한 결과를 볼 수 있다.

Array

<화면 1> CPU 사용량을 높인 후의 쓰레드 푸 작동 화면

<화면 1>을 보면 CPU 사용량이 100%임을 확인할 수 있다. 그리고 결과를 보면 3번 작업이 1번 작업과 같은 해시코드를 사용하고 있다. 즉 같은 쓰레드를 재사용하고 있는 것이다. 그래서 1번 작업이 끝난 후에, 1번 작업이 쓰던 쓰레드를 3번 작업이 다시 사용하고 있는 것이다. 이처럼 쓰레드 풀이라는 것은 현재 시스템의 상황에 따라 적절히 쓰레드 개수를 유지시켜 줌으로써 효율성을 높이고 있다. 그럼 이제 정말 비동기 호출이 쓰레드 풀을 이용하는지 확인해 보자.

<리스트 4> 비동기 호출 확인하기 Array
class Class1
{
     public static void Calc()
     {
          Console.WriteLine(“Is pool:{0}”, Thread.CurrentThread.
               IsThreadPoolThread);
          for(int i=1; i < 10 ; i++)
     {
               for(int j=0; j < 10000000 ; j++) {} // 많은 계산을 요구하는 작업
               Console.WriteLine(“SubWork :{0}”,i);
     }
}
[STAThread]
static void Main(string[] args)
{
     SubWork d = new SubWork(Calc);
     d.BeginInvoke(null,null);
     for(int i=0; i < 10 ; i++)
     {
          for(int j=0; j < 10000000 ; j++) {} // 많은 계산을 요구하는 작업
          Console.WriteLine(“MainWork:{0}”,i);
     }
}
Array

<리스트 4>는 지난 시간에 했던 예제이다. 그곳에 단지 쓰레드 풀임을 확인할 수 있는 문장을 하나 추가했을 뿐이다. 결과는 다음과 같다.

Is pool:True
SubWork : 1
MainWork : 0
SubWork : 2
MainWork : 1
SubWork : 3
MainWork : 2
SubWork : 4
MainWork : 3
MainWork : 4
SubWork : 5
SubWork : 6
MainWork : 5
SubWork : 7
MainWork : 6
SubWork : 8
MainWork : 7
SubWork : 9
MainWork : 8
MainWork : 9

이제 비동기 호출이 쓰레드 풀을 이용하는 것이 확실해졌다. 그러면 모든 비동기 호출은 이와 같이 델리게이트를 만들어서 해야 할까? 그것은 아니다. 우리가 매번 비동기 호출을 위해서 델리게이트를 만들어야 한다면 그것 또한 귀찮은 일일 것이다.
그래서 닷넷에서는 미리 비동기 호출을 위한 함수들을 마련해 두고 있다. Begin×××, End×××로 표기되는 메쏘드들이 비동기 호출을 위해 미리 만들어 둔 함수들이다. 델리게이트의 BeginInvoke도 이와 같은 연장선에 보면 될 것이다. 우리는 소켓을 이용한 비동기 통신 방법에 대해 알아볼 것이므로 소켓과 관련된 비동기 함수들을 살펴볼 것이다. 그전에 소켓의 기본 개념부터 설명하겠다.
소켓이란?
일반적인 의미로 소켓이란, 전구의 소켓처럼 꽂는 구멍을 말한다. 즉 다른 것과 연결시켜 주는 구멍이다. 컴퓨터에서의 소켓도 이와 비슷한 의미이다. 네트워크의 다른 대상과 정보를 교환하기 위한 구멍인 것이다. 일종의 네트워크 자료 교환을 위한 파이프라인(pipeline)으로 생각하면 된다.
일 반적으로 네트워크에서 정보를 주고받기 위한 주소로 IP 어드레스라는 것을 사용한다. 그런데 이 주소는 대개 하나의 컴퓨터에 한 개의 주소가 할당된다. 그런데 네트워크 정보 교환은 하나의 컴퓨터뿐만 아니라 여러 컴퓨터와 정보를 주고받아야 하므로 하나의 IP 주소로는 이 정보를 어디로 보내야 하는지 구분할 수 없다.
그래서 포트(Port)라는 개념을 쓴다. 이는 항구라는 뜻으로 각 네트워크 정보들이 통신하는 입구인 것이다. 일반적으로 HTTP는 80 포트를 사용하고, FTP는 21 포트를 사용한다. 그래서 어떤 한 컴퓨터에 네트워크 데이터를 보내더라도 포트 번호가 다르므로, HTTP용 데이터와 FTP용 데이터가 각각 제 자리를 찾아가는 것이다. 이를 그림으로 나타내면 <그림 2>와 같다.

Array

<그림 2> 포트의 개념

일반적으로 포트 번호는 0∼65535까지 쓸 수 있지만 0∼1024번까지는 80번이나 21번처럼 미리 정해진 포트 번호를 사용하므로 사용자가 임의의 포트 번호를 사용하려면 그 이상의 번호를 사용하면 된다.
< 그림 2>를 보면 포트에 소켓이 연결되어 있음을 볼 수 있을 것이다. 특히 서버쪽을 보면 하나의 포트에 여러 개의 소켓이 달려있음을 볼 수 있을 것이다. 이는 다중의 클라이언트가 하나의 포트로 접속하기 때문이다. 각 클라이언트마다 이들의 데이터를 맡아서 중개해주는 파이프라인(소켓)이 따로 있어야 하기 때문에 하나의 포트에 여러 개의 소켓이 달려 있는 것이다.
그 런데 여기서 한 가지 의문점이 있을 수 있다. 하나의 포트에 여러 개의 네트워크 데이터들이 몰려들어 올텐데 서버는 이를 어떻게 구분해서 각자의 전담 파이프라인(소켓)으로 보내주는 것일까? 이는 TCP/IP의 헤더를 보면 쉽게 해결이 된다.

Array

<표 1> TCP/IP 헤더

<표 1>을 보면 IP 프로토콜의 헤더에는 보내는 곳과 받는 곳의 IP 주소가 들어 있다. 한편 TCP 헤더에는 보내는 곳과 받는 곳의 포트 번호가 들어 있다. 이들 4가지의 정보는 서로의 데이터를 확실히 구분하는 기준이 되므로, 서버측에서는 이 헤더를 보고 각자에 맞는 소켓으로 데이터를 보내주는 것이다.
다시 <그림 2>를 보면 서버측의 포트 번호는 지정되어 있는 반면에 클라이언트측의 포트 번호는 일관성 없이 중구난방으로 아무 번호나 할당되어 있음을 볼 수 있을 것이다. 그 이유는 클라이언트 입장에서는 데이터를 보내야 하는 서버측의 포트 번호는 알아야 하지만 자신의 포트 번호는 그냥 비어있는 아무 번호나 써도 상관없다. 굳이 자신의 포트 번호를 미리 정하지 않아도 되는 것이다. 그래서 클라이언트가 서버로 연결할 때, 자신의 남는 포트 번호 중 아무나 한 개를 할당해서 소켓과 연결시켜 주는 것이다.
소켓의 구현 과정
소 켓은 <그림 3>과 같은 일련의 과정을 거쳐 작업이 진행된다. 먼저 서버측에서는 소켓을 생성하고 그 소켓을 특정 포트 번호와 연결(bind)시킨다. 그리고 상대방으로 연결이 오기를 허락하는 듣기(listen) 작업을 수행한다. 그러다가 클라이언트가 접속을 하게 되면 서버는 이를 받아들이고(accept) 새로운 소켓을 만들어서 그 새로운 소켓이 계속 통신을 담당하게 하고 자신은 다시 듣기(lisetn)상태로 들어간다.

Array

<그림 3> 소켓의 구현 과정

그런데 이 때 한 가지 주의할 것이 있다. 하나의 포트에는 한 개의 소켓만 bind할 수 있다는 것이다. 여기서 조심해야 할 것이 bind라는 말이다. 하나의 포트에 여러 개의 소켓이 있을 수는 있지만 bind는 오직 한 개만 된다. 하나의 포트에 두 개의 소켓을 bind하려 하면 에러가 나면서 bind가 실패하게 된다.
그럼 왜 bind는 하나만 되는 것일까? 그 이유는 앞에서 보았듯이 데이터를 구분할 방법이 없기 때문이다. 데이터를 구분할 때 TCP/IP 헤더를 보고 구분한다고 했다. 그런데 하나의 포트에 두 개 이상의 소켓이 bind되면 이들 데이터를 구분할 방법이 없는 것이다.
예 를 들어 <그림 2>에서 80번 포트에 HTTP와 FTP용 소켓 두 개를 bind시켰다고 해보자. 그러면 서버는 포트로 들어오는 패킷의 TCP/IP 헤더 정보를 보고 데이터를 구분하는데 그 헤더에는 IP와 포트 번호밖에 없다. 그래서 이 패킷이 HTTP용인지 FTP용인지 구분할 방법이 없는 것이다. 그래서 하나의 포트번호에는 하나의 소켓만 bind할 수 있다. 그러면 이제 실제로 간단한 소켓 통신 프로그램을 만들어 보자.

<리스트 5> 서버소켓 예제 Array
[STAThread]
static void Main(string[] args)
{
     Socket listeningSocket = new Socket(AddressFamily.InterNetwork,
          SocketType.Stream, ProtocolType.Tcp);
     IPAddress hostadd = Dns.Resolve(“localhost”).AddressList[0];
     IPEndPoint EPhost = new IPEndPoint(hostadd, 7000);
     listeningSocket.Bind(EPhost);
     listeningSocket.Listen( 10 );
     Console.WriteLine(listeningSocket.LocalEndPoint +
          “에서 접속을 listening하고 있습니다.”);
     Socket newSocket;
     while(true)
     {
          newSocket = listeningSocket.Accept(); // blocking
          Console.WriteLine(newSocket.RemoteEndPoint.ToString() +
          “에서 접속하였습니다.”);
     byte[] msg = Encoding.Default.GetBytes(“접속해 주셔서 감사합니다.”);
     int i = newSocket.Send(msg);
     }
}
Array

간단한 소켓 예제
< 리스트 5>는 서버 소켓 예제이다. 먼저 TCP 방식의 소켓을 생성하고 7000번 포트에 bind한 후 listen하고 있다. 그러다가 클라이언트가 접속을 하게 되면, 클라이언트의 주소를 표시해 주고 메시지를 전송해 주고 있다.

<리스트 6> 클라이언트 Array
[STAThread]
static void Main(string[] args)
{
Socket s = new Socket(AddressFamily.InterNetwork,
SocketType.Stream, ProtocolType.Tcp);
IPAddress hostadd = Dns.Resolve(“localhost”).AddressList[0];
IPEndPoint EPhost = new IPEndPoint(hostadd, 7000);
s.Connect(EPhost); // blocking
if ( s.Connected == true)
{
byte[] bytes = new byte[1024];
s.Receive(bytes); // blocking
Console.WriteLine(Encoding.Default.GetString(bytes));
s.Shutdown(SocketShutdown.Both);
s.Close();
}
}
Array

< 리스트 6>은 클라이언트의 코드이다. 클라이언트는 특정 포트와 bind할 필요가 없으므로 connect할 때 자동으로 임의의 포트가 할당된다. 서버로의 접속이 성공하면 메시지를 받아서 화면에 표시해 준다. 그럼 이제 이들의 결과 화면을 보자.

◆ 서버 화면
127.0.0.1:7000에서 접속을 listening하고 있습니다.
127.0.0.1:3912에서 접속하였습니다.
◆ 클라이언트 화면
접속해 주셔서 감사합니다.

서버 화면을 보면 클라이언트 측에서는 임의의 포트 번호에 소켓을 할당해서 접속하고 있다는 것을 확인할 수 있을 것이다. 그러면 이제 정말 하나의 포트에 여러 개의 소켓이 존재하는지 보자. 먼저 클라이언트를 두 개 실행시켜 서버에 접속하도록 하자. 다음은 서버 화면이다.

127.0.0.1:7000에서 접속을 listening하고 있습니다.
127.0.0.1:3916에서 접속하였습니다.
127.0.0.1:3917에서 접속하였습니다.

두 개의 클라이언트를 실행시켜서 7000번 포트에 두 개의 클라이언트가 접속을 했다. 이제 netstat -a라는 명령어를 ‘명령프롬프트’창에서 입력해 네트워크 상태를 확인해 보자.

C:>netstat -a
Active Connections
Proto Local Address Foreign Address State
TCP 한용희:7000 한용희:0 LISTENING
TCP 한용희:7000 한용희:3916 CLOSE_WAIT
TCP 한용희:7000 한용희:3917 CLOSE_WAIT

앞의 화면에서 다른 부분은 생략하고, 우리가 보기를 원하는 화면만 표시를 했다. 현재 로컬 컴퓨터의 7000번 포트의 상태를 보면 listening하는 상태가 있고, 이미 연결된 두 개의 정보가 나온다. 모두 같은 7000번 포트에 연결된 것들이다. 이로써 하나의 포트에 여러 개의 소켓이 있을 수 있다는 것을 확인할 수 있을 것이다.
이제 소켓에 대한 궁금증을 풀었다. 그런데 앞의 예제를 응용해서 게임 서버로 만들기에는 무리가 있다. 왜냐하면 accept할 때나 receive할 때 블러킹이 걸려서 다른 일을 하지 못하기 때문이다. 그러므로 우리가 지금껏 익혀온 비동기 호출을 이용해서 이 문제를 해결해 보자.
비동기 소켓 통신을 이용해 블러킹 해결
앞서 닷넷에서는 델리게이트를 따로 이용하지 않고서도 미리 준비된 Begin×××와 End×××를 이용해서 비동기 프로그래밍을 할 수 있다고 했다. 이를 이용해 앞서 만든 예제에 적용해 보자(<리스트 7>).

<리스트 7> 비동기 통신으로 작성한 서버 Array
class Class1
{
     static void AcceptCallBack(IAsyncResult ar)
     {
          Socket listener = (Socket)ar.AsyncState;
          Socket newSocket = listener.EndAccept( ar );
          Console.WriteLine(newSocket.RemoteEndPoint.ToString() +
               “에서 접속하였습니다.”);
          byte[] msg = Encoding.Default.GetBytes(“접속해 주셔서 감사합니다.”);
          int i = newSocket.Send(msg);
     }
[STAThread]
static void Main(string[] args)
{
     Socket listeningSocket = new Socket(AddressFamily.InterNetwork,
          SocketType.Stream, ProtocolType.Tcp);
     IPAddress hostadd = Dns.Resolve(“localhost”).AddressList[0];
     IPEndPoint EPhost = new IPEndPoint(hostadd, 7000);
     listeningSocket.Bind(EPhost);
     listeningSocket.Listen( 10 );
     Console.WriteLine(listeningSocket.LocalEndPoint +
          “에서 접속을 listening하고 있습니다.”);
     while(true)
     {
          IAsyncResult ar = listeningSocket.BeginAccept (
               new AsyncCallback(AcceptCallBack), listeningSocket);
          // non-blocking
          ar.AsyncWaitHandle.WaitOne();
          }
     }
}
Array

먼 저 예제에서 블러킹이 되었던 accept 부분을 비동기 함수인 BeginAccept로 바꾸었을 뿐 결과는 동일하다. 만약 이 프로그램을 윈도우폼으로 만들었다면 accept할 때 윈도우가 움직이는 것을 보면 확실히 블러킹되지 않았다는 것을 확인할 수 있을 것이다. 그러나 여기서는 간결한 예제를 위해서 콘솔 프로그램으로 만들었다.
<리스트 8>은 클라이언트를 비동기 방식으로 수정한 것이다. 이번에는 connect와 receive 두 개를 비동기 방식으로 만들었다. 결과는 먼저 예제와 동일하다. 이 예제들은 간단하기 때문에, 별 어려움이 없을 것이라 생각한다. 그러면 이 비동기 통신이 쓰레드 풀을 이용하는지 직접 확인해 보고 쓰레드 풀에 남아 있는 쓰레드의 갯수에 대해 알아보자.

<리스트 8>비동기 통신을 이용한 클라이언트 Array
class Class1
{
     static byte[] bytes = new byte[1024];
     static void ConnectCallBack(IAsyncResult ar)
     {
          Socket s = (Socket)ar.AsyncState;
     if ( s.Connected == true)
     {
          s.BeginReceive(bytes, 0, bytes.Length, SocketFlags.None,
               new AsyncCallback( ReceiveCallBack) , s);
          // non-blocking
     }
}
static void ReceiveCallBack(IAsyncResult ar)
{
     Socket s = (Socket)ar.AsyncState;
     int nLength = s.EndReceive(ar);
     if ( nLength > 0 ) // 0보다 작다면 접속이 끊어진 것이다.
     {
          Console.WriteLine(Encoding.Default.GetString( bytes ) );
     }
}
[STAThread]
static void Main(string[] args)
{
     Socket s = new Socket(AddressFamily.InterNetwork,
          SocketType.Stream, ProtocolType.Tcp);
     IPAddress hostadd = Dns.Resolve(“localhost”).AddressList[0];
     IPEndPoint EPhost = new IPEndPoint(hostadd, 7000);
     s.BeginConnect(EPhost, new AsyncCallback(ConnectCallBack) , s);
     // non-blocking
     Console.ReadLine();
     s.Shutdown(SocketShutdown.Both);
     s.Close();
     }
}
Array

I/O completion ports
< 리스트 8>에 다음과 같은 코드를 추가해서 <리스트 9>와 같이 현재 쓰레드의 상태에 대해 알아보자. ShowThreadInfo()라는 함수를 만들었다. 이는 현재 쓰레드의 해시코드, 쓰레드 풀인지 여부, 그리고 남아있는 쓰레드 풀의 여분 갯수를 표시한다. 앞서 쓰레드 풀은 시스템에 따라 적절한 쓰레드 갯수를 유지시켜 준다고 했다.

<리스트 9> 쓰레드의 상태를 알아보기 위한 코드 Array
static void ShowThreadsInfo()
{
     int workerThreads, completionPortThreads;
     Console.WriteLine(“Thread HashCode: {0}”,
          Thread.CurrentThread.GetHashCode());
     Console.WriteLine(“Is Thread Pool? : {0}”,
          Thread.CurrentThread.IsThreadPoolThread);
     ThreadPool.GetAvailableThreads(out workerThreads,
          out completionPortThreads);
     Console.WriteLine(“Available Threads”);
     Console.WriteLine(“WorkerThreads: {0}, CompletionPortThreads: {1}”,
          workerThreads, completionPortThreads);
     Console.WriteLine();
}
static void ConnectCallBack(IAsyncResult ar)
{
     Socket s = (Socket)ar.AsyncState;
     ShowThreadsInfo();
     if ( s.Connected == true)
     {
     s.BeginReceive(bytes, 0, bytes.Length, SocketFlags.None,
          new AsyncCallback( ReceiveCallBack) , s);
          // non-blocking
     }
     Thread.Sleep(2000);
}
static void ReceiveCallBack(IAsyncResult ar)
{
     Socket s = (Socket)ar.AsyncState;
     ShowThreadsInfo();
     int nLength = s.EndReceive(ar);
     if ( nLength > 0 ) // 0보다 적다면 접속이 끊어진 것이다.
     {
          Console.WriteLine(Encoding.Default.GetString( bytes ) );
     }
}
Array

기 본적으로 25개가 최대인데 쓰레드 풀에서 쓰레드를 하나씩 돌릴 때마다 이 최대 수치는 줄어들게 된다. 이를 표시해 주는 함수가 GetAvailableThreads라는 함수이다. 앞에서 Connect의 콜백 함수의 경우 비동기 호출 후 바로 끝나는 것을 막기 위해서 2초간 잠시 잠을 재웠다. 어떤 결과가 나올 것인가? 그냥 생각하기로는 connect에서 쓰레드 하나 쓰고 receive에서 쓰레드 하나 쓰니 남아있는 쓰레드 갯수는 23개가 돼야 할 것이다. 과연 그럴까?

Thread HashCode : 30
Is Thread Pool? : True
Available Threads
WorkerThreads : 24, CompletionPortThreads : 25
Thread HashCode : 33
Is Thread Pool? : True
Available Threads
WorkerThreads : 24, CompletionPortThreads : 24

쓰레드 풀인 것은 확인이 됐고 문제는 남아있는 쓰레드 개수이다. 비동기 호출인 receive를 했는데도 WorkerThread 개수가 변함이 없다. 대신 completionPortThread에서 숫자가 하나 줄었다. 왜 이런 현상이 일어나는 것일까?
그것은 또 다른 쓰레드 풀을 사용했기 때문이다.
앞에서 프로세스당 하나의 쓰레드 풀이 존재한다고 했는데 사실 하나가 더 존재한다. 그것은 바로 I/O 전용으로 또 하나의 쓰레드 풀, 즉 I/O completion 포트용 쓰레드 풀이다. 이는 I/O 작업 전용의 쓰레드 풀로서 I/O 작업을 완료했는지 안 했는지에 대한 체크를 담당하게 된다. 그럼 왜 I/O 전용 쓰레드 풀을 사용하는 것일까? 이것을 쓰는 것이 성능이 더 좋기 때문이다.
그러나 이 기능을 사용하려면 Winsock에서 이 기능을 지원해야만 한다. 그래서 앞 프로그램을 윈도우 95나 윈도우 98에서 실행하면 이들 운영체제의 Winsock에는 이 기능이 없기 때문에 닷넷에서는 자동으로 I/O completion 포트용 쓰레드 풀 대신에 workerThread를 이용해서 처리를 하게 해 준다.
그러나 윈도우 NT/2000/XP의 경우 Winsock2가 설치돼 있는데 이 Winsock2가 IOCP 기능을 지원하므로 별도의 IOCP용 쓰레드 풀을 가동해서 일을 처리하게 된다. 과거 비주얼 C++로 IOCP를 구현하려면 복잡하게 코딩을 해야 했으나 닷넷에서는 손쉽게 비동기 호출 중, 네트워크 I/O 관련 함수를 호출하면 자동으로 IOCP를 이용하게 되어 있어 보다 손쉽게 코딩을 할 수 있다.
Deadlocks
비동기 함수를 이용하는 데 있어 한 가지 주의 사항이 있다. <리스트 10>을 보자.

<리스트 10> Deadlocks Array
class ConnectionSocket
{
     public void Connect()
     {
          IPHostEntry ipHostEntry = Dns.Resolve( “localhost”);
          IPEndPoint ipEndPoint = new IPEndPoint(
               ipHostEntry.AddressList[0], 7000 );
          Socket s= new Socket( ipEndPoint.AddressFamily,
               SocketType.Stream, ProtocolType.Tcp );
          IAsyncResult ar = s.BeginConnect(ipEndPoint, null, null);
          s.EndConnect(ar);
          Console.WriteLine(“비동기 호출 완료”);
     }
}
class Class1
{
[STAThread]
static void Main(string[] args)
{
     for(int i=0; i < 30 ; i++)
     {
          ThreadPool.QueueUserWorkItem(new WaitCallback(PoolFunc) );
     }
     Console.WriteLine(“ThreadPool큐에 30개 적재”);
     Console.ReadLine();
}
static void ShowThreadsInfo()
{
     int workerThreads, completionPortThreads;
     Console.WriteLine(“Thread HashCode: {0}”
          ,Thread.CurrentThread.GetHashCode());
     ThreadPool.GetAvailableThreads(out workerThreads,
     out completionPortThreads);
          Console.WriteLine(“WorkerThreads: {0},
          CompletionPortThreads: {1}”,
     workerThreads, completionPortThreads);
     Console.WriteLine();
}
static void PoolFunc(object state)
{
     ShowThreadsInfo();
     ConnectionSocket connection = new ConnectionSocket();
     connection.Connect();
     }
}
Array

먼 저 비동기 호출을 하는 connectionSocket이라는 클래스를 만들었다고 하자. 그런데 어떤 사람이 이 클래스를 쓰면서 이를 쓰레드 풀 내에서 호출하기로 했다. 그래서 그는 30개 쓰레드를 연속으로 만들고 이를 쓰레드 풀에 적재하였다. 그리고 각 쓰레드가 비동기 호출을 하는 이 클래스를 사용하였다. 서버는 먼저 만든 서버를 그대로 이용하기로 하자. 어떤 결과가 나올 것인가? 다음 결과를 보자.

ThreadPool큐에 30개 적재
Thread HashCode : 3
WorkerThreads : 24, CompletionPortThreads : 25
Thread HashCode : 18
WorkerThreads : 23, CompletionPortThreads : 25
Thread HashCode : 19
WorkerThreads : 22, CompletionPortThreads : 25
Thread HashCode : 1
WorkerThreads : 21, CompletionPortThreads : 25
Thread HashCode : 20
WorkerThreads : 20, CompletionPortThreads : 25
Thread HashCode : 21
WorkerThreads : 19, CompletionPortThreads : 25
Thread HashCode : 22
WorkerThreads : 18, CompletionPortThreads : 25
Thread HashCode : 23
WorkerThreads : 17, CompletionPortThreads : 25
Thread HashCode : 24
WorkerThreads : 16, CompletionPortThreads : 25
Thread HashCode : 25
WorkerThreads : 15, CompletionPortThreads : 25
Thread HashCode : 26
WorkerThreads : 14, CompletionPortThreads : 25
Thread HashCode : 29
WorkerThreads : 11, CompletionPortThreads : 25
Thread HashCode : 30
WorkerThreads : 10, CompletionPortThreads : 25
Thread HashCode : 31
WorkerThreads : 9, CompletionPortThreads : 25
Thread HashCode : 32
WorkerThreads : 8, CompletionPortThreads : 25
Thread HashCode : 33
WorkerThreads : 7, CompletionPortThreads : 25
Thread HashCode : 34
WorkerThreads : 6, CompletionPortThreads : 25
Thread HashCode : 35
WorkerThreads : 5, CompletionPortThreads : 25
Thread HashCode : 36
WorkerThreads : 4, CompletionPortThreads : 25
Thread HashCode : 17
WorkerThreads : 3, CompletionPortThreads : 25
Thread HashCode : 4
WorkerThreads : 2, CompletionPortThreads : 25
Thread HashCode : 5
WorkerThreads : 1, CompletionPortThreads : 25
Thread HashCode : 7
WorkerThreads : 0, CompletionPortThreads : 25

실행을 해 보면 프로그램이 멈춰버릴 것이다. WorkerThread가 0이 되면서 프로그램이 더 이상 작동 안 하는 데드록(deadlock) 현상이 일어난다. 왜 이런 현상이 일어나는 것일까? 먼저 어떤 사용자가 쓰레드 풀을 사용하면서 30개의 쓰레드를 쓰레드 풀에 적재를 했다. 쓰레드 풀의 기본적인 최대치는 25인데 한꺼번에 30개의 쓰레드를 적재해 버린 것이다.
그 래서 이미 쓰레드 풀은 포화 상태가 되었다. 그런데 비동기 호출인 BeginConnect를 하려고 큐에 적재를 했는데, 이미 차지하고 있는 쓰레드 풀에서 빈 공간이 나올 기미가 안 보이는 것이다. 이미 connect 함수에서는 EndConnect 함수를 이용해 비동기 호출이 끝나기를 블럭되면서 기다리고 있는데 끝나질 않으니 한없이 기다리게 된다. 그렇다고 끝날 수도 없다. 이미 쓰레드 풀은 포화 상태이기 때문에 더 이상의 비동기 호출이 끼어들 자리가 없기 때문이다.
이 문제를 해결하기 위해서는 BeginConnect라는 비동기 호출을 동기 호출 함수로 바꿔주거나 처음 쓰레드 30개를 적재할 때, 한꺼번에 적재하지 말고 비동기 함수가 실행될 여지를 남겨주기 위해서 앞의 for문에서 Thread.Sleep(1000);이라는 문장을 주어 잠시 기다려 주면 비동기 호출이 실행될 여지가 있어서 데드록이 발생하지 않는다. 이러한 현상은 일반적으로 쓰레드 풀 내의 쓰레드가 비동기 호출이 끝나기를 기다릴 때 발생한다. 그러므로 쓰레드 풀과 비동기 호출을 같이 쓸 때는 주의해야 한다.
게임 서버 소개
지금까지 소개한 내용을 가지고 본격적으로 온라인 게임 서버를 만들어 보겠다.
네트워크 데이터 통신 방법
플 래시와 소켓 통신을 하는데, 데이터 통신 방법은 단순하게 문자열로 보내고 받는 방법을 택하였다. 원래 플래시에는 XMLSocket이라는 것을 제공한다. 이는 XML 데이터를 위한 소켓으로 데이터를 XML 방식으로 보내야만 하는 것이다. 그러나 게임과 같이 속도가 중요한 프로그램에서는 XML로 데이터를 처리하면 이를 파싱하는 데 오버헤드가 있어 바람직하지 않다. 그래서 플래시의 XMLSocket을 이용하기는 하지만 이를 파싱하지 않고 데이터를 콤마로 구분한 문자열로 보내서 쓰는 방법을 택하였다.
이때 주의할 것은 플래시의 XMLSocket은 맨 마지막에 문자열의 끝임을 나타내주는 ‘’ 표시가 있어야 제대로 받아들인다. 그래서 서버에서 데이터를 전송할 때 데이터의 끝에 ‘’을 추가해 주었다. 서버에서 네트워크 데이터를 보낼 때는 보통 서버에 접속한 모든 사용자에게 데이터를 전송하는데 자기 자신을 포함하는 경우가 있고, 자기 자신을 제외한 나머지에게 데이터를 전송할 경우가 있어 브로드캐스트(broadcast) 함수를 두 가지로 만들었다.

Array

<그림 4> 로그인 부분

Array

<그림 5> 대기실 부분

사용자 처리 방법
각 사용자마다 이를 담당하는 user 클래스를 따로 만들었다. 이 클래스의 멤버는 <리스트 11>과 같다.

<리스트11> User 클래스 Array
class User
{
    private Socket m_sock; // Connection to the user
    private byte[] m_byBuff = new byte[50]; // Receive data buffer
    public string m_sID; // ID 이름
    public string m_sTank; // 탱크 종류
    public string m_sTeam; // 팀 종류
    public int m_nLocation; // 방에서의 자신의 위치
    public Point m_point; // 자신의 위치
}
Array

각 사용자마다 자신의 네트워크 데이터를 처리할 소켓을 가지고 있고, 자신의 각종 정보를 가지고 있다. 메인에서는 이들을 arraylist로 유지해 새로운 사용자가 들어올 때마다 리스트에 추가해 준다.
방 관리
본 게임 서버에는 방이 하나밖에 없다. 최초에 들어온 사람이 방장이 되는 것이다. 이렇게 만든 이유는 간단하게 만들기 위해서이다. 본 게임 서버를 소개하는 목적이 소스를 이해하는 데 있으므로 가능한 최소한의 기능만 구현하여 소스 코드 크기를 줄였다. 아마 이 소스를 분석해 보면 쉽게 여러 개의 방도 만들 수 있을 것이다. 방이 하나밖에 없으므로 이미 게임중이면 다른 사용자가 들어오지 못하게 하였다.

Array

<그림 6> 게임 시작전 초기화 부분

Array

<그림 7> 게임중 부분

패킷 정보
서버와 클라이언트가 정보를 주고 받기 위해서는 서로 약속된 정보를 주고 받아야 한다. 이때 패킷의 처음 부분에는 이 패킷의 종류를 나타내는 정보를 담고, 나머지에 데이터를 담았다. 게임의 상태를 크게 세 부분으로 나눌 수 있는데 처음 로그인 부분, 대기실 부분, 게임 시작 전 초기화 부분, 게임 중 부분으로 나눌 수 있다. 이 세 부분에서 주고받는 패킷 정보는 <그림 4~7>과 같다.
플래시 MX를 이용한 게임 완성
이 번 연재에서는 쓰레드 풀을 이용한 비동기 프로그래밍과 소켓의 개념에 대해 알아 보고 최종적으로 게임 서버를 완성했다. 크게 네 가지 주제로 다시 한 번 정리하자면, 첫 번째는 쓰레드 풀에 대한 개념이다. 쓰레드 풀은 다수의 쓰레드를 운영하는 데 있어, 많은 쓰레드를 교환하는 데 있어 생기는 오버헤드를 해결할 수 있는 것이다. 이 쓰레드 풀의 핵심 개념은 바로 재사용이다.
두 번째 소켓은 한 마디로 네트워크 프로그래밍을 하는 데 있어 마치 파일을 조작하듯 좀더 쉽게 접근할 수 있도록 도와주는 도구라고 할 수 있다. 세 번째는 IO completion 포트에 대해 알아 봤다. 이는 Winsock2에서 제기된 기능으로써, 대량의 네트워크 접속을 처리하는 데 있어 쓰레드 풀을 이용하여 각각의 네트워크 접속을 효과적으로 처리하는 것을 말한다. 마지막으로는 간단한 게임 서버를 완성해 보았다. 다음 호에서는 플래시 MX를 이용해 게임의 클라이언트 부분을 완성해 볼 것이다. @

요즘 대부분의 웹 사이트에 플래시가 많이 이용되고 있다. 플래시의 특징이라고 한다면 적은 용량으로 역동적인 표현을 할 수 있고, 별도의 다운로드 없이 스트리밍 방식으로 볼 수 있다는 점이다. 이를 게임에도 응용해 보면 별도로 다운받아 설치하거나, CD로 설치하는 작업이 필요없는 온라인 게임을 만들 수 있다.
스트리밍 방식이므로 단지 사이트에 접속하기만 하면 게임이 시작되는 것이다. 이러한 방식은 게임 소프트웨어를 다운받아 설치하는 데 어려움을 느끼는 초보자들에게도 쉽게 접근할 수 있는 방법일 것이다. 이제 이를 이용하여 온라인 게임의 클라이언트 부분을 완성해 보자.
플래시MX의 새로운 기능
지난해 플래시MX가 출시되면서 여러 가지 새로운 기능들이 추가됐다. 먼저 그 기능들에 대해 알아보자.
비디오 지원
플 래시MX에서는 동영상 기능이 통합돼 플래시 내에서 동영상 제어가 가능하다. MPEG, DV, MOV, AVI 등의 비디오 포맷을 플래시로 가져오면 자체 포맷인 Sorenson Spark 코덱으로 변환해 플레이된다. 또한 비디오 객체에 대한 수정, 크기 조절, 회전, 가울이기, 마스크 작업을 할 수 있어 다양하게 보여줄 수 있다. 이는 게임 동영상을 보여줄 때 유용하게 쓰일 것이다.
이미지와 사운드의 동적 로딩
플래시MX에서는 외부 JPEG 및 MP3 파일을 런타임에 동적으로 로드할 수 있다. 이번 게임에서도 배경음악이 MP3 파일인데 용량이 1MB가 넘는다. 이를 다 받고 플레이하려면 좀 기다려야 하지만, 여기서는 스트리밍 방식도 지원하므로 다운로드하면서 플레이가 가능하다.
새로운 UI 컴포넌트
UI에 많이 사용되는 콤보박스, 스크롤 바, 체크박스 등을 따로 UI 컴포넌트로 만들어서 더 쉽게 이들을 다룰 수 있도록 했다.
새로운 이벤트 핸들러 메쏘드의 추가
기 존 플래시의 액션 스크립트를 이용해 프로그래밍하다 보면, 프로그래밍의 전체 구조를 파악하는 데 시간이 오래 걸렸다. 각각 오브젝트의 이벤트에서 일어나는 일은 그 오브젝트 내의 이벤트 핸들러에서 정의해야 하므로 소스 코드를 한 눈에 다 볼 수 없었기 때문이다. 일일이 각 오브젝트마다 그 안에 뭐라고 코딩을 해놓았는지 확인해 봐야만 전체구조를 파악할 수 있었다. 그래서 코딩 분량이 길어질수록 프로그램 파악하기는 어려워지고 수정하기도 어려웠다. 그러나 이제는 모든 코드를 한 곳에서 다 작성할 수 있다. 그것은 이벤트 핸들러를 메쏘드화해서 메쏘드 형식으로 기술할 수 있기 때문이다. 예를 들면 기존에는 버튼을 클릭할 때 대한 행동을 정의하려면 다음과 같이 직접 그 오브젝트에 가서 정의해야 했다.

On(release)
{
trace(“버튼을 눌렀습니다.”);
}

그러나 이제는 만약 그 버튼의 인스턴스 이름이 BUTTON이라면 프레임코드에서 다음과 같이 할 수 있다.

BUTTON.onRelease = function()
{
trace(“버튼을 눌렀습니다.”);
}

즉 그 오브젝트에 직접 가지 않아도 프레임에서 이와 같이 정의하면 앞과 똑같이 작동한다. 코드를 한 곳에 모아 두느냐, 아니면 각 오브젝트에 관련된 이벤트를 각 오브젝트 코드 내에 기술하느냐는 각각 장단점이 있다. 기존 방식은 각 오브젝트에 기술했으므로 이번에는 코드를 한 곳에 모두 모아둘 것이다. 과연 어떤 방법이 더 가독성이 좋고 유지 보수하기 좋은지는 독자들의 판단에 맡기겠다.
심플 포트리스를 위한 액션 스크립트
플래시로 프로그래밍을 하려면 액션 스크립트(Action script)에 대해 알아야 한다. 이는 자바 스크립트와 유사한 점이 많은데, 그 이유는 둘다 ECMA-262 표준을 기반으로 하는 스크립트 언어이기 때문이다. 아마 자바 스크립트를 다뤄본 경험자라면 쉽게 액션 스크립트도 사용할 수 있을 것이다. 액션 스크립트에 관한 주요사항 몇 가지를 살펴보겠다.
배열
액 션 스크립트에서의 배열은 많은 융통성을 가지고 있다. 배열의 크기는 사용자의 요구에 따라 마음대로 늘리고 줄일 수 있으며, 해시 테이블로도 이용이 가능하다. 또한 스택으로 이용이 가능하며, sort, slice, join과 같은 기능도 있다. 다음은 배열의 선언 방법이다.

[1] var a = new Array(“apple”, “banana”, “orange”);
[2} var a = [“apple”, “banana”, “orange”];
[3] var a = new Array(3);
   a[0] = “apple”;
   a[1] = “banana”;
   a[2] = “orange”;
[4] var a = new Array();
   a[“one”] = “apple”;
   a[“two”] = “banana”;
   a[“three”] = “orange”;
[5] var a = new Array();
   a.one = “apple”;
   a.two = “banana”;
   a.three = “orange”;

이 5가지 방법은 모두 동일하게 과일을 저장하는 배열을 생성하고 있다. 특히 [4]와 [5]를 보면 배열 첨자로 숫자뿐만 아니라, 문자를 쓸 수 있다는 것을 볼 수 있을 것이다. 액션 스크립트에서 처음에 배열 크기를 [3]과 같이 3으로 정해도 나중에 얼마든지 늘일 수 있다. 즉 a[10] = “melon”;과 같은 식을 써도 아무 이상이 없다. 이때 인터프리터는 a[3]∼ a[9]까지는 undefined로 채워준다.
변수 영역
액 션 스크립트 프로그래밍을 하다보면 가장 헷갈리는 것이 변수의 scope이다. 이 변수가 어디까지 영향력을 행사하는지 알고 있어도 코딩하다 보면 자주 실수를 하게 된다. 그러므로 이 부분을 확실히 알아둬야 에러없는 프로그램을 만들 수 있다. 다음과 같은 예제를 보자. 1번 타임 라인에 다음과 같이 변수 a에 값을 넣어 보자.

var a = 10;

이번에는 2번 타임라인에 ‘Inser Keyframe’ 명령어로 프레임을 삽입하고 다음과 같이 a값을 확인해 보자.

trace ( a );

a값이 제대로 나온다. 그러므로 프레임 번호가 다르더라도 같은 타임라인 안에서 정의된 변수는 모두 사용이 가능한 것이다. 그러나 한 가지 주의할 것은 액션 스크립트가 인터프리터 언어이므로 순차적으로 해석을 한다는 것이다. 그러므로 2번 프레임에 var b=20;이라고 선언하고 1번 프레임에서 trace(b);라고 하면 undefined라는 메시지가 나온다. 즉 1번 프레임을 해석하는 순간에는 2번 프레임에 무슨 변수가 있는지 모르므로 undefined가 출력되는 것이다. 여기서 또 한 가지 생각할 점이 있는데 다음과 같은 코드를 1번 프레임에 넣었다고 하자.

var a = 10;
function display()
{
    trace(b);
}

변수 a는 정의하고, 변수 b는 아직 정의하지 않았다. 하지만 display()라는 함수는 b를 출력하는 기능을 한다. 이제 2 프레임에 다음과 같은 코드를 추가해 보자.

var b=20;
display();
stop();

b값을 정의하고, 1번 프레임에 있는 display()를 호출한다. 이 때에는 b값이 정해져 있으므로, 20이라는 값을 출력한다. 이와 같이 액션 스크립트는 한번 정의된 변수는 같은 타임라인 안에서는 기억하고 있으므로 계속 사용이 가능하다. 한편, 변수는 같은 타임라인이기만 하면 되므로, 레이어에도 상관이 없다. 즉 레이어 2에 var c=30;이라고 정의하고 레이어 3에서 trace(c);를 해도 제대로 된 결과 값이 나온다.
무비클립 변수 영역
플 래시는 기본적으로 처음에 하나의 타임라인을 가지고 있지만, 무비클립 오브젝트에 따라 각자의 타임라인을 가지고 있다. 플래시를 처음 시작했을 때 나타나는 타임라인도, 하나의 메인 무비클립 오브젝트의 타임라인이라고 봐도 무방하다. 무비클립 오브젝트들은 각자의 타임라인을 가지고 있으므로 이들끼리는 서로 다른 변수 영역을 갖는다. 그러므로 서로 경로를 확실히 명시해 주어서 사용해야만 한다. 다음과 같은 예제를 보자.
메인 타임라인의 1번 프레임에 사각형 하나를 그리고, 이를 MoveClip로 변환한다. 그러면 이 사각형 MovieClip은 자신만의 타임라인을 가지게 된다. 이곳에 변수 a를 다음과 같이 정의하자.

var a=20;

그 다음, 메인 타임라인으로 돌아와서 2번 프레임에 다음과 같은 코드를 기술하자.

trace ( a );

a값이 제대로 나올까? 정답은 undefined가 나오게 된다. 메인 타임라인에서는 a가 정의되어 있지 않기 때문에 새로운 변수로 생각하고 undefined를 출력하는 것이다. 이를 제대로 출력하려면 다음과 같이 하면 된다. 먼저 사각형 무비클립에 ‘BOX’라는 인스턴스 이름을 주고 메인 타임라인의 2번 프레임을 다음과 같이 고친다.

trace ( BOX.a );

이렇게 하면 20이라는 제대로 된 값을 출력할 수 있다. 액션 스크립트에는 경로를 지정하기 위해 절대 경로를 위한 _root와 상대 경로를 위한 _parent라는 속성을 지원한다. _root라는 것은 메인 타임라인을 지칭하는 것으로 앞의 표현을 _root를 써서 표현하면 _root.BOX.a가 된다. _parent는 부모를 의미하는 것이다. 만약 메인 타임라인에 var c라는 변수가 있고, BOX에서 이를 참조하려면, _root.c라고 해도 되고 _parent.c라고 해도 된다. 이를 계층구조로 나타내면 <그림 1>과 같다.

Array

<그림 1> 무비 클립 계층 구조

지역 변수의 범위
액션 스크립트에는 진정한 전역변수가 없다. 메인 타임라인 안에 선언한 변수도 결국은 메인 타임라인 안에서만 효력을 발휘하기 때문이다. 메인 타임라인 밖에서 이를 참조하려면 앞에서처럼 경로를 명시해줘야만 한다. 액션 스크립트에서는 변수를 쓰는 데 있어 주의할 점이 있는데 그것은 변수를 선언없이 사용할 수 있다는 것이다. 그렇기 때문에 뜻하지 않는 오류가 발생하기도 한다. 그러므로 이들의 관계를 잘 파악해둬야만 한다. <리스트 1>의 예제를 보자.

<리스트 1> 지역변수의 범위 Array
var x = 10;
var y = 20;
trace( “ff를 호출하기 전의 변수 값” );
trace(“x: “ + x);
trace(“y: “ + y);
trace(“z: “ + z);
trace(“w: “ + w );
function ff()
{
     var x = 100;
     y = 200;
     z = 300;
     var w = 400;
     trace(“ff 함수 내에서의 변수 값”);
     trace(“x: “ + x);
     trace(“y: “ + y);
     trace(“z: “ + z);
     trace(“w: “ + w );
}
ff();
trace(“ff를 호출하고 난 후의 변수 값”);
trace(“x: “ + x);
trace(“y: “ + y);
trace(“z: “ + z);
trace(“w: “ + w );
Array

<리스트 1>을 실행하면 어떤 결과가 나올까? 실행 결과는 다음과 같다.

ff를 호출하기 전의 변수 값
x : 10
y : 20
z :
w :
ff 함수 내에서의 변수 값
x : 100
y : 200
z : 300
w : 400
ff를 호출하고 난 후의 변수 값
x : 10
y : 200
z : 300
w :

먼저 ff를 호출하기 전에는 w와 z값을 정의하지 않았기 때문에 아무런 값이 출력되지 않는다. 그 다음에 ff에 들어와서는, 먼저 x를 새로 선언(var)했다. 이는 밖에 있는 x와는 별도로 함수 ff 내에서 쓰기 위한 전용 변수를 선언한 것이다. 따라서 밖의 x와는 전혀 다른 변수이다. 그리고 y라는 변수를 사용했는데, 이것은 외부에 선언된 y변수와 동일한 변수이다. 그리고 z라는 변수를 쓸 때 선언없이 그냥 사용하였다. 그러면 이는 지역변수로 취급이 되지 않고 외부에서 선언한 것과 동일한 효력을 발휘한다.
액션 스크립트가 변수 선언없이 사용할 수 있기 때문에 가능한 일이다. 그러므로 z는 이제부터는 외부에서도 사용할 수 있는 변수가 되었다. w는 var이라는 키워드를 사용하여 지역변수로 선언하였기 때문에 ff내에서만 사용이 가능하다. 이제 ff를 벗어나서 각 변수를 다시 출력해 보면 변한 값들을 알 수 있을 것이다. 이처럼 액션 스크립트는 변수의 선언없이 아무 곳에서든 마음대로 선언하고 쓸 수 있다. 단 변수에 오타가 나더라도 이를 새로운 변수로 인식하기 때문에 코딩시 주의를 해야 한다.
액션 스크립트에서 소켓 통신
기존 액션 스크립트에서 서버와 통신을 하려면, loadVariable() 함수를 이용해 통신을 했다. 하지만 이 방식은 데이터 전송이 끝나면 채널도 끊어버리기 때문에, 지속적인 통신에는 어려움이 있다. 또한 항상 플래시에서 서버로 요청을 하기 때문에 서버 쪽에서 플래시 쪽으로 요청을 할 수가 없었다. 그러나 플래시 5에 와서 XML을 지원하면서부터 XML 데이터를 지속적으로 통신하기 위한 소켓인 XMLSocket이라는 클래스를 지원하기 시작했다.
게임에서 서로의 통신을 위해 XML 데이터를 써야 한다면 데이터를 일일이 파싱하는 데에 따른 오버헤드를 걱정해야만 한다. 게임에서는 속도가 중요하기 때문이다. 그래서 이번 게임에서는 XML Socket을 이용하기는 하되, 데이터는 XML을 이용하지 않는다. 단지, 콤마로 구분되는 스트링 데이터를 이용하여 통신을 할 것이다. 단 이때 주의해야 할 것은 플래시에서는 제로바이트로 구분되는 데이터를 줘야만 XMLSocket이 데이터를 받을 수 있다는 점이다.

<리스트 2> 플래시 서버Array
class Class1
{
     [STAThread]
     static void Main(string[] args)
     {
          Socket listeningSocket = new Socket(AddressFamily.InterNetwork,
          SocketType.Stream, ProtocolType.Tcp);
          IPAddress hostadd = Dns.Resolve(“localhost”).AddressList[0];
          IPEndPoint EPhost = new IPEndPoint(hostadd, 7000);
          listeningSocket.Bind(EPhost);
          listeningSocket.Listen( 10 );
          Console.WriteLine(listeningSocket.LocalEndPoint +
               “에서 접속을 listening하고 있습니다.”);
     Socket newSocket;
     while(true)
          {
          newSocket = listeningSocket.Accept(); // blocking
          Console.WriteLine(newSocket.RemoteEndPoint.ToString() +
               “에서 접속하였습니다.”);
          byte[] msg = Encoding.UTF8.GetBytes(“메세지 종류,내용”+ “”);
          int i = newSocket.Send(msg);
          }
     }
}
Array

즉 문자열의 끝을 나타내는 null 문자(‘’)가 데이터 끝에 꼭 있어야만 된다. 이것을 빠뜨리면 플래시는 XML 데이터로 인식하지 못해 데이터를 받지 않는다. 또한 서버 쪽에서 데이터를 보낼 때 UTF8 형식으로 데이터를 인코딩해서 보내야 제대로 된 내용을 받아 볼 수 있다. 그럼 이들의 간단한 예제를 살펴보자. <리스트 2>는 지난 호에 소개한 간단한 서버와 비슷한 예제이다.
지난 호 다른 점은 데이터 전송시 제로바이트를 추가했다는 것과 인코딩을 UTF8 형식으로 했다는 것뿐이다. 이 게임에서는 패킷을 주고받을 때 콤마로 구분된 문자열을 주고 받는데, 처음에는 메시지 종류, 그리고 그 다음에는 메시지 내용을 담고 있다. 이 내용은 지난 달에 이미 소개했기 때문에 그 부분에 대해서는 지난 호를 참고하기 바란다. 그러면 플래시에서 어떻게 통신을 하는지 클라이언트 예제를 보자.

<리스트 3> 플래시 소스 Array
var socket;
socket = new XMLSocket(); // 소켓 생성
socket.onConnect = handleConnect; // 접속했을 때 호출되는 함수
socket.onClose = handleClose; // 접속을 close했을 때 호출되는 함수
socket.onXML = handleIncoming; // 서버에서 메시지를 받을 때 호출되는 함수
var success = socket.connect(“localhost”, 7000); // 접속 시도
if ( success == true )
{
     trace(“연결 초기화 성공”);
}
else
{
     trace(“연결 초기화 실패”);
}
// 연결 완료시 호출되는 함수
function handleConnect( succeeded )
{
     if ( succeeded == true )
{
          trace(“연결 성공”);
     }
     else
     {
          trace(“연결 실패”);
     }
}
// 연결 종료시 호출되는 함수
function handleClose()
{
     trace(“연결 종료”);
}
// 서버로부터 메시지를 받을 때 호출되는 함수
function handleIncoming( msg )
{
     var msg = string(msg); // msg가 object형이므로 string형으로 바꾼다.
     var token = msg.split(“,”); // 토큰 나누기
     trace(“첫 번째 토큰:”+ token[0]);
     trace(“두 번째 토큰:”+ token[1]);
}
Array

< 리스트 3>을 보면 플래시의 XMLSocket을 이용해 소켓 통신을 하고 있다. 메시지를 받는 부분인 handleIncoming이라는 함수를 보면 split이라는 함수를 이용하여 콤마 단위로 토큰을 나누고, 토큰별 의미가 무엇인지 표시하고 있다. 다음은 실행 결과이다.

연결 초기화 성공
연결 성공
첫번째 토큰 : 메세지 종류
두번째 토큰 : 내용

심플 포트리스의 몇가지 알고리즘
이제 이 연재의 마지막 부분이다. 소켓 통신까지 되었으니 게임을 완성하는 일만 남았다. 전체 소스는 ‘이달의 디스켓’으로 제공하니 참고 바란다. 대신 포트리스 게임을 구현하는 데 있어 몇가지 알고리즘을 소개하고 마치겠다.
탱크로 구불구불한 지형 따라가기
게 임을 해보면 탱크가 구불구불한 구름 위를 따라서 자연스럽게 움직이는 것을 볼 수 있을 것이다. 이는 플래시의 액션 스크립트 중 무비 클립에 있는 hitTest(x, y, shapeFlag)라는 함수를 이용해 구현했다. 플래시에서의 충돌체크는 전통적인 방법인 사각형 충돌체크 외에도 앞의 메쏘드를 이용하면 한 점에 대한 충돌체크를 할 수 있다. 이를 이용해 탱크의 바닥 좌표와 구름과의 충돌 여부를 탐지해 이동위치를 정하는 것이다. 먼저 <그림 2>를 보자.

Array

<그림 2> 탱크의 이동

<그림 2>를 보면 탱크의 이동에 있어서 두 가지 경우가 있다. 하나는 갈 곳의 지면이 현재보다 아래에 있어 탱크의 y좌표를 증가시키는 경우와 다른 하나는 갈 곳의 지면이 현재보다 위에 있어 탱크의 y좌표를 줄이는 경우가 있을 것이다.
첫 번째 경우는 탱크와 구름의 충돌체크를 하여 충돌을 안 하면 탱크는 구름과 충돌할 때까지 y좌표를 늘려 나간다. 두 번째 경우는 탱크와 구름의 충돌체크를 하여 충돌을 하면 탱크와 구름이 충돌 안할 때까지 y좌표를 줄여 나간다. 이런 식으로 하면 탱크가 구름 위를 자연스럽게 움직이는 것이다. <리스트 4>는 이를 구현한 소스의 일부이다.

<리스트 4> 탱크 이동시키기Array
// 바닥면을 따라 움직이게 하기
var x,y;
x = eval(“TANK_IMG”+ gMyLocation)._x ;
// 탱크의 x위치 받아오기, 이때 움직일 x값은 이미 변경시켰다.
y = eval(“TANK_IMG”+ gMyLocation)._y ;
// 탱크의 y위치 받아오기, 아직 y값은 변경시키지 않았다.
if ( CLOUDS.hitTest(x,y,true) )
{
// 자신이 구름 속으로 들어간 경우, 가야할 곳이 구름과 충돌이면,
// 위로 올라와야 한다.
     do {
          y -= 2;
          if ( CLOUDS.hitTest(x,y,true) == false ) break;
     } while(y > 50 ); // 위로 올라올 때까지 y값 감소
     if ( y > 50 ) { // 유효한 값일 때에만
          eval(“TANK_IMG”+ gMyLocation)._y = y;
          eval(“TANK_ID”+ gMyLocation)._y = y - cText_y;
          eval(“TANK_HP”+ gMyLocation)._y = y + cHp_y;
     }
}
else {
// 탱크가 구름 밖에 있을 때 가야할 곳이 구름 밖이므로 탱크를 밑으로 내려줘야 한다.
     do {
          y += 2;
          if ( CLOUDS.hitTest(x,y,true) == true) break;
     } while(y < 500); // 구름과 닿을 때까지 y값 증가
     if ( y < 500) { // 유효한 값일 때에만
          eval(“TANK_IMG”+ gMyLocation)._y = y;
          eval(“TANK_ID”+ gMyLocation)._y = y - cText_y;
          eval(“TANK_HP”+ gMyLocation)._y = y + cHp_y;
     }
}
Array

포탄의 움직임
포탄은 포물선으로 운동하기 때문에, 우리가 고등학교 때 배운 포물선 운동의 공식을 대입하면 쉽게 포물선 운동을 시킬 수 있다. 먼저 우리가 고등학교 때 배운 포물선 운동의 공식은 다음과 같다.
Array
여 기에서 g는 중력가속도이다. 그런데 이 공식을 쓰면, 한 가지 문제점이 나온다. tan가 90도에서는 무한대의 값을 가지기 때문에, 대포를 수직 방향으로 쏘았을 경우, 이상한 값이 나와버린다. 그래서 x, y를 서로 따로 풀어서 구하는 공식을 사용했다. 다음은 그 공식이다.
Array
x 와 y를 분리하여 tan를 제거했다. 따라서 수직으로 대포를 쏘아도 제대로 된 값을 나타내 준다. 그런데 여기서 한 가지 주의할 것은 중력 가속도이다. 현실 세계에서 중력 가속도는 9.8m/s이다. 즉 지구가 밑으로 당기는 힘이 초당 9.8m나 된다는 것이다.
그런데 우리 게임은 픽셀 단위로 나타내는 세계이므로 이를 적절하게 바꿔줄 필요가 있다. 그냥 9.8이라고 쓰면 한 타임당 9.8픽셀씩 당기는 힘이 되므로, 너무 빨라서 미처 올라가지도 못하고 떨어져버린다. 그러므로 이 수치는 낮게 주어야 하는데, 실험 결과 0.04pixel/time 정도가 적당했다. <리스트 5>는 이를 구현한 소스의 일부다.

<리스트 5> 포탄의 포물선 운동 Array
// 콜백 함수 정의
BULLET.onEnterFrame = function()
{
     if ( CLOUDS.hitTest(BULLET._x, BULLET._y, true) ) {
     // 충돌했다면
          collide();
     }
     else if ( BULLET._x < 0 || BULLET._x > Stage.width ) {
     // 범위를 벗어낫다면
          out();
     }
     else {
          BULLET._x = gBulletsx + gV0*Math.cos(gTheta)*gTime;
          BULLET._y = gBulletsy + (-1)*(gV0*Math.sin(gTheta)
               *gTime - 0.04*gTime*gTime);
          gTime+=2;
     }
}
Array

이제 플래시로 게임을 만들자
지금까지 4회에 걸쳐 C#과 플래시를 이용해 심플 포트리스라는 온라인 게임을 만들어 보았다. 이 온라인 게임을 만드는 데 있어 이 두 플랫폼을 선택하게 된 데에는 크게 두 이유가 있다. 먼저 C#(닷넷)을 이용하게 된 이유는 대용량 서버를 구축하기 위해 Winsock2의 IOCP 기능을 이용해야 하는데, 이를 닷넷 환경에서 구축하면 보다 손쉽게 구현할 수 있기 때문이다. 닷넷에서는 기본적으로 비동기 소켓 통신을 하게 되면, 내부적으로 IOCP 기능을 이용해 보다 쉽게 코딩할 수 있다.
두 번째로 플래시를 선택한 이유는 클라이언트 쪽에서 별도의 프로그램을 다운로드할 필요없이 언제 어디서나 즉석에서 온라인 게임을 할 수 있다는 점 때문에 택하게 되었다. XMLSocket을 지원하면서 소켓 통신이 가능해졌기 때문에 이 소켓을 이용해 서버와 지속적인 연결을 할 수 있는 온라인 게임을 만들 수 있는 것이다.
이번 필자의 강의를 꾸준히 따라온 독자라면 이를 응용해 장기, 바둑, 테트리스, 포트리스, 오목과 같은 간단한 게임을 별도의 다운로드가 필요없는 플래시 게임으로 만들 수 있을 것이다. @

원본출처: ZDNet