본문 바로가기

03. Development/11. XML

기존 웹 리소스 이용

SOAP 기반 XML 웹서비스에 아무리 주의를 기울이고 있다 하더라도 SOAP과 호환되지 않는 수많은 유용한 서비스와 컨텐츠가 있다는 사실을 잊기 쉽다. 이번 테크팁에서는 웹 티어로부터 기존의 웹 리소스들을 사용하기 위해 서블렛을 어떻게 사용하는 지 보여준다. 이 경우에 기존 웹 서비스란 비표준 기반 XML 서비스를 말한다.

기존 리소스 호환

적어도 지금까지는 웹의 대부분의 컨텐츠를 표준 기반 웹 서비스 형식에서 사용할 수 없었다. HTML 페이지, 텍스트 파일, PDF나 워드 프로세서 같은 문서 파일, ftp 사이트 안의 파일들과 이미지들 형식에서 많은 정적 컨텐츠가 존재한다. 어떤 서비스들은 HTTP POSTGET 요구에 따라 활발하게 XML을 생산하기도 하며, RSS feeds 같은 다른 서비스들은 정적인 URL을 가지고 있으나, 그들의 데이터가 동적이어서 보통 직접 사용자에 의해 사용되기 보다는 프로그램에 의해 사용되기 때문에 서비스의 역할을 한다.

엔터프라이즈Java 테크놀로지는 조직 내 인트라넷과 일반 인터넷에서 모두 기존 리소스들과 훌륭히 호환된다. 엔터프라이즈Java 테크놀로지는 새로운 시스템에 기존 정보 자산을 통합하기 위해 어느 티어에서나 사용될 수 있다. SOAP 기반 웹 서비스들이 기존 리소스들을 통합하는 새로운 시스템을 생성하는 것을 기다릴 필요가 없는 것이다.

다음은 통합적인 애플리케이션을 제공하기 위해 엔터프라이즈 Java 서버가 다양한 기존 웹 리소스들을 통합하는 구성도이다.

업무에서의 검색엔진

The National Library of Medicine은 미국방부의 건강 관련 기관 중 하나이다. PubMed*는 NLM의 서비스 중 하나로, 생체의학분야의 출판물 요약본 DB를 검색 가능하도록 제공한다. PubMed의 검색엔진은 Entrez로 불리며, 주소창에pubmed라고 입력하거나 http://www.ncbi.nlm.nih.gov/entrez/query.fcgi 로 들어가서 이 Entrez를 사용해볼 수 있다.

다음의 그림은 PubMed 사이트에서"neanderthal dna" 를 검색한 결과 중 일부이다.

PubMed는 또한 e-uilities라고 불리는 서비스를 제공한다. 이 서비스는 검색엔진에 프로그램적 인터베이스를 제공하는데, 클라이언트로부터 HTTP GET 요구를 받아 검색 결과를 나타내는 XML을 리턴할 수 있다.

이 테크팁을 수행하는 샘플 코드는 e-utilities 중 두가지에 접근하는 서블렛이다.

  • esearch. 검색을 실행하고 ID 리스트를 리턴한다.
  • efetch. 다양한 형식으로 요청된 문서를 불러온다.

esearch 이나 efetch로부터 리턴된 XML을 브라우저에 표시할 수 있게끔 XSLT스타일시트에 의해 변환되도록 지정하는 옵션도 있다. 서블렛은 클라이언트가 어떤 매개변수를 제공하는가에 따라 적절한 스타일시트를 사용할 것이다.

서블렛 개요

Jul2004Servlet라 불리는 샘플코드 서블렛은 새로운 기능을 제공하기 위해 기존 웹 서비스인 esearchefetch를 사용한다. 서블렛은 입력 매개변수를 GET URLs로 변환하여, 분석하고 표시하기 위해 데이터를 검색하는데 이를 사용한다. 서블렛은 먼저 검색을 실행하기 위해 esearch를 호출하고, 그 후 결과를 검색하기 위해 efetch를 호출한다.

다음은 서블렛이 결과를 도출하기 위해 진행되는 과정을 보여주는 순서도이다. 서블렛이 결과를 도출하기 위해 다양한 HTTP GETs의 결과를 사용하였다는 것에 주의하기 바란다.

  1. 서블렛은 유저 형식로부터 POST 매개변수를 수신하고 esearch가 요청하는 매개 변수로 HTTP GET URL를 구축한다.
  2. 서블렛은 URL로 HTTP GET을 실행한다. esearch e-utility는 서버용 결과 집합을 식별하는 데이터를 가진 XML을 리턴한다.
  3. 서블렛은 esearch로부터 리턴된 XML을 파싱하고, WebEnvQueryKey를 검색하기 위해 DOM API를 호출한다. 그 후 이 값들을 이용해 URL을 구축하여 데이터를 얻는다.
  4. 서블렛은 이번엔 efetch에 또다른 HTTP GET를 실행시킨다. 이는 결과 집합을 식별하고 원래의 요청에서 지정된 매개변수들을 형식화한다.
  5. 서블렛은 efetch로부터 요청된 문서 데이터를 수신하여 데이터를 XSL로 임의 변환하고 브라우저에 결과를 표시한다.

데이터베이스 검색

Entrez 서버에서 문서정보를 검색하는 첫번째 단계는 애플리케이션의 index.html에 있는 입력폼의 몇가지 매개 변수에 대해 esearch를 실행시키는 것이다. esearch는 Entrez 서버를 조회하여 조회 결과에 부합되는 데이터를 리턴한다. HTTP GET 쿼리 문자열이 "usehistory=y"라는 매개변수를 포함하고 있다면, Entrez 서버는 WebEnv 문자열과 QueryKey 라는 두가지 데이터 아이템을 추가하여 리턴한다. WebEnv 문자열은 Entrez 서버 안의 세션에서 유저 상태(과거에 있었던 쿼리와 그 결과 집합을 포함)에 대한 독자적인 식별자이며, QueryKey는 세션 안의 특정 쿼리를 식별하는 작은 정수이다. 종합하자면, WebEnvQueryKey를 합하면 서버에서의 쿼리 결과를 나타내게 된다. 서블렛은 애플리케이션의 index.html 페이지에 있는 입력폼로부터 몇 가지 매개 변수를 수신한다. 다음은 입력폼 샘플이다.

서블렛은 애플리케이션의 index.html 페이지에 있는 입력폼로부터 몇 가지 매개 변수를 수신한다.

다음은 입력폼 샘플이다.

서블렛 코드는URL을 구축하기 위해 다음과 같이POST매개 변수를 사용한다.

   AbstractMap paramMap = new TreeMap();
   
   res.setContentType("text/html");
   OutputStream os = res.getOutputStream();
   
   // Get parameters
   String query = req.getParameter("query");
   
   // Execute esearch
   // db=PubMed: search PubMed database
   // usehistory=y: return key to server-side result set
   // term=$query: search for "$term" in PubMed
   paramMap.put("db", "PubMed");
   paramMap.put("usehistory", "y");
   paramMap.put("term", query);
   
   // Create the URL and get its content
   String surl = buildUrl(BASEURL_ESEARCH, paramMap);
   InputStream is = getContent(surl);

여기서 사용된 buildUrl메서드는 기본 URL을 받아서 맵 상의 각각의 키와 컨텐츠 쌍에 대해 key=content라는 키를 생성함으로써 HTTP GET URL을 만든다. 컨텐츠는 URL로 인코딩 되어있으며 매개 변수는 스트링"&"와 결합하여 HTTP GET URL을 생성한다. 예를 들어 형식 쿼리가 "neanderthal DNA"이면 쿼리 문자열은 다음과 같다.

   db=PubMed&term=neanderthal+dna&usehistory=y

getContent 메서드는 요청된 URL에게 간단히 HttpUrlConnection을 개방하고, 결과 컨텐츠에 대해 다음과 같이 InputStream을 리턴한다.

   protected InputStream getContent(String surl)
      throws ServletException {
      Object content = null;

      try {
         // Connect to URL
         URL url = new URL(surl);
         HttpURLConnection conn =
            (HttpURLConnection)url.openConnection();
         conn.connect();

         // Get content as stream
         content = conn.getContent();
      } catch (Exception e) {
         throw new ServletException(e);
      }
      return (InputStream)content;
   }

esearch 요청에 대한 입력 스트림은 다음과 같은 XML 문서를 포함한다.

   <?xml version="1.0"?>
   <!DOCTYPE eSearchResult PUBLIC
     "-//NLM//DTD eSearchResult, 11 May 2002//EN"
     "http://www.ncbi.nlm.nih.gov/entrez/query/DTD/eSearch_020511.dtd">
   <eSearchResult>
      <Count>19</Count>
      <RetMax>19</RetMax>
      <RetStart>0</RetStart>
      <QueryKey>1</QueryKey>

      <WebEnv>0ed8yFoq_CFyEEP6hW9aZ9UoTCVrrPm2w343S9MRNaT9MQmwbnjI
      </WebEnv>
      <!-- additional data removed for brevity -->
   </eSearchResult>

서블렛은 이 XML 문서에서 QueryKeyWebEnv 요소의 컨텐츠를 추출해야하며, 이 컨텐츠를 efetch 후속 호출에 포함시켜야한다. 그러면 디스플레이를 위한 문서 데이터를 리턴하게 될 것이다.

결과 도출

efetch 결과가 작기 때문에 in-memory DOM 트리로 파싱이 가능하다. esearch로 리턴된 스트링을 파싱하는 서블렛은, 파싱하는 동안 Document오브젝트를 메모리로 가져오기 위해 DocumentBuilder를 사용한다.

   // Create DOM parser and parse search result
   DocumentBuilderFactory dbf =
   DocumentBuilderFactory.newInstance();
   DocumentBuilder db = dbf.newDocumentBuilder();
   Document esearchDoc = db.parse(is);
   
   // Get WebEnv, Count, and QueryKey from result
   // WebEnv - result key
   // QueryKey - history index
   // Count - result set length
   String webenv = getElementString(esearchDoc, "WebEnv");
   String count = getElementString(esearchDoc, "Count");
   String querykey = getElementString(esearchDoc, "QueryKey");

getElementString메서드는 주어진 이름(파일명)의 노드를 찾고 그 노드의 첫번째 Text 하위노드를 리턴하는 간단하고 편리한 기능을 한다. 서블렛은 파싱된 DOM 문서로부터 WebEnvQueryKey 를 추출한다.

이 때에, 서블렛은 서버에서 대기하는 결과 집합을 포함하고 있다. 다음 단계는 efetch를 사용하여 데이터를 검색하고 포맷하는 것이다.

Fetch 매개변수 지정

Efetch에서는 esearch의 결과를 식별하기 위해 단지 몇가지의 매개변수만을 필요로한다.

  • db. 데이터베이스 식별("PubMed")
  • WebEnv. 세션 식별
  • query_key. 세션 안의 쿼리 식별

추가 매개 변수들은 사이즈를 제한하거나, 리턴되는 데이터의 포맷 제어하는 역할을 한다.

  • retstartretmaxefetch 가 결과 중 일부 집합을 리턴하도록 명령한다. 이 때 이 결과치는 retstart에서 시작하고 retmax 레코드보다 작은 값을 리턴한다. 이 매개 변수들이 없으면 efetch는 종종 몇만 메가바이트가 되곤 하는 전체 결과를 리턴하고 만다.
  • retmode는 XML, HTML, text, ASN.1 중 어떤 형식으로 데이터를 생성할 것인지 지정한다. 기본값은 ASN.1, PubMed 의 네이티브 스토리지, 그리고 교환 포맷이다.
  • rettype은 각 레코드에서 어떤 것을 리턴할 지 명령한다. efetch은 기본값으로 abstracts을 리턴한다.

서블렛은 esearch에서와 같이 요청된 매개변수들의 Map을 생성한다. 서블렛이 esearch에서 검색한 WebEnvQueryKey스트링을 사용하고, 또한 원래 형식에서 수신한 몇가지 매개변수들을 포함한다. getParameter메서드는, 매개변수가 설정되지 않았을 때 기본값으로 대체하는 요청으로부터 간단히 매개변수를 얻는다.

   paramMap = new TreeMap();
   paramMap.put("WebEnv", webenv);
   paramMap.put("query_key", querykey);
   paramMap.put("db", "PubMed");
   paramMap.put("retstart", getParameter(req, "start", "0"));
   paramMap.put("retmax", getParameter(req, "max", "20"));
   paramMap.put("retmode",
   retmode = getParameter(req, "retmode", "xml"));
   paramMap.put("rettype", getParameter(
           req, "rettype", "abstract"));
   
   // Create URL and get its content
   surl = buildUrl(BASEURL_EFETCH, paramMap);
   is = getContent(surl);

서블렛은 요청된URL을 구축하기위해 맵을 사용하며, 요청 결과로 InputStream 을 얻기 위해 getContent 을 사용한다.

문서 변환

요청된 데이터 형식이 XML이 아니거나 유효한 스타일시트가 없다면 서블렛은 efetch로 리턴된 데이터를 복사하여 브라우저에 표시한다. 이 기능은 e-utilities로 직접 실험할 때 유용하다.

데이터가 XML형식이고 비어있지 않은 유효한 스타일시트가 있다면 서블렛은 이 스타일시트를 데이터에 적용하여 결과를 브라우저에 리턴한다. 브라우저에 리턴엔 XML은 각 레코드에 대해 약간씩의 데이터를 포함하고 있다. 다음은 리턴된 데이터를 발췌한 것이다.

    <?xml version="1.0"?>
    <!DOCTYPE PubmedArticleSet PUBLIC "...">
    <PubmedArticleSet>
     <PubmedArticle>
      <MedlineCitation Owner="NLM" Status="In-Process">
       <!-- ... -->
       <Article>
       <!-- ... -->
        <ArticleTitle>The thermal history of human fossils
        and the likelihood of successful DNA amplification.
        </ArticleTitle>
       <!-- ... -->

이 데이터는 결과를 도출하기 위해 수행되는 스타일시트의 컨텐츠이다.

적용할 스타일시트를 지정하는 데에는 두가지 방법이 있다. 첫번째로는, 사용자형식에는 라디오 버튼으로 되어있는 "isFile" 사용자 형식인제, 변환하기 위해 어떤 시트를 사용할 것인지 지정한다. IsFile이 1이면 매개변수 stylesheet는 웹애플리케이션 아카이브(WAR file)의 스타일시트 이름을 포함하게된다. IsFile이 0이면 매개변수 sstext는 사용자가 형식 안의 TEXTAREA에 넣는 스타일시트를 사용한다. 서식화된 레코드를 보고자 할 때 이 기능을 사용하면 된다. 또한 텍스트 에디터로부터 바로 형식으로 XSL을 카피하여 새로운 레포트를 생성할 때도 사용할 수 있다.

변환을 수행하는 코드는 다음을 얻는다.

  • efetch 결과로부터 읽어드린 입력 스트림
  • 서블렛 결과에 작성되는 결과 스트림
  • 매개 변수 isFile
  • 스타일시트의 이름이나 스타일시트 안의 텍스트 자체를 포함하는 스트링

이 메서드는 스타일시트로부터 Transformer 오브젝트를 구축한 후 소스와 수신자로서 입력과 출력 스트림을 전달하는 Transformer의 transform메서드를 호출한다.

   // Create XSLT transformer for output.
   TransformerFactory tf = TransformerFactory.newInstance();
   Transformer t = null;
   
   // Use identity transform if no transformer was specified
   if (stylesheet == null || stylesheet.equals("")) {
      t = tf.newTransformer();
   } else {
      StreamSource xslsource = null;

      if (isFile) {
         // Read XSL stylesheet from app archive and wrap it as
         // a StreamSource. Then use it to construct 
         // a transformer.
         InputStream xslstream = _config.getServletContext().
            getResourceAsStream(stylesheet);
         xslsource = new StreamSource(xslstream);
      } else {
         // Read XSL stylesheet from string
         xslsource = new StreamSource(
                 new StringReader(stylesheet));
      }
      t = tf.newTransformer(xslsource);
   }
   
   // Do transform from input (e-utilities result) to
   // output (servlet writer)
   StreamSource ss = new StreamSource(is);
   StreamResult sr = new StreamResult(os);
   t.transform(ss, sr);

변환이 완료되면 출력 스트림에 결과 HTML을 작성하게 되며, 서버는 결과 컨텐츠를 브라우저에 표시한다.

결과 보기

애플리케이션 아카이브는 두가지 스타일시트를 포함한다.

  • titles.xsl는 문서의 타이틀만을 표시한다.

다음은 타이틀 형식의 결과 집합의 일부를 출력한 것이다.

  • extended.xsl는 국립의학 도서관에서 지정한 표준 추천 도서목록에서 각 레코드를 서식화한다. 덧붙여 실제 인용 정보(저널 종류, 출판 날짜, 쪽수 등)가 PubMed 사이트의 전체 요약문으로 하이퍼링크된다.

따라서 다음의 출력물을 보면 이전과 같은 결과이지만 좀 더 확장된 형식을 보여줌을 알 수 있다.

입력폼에 삽입된 스타일시트를 이용하여 사용자의 기호에 맞는 스타일을 정의할 수도 있다. TEXTAREA 스타일시트는 스타일시트의 골격을 제공한다. 서블렛을 이용하여 간단한 검색을 실행해보자. 스타일시트를 지정하지 않은 채 결과 XML을 파일에 복사하고 이를 텍스트 에디터에서 새로운 스타일시트를 생성하기 위한 XML예제로 사용한다. 스타일시트 전체를 복사하여 form에 붙인 후 "Using custom XSL style:" 라디오 버튼을 클릭하여 Search를 선택한다. 스타일시트가 유효하다면 서블렛은 검색결과를 포맷하기 위해 이 스타일시트를 사용하게 될 것이다.

이 예제 서블렛을 확장하여 좀 더 유용하게 만드는 것도 어려운 일은 아니다. 예를 들어 PubMed로부터 대형 데이터를 다운로드 받아 분석하거나, 링크된 DNA 시퀀스를 표시하는 바이오정보 애플리케이션을 작성하기 위해 e-utilities의 다른 데이터베이스를 사용할 수도 있을 것이다.