기술자료

DBMS, DB 구축 절차, 빅데이터 기술 칼럼, 사례연구 및 세미나 자료를 소개합니다.

Cloudscape와 Ajax

작성자
dataonair
작성일
2000-01-01 00:00
조회
371






Cloudscape와 Ajax - 예제

임베디드 데이터베이스와 웹 서비스 애플리케이션

Susan L. Cline _ Cloudscape Engineer, IBM

Cloudscape와 Derby는 Ajax 애플리케이션을 위한 훌륭한 데이터베이스 서버입니다. 특히, 클라이언트와 서버가 같은 호스트에 있을 때 관리 조건이나 기능들이 필요가 없습니다. 이 글에서는 임베디드 데이터베이스와 웹 서버 애플리케이션을 만드는 단계와 조건들을 설명합니다. 소스 코드와 애플리케이션은 zip 파일 형태로 다운로드 받을 수 있습니다. Derby나 Cloudscape 데이터베이스는 데이터 리파지토리로서 작동합니다. Jetty 웹 서버나 서블릿 컨테이너는 HTTP 요청을 관리하고, Ajax 기술은 클라이언트의 표현과 반응성을 향상시킬 수 있습니다.

머리말

Ajax는 웹 애플리케이션 개발자들의 사랑을 받고 있다. Google Maps, Yahoo email, 기타 인터랙티브 웹 사이트는 Ajax를 사용하여 웹 브라우저를 통해 인터랙션을 강화하고 있다.

Ajax는 스팩도, 프레임웍도 API도 아니다. JavaScript, XML, the Document Object Model (DOM), Cascading Style Sheets (CSS), 웹 서버나 서블릿 컨테이너에 대한 비동기식 HTTP 요청 등으로 구성된 기존 기술 세트라고 보면 된다. Ajax를 통해 웹 애플리케이션에서 할 수 있는 것은 클라이언트에 있는 데이터의 추가적인 프로세싱, 조작, 포맷팅이다. 같은 호스트 상에 클라이언트로서 존재할 수 있는 서버는 웹 서버나 서블릿 컨테이너 그리고 데이터 스토어로 구성된다. 데이터는 플랫 파일이나 데이터베이스에 존재한다. 이 글에서 논의되는 샘플 애플리케이션인 My Address Book에서 데이터는 Derby 데이터베이스에 속해있다.

다음은 Derby의 특징들이다:

Cloudscape 10.2에서는 XML 지원이 가능하다.
Java Database Connectivity (JDBC) Callable 문을 사용하여 스토어드 프로시저를 생성 및 호출한다.
EmbeddedDataSource를 사용하여 연결한다.
이미지 파일을 BLOBS로서 데이터베이스에 저장한다.

이 글에서는 Cloudscape와 Ajax를 사용하여 간단한 Address Book 웹 애플리케이션을 구현하는 방법을 설명한다. 이 글을 충분히 이해하려면 데이터베이스와 Ajax 기술에 대한 기본적인 지식이 필요하다. 참고자료 섹션에서 Cloudscape와 Ajax를 공부하기 바란다.

Derby와 Cloudscape 이름에 대하여

Cloudscape는 근본적으로 관리가 필요 없는(zero-admin) 삽입 가능한 백 퍼센트 자바 관계형 데이터베이스로서 1996년에 시장에 진입했다. 2004년 8월, IBM은 Cloudscape 10.0 관계형 데이터베이스 제품의 카피인 Derby를 Apache Software Foundation (ASF)에 기여를 하여 데이터 중심의 자바 애플리케이션을 혁신시켰다. IBM은 계속하여 Cloudscape 상용 오퍼링을 무료 다운로드로 제공하여 Derby 코어 엔진에 기능들을 추가하고 있다. 최신 릴리스는 Cloudscape 10.1이고 여기에는 Apache Derby 10.1이 포함된다.

임베디드 애플리케이션

"임베디드(embedded)" 란 단어는 애플리케이션이라는 정황 속에서, 엔드 유저가 관리에 책임을 지지 않고 기반 하위 시스템들을 모르는 상태에서 애플리케이션에 서비스와 기능을 제공하는 컴포넌트를 의미한다. 애플리케이션은 "그저 작동할 뿐이다." Wiktionary에서 찾은 "임베디드" 라는 단어의 일반적인 정의는 "어떤 것의 부분이거나, 단단하게 또는 안전하게 둘러싸인; 어딘가에 단단하게 머무르는"으로 되어있다.

이 샘플 애플리케이션에는 데이터베이스 엔진인 Derby와 서블릿 컨테이너인 Jetty가 애플리케이션 안에 삽입(embedded)되어 있다. Derby와 Jetty 모두 백 퍼센트 자바 구현이며 사용자가 추가 프로세스나 애플리케이션을 시작 또는 중지할 필요 없이 애플리케이션에 의해 시작된다.

Derby 데이터베이스 엔진과 JDBC 드라이버는 jar 파일인 derby.jar에 포함되어 있다. Jetty 웹 서버 역시 jar 파일인 org.mortbay.jetty.jar에 패키지 되어 있다. 하지만 Jetty를 서블릿 컨테이너로서 사용하고 Jetty의 로깅 조건을 완성하려면 추가 jar 파일이 필요하다.

다음 섹션에서는 애플리케이션의 아키텍처를 설명하고, 특정 웹 애플리케이션 유스 케이스에 어떻게 임베디드 솔루션이 맞는지를 설명하겠다.

My Address Book - 아키텍처

My Address Book 애플리케이션은 다음과 같은 기능을 갖춘 웹 애플리케이션이다.

개인별 연락처 정보를 추가한다.
연락처를 변경(편집)한다.
연락처를 삭제한다.
사진을 업로드 한다.
이름, 성 별로 연락처 리스트를 정렬하거나 데이터베이스에 저장된 사진을 가진 연락처만 보여준다.

Model View Controller (MVC) 방식이 웹 애플리케이션 디자인에 사용된다. 뷰는 HTML 페이지가 제공하고 JavaScript와 CSS를 활용한다. 컨트롤러는 서블릿이 제공하고 이 서블릿은 임베디드 Jetty 웹 서버에서 실행되며, Derby 데이터베이스는 백엔드로서 작동하고 애플리케이션의 모델 레이어를 나타낸다.

이 아키텍처는 여느 MVC 애플리케이션과 다를 바 없다. 애플릿, 모델, 컨트롤러를 사용하고 같은 자바 가상 머신(JVM)과 같은 호스트에서 실행된다.

그림 1은 이 애플리케이션의 전체적인 디자인이고 다음 섹션에서는 상세히 다루고자 한다.

060920_ajax_01.gif
그림 1. My Address Book 아키텍처


웹 서버와 데이터베이스를 실행하고 있는 JVM을 호스팅하는 브라우저

화살표 옆에 있는 숫자들은 애플리케이션의 흐름과 웹 서버와 데이터베이스 엔진을 부팅하는 순서이다.

1. HTML 페이지는 파일 시스템에 있는 브라우저에서 요청을 받는다.
2. startapplet.html 페이지가 브라우저에서 열린다. 이것은 Firefox 브라우저에서 JVM 내에서 실행되는 애플릿을 시작한다. 완벽한 자바 환경을 갖춘 애플릿은 임베디드 Jetty 웹 서버를 시작한다.
3. Start Cloudscape Ajax Demo 버튼이 눌리면 요청이 웹 서버로 가고 다음 페이지인 firstpage.html을 리턴한다.
4. firstpage.html은 HTTP 응답을 통해 브라우저에 제공된다.
5. 사용자 이름과 패스워드가 firstpage.html의 폼에 제출되면 요청이 Jetty로 보내진다.
6. Jetty는 임베디드 Derby 엔진을 부팅하는 애플리케이션 클래스의 인스턴스인 DerbyDatabase를 만들고, 데이터베이스를 만들고row)을 로딩한다.
7. 데이터베이스는 CONTACTS 테이블에서 행을 선택하고 이 값을 웹 서버로 전달한다.
8. 웹 서버는 HTML 페이지인 AddressBook.html의 형태로 클라이언트에 결과를 제공하고 프로세스에 JavaScript와 CSS를 활용한다.

잠깐만이라도 위 단계를 이미지로 그려보면 이전 섹션에서 "임베디드"라는 단어를 왜 그렇게 강조했는지 알 수 있을 것이다. 하나의 JVM이 Firefox 브라우저에서 실행된다. JVM은 애플릿이 임베디드 자바 웹 서버를 시작하도록 하고 이것은 임베디드 자바 데이터베이스 엔진인 Derby를 부팅한다. 모두가 하나의 JVM안에서 실행된다.

왜 이렇게 해야 하는가 그 질문에는 자세히 답을 해야겠지만 여기에서는 간단한 이유만 설명하겠다:

임베디드 애플리케이션은 전통적인 클라이언트/서버 애플리케이션 보다 관리가 더 쉽다.

클라이언트(Ajax의 순수한 요소 중 하나)에서 서버로 비동기식 데이터 요청에는 HTTP 프로토콜이 필요하다. 따라서 비동기식 데이터 요청을 하려면(이 경우 데이터베이스 안에 있는 데이터) HTTP 서버가 필요하다

애플리케이션의 컴포넌트

샘플 애플리케이션은 다음과 같은 소프트웨어 컴포넌트와 기술을 사용한다:
- Derby 데이터베이스 엔진과 JDBC 드라이버

  • "서버" 측의 데이터 리파지토리
- Jetty 웹 서버와 서블릿 컨테이너
  • 데이터베이스 요청에 대해 서블릿을 통해 HTML 페이지를 제공하고 컨트롤러로서 작동한다.
- Firefox 브라우저와 자바 런타임 환경 (JRE)
  • Mozilla Firefox version 1.5.x는 Java 1.5.x JRE를 사용하여 JVM을 시작한다.
- JavaScript
  • 웹 서버로 비동기식 요청을 하여 컨트롤러 서블릿을 통해 데이터를 가져온다. DOMParser를 통해 리턴된 데이터를 조작한다. 클라이언트에 있는 데이터를 정렬한다.
- CSS
  • HTML 페이지의 내용을 포맷팅 한다.

소프트웨어 조건

소프트웨어는 무료로 다운로드 할 수 있고 샘플 웹 애플리케이션을 실행 및 설치하기 전에 설치되어야 한다.

Mozilla Firefox 1.5.0.x 또는 이후 버전(참고자료)
Sun JRE, Version 1.5.0.x 또는 이후 버전(참고자료)
Windows (이 애플리케이션은 리눅스와는 호환되지 않는다.)

JRE와 Firefox 확인하기:

Firefox와 JRE를 다운로드 및 설치한 후에 JAVA SOFTWARE for Your Computer를 방문하여 브라우저에 JRE가 설치되었는지를 확인한다.
정확히 설치되었을 경우 다음과 같은 아웃풋이 생긴다.

We detected your Java environment as follows;
Description Your Environment
Java Runtime Vendor: Sun Microsystems Inc.
Java Runtime Version 1.5.0_06

이렇게 확인을 하는 이유는 애플리케이션이 성공적으로 작동하도록 하기 위해서이다. 성공하지 못했다면, 아마도 올바른 버전의 JRE를 다운로드 및 설치했더라도 밸리데이션 테스트에 보고된 JRE의 버전이 1.5.x 또는 이후 버전이 아니다. 브라우저에 애플릿을 실행할 때 어떤 JRE가 사용될 것인지를 설정해야 한다.

control panel을 열고 Java 아이콘을 시작한다. Java 탭을 선택하고 Java Applet Runtime Settings 섹션에서 View 버튼을 클릭한다. 위치가 실제로 유효한지를 확인해야 한다. 이 예제에서 JRE들 중 하나가 제거되었지만 여전히 무효 엔트리에 있다. 여러 JRE들을 설치했기 때문에 애플리케이션을 설치하기 전에 올바른 JRE가 사용되는지를 확인해야 한다.


이 예제에 사용된 Java control panel JRE 애플릿은 JRE 밸리데이션 테스트가 version 1.5.0_06으로서 사용되고 있다는 것을 보고할 때 다음과 같이 보인다.

060920_ajax_02.gif
그림 2. Java control panel, 애플릿 설정

샘플 애플리케이션 zip 파일:
파일 시스템에 Cloudscape_Ajax_Demo.zip 파일을 다운로드 한다. (하단 다운로드 참조) 모든 HTML, JavaScript, CSS, 자바 소스 파일들은 물론이고 Derby 데이터베이스 엔진과 Jetty 웹 서버 클래스 파일이 들어있다.

애플리케이션 설치하기

1. 위에 나열된 필수 소프트웨어를 설치한다. 2. Firefox에 사용되는 JRE의 버전이 소프트웨어 조건섹션의 "JRE와 Firefox 확인하기"에서 설명한 대로 정확한 것인지를 확인한다. 3. Cloudscape_Ajax_Demo.zip의 압축을 푼다. 다음과 같은 하위 디렉토리와 파일들이 포함된 Cloudscape_Ajax_Demo가 생길 것이다:

  • src
    • 자바 소스 파일
  • licenses
    • 이 애플리케이션에 포함된 라이 브러리용 라이센스 파일
  • cloudscape_ajax_webapp.zip
4. Mozilla Firefox 홈 디렉토리에 cloudscape_ajax_webapp.zip 파일을 푼다. Jetty 웹 서버는 이 디렉토리에서 시작되기 때문에 HTML 페이지를 공급하는 "홈" 디렉토리로서 간주된다. 이 예제에 사용되는 머신에서 Firefox 실행파일이 상주하는 곳의 경로는 C:\projects\Ajax\Mozilla Firefox이다. 일단 압축이 풀리면 Mozilla Firefox 홈 디렉토리 밑에 webapps/Ajax 디렉토리가 생긴다. 이 디렉토리에는 애플릿 jar 파일인 cloudscape_demo.jar (모든 의존 라이브러리 포함), JavaScript, CSS, HTML 파일들이 들어있다.

애플릿 jar 파일인 cloudscape_demo.jar에는 다음과 같은 라이브러리들이 포함되어 있다:


derby.jar -- Derby 데이터베이스 엔진의 10.2.x 스냅샷 버전과 XML 기능을 제공하는 임베디드 JDBC 드라이버이다. Derby 스냅샷은 지원되지 않는다.
org.mortbay.jetty.jar -- Jetty HTTP 서버와 서블릿 컨테이너
javax.servlet.jar -- 서블릿 API 클래스
commons-logging.jar -- Jetty 웹 서버에 필요한 로깅 클래스
xercesImpl.jar -- XML 지원을 위해 Derby에서 요구되는 Xerces 파서
cos.jar -- 이 애플리케이션에 사용되는 웹 서버로 파일을 업로딩 하는데 사용되는 O'Reilly 라이브러리 (jar 파일을 사용하는 것과 관련한 승인과 배포 규제에 대한 라이센스 디렉토헬퍼 클래스를 통해 Derby 데이터베이스에 액세스 하는 애플릿, 컨트롤러 서블릿, 클래스를 포함하고 있는 애플리케이션 클래스들. (이 모든 클래스들에 대한 소스는 메인 zip 파일의 src 하위 디렉토리에 있다.)

위 jar 파일에 포함된 클래스들은 추출되어 하나의 jar 파일인 cloudscape_demo.jar에 저장된다. 초기 애플리케이션 클래스는 위 라이브러리에 포함된 Jetty와 로깅 클래스들로 액세스 해야 한다. 각 jar 파일 마다 서명을 해야 하는 애플릿 태그에 모든 jar 파일들을 리스팅 하는 대신 다른 jar 파일에서 클래스들을 추출하여 하나의 자가 서명된 jar 파일을 만들었다.

애플리케이션 검사

설정과 사전 조건이 완성되면 애플리케이션을 시작하고 어떤 일이 막후에서 발생하는지 살펴보자. 각 아이템들이 큰 그림 속에서 어떻게 맞춰지는지에 대한 이해를 돕기 위해 그림 1의 아키텍처 다이어그램을 참조하겠다.

.5.0 Firefox 브라우저를 시작하고 Firefox 설치 디렉토리 밑에 있는 webapps/Ajax 디렉토리에 있는 startapplet.html 파일을 연다. 이 예제에서, 이 파일의 전체 경로는 C:\projects\Ajax\Mozilla Firefox\webapps\Ajax\startapplet.html 이다.

cloudscape_ajax_webapp.zip 파일을 올바른 위치에 압축을 풀었다면 startapplet.html이 열릴 때 보안 창이 생길 것이다. (그림 3) Susan Cline for the StartJetty 애플리케이션(애플릿 클래스)에 디지털 서명을 하고 계속 진행한다. startapplet.html을 불러올 때 이러한 보안 경고를 피하려면 Always trust content from this publisher 체크박스를 클릭하고 Run을 선택한다.

보안 경고에 대해서: 이 애플리케이션은 클라이언트 상에서(이 경우 클라이언트와 서버는 같은 호스트이다.) 파일을 읽고 써야 하고 애플릿에 의해 시작된다. 애플릿과 관련된 보안 모델은 이를 허용하지 않는다. 애플릿을 서명해야지만 파일의 읽기와 쓰기 액세스가 허용된다. 이 애플리케이션은 이를 실행하기 위해 서명된 jar 파일을 사용한다.

060920_ajax_03.gif
그림 3. 서명된 애플릿에 대한 보안 창

보안 창에서 Run을 선택하면 Firefox의 상태 바는 Applet StartJetty started를 가리킨다. 첫 번째 페이지는 임베디드 Jetty 웹 서버를 시작하는 애플릿을 포함하고 있는 HTML 파일이다.

Applet

startapplet.html 에는 애플릿을 시작하는 HTML APPLET 태그가 포함되어 있다. 이 APPLET 태그(Listing 1)는 애플릿을 포함하고 있는 자바 클래스의 이름과 리스팅 된 애플릿에 필요한 jar 파일을 필요로 한다. Firefox 브라우저 메뉴에서 View를 선택하고 Page Source를 선택하여 이 페이지의 HTML 소스를 본다.

Applet 클래스를 확장하는 모든 자바 클래스들이 오버라이드 해야 하는 init 메소드와 startJetty 메소드는 cloudscape.ajax.StartJetty 자바 소스 파일에서 발췌한 것이다. (Listing 2) zip 파일을 다운로드 하여 압축을 풀면 Cloudscape_Ajax_Demo/src/cloudscape/ajax 디렉토리에서 이 파일을 볼 수 있다.

Listing 1. startapplet.html의 Applet 태그

<APPLET code="cloudscape.ajax.StartJetty.class" codebase="."
width="1" height="1" name="StartJetty"
archive="cloudscape_demo.jar" alt=""></APPLET>

Listing 2. cloudscape.ajax.StartJetty를 시작하는 Applet 클래스

public void init() {
System.out.println("StartJetty: init() was called");
setBackground(Color.white);
startJetty();
}

private int startJetty() {
int startStatus = 0;

try {
// Create the server
server = new HttpServer();

// Create a port listener
SocketListener listener = new SocketListener();

listener.setHost("localhost");
listener.setPort(HTTP_SERVER_PORT);
listener.setMinThreads(5);
listener.setMaxThreads(250);
server.addListener(listener);

// Create a context
HttpContext context = new HttpContext();
context.setContextPath("/Ajax/*");
server.addContext(context);

// Create a servlet container
ServletHandler servlets = new ServletHandler();
context.addHandler(servlets);

// Map a servlet onto the container
servlets.addServlet("ControlServlet", "/ControlServlet/*",
"cloudscape.ajax.ControlServlet");

// Serve static content from the context
String home = System.getProperty("jetty.home", ".");
context.setResourceBase(home + "/webapps/Ajax/");
context.addHandler(new ResourceHandler());

// Start the http server
server.start();
startStatus = STARTED_OK;

} catch (java.net.BindException addressUsedExcept) {
System.out.println("Jetty server has already been started on port"+HTTP_SERVER_PORT);
startStatus = STARTED_ALREADY;
} catch (org.mortbay.util.MultiException multiServerExcept) {
System.out.println("Jetty server has already been started on port"+HTTP_SERVER_PORT);
startStatus = STARTED_ALREADY;
} catch (Exception e) {
e.printStackTrace();
startStatus = NOT_STARTED;
}
return startStatus;
}

startJetty 메소드는 HTTP 서버의 서버와 포트를 설정하고 서블릿 핸들러를 설치하며 애플리케이션의 콘텍스트를 만들고 애플리케이션용 서블릿을 등록하고될 때 호출되는 init 메소드는 startJetty 메소드를 호출한다.

그림 1은 1단계와 2 단계를 보여준다.

startapplet.html 페이지에서 Start Cloudscape Ajax Demo버튼을 클릭한다. 이것은 Jetty에서 firstpage.html 페이지를 보내고 (그림 1의 3, 4 단계) 사용자 이름과 패스워드를 띄운다.

여기에서 사용자 이름에는 cloudscape를 패스워드에는 ajax를 입력하여 로그인 하거나 새로운 사용자를 만든다.

firstpage.html에서 Login to Address Book 또는 Create New User 버튼을 클릭하여 다음을 수행한다. (그림 1의 5단계부터 8단계까지):

사용자 이름과 패스워드를 Jetty 웹 서버에 있는 서블릿으로 보낸다. 서블릿은 Derby 데이터베이스 엔진을 부팅하고 Derby 데이터베이스를 만드는 애플리케이션 클래스의 인스턴스를 만든다. 두 개의 테이블 USERS 테이블과 CONTACTS 테이블이 만들어진다. 샘플 행이 이 두 테이블에 삽입된다. 애플리케이션을 호출하면 Derby 엔진이 부팅되지만 데이터베이스를 다시 만들지는 않는다.
사용자 이름이 이미 존재하면 사용자는 인증을 받게 되고, 새로운 사용자 이름이라면 하나의 행이 USERS 테이블에 삽입된다. 다음 페이지는 애플리케이션의 메인 페이지인 AddressBook.html이다.

서블릿

서블릿 클래스, ControlServlet.java의 init 메소드를 보자. 이것 역시 다운로드 가능하다.

서블릿 컨테이너에 의해 처음으로 서블릿이 로딩될 때 호출되는 메소드가 init 메소드이다. 이 메소드는 애플리케이션 클래스인 DerbyDatabase를 만들고 이것의 initialize 메소드를 호출한다. 앞서 설명한 것처럼 이 메소드는 데이터베이스 엔진을 부팅하고 이들이 없다면 데이터베이스와 테이블을 만든다.

Listing 3. 제어 서블릿의 init() 메소드, cloudscape.ajax.ControlServlet

public void init() throws ServletException {
derbyDB = new DerbyDatabase();
derbyDB.initialize();
}

DerbyDatabase 클래스의 initialize 메소드의 일부를 통해서 EmbeddedDataSource를 만들고, setCreateDatabase 메소드를 사용하여 데이터베이스를 만들고, 데이터베이스 이름을 지정하고, 데이터베이스로 연결하는 방법을 볼 수 있다.

Listing 4. Derby EmbeddedDataSource, cloudscape.ajax.DerbyDatabase 생성하기

public boolean initialize() {
Connection conn = null;
boolean success = true;
if (isInitialized) {
return success;
}

if (ds == null) {
ds = new EmbeddedDataSource();
ds.setCreateDatabase("create");
ds.setDatabaseName("DerbyContacts");
Statement stmt = null;
try {
conn = ds.getConnection();
} catch (SQLWarning e) {
...

지금, 우리는 어디에 와 있는가 애플리케이션에 로그인 하여 사용자 이름과 패스워드를 확인하거나, 새로운 사용자 이름과 패스워드를 만들어 이를 Derby 데이터베이스에 삽입했다. 이 모든 과정을 성공하면 애플리케이션의 메인 페이지인 AddressBook.html은 다음과 같이 나타난다.

060920_ajax_04.gif
그림 4. 애플리케이션의 메인 페이지, AddressBook.html

이 페이지에서 연락처 추가, 편집 또는 삭제, 리스트 정렬 같은 다양한 작업들이 수행될 수 있다. 우선, 연락처를 추가해 보자. 아래 이미지는 Add 버튼이 클릭되고 필드가 채워져서 새로운 연락처를 추가한 후 나타나는 페이지 모습이다.

060920_ajax_05.gif
그림 5. 연락처 추가하기, AddContact.html

Add Contact 버튼을 클릭한다. 모든 필드가 채워지면 JavaScript 팝업 박스가 이 연락처의 사진을 실을 것인지를 묻는다. 필드에 알맞은 값이 채워지지 않으면 JavaScript 함수가 경고하여 페이지를 제출하기 전에 모든 필드를 채우도록 한다. Add Contact 버튼이 클릭되면 많은 일들이 막후에서 진행된다.

JavaScript

Add Contact 버튼이 클릭되면 이 버튼은 insertContact 함수가 호출되도록 지정하여 JavaScript의 이벤트 핸들링 기능을 사용한다. 이 함수가 하는 첫 번째 일은 형식의 모든 필드들이 채워졌는지를 확인하는 것이다. 채워졌다면 변수가 만들어져서 이 폼에 포함된 모든 값을 포함시킨다. makeDerbyRequest라고 하는 또 다른 JavaScript 함수가 호출된다. makeDerbyRequest 함수는 Jetty 웹 서버에 실행되는 서블릿으로 비동기식 요청을 하는 제어 포인트이다.

Listing 5는 Add Contact 버튼의 HTML을, Listing 6은 JavaScript의 insertContact와 makeDerbyRequest 함수 코드를 보여준다.

Listing 5. AddContact.html의 JavaScript 온클릭 이벤트 핸들러

<INPUT type="button" name="add_button" value="Add Contact"
onclick="insertContact()" class="buttons">

Listing 6. Add Contact 버튼 클릭, address_book.js 처리하기

var request = false;

try {
request = new XMLHttpRequest();
} catch (trymicrosoft) {
try {
request = new ActiveXObject("Msxml2.XMLHTTP");
} catch (othermicrosoft) {
try {
request = new ActiveXObject("Microsoft.XMLHTTP");
}
catch (failed) {
request = false;
}
}
}

function insertContact() {
var isFormValid = validateForm("add_contact_form");
if (!isFormValid) {
showErrors("add_contact_form");
}
else {
var parameters = "&firstname=" + document.add_contact_form.firstname.value ...
...
makeDerbyRequest("insert", parameters);
}

return isFormValid;
}

function makeDerbyRequest(databaseAction, parameters, selectedOption) {
var url = "/Ajax/ControlServlet/querytype=";
if (databaseAction == "show") {
url = url + "show";
request.open("GET", url, true);
request.onreadystatechange = updateContactList;
request.send(null);
}

else if (databaseAction == "login") {
url= url + "login" + parameters;
request.open("GET", url, true);
request.onreadystatechange = verifyLogin;
request.send(null);
}

else if (databaseAction=="insert") {
url = url + "insert"+ parameters;
request.open("GET", url, true);
request.onreadystatechange = showInsertResults;
request.send(null);
}
...
...

makeDerbyRequest 함수는 insert의 databaseAction 매개변수와 이름 리스트와 함께 호출되거나 성과 이름 같이 연락처 애트리뷰트의 모든 것을 나타내는 값 쌍으로 호출된다. makeDerbyRequest 함수에 있는 request 변수는 초기화 되고 XMLHttpRequest 객체를 나타낸다. XMLHttpRequest 객체는 HTTP 서버에서 XML이나 텍스트를 비동기식으로 받을 때 Ajax에서 사용되는 웹 브라우저 DOM의 확장이다. XMLHttpRequest 함수의 onreadystatechange 속성인 showInsertResults의 콜백 함수는 나중에 설명하겠다.

서블릿 요청과 데이터베이스 액세스

http://localhost:8095/Ajax/ControlServlet/querytype=insert에서 XMLHttpRequest 객체가 Jetty 웹 서버로 요청을 보내면 서버 측에서 어떤 일이 발생하는 지를 알아보고, 이에 더하여 새로운 연락처를 추가했을 때 폼 값에 상응하는 이름 값 쌍에 대해 알아보자.

ControlServlet 클래스의 doGet 메소드는 인커밍 매개변수를 파싱하고 쿼리타입 매개변수가 insert 또는 update로 설정되면 헬퍼 클래스인 ContactXMLBean을 만든다. 이는 연락처 정보를 포함하고 있는 필수 JavaBean이다. 마지막으로 ContactXMLBean 객체는 DerbyDatabase 객체인 insertContact 메소드로 전달된다.

Listing 7. ControlServlet 클래스의 doGet 메소드

public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {

String queryType = "";
String user = "";
String password = "";
String newuser = "";
queryType = request.getParameter("querytype");
...

String returnXML = "";

...

if (queryType != null) {
if (queryType.equals("show")) {
returnXML = derbyDB.getContacts();
}

else if (queryType.equals("insert") || queryType.equals("update")) {
String firstname = request.getParameter("firstname");
String lastname = request.getParameter("lastname");
String email = request.getParameter("email");
String phone1 = request.getParameter("phone1");
String address = request.getParameter("address");
String city = request.getParameter("city");
String state = request.getParameter("state");
String zip_code = request.getParameter("zip_code");
String phone2 = request.getParameter("phone2");
String bday = request.getParameter("bday");

ContactXMLBean myContact = new ContactXMLBean(address, bday, city, \
email, firstname, lastname, phone1, phone2, state, zip_code);

if (queryType.equals("insert")) {
returnXML = derbyDB.insertContact(myContact);
}
...

response.setContentType("text/plain");
PrintWriter out = response.getWriter();
out.write(returnXML);
out.flush();
out.close();
}

DerbyDatabase insertContact 메소드를 시험하기 전에 DerbyDatabase 클래스의 initialize 메소드에서 Derby 엔진을 처음 부팅했을 때 만들어졌던 CONTACTS 테이블과 스토어드 프로시저용 CREATE TABLE Data Definition Language (DDL) 문을 봐야 한다.

CONTACTS 테이블은 현재 버전의 Cloudscape에서 지원되지 않는 XML 데이터 유형을 사용하지만 10.2 릴리스에서는 지원될 것이다. 이 데모에 사용되는 연산은 XML 데이터를 읽고 이를 가져오는 것이다.

주: Cloudscape와 Derby의 공식 10.2 버전은 현재 사용할 수 없다. 하지만 알파 스냅샷 버전은 Apache Derby 웹 사이트에서 다운로드가 가능하다. 이 데모에 포함된 Derby 데이터베이스 클래스들은 알파 버전의 10.2이고 제품 환경에서 사용해서는 안된다.

Derby나 Cloudscape를 사용하여 애플리케이션을 구현하려면 IBM Cloudscape나 Apache Derby 에서 현재 버전인 10.1을 다운로드 한다. (참고자료)

Listing 8. XML datatype을 사용하여 테이블 문 만들기

CREATE TABLE APP.CONTACTS(ID INT CONSTRAINT CONTACT_PK PRIMARY KEY, XML_COL XML)

Derby에서 스토어드 프로시저를 사용하는 세 단계가 있다. 최소한 하나의 정적 메소드를 가진 퍼블릭 클래스를 만들고, SQL을 실행하여 스토어드 프로시저를 등록하고 이를 호출한다. DerbyProcedures 클래스는 첫 번째 조건에는 부합한다. 이 메소드에 대해 기억해야 할 것은 디폴트 연결을 사용하고 autocommit 모드를 false로 설정한다. 기본적으로 이것은 true로 설정된다. 이 프로시저는 현재 CONTACTS 테이블에서 ID 칼럼의 최대 값을 가진다. 그런 다음 이 값을 늘리고 이를 두 장소의 새로운 행에 삽입한다. (ID 칼럼과 XML column XML_COL의 일부로서). XML 칼럼에 ID를 저장하는 이유는 CONTACTS 테이블의 선택이 이루어지고 내용이 클라이언트로 보내져서 JavaScript를 통해 파싱될 때 클라이언트에 리턴된 것을 확인하면 명확히 알 수 있다.

XMLPARSE SQL 함수는 XML을 컬럼에 삽입하는데 사용되는 새로운 XML 연산자이다. Cloudscape의 10.2 릴리즈에 포함될 예정이다.

Listing 9. cloudscape.ajax.DerbyProcedures의 스토어드 프로시저용 정적 메소드

public class DerbyProcedures {
public static void insertContact(int[] rowNum, String myXMLContact)
throws SQLException {
Connection conn = DriverManager.getConnection("jdbc:default:connection");
conn.setAutoCommit(false);
Statement stmt = conn.createStatement();
int currentValue = 0;

ResultSet rs = stmt.executeQuery("select max(id) from APP.CONTACTS");

while (rs.next()) {
currentValue = rs.getInt(1);
}
rs.close();

currentValue++;
stmt.close();
String sql = "insert into APP.CONTACTS (id, xml_col) values ("
+ currentValue + ", xmlparse(document '<contact><id>"
+ currentValue + "</id>" + myXMLContact + "</contact>' preserve whitespace))";
stmt = conn.createStatement();
int numRows = stmt.executeUpdate(sql);
if (numRows > 0) {
rowNum[0] = currentValue;
} else {
rowNum[0] = 0;
}
stmt.close();
conn.commit();
conn.close();
}
}

스토어드 프로시저를 만드는 다음 단계는 CREATE PROCEDURE DDL을 실행하여 데이터베이스에 프로시저를 등록하는 것이다. Derby에서 SQL 함수와 스토어드 프로시저의 차이점 중 하나는 함수가 데이터를 변경하지 못한다는 점이다. 또한 스토어드 프로시저는 IN, OUT, INOUT 매개변수들을 지원한다. 이 프로시저는 Java String으로서 나타난 새로운 연락처인 IN 매개변수를 취한다. OUT OUT 매개변수는 데이터베이스에 삽입된 행 넘버를 리턴한다.

Listing 10. 데이터베이스에서 스토어드 프로시저를 등록하는 SQL

create procedure InsertXMLContact(OUT rowNum integer, IN contact VARCHAR(500))
parameter style java language java external name
'cloudscape.ajax.DerbyProcedures.insertContact'

이제, 스토어드 프로시저를 호출할 수 있다. DerbyDatabase 클래스에 있는 이 메소드는 JDBC CallableStatement 클래스와 CALL SQL 신택스를 사용하여 이 단계를 수행한다. 이 메소드에서 리턴된 정수는 테이블에 삽입된 id 칼럼의 값에 상응한다.

Listing 11. JDBC를 통해서 스토어드 프로시저 호출하기

public String insertContact(ContactXMLBean myContact) {
// the id of the row just inserted
int idNum = 0;

try {
Connection conn = ds.getConnection();
CallableStatement cstmt = conn.prepareCall("call InsertXMLContact(,)");
cstmt.setString(2, myContact.toXMLString());
cstmt.registerOutParameter(1, Types.INTEGER);
cstmt.execute();
idNum = cstmt.getInt(1);
cstmt.close();
conn.close();
}
catch (SQLException sqlExcept) {
sqlExcept.printStackTrace();
}
if (idNum > 0) {
return String.valueOf(idNum);
} else {
return String.valueOf(0);
}
}

Add Contact 버튼을 누른 후에 발생하는 마지막 단계는 서블릿에서 클라이언트로 보내진 결과를 처리하는 일이다. 웹 페이지에 Add Contact 버튼을 누른 후에 많은 것들을 보여주었기 때문에 요약만 하겠다.

JavaScript onclick() 이벤트 핸들러는 insertContact() JavaScript 함수를 호출했다. 이 함수는 처리하기 전에 모든 필드에 값이 포함되었는지를 확인한다. 여기에 값이 포함되있지 않으면 JavaScript 함수인 makeDerbyRequest가 호출된다. 이 함수는 요청을 폼의 필드를 포함하고 있는 Jetty 웹 서버에 있는 서블릿으로 보낸다. showInsertResults 콜백 함수는 XMLHttpRequest 객체인 onreadystatechange 속성과 함께 등록되었다.

서버 측에서 서블릿은 매개변수를 파싱했고 요청을 Derby 데이터베이스로 보내서 연락처가 테이블로 삽입되도록 했다. 행이 xml datatype을 활용하는 테이블로 삽입되었고 스토어드 프로시저는 JDBC Callable 인터페이스를 사용하여 호출되었다. 연락처를 테이블에 삽입한 후에 데이터베이스는 막 삽입된 행의 id 칼럼의 값을 리턴했다.

이제 클라이언트에서 콜백 함수를 볼 수 있다. (showInsertResults)

Listing 12. 클라이언트에서 삽입 결과 처리하기 (address_book.js)

function showInsertResults() {
if (request.readyState == 4) {
if (request.status == 200 ) {
var response = request.responseText;
if (response != "0") {
confirmAddPhoto(response);
}

else {
alert("There was a problem inserting the contact.");
}
}
else if (request.status == 404) {
alert("Request URL does not exist");
}
else {
alert("Error inserting data, please try again.");
}
}
}

이 함수는 매우 표준적인 방식으로 비동기식 요청을 처리하고 있다. readyState와 status를 체크하여 응답이 완료되고 "OK" 또는 200 상태가 될 때에만 웹 서버에서 리턴된 값을 내보낸다.

지금까지 Address Book 애플리케이션에 추가했고 JavaScript와 XMLHttpRequest 객체를 사용하여 막후에서 어떤 일이 발생하는지를 보았다. 이제 내 친구 마틴의 사진을 업로드 해보자.

다음 페이지인 AddPhoto.html은 표준 파일 인풋 필드로서 파일 시스템 상의 파일을 검색할 수 있다. 이미지 파일을 검색한 후에 Upload to Cloudscape 버튼을 클릭한다. Derby 데이터베이스에 이미지가 업로드 된 것이다.

CONTACT_PHOTO 테이블용 SQL DDL 이 아래 보인다.

Listing 13. DerbyDatabase.java의 CONTACT_PHOTO 테이블

CREATE TABLE APP.CONTACT_PHOTO
(id int constraint CONTACT_FK references APP.CONTACTS(id),
fname varchar(30),
lname varchar(30),
filename varchar(30),
picture BLOB(150000),
filesize int)
);

DerbyDatabase 애플리케이션 클래스의 insertPhoto 메소드는 PreparedStatment를 사용하고 사진 파일을 BLOB으로서 데이터베이스에 삽입한다. 아래는 코드표는 읽기 쉽도록 라인을 자른 것이다. 실제 소스에는'\' 문자가 없다.

Listing 14. setBinaryStream을 사용하여 데이터베이스에 BLOB 삽입하기

public int insertPhoto(int id, String fname, String lname, String filename,
File photoFile) {
int numRows = 0;
String sql = "insert into APP.CONTACT_PHOTO (id, fname, lname,
filename, picture, filesize) \
values (,,,,,)";

Connection conn;
try {
conn = ds.getConnection();
PreparedStatement prepStmt = conn.prepareStatement(sql);
prepStmt.setInt(1, id);
prepStmt.setString(2, fname);
prepStmt.setString(3, lname);
prepStmt.setString(4, filename);

InputStream fileIn = new FileInputStream(photoFile);
prepStmt.setBinaryStream(5, fileIn, (int) photoFile.length());
prepStmt.setInt(6,(int) photoFile.length());
numRows = prepStmt.executeUpdate();
fileIn.close();
prepStmt.close();
conn.close();
} catch (SQLException sqlExcept) {
SQLExceptionPrint(sqlExcept);
} catch (Exception e) {
e.printStackTrace();
}
return numRows;
}

CONTACT_PHOTO 테이블의 id는 원래 서버에서 JavaScript 함수인 confirmAddPhoto 함수로 응답을 통해 클라이언트 측에 전달되었다. 이 함수는 이것을 다음 페이지인 AddPhoto.html로 전달한다. 이는 결국 위에 보인 insertPhoto 메소드를 호출했던 서블릿에 의해 처리되었다.

웹 서버와 데이터베이스를 실행하는 JVM을 호스팅하는 브라우저 Part II

일찍이 웹 애플리케이션에 임베디드 아키텍처가 특정 유스 케이스의 목적에 맞는지를 설명했다. 다음은 부가적인 이유들이다.

네트워크 연결 없이 웹 애플리케이션을 통해 Derby 데이터베이스에 정보를 저장하고 액세스 할 수 있다.
웹 브라우저는 신참 사용자들에게도 익숙한 사용자 인터페이스이다.
모바일 사용자들은 인터넷 연결 없이 연락처 정보를 추가 및 업데이트 할 수 있다.
이러한 유형의 애플리케이션의 추가 확장을 통해 연결이 가능하다면 원격 서버와 동기화 할 것이다.

애플리케이션 검사- Part II

애플리케이션 검사 섹션에서는 클라이언트와 서버 그리고 전체 아키텍처간 통신에 초점을 맞췄다. Part II에서는 Show All 버튼이 눌려지고 애플리케이션의 메인 페이지가 액세스 될 때 마다 데이터베이스에서 가져오는 결과를 클라이언트가 어떻게 처리하는지를 볼 것이다. 또한 이름과 성으로 리스트를 정렬하는데 사용되는 JavaScript도 볼 것이다.

연락처가 데이터베이스에서 리턴될 때 어떤 일이 일어나는지 보려면 이들이 데이터베이스에 어떻게 삽입되었는지를 떠올리면 알 수 있다. CONTACTS 테이블에 연락처 내용-성과 이름 같은 이름 값 쌍-을 나타내는 값을 삽입할 때 마다 삽입되기 전에 Derby XML 데이터 유형으로 변환되어야 한다. 헬퍼 클래스 ContactXMLBean은 toXMLString이라는 유틸리티 메소드를 갖고 있는데 이것은 XML 엘리먼트에 빈을 래핑한다. Listing 11을 보면 toXMLString 메소드가 새로운 연락처를 삽입했을 때 CallableStatement의 매개변수 설정 시 호출되는 모습을 볼 수 있다.

Listing 15. 엘리먼트 태그에 JavaBean 래핑하기

public String toXMLString() {
return "<firstname>" + firstname + "</firstname><lastname>"
+ lastname + "</lastname><email>" + email + "</email><phone1>"
+ phone1 + "</phone1><address>" + address + "</address><city>"
+ city + "</city><state>" + state + "</state><zip_code>"
+ zipcode + "</zip_code><phone2>" + phone2 + "</phone2><bday>" + bday + "</bday>";
}

테이블에서 모든 연락처를 선택하는 데이터베이스에 대한 JDBC 호출은 Listing 16을 참조하라.

Listing 16. 데이터베이스에서 모든 연락처 가져오기

public String getContacts() {
StringBuffer sbuf = new StringBuffer(2000);
try {
Connection conn = ds.getConnection("APP", "APP");
Statement stmt = conn.createStatement();
sbuf.append("<Results>");
ResultSet rs = stmt.executeQuery("SELECT XMLSERIALIZE(XML_COL AS \
VARCHAR(500)) from APP.CONTACTS");
while (rs.next()) {
sbuf.append(rs.getString(1));
}

sbuf.append("</Results>");
rs.close();
stmt.close();
conn.close();
} catch (SQLException sqlExcept) {
sqlExcept.printStackTrace();
}
return sbuf.toString();
}

SQL 함수는 Cloudscape 10.2 릴리스에서 사용할 수 있는 새로운 함수이다. 현재는 사용할 수 없다. XML이 VARCHAR(500)로 던져진다. VARCHAR는 Cloudscape 10.1에서 지원되는 데이터 유형이기 때문에 이 애플리케이션을 변경하여 현재 Cloudscape와 작동하도록 하려면 APP.CONTACTS 테이블을 XML_COL과 함께 VARCHAR(500) 유형으로서 정의하고 ContactXMLBean 클래스의 toXMLString 메소드를 사용하고 선택이 XMLSerialize가 되도록 한다:

SELECT XML_COL from APP.CONTACTS;

APP.CONTACTS 한 개의 행만이 있을 때에만 선택 아웃풋은 그림 17처럼 된다.

Listing 17. XMLSerialize를 통해 데이터베이스에서 리턴된 연락처

<firstname>Susan</firstname>
<lastname>Cline</lastname>
<email>susanc@mycorp.com</email>
<phone1>510 589-8888</phone1>
<address>300 East 2nd Street</address>
<city>Oakland</city>
<state>California</state>
<zip_code>98654</zip_code>
<phone2>415 703-6345</phone2>
<bday>July 15</bday>

getContacts 메소드는 스트링 형태로 결과 세트만 리턴한다. 전체 결과 세트에 루트 태그를 어떻게 붙일까 이 루트 엘리먼트는 구성이 잘 된 XML 문서에 필요하다. 리턴된 결과인 연락처를 파싱하여 디스플레이 하는 방법을 설명하겠다.

연락처 파싱과 디스플레이

Show All 버튼이 클릭될 때 마다 또는 AddressBook.html 페이지가 로딩될 때 마다 연락처는 데이터베이스에서 비동기식으로 검색되고 파싱되며 페이지에 디스플레이 된다.

페이지가 로딩될 때 JavaScript 함수를 실행하려면 onload 이벤트용 이벤트 핸들러가 지정되어야 한다. HTML BODY 태그가 AddressBook.html에서 취해져서 OnPageLoad 함수와 onload 이벤트를 제휴시키고 페이지가 로딩될 때 마다 호출된다.

<BODY onLoad="OnPageLoad(queryStringText)" id="thebody">

OnPageLoad 함수는 show의 매개변수를 가진 makeDerbyRequest함수를 호출한다. Listing 18에서 콜백 함수인 updateContactList가 바로 그 부분이다.

Listing 18. address_book.js의 makeDerbyRequest와 updateContactList 함수

function makeDerbyRequest(databaseAction, parameters, selectedOption) {
var url = "/Ajax/ControlServlet/querytype="
if (databaseAction == "show") {
url = url + "show";
request.open("GET", url, true);
request.onreadystatechange = updateContactList;
request.send(null);
}
...

function updateContactList() {
if (request.readyState == 4) {
if (request.status == 200 ) {
// the servlet is returning a string, so I need to
// use responseText vs responseXML
var response = request.responseText;
var parser = new DOMParser();
// this turns the doc into an xml doc so it can be parsed.
var doc = parser.parseFromString(response, "text/xml");

// The xml returned from the server is of the format:
//<Results><contact><firstname>Susan</firstname><lastname>Cline</lastname>
//<email>susan@yahoo.net</email>
//<phone1>510 547 -8888</phone1><address>1569 Balboa</address><city>Oaktown</city>
//<state>California</state><zip_code>94618</zip_code><phone2>510 774-6345</phone2>
//<bday>July 15</bday></contact></Results>
var Results = doc.getElementsByTagName("Results")[0];
if(!Results.getElemen