메뉴 바로가기 검색 및 카테고리 바로가기 본문 바로가기

한빛출판네트워크

IT/모바일

구글 웹 툴킷을 사용한 AJAX 프로그래밍

한빛미디어

|

2006-06-28

|

by HANBIT

13,713

제공: 한빛미디어 네트워크 기사
저자: Robert Cooper, 한동훈 역

구글 웹 툴킷(GWT)은 자바원 2006에서 발표되었다. 시기적인 문제인지, 구글 블로그에서 프레젠테이션을 등록했지만, 그 영향은 거의 없었다. GWT 자체는 자바스크립트 생성기다. 흥미로운 점은 이 자바스크립트가 자바로부터 생성된다는 점이다. GWT는 특별한 API를 이용해 작성된 자바 코드를 가져와서, 브라우저에서 실행할 수 있는 에이잭스(Ajax) 코드로 변환한다는 점이다. 뿐만 아니라, 테스트 브라우저에 포함된 자바 코드를 실행할 수 있는 테스트 도구(그림1)까지 포함되어 있다. 이를 이용하면 명령줄이나 선호하는 IDE 환경에서 단계별 디버그(그림2), 프로파일, 에이젝스 프론트를 단위 테스트(unite test)할 수 있다.

그림1
그림1. GWT의 계층구조로 로깅을 보여주는 콘솔 윈도우

그림2
그림2. 테스트 브라우저에서 자바를 실행하면 단계별 코드 디버깅이 가능하다

구글 블로그에서 얘기한 것처럼, "잠시만 기다리세요! 더 있습니다!" GWT는 서블릿의 형태로 서버 응용프로그램에 콜백(call back)을 요청하는 기본적인 RPC 프레임워크를 제공한다. RemoteServiceServlet은 구현물에 서비스 메서드를 구현할 수 있게 해주며, 이들 메서드를 생성된 에이잭스 응용프로그램에 제공할 수 있다. 이런 작업은 서블릿에 public 메서드를 추가하기만 하는 것으로 끝나며, GWT가 클라이언트와의 순차화(serialization)와 비순차화(deserialization)를 처리해준다. 이 같은 프레임워크에 얼마나 많은 관심을 기울여야 할까? 아직 대답하지 말 것!

GWT는 독 패널(dock panel)수평 패널(horizontal panel)에 익숙한 데스크톱 응용프로그램 개발자들이 친숙하게 사용할 수 있도록 DOM과 매핑된 클래스와 레이아웃을 포함한다. 키워드 팝업에 쓸 수 있는 PopupPanel, 팝업 차단기 사용자를 위한 DialogBox, 아웃룩 바 스타일로 표시하는 StackPanel과 같은 조합해서 사용할 수 있는 위젯들을 제공한다. 또한, 응용프로그램의 상태에 따라 즐겨찾기가 가능하고, "뒤로 이동 버튼"을 관리할 수 있는 URI와 히스토리 관리 시스템도 제공한다.

시작하기

GWT는 톰캣안에서 몇가지 후킹(hooking)을 이용하고, 리눅스와 윈도우 환경에서 사용할 수 있는 모질라나 인터넷 익스플로러에서 실행할 수 있는 자바 응용프로그램일 뿐이다.(맥 사용자들에겐 유감이지만, 사용할 수 없다) GWT를 다운로드 받으면, "KitchenSink"를 비롯한 며가지 예제들이 있다. sample/KitchenSink 디렉터리에서 배치파일을 실행해서, 예제를 실행할 수 있다. 두 가지가 화면에 나타난다. 하나는 GWT 서버 모니터 응용프로그램이며, 다른 하나는 응용프로그램을 실행하는 것을 볼 수 있는 브라우저 윈도우다. java com.google.gwt.dev.GWTShell --help를 실행하면, 이용할 수 있는 옵션을 볼 수 있다.
Google Web Toolkit 1.0.20
GWTShell [-port port-number] [-noserver] [-logLevel level] 
    [-gen dir] [-out dir] [-style style] [-notHeadless] [url]

where 
  -port         Runs an embedded Tomcat instance on the specified
                port (defaults to 8888)
  -noserver     Prevents the embedded Tomcat server from running,
                even if a port is specified
  -logLevel     The level of logging detail: ERROR, WARN, INFO, 
                TRACE, DEBUG, SPAM, or ALL
  -gen          The directory into which generated files will be
                written for review
  -out          The directory to write output files into 
                (defaults to current)
  -style        Script output style: OBF[USCATED], PRETTY, 
                or DETAILED (defaults to OBF)
  -notHeadless  Causes the log window and browser windows to be
                displayed.  Useful for debugging.
and 
  url           Automatically launches the specified URL
새로운 GWT 프로젝트를 위한 배치 파일, 보조 클래스(scaffolding classes), 디렉터리 등을 생성하는 applicationCreator를 포함한다. 그러나, 우리의 목적은 이런 것들은 무시하고, 처음부터 시작하는 것이다. 먼저, GWT Maven 플러그인을 설치해야 한다. 플러그인 프로젝트의 압축을 풀고, install-now를 입력해서 maven 플러그인을 설치한다. 또한, gwt-user.jar를 ~/.maven/repository/com.google.gwt/jars/에 복사한다. JAR는 최종 WAR 파일에 포함되어야하므로, Maven POM 파일에 의존성으로 등록되어야 한다.

프로젝트 기본

GWT는 처음에는 약간 이상해보이는 기본 구조를 사용한다. 이에 대해 간단하게 살펴보자. 우리가 이용하게될 코드는 "참고자료"절에서 바로 실행가능한 응용프로그램으로 이용할 수 있다. 이 응용프로그램은 선택된 행에 대해 CSS 요소를 변경하고, 서버로부터 테이블의 정보를 가져오기 위해 RPC 호출을 이용하는 간단한 Table 클래스를 구현한 것이다.

첫번째는 [Module].gwt.xml 파일이다. 이 파일은 프로젝트에서 XML 마커(marker)로 EntryPoint 클래스와 특정 모듈을 연결한다. 모듈은 JFrame처럼 화면에 표시하는 기능을 포함한 것으로 생각하자. "Table.gwt.xml" 모듈의 정의는 다음과 같다.


 
 

 
 
  

다음은 우리의 모듈을 시작할 차례다. 실행하려면 루트 HTML 문서가 필요하다. 여기서는 src/java/com/totsp/gwt/public/Table.html을 사용한다. 진입점(entry point)를 HTML 문서에 삽입하기 위해 두 가지를 포함시켜야 한다. 먼저, 포함시킬 모듈을 가리키는 메타 태그를 다음과 같이 추가한다.

두번째, HTML에서 GWT가 렌더링을 시작할 위치에서 gwt.js 자바스크립트 파일을 포함시킨다.

이는 기본 스크립트를 실행하고, 시작하기 위해 필요한 것들을 알아내기 위해 gwt:module 값을 검사한다. 또한, 웹 응용프로그램이 아니라 자바 소스 트리의 공개 패키지(public package)에 HTML 페이지, CSS 파일, 그래픽 파일을 두어야 한다는 것에 주의해야 한다. 이는 다소 직관적이지 않지만, GWTShell과 GWTCompile 단계는 자바 소스 트리에서 이들을 로드하게 되어 있다. 그러나, GWT는 가능하면 기본설정에서 이것들을 읽어오기 때문에, 웹 응용프로그램 폴더에 이들을 넣어둘 필요는 없다.

지금까지는 단순했다. 이제, 클래스를 작성하는 것을 시작해보자. 첫번째는 EntryPoint를 구현하는 것이다. EntryPoint에서 실제 렌더링 관련 작업을 수행한다.
import com.google.gwt.core.client.EntryPoint;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.ui.RootPanel;
import com.google.gwt.user.client.ui.Widget;

public class TableEntryPoint implements EntryPoint {
    
  private Table table;

  public void onModuleLoad() {
    table = new Table( null, "myTable" );
    table.setStyleName("myTable");
    RootPanel.get().add(table);
    getData(); //just ignore this for the moment.
  }
Looking over these classes, we have a few things here. First is the Window class. 이들 클래스들을 보면서 몇 가지를 살펴보자. 먼저, Window 클래스가 있다. 이는 자바스크립트의 window 객체를 연상시킬 것이마. 이 클래스는 alert(), getClientWidth() 등의 함수를 제공한다. RootPanel은 get()을 이용해 위짓(widgets)을 배치하기 위한 루트를 반환하는 싱글톤 클래스다. 싱글톤을 이용한 방법은 작업하기에 매우 편하기 때문에, 구현하게될 클래스에 이를 추가할 것이다. 다음은 Widget이다. 이는 위짓들을 위한 기본 클래스이며, 다음 "Hello" 예제처럼 이벤트 처리를 위해 사용된다.
 Button b = new Button("Click me", new ClickListener() {
  public void onClick(Widget sender) {
    Window.alert("Hello, Ajax");
  }
});
EntryPoint 클래스를 시작하기 위해, 우리의 디스플레이를 빌드하기 위해 코드에 onModuleLoad() 메서드를 재정의(override)해야 한다. 이 기사에서의 목적에 따라, 간단한 Table 클래스를 사용할 것이다.

Table 클래스는 매우 간단한 연산을 포함한다. 이는 FlexTable를 확장(상속)하고, 자신을 자신의 리스너에 추가한다. 제목 표시줄이 아닌 행을 클릭하면, 해당 행에 -selected라는 접미어를 가진 CSS 스타일을 추가한다. 이외의 다른 작업은 하지 않는다.
public void onCellClicked(SourcesTableEvents sender, 
            int row, int cell) {
    this.getRowFormatter()
        .removeStyleName(selectedRow, selectedStyle);
    if ((source.getHeaderRow() == null) || (row > 0)) {
        this.getRowFormatter()
            .addStyleName(row, selectedStyle);
        selectedRow = row;
    }
}
테이블은 또한 TableDataSource로부터 데이터를 채우기 위한 메서드를 갖고 있다. 이에 대해서는 다음 절에서 살펴보자.

테이블 만들기

테이블 클래스는 매우 간단한 작업을 수행한다. TableDataSource 구현으로부터 테이블을 그리고, 선택된 행에 대해 스타일시트 클래스를 변경해서, 선택된 행을 강조하는 것이다. 이 작업을 위해 GWT의 FlexTable 클래스를 확장할 것이다. 이는 HTML 테이블을 표현하는 클래스이지만, 테이블 셀을 빠르게 생성해낼 수 있다. RPC 서블릿에서 가져올 수 있는 매우 간단한 TableDataSrouce를 이용해서 테이블을 채울 것이다. 이제, 코드를 살펴보자.

첫번째 단계는 서비스 인터페이스를 빌드하는 것이다. 우리의 구현은 매우 단순하다.
public interface DataService extends RemoteService {    
    public Person[] getData();
}
다음은 서비스에서 Async 인터페이스를 생성한다. 이는 전적으로 명명규칙으로 끝난다. 서비스 인터페이스와 모든 메서드에 맞는 인터페이스를 생성해야 한다. 그러나, 반환 형식 대신에 모든 메서드는 void이며, 마지막 인자는 AsyncCallback 구현물이어야 한다. 따라서, public void myMethod( Param p..., AsyncCallback callback )과 같이 되어야 한다. 마지막으로, 인터페이스 클래스 이름은 접미어 Async를 가져야 하며, RemoteService 인터페이스 이름과 일치해야 한다. 간단한 서비스를 구현하는 경우, 다음과 같다.
public interface DataServiceAsync {
    public void getData(AsyncCallback callback);   
}
다음은 서비스를 구현하는 것이다. 이는 GWT의 RemoteServiceServlet 서블릿을 확장하고, 서비스 인터페이스를 구현한 것이다.
public class DataServlet 
    extends RemoteServiceServlet implements DataService {
    
    public Person[] getData(){
    //...
    }
}
휴. 이제 거의 다 끝났다. 이제, 이 서블릿을 web.xml에 추가하고, 선언을 Table.gwt.xml 파일에 추가하면 된다. 두번째 항목은 GWTShell에게 서블릿을 로드하고, 톰캣에 테스트베드를 마운트하고, 클라이언트 스텁을 생성하라고 알려준다.

이제, 호출 자체를 다룰 차례다. GWT는 오직 비동기 호출만 지원한다. 이렇게 된 데에는 많은 기술적인 이유들이 있다. 잘 알려진 이유로는 비동기 호출을 하는 가장 분명한 경로는 응답이 올때까지 대기를 반복하는 것이다. 불행하게도, 많은 브라우저들은 실제로 이런 "반복"이 완료될 때 까지 XmlHttpRequest 이벤트를 일으키지 못한다. 그러나, 이건 에이잭스(Ajax)다. Sjax가 아니다. 오키?

역주: Sjax: Synchronous Javascript and XML을 의미

이제, EntryPoint 클래스에 getData() 메서드를 추가하자. 추가하려면, GWT 클래스에서 DataServiceAsync 구현을 얻고, AsyncCallback 핸들러를 만들어야 한다. 이 핸들러는 콜백의 반환 상태를 처리하는 메서드 onSuccess(), onFailure()를 갖는 간단한 인터페이스이다.
private void getData(){
    DataServiceAsync dataService = 
       (DataServiceAsync) GWT.create( DataService.class );
    ServiceDefTarget endpoint = (ServiceDefTarget) dataService;
    endpoint.setServiceEntryPoint("/DataService");
    dataService.getData(new AsyncCallback(){
        public void onSuccess(Object result) {
            table.setSource( 
                new SimpleDataSource( (Person[]) result ) );
        }

        public void onFailure(Throwable caught) {
            Window.alert("Unable to get data from server: "
                +caught.toString());
        }

    });
}
ServiceDefTarget으로 형변환하는 것을 알 수 있다. 왜 GWT.create( Class clazz, String url) 메서드를 만들지 않았는지는 설명할 수 없지만, 여러분은 그 이유를 알 수 있을 것이다.

이제, 테이블 객체와 voila에 setSource() 메서드를 호출하면 된다. 테이블은 새로운 데이터를 갖고, 스스로를 렌더링할 것이다. 또한, 서버에서 자바스크립트로 옮기는 객체들은 IsSerializable을 구현해야한다. 내 경험으로는 컬렉션은 때때로 완전하지 않으며, 치명적인 오류를 일으킬수도 있지만, 이는 배열과 기본 컬렉션들을 지원한다. 가능하면 컬렉션을 사용하는 것은 피하는 것이 좋다.

개정된 내용

에이잭스(Ajax) 또는 리아(RIA) 응용프로그램들을 사용해본 사람이라면, 뒤로 이동 버튼이 이들 응용프로그램에 쥐약이라는 점이다. 다행히도 GWT는 이를 처리할 수 있는 방법을 제공한다. History 클래스를 이용하면 화면 상태를 저장하고, 필요에 따라 그릴 수 있도록 String 토큰 하나를 저장할 수 있다. 페이지에 URL의 A 태그 부분에 토큰을 삽입하고, 필요에 따라 location.history를 써서 할 수 있다.

예제를 뒤로 이동 버튼을 지원하도록 변경해보자. 먼저, 상태 변화를 캡처하고, 이를 History 객체에 저장해보자. 이를 위해, onModuleLoad()의 테이블에 TableEventListener를 추가한다.
// Add a new listener to record the row history
table.addTableListener( new TableListener(){
    public void onCellClicked(SourcesTableEvents sender, 
        int row, int cell) {
        History.newItem( ""+row );
    }
});
이제, 뒤로 이동 버튼에 변경사항을 캡처할 수 있으며, 테이블에 선택된 행을 적절하게 업데이트할 수 있다. 이제, EntryPoint에 HistoryListener를 만든다.
public class TableEntryPoint 
    implements EntryPoint, HistoryListener {
//...

    public void onHistoryChanged(String historyToken){
        table.onCellClicked( table, 
            Integer.parseInt( historyToken ), 0);
    }
사용자가 뒤로 이동 버튼을 클릭할 때마다, 마지막으로 선택된 항목을 추적한다. 마지막으로 할 일은 북마크된 페이지나 페이지를 다시 불러오는 경우에도 초기화 작업이 동작하게 하는 것이다. 이 작업을 하기 위해 EntryPoint에 getData() 메서드를 다시 살펴보고, AsyncCallback의 onSuccess() 핸들러를 수정한다. onSuccess() 핸들러에서는 History 객체에 저장된 토큰이 있는지 검사하고, 테이블의 상태를 히스토르 토큰에 저장된 선택된 열로 설정한다.
public void onSuccess(Object result) {
    table.setSource( new 
        SimpleDataSource( (Person[]) result ) );
    if(History.getToken().length() != 0){
        table.onCellClicked( table, 
            Integer.parseInt( History.getToken()), 0);
    }
}
GWT 스크립트가 히스토리 조작을 위해 사용하는 숨겨진 iframe을 가진 HTML 페이지를 수정한다.

이는 상당히 원시적인 예제지만, 보다 복잡한 응용프로그램에서 History 클래스를 이용할 정도로 이해되기를 희망한다. 그림3은 예제 응용프로그램의 테이블을 표시한 것이다.

그림3
그림3. GWT 브라우저로 본 테이블 위짓

이게 다가 아니야!

GWT는 보다 멋진 기능들을 갖고 있다. 여기서는 다루지는 않았지만, JavaDoc 기반의 annotation 시스템(자바5 SE 제네릭은 IsSerializable을 지원하지 않는다), JUnit 테스팅 프레임워크, 사용하고 확장할 수 있는 다양한 위짓들을 포함하고 있다. 또한, GWT에서 제공하는 예제들도 살펴봐야 한다. 약간 이상한 프로그래밍 용례(idioms)를 보긴 했지만, 내 개발이력이 "GUI 개발자가 아니기 때문"인지도 모른다. 실제로, DynaTable 예제는 여기서 구축한 것 보다 훨씬 더 강력한 버전이다.

Maven 모듈

이 기사에서 제공하는 GWT Maven 모듈은 Maven 프로젝트에서 구글 웹 툴킷을 이용하는 데 몇가지 선택사항을 제공한다. 예제에서 제공하는 project.properties에서 다음을 볼 수 있다.
google.webtoolkit.home=/home/cooper/local/gwt-linux-1.0.21
google.webtoolkit.runtarget=com.totsp.gwt.Table/Table.html
google.webtoolkit.compiletarget=com.totsp.gwt.Table
google.webtoolkit.logLevel=DEBUG
첫번째 줄은 GWT 설치 경로이다. 두번째는 GWTShell이 시작할 대상을 지정한다. 이는 쉘을 실행하고 테스트 브라우저를 연다. compiletarget은 gwt:compile을 호출할 때 사용하길 원하는 모듈 선언이다. 마지막으로, logLevel은 테스트할 때 GWTShell에서 보여줄 로그 레벨이다. 프로젝트 속성에서 Mavenide properties 창에서 나머지 설정 사항을 볼 수 있다. 이들 속성들은 GWT에서 명령줄 인자와 일대일로 매핑되어 있다.

마지막 대상은 gwt:debug이다. gwt:debug는 GWTShell을 디버그 모드로 시작하며, 자바 코드를 단계별로 디버깅할 수 있다. 일단, 이 타겟을 호출하면, 디버거는 자동이로 실행되며, 어떤 것이 렌더링되기까지 연결을 대기한다. NetBeans에서 "Attach To Debugger"를 클릭해서, 로컬 머신을 선택하고, 포트 8888에 연결할 수 있다. 코드에서 중단점을 설정할 수 있으며, 디버그 브라우저에서 웹 응용프로그램을 이동할 수 있다. 이는 GWT에서 가장 훌륭한 기능이다. 자바 스크립트 디버깅은 Yenkman을 사용하더라도, 항상 부족함이 있다. GWT는 변화없이 사용하던 도구를 계속해서 사용할 수 있게 해준다.

마지막으로, war:war Maven 전에 gwt:compile을 실행해야 한다. 이는 프로젝트 maven.xml에 추가하는 것으로 할 수 있다.

 

라이선스에 대하여

라이선스에 대해 관심이 얼마나 있었는지? 일단, 이건 무료다. 그러나, 여러분이 익숙한 오픈소스 라이선스(OSI license)는 아니다. 구글은 이용약관을 갖고 있다. 기본적으로, 이를 이용하는 것은 자유지만, 조직 외부로 이를 재배포하는 것은 할 수 없다. 자, 나는 변호사가 아니지만, 이 조항으로 인해 심각한 곤경에 처할 수 있다.
구글 웹 툴킷 개발 도구나 이와 관련된 어떤 서비스나 소프트웨어도 배포할 수 없다. 구글에서 서면으로 권한을 획득하지 않는 한 구글 웹 툴킷 개발 도구로부터 파생된 어떤 작업- 수정, 복사, 라이선스, 만들기-도 허용되지 않는다.
GWT가 의도하는 바는 사람들이 새롭고, 흥미로운 커포넌트를 만드는 것을 허용하는 것이다. 그러나, 내게는 이것이 파생된 작업을 금지하는 것으로 여겨진다. 또한, 다운로드 페이지에 정보보호에 관한 지침(privacy notice)도 눈여겨 볼 필요가 있다.
정보보호에 관한 지침: 당신이 구글 웹 툴킷에서 호스팅되는 웹 브라우저를 이용할 때, 응용프로그램은 당신이 최신 버전의 제품을 사용하고 있는지 확인하기 위해 구글의 서버에 요청을 보낼 것입니다. 이 요청의 일부로, 구글은 당신이 구글 웹 툴킷을 다운로드 받은 날짜와 시간과 같은 타임스, 컴퓨터의 IP 주소를 포함한 사용 데이터를 기록할 것입니다.
리소스 Robert Cooper는 애틀랜타 지역의 J2EE와 웹 서비스 프로젝트에서 활동하는 자유 자바 개발자이다.
TAG :
댓글 입력
자료실