저작권 안내: 저작권자표시 Yes 상업적이용 No 컨텐츠변경 No

스프링5 입문

JSP 2.3

JPA 입문

DDD Start

인프런 객체 지향 입문 강의
간단한 예를 통해 웹 기반의 데이터 송수신이 어떻게 이루어지는 지 그리고 웹-투-웹의 장단점에 대해서 살펴본다.

가상 인터넷 서점 시나리오

지난 Part 1 에서는 java.net.URL 클래스와 java.net.URLConnection 클래스를 사용하여 웹-투-웹 통신의 기본이 되는 HttpMessage 클래스를 작성해보았다. 이번 Part 2에서는 가상의 기업들을 설정해서 웹-투-웹 프로그래밍이 어떤 식으로 구현되는지에 살펴보도록 하자. 또한, 이 과정에서 우리는 XML을 사용하여 데이터를 주고 받을 때의 장단점에 대해서 살펴볼 것이다.

이 글에서 웹을 기반으로 하여 데이터를 주고 받는 것에 대해서 설명하기 위해 가상의 시나리오를 작성해보자. 이 시나리오에는 크게 고객, 인터넷서점, 출판사, 택배사가 출현한다. 고객은 인터넷 서점을 통해서 서적을 주문하며, 인터넷 서점은 사용자가 서적 주문을 하면(단, 여기서는 신용카드와 같이 구매와 동시에 결재가 된다고 가정한다) 실시간으로 구매 정보를 택배사에 전송한다. 이때, 고객은 인터넷 서점에 웹을 통해서 서적을 주문하게 되며 인터넷 서점은 웹을 기반으로 하여 택배사에 배송 정보를 전송한다. 이 과정을 간단하게 그림으로 표현하면 다음과 같다.


그림1 인터넷 서점과 택배사 사이의 데이터 이동 흐름
그리고, 인터넷 서점은 하루 간격으로 각 출판사의 신간 목록을 읽어오는데, 이 과정 역시 웹 기반의 통신을 통해서 이루어진다. 이때, 출판사는 신간 목록을 인터넷 서점에서 정한 DTD에 알맞도록 생성된 XML 문서를 인터넷 서점에 전송한다. 인터넷 서점은 전송받은 XML 문서를 알맞게 처리하여 자신들의 서적 목록 DB에 추가한다.

여기서 작성한 가상 시나리오는 인터넷 서점과 관련된 간단한 업무 흐름을 웹 기반으로 처리하고 있다. 모든 것이 웹 기반으로 이루어지기 때문에 웹 어플리케이션만을 개발하면 되며, 그 외에 별도의 클라이언트 프로그램은 필요하지 않게 된다. 즉, 모든 사용자들은 웹 브라우저만으로 자신의 업무를 처리할 수 있게 되는 것이다.

시나리오의 구현

가상 인터넷 서점의 구현은 기본적으로 다음과 같은 3 단계로 이루어진다.

  1. HTTP의 파라미터를 사용하여 다른 업체가 요구하는 데이터를 전송한다.(즉, HTTP 요청)
  2. 데이터를 전송받은 업체는 알맞은 프로토콜을 사용하여 전송 결과를 보낸다. (즉, HTTP 응답)
  3. HTTP 응답을 받은 업체는 결과에 알맞게 나머지 처리를 한다.
예를 들어, 사용자가 어떤 서적을 구매한다고 해 보자. 사용자는 인터넷 서점의 웹 사이트에서 구매할 책을 선택한 후 "구매" 버튼을 클릭할 것이다. 그러면 그 "구매" 요청을 처리하는 인터넷 서점의 서블릿은 사용자의 "구매" 요청 정보를 DB에 저장한다. 그런 후 (1) 인터넷 서점의 서블릿은 HTTP 프로토콜을 사용하여 택배 회사에서 발송 요청 정보를 전송한다. 그러면 (2) 인터넷 서점의 서블릿으로부터 요청을 받은 택배 회사의 서블릿은 처리 결과를 인터넷 서점과 미리 결정한 프로토콜에 맞춰 출력 스트림으로 전송한다. 마지막으로 (3) 인터넷 서점은 택배 회사로부터 전달받은 결과값을 사용하여 나머지 처리를 한다. 나머지 처리에 속하는 것으로는 사용자에게 구매한 서적이 발송되었음을 알리는 메일을 발송한다던가 데이터베이스에 기록을 남기는 등이 있을 것이다.

모든 웹-투-웹 기반의 프로그램은 이와 같이 3 단계 과정을 기본으로 하고 있으며, 서로 주고 받을 데이터에 대한 규약만 명확하게 정의해 놓는다면 안정적으로 기업간 통신을 이룰 수 있게 된다. (웹 기반 통신의 장점에 대해서는 이 글의 뒷 부분에서 언급할 것이다.)

발송 요청 및 발송확인 통보

이제부터 본격적으로 구현에 대해서 살펴보자. 먼저 알아볼 내용은 인터넷 서점이 택배 회사에 발송 요청을 하는 것과 택배 회사가 인터넷 서점에 발송 확인 통보를 하는 과정이다.

첫번째로 발송 요청부터 살펴보자. 가장 먼저 해야 할 일은 사용자가 서적을 구매할 경우 인터넷 서점에서 택배회사에 실시간으로 전송되어야 하는 파라미터를 결정하는 것이다. 이 예제에서는 표1에 나타낸 파라미터를 택배회사에 전송할 것이다.

파라미터 이름 의미
orderNo 주문번호
title 책이름
isbn ISBN
quantity 수량
receiver 받는사람
address 받는사람주소
contact 연락처

인터넷 서점의 구매 요청을 처리하는 JSP 페이지는 다음과 같은 순서로 구매 요청을 처리하게 된다.

  1. 사용자가 입력한 내용에 기반하여 주문번호를 생성한다.
  2. 해당하는 주문번호와 관련된 정보를 DB에 저장한다.
  3. 표1에 표시한 파라미터 정보를 저장하고 있는 java.util.Properties 객체를 생성한다.
  4. HttpMessage를 사용하여 3 에서 생성한 파라미터 정보를 택배회사 서블릿에 전송한다.
  5. 택배회사 서블릿으로부터 응답 결과에 따라 알맞은 처리를 한다.
  6. 최종 처리를 사용자에게 보여준다.
이 과정에서 우리가 살펴볼 부분은 과정 3에서 과정 5까지이다. 나머지 과정은 이 글의 목적과 관련성이 적으므로 생략하기로 한다. 먼저 과정 3에 해당하는 코드를 살펴보자. 이 코드는 다음과 같을 것이다.

  public void doPost(HttpServletRequest request,
                     HttpServletResponse response)
                     throws IOException, ServletException {
  
     // 책 주문 관련 정보를 OrderBean 이라는 자바빈 컴포넌트에 저장한다.
     ...
     ...
     
     Properties params = new Properties();  // 택배사에 전송될 파라미터를 저장한다.
     params.setProperty("orderNo", orderBean.getOrderNo());
     params.setProperty("title", orderBean.getTitle());
     params.setProperty("isbn", orderBean.getIsbn());
     params.setProperty("quantity", orderBean.getQuantity().toString());
     params.setProperty("receiver", orderBean.getReceiver());
     params.setProperty("address", orderBean.getAddress());
     params.setProperty("contact", orderBean.getContact());
     
     ...
     ...
  }

위와 같이 Properties 객체를 생성한 다음에는 4단계로 들어가면 된다. 4단계 코드는 다음과 같다.

  public void doPost(HttpServletRequest request,
                     HttpServletResponse response)
                     throws IOException, ServletException {
     
     // 책 주문 정보를 읽어오고 Properties 객체를 사용하여
     // 택배사 서블릿에 전송할 파라미터의 값을 지정한다.
     ...
     ...
     
     BufferedReader br = null; // 택배사 서블릿으로부터 결과를 읽어올 때 사용되는 스트림.
     try {
        HttpMessage httpMessage = new HttpMessage(url);
        InputStream is = httpMessage.sendPostMessage(params);
        br = new BufferedReader(new InputStreamReader(is));        
        // 응답 결과에 따라 알맞은 처리를 한다.
        ...
        ...
        
     } catch(IOException ex) {
        // 택배사 서버와 통신 과정에서 예외 발생
        ...
     } finally {
        if (br != null)
           try { br.close(); } catch(SQLException ex) {}
     }
     ...
  }

이제 마지막으로 해야 할 일은 택배사의 서블릿이 보내온 결과값을 이용하여 마무리 처리를 최종 처리를 하는 것이다. 여기서 필요한 것이 바로 프로토콜이다. 물론, 인터넷 서점의 서블릿과 택배사의 서블릿이 서로 정보를 몇 차례 주고 받는 것이 아니라 단 한번만 정보를 주고 받는 것이기 때문에(즉 인터넷 서점의 서블릿은 택배사 서블릿에 요청을 전송하고 택배사 서블릿은 그에 대한 응답을 전송한다) 특별히 복잡한 프로토콜은 필요없다. 택배 서블릿은 단순히 인터넷 서점의 택배 발송 요청에 대한 성공 실패 여부 정도만 알려줄 수 있으면 된다. 다음 표는 택배사 서블릿이 인터넷 서점에 결과 값을 전송할 때 사용하는 프로토콜을 나타내고 있다.

프로토콜 메시지 의미
OK [주문번호] 인터넷 서점의 택배 서비스 요청이 성공적으로 이루어졌음을 나타낸다. [주문번호]는 인터넷 서점에서 사용하는 주문 번호를 나타낸다.
ERROR [주문번호] [에러메시지] 택배 발송 서비스 요청 처리 과정에서 에러가 발생했음을 나타낸다. [에러메시지]는 발생한 에러의 원인을 나타낸다.

프로토콜이라는 용어를 사용해서 거창한 것 처럼 느꼈을지도 모르지만, 택배사 서블릿이 요청 결과로서 보내는 프로토콜은 표2와 같이 간단하다. 택배사 서블릿은 택배 서비스 요청 처리 결과를 표2에 표시한 프로토콜에 따라 인터넷 서점에 전송한다. 예를 들어, 택배사 서블릿은 요청한 주문번호에 대하여 이미 택배 서비스 요청이 처리되었을 경우 다음과 같은 메시지를 인터넷 서점에 전송할 것이다.

  ERROR S-2001-878124 이미 서비스 요청이 처리된 주문 번호입니다.

다음 코드는 앞에서 살펴본 코드에 프로토콜 처리 부분을 추가한 것이다.

  public void doPost(HttpServletRequest request,
                     HttpServletResponse response)
                     throws IOException, ServletException {
  
     // 책 주문 정보를 읽어오고 Properties 객체를 사용하여
     // 택배사 서블릿에 전송할 파라미터의 값을 지정한다.
     ...
     ...
     
     BufferedReader br = null; // 택배사 서블릿으로부터 결과를 읽어온다.
     try {
        HttpMessage httpMessage = new HttpMessage(url);
        InputStream is = httpMessage.sendPostMessage(params);
        br = new BufferedReader(new InputStreamReader(is));
        
        // 응답 결과에 따라 알맞은 처리를 한다.
        String line = br.readLine(); // 첫줄을 읽어온다.
        int indexOfSpace = line.indexOf(' ');
        if (indexOfSpace > 0) {
           String cmd = line.substring(0, indexOfSpace);
           if (cmd.equals("OK") ) {
              // 택배 서비스 요청 처리가 성공적으로 이루어졌음을 나타낸다.
              // 이 경우 사용자에게 주문 요청이 성공적으로 이루어졌다는
              // 메시지를 보여준다.
              ...
              
           } else if (cmd.equals("ERROR") ) {
              // 택배 서비스 요청 처리 과정에서 에러가 발생했음을 나타낸다.
              // 이 경우 사용자에게 택배 서비스 신청 과정 중 에러가
              // 발생하였음을 알린다.
              ...
           } else {
             // 프로토콜을 따르지 않는 응답 결과를 보내온 것임
             ...
           }
        } else {
           // 프로토콜을 따르지 않는 응답 결과를 보내온 것임
           ...
        }
        ...
        ...
     } catch(IOException ex) {
        // 택배사 서버와 통신 과정에서 예외 발생
        // 이에 대한 알맞은 메시지를 사용자에게 보여준다.
        ...
     } finally {
        if (br != null)
           try { br.close(); } catch(SQLException ex) {}
     }
     ...
  }

위 코드를 보면 웹-투-웹 프로그래밍이 복잡하지 않다는 것을 알 수 있다. 주고 받아야 하는 데이터에 따라 프로토콜이 더 복잡해질 수는 있겠지만, 대부분의 경우 위 코드의 형태를 크게 벗어나지 않는다. 물론, 위 코드에서 프로토콜에 따라 처리하는 부분을 좀더 객체 지향적으로 변경할 수는 있겠지만, 기본 구조는 위 코드와 같다.

택배사가 배송 결과를 다시 알려주는 과정은 인터넷 서점이 택배사에 택배 서비스를 요청하는 것과 크게 다르지 않으므로 그에 대한 설명은 하지 않겠다.

XML을 이용한 데이터 송수신

지금까지 살펴본 내용을 통해서 웹-투-웹 프로그래밍이 어떤식으로 구현된다는 것을 알 수 있었을 것이다. 하지만, 앞에서 살펴본 내용들이 모두 코드의 일부만을 보여줬기 때문에 실제 구현이 어떤식으로 이루어지는 지에 대해서는 여전히 궁금해할 것이라 생각된다. 그래서 이제부터는 웹-투-웹을 기반으로 XML 데이터를 주고 받는 것에 대해서 살펴보도록 하자.

여기서 살펴볼 예제는 앞에서 언급했던 것으로서 인터넷 서점이 출판사로부터 신간 목록을 읽어오는 부분이다. 출판사의 서블릿인 NewBookListServlet은 최근에 추가된 신간 목록을 XML 문서로 출력해준다. 출력되는 XML 문서는 다음과 같은 형식을 갖는다.

  <?xml version="1.0" encoding="euc-kr" ?>
  <book-list>
     <book>
        <isbn>8980781032</isbn>
        <title>JSP Professional</title>
        <publisher>가메</publisher>
        <author>이동훈,최범균</author>
        <page>873</page>
        <price>25000</price>
     </book>
     <book>
        <isbn>8980781024</isbn>
        <title>Windows 2000 Server 완전정복</title>
        <publisher>가메</publisher>
        <author>김형백</author>
        <page>891</page>
        <price>25000</price>
     </book>      
  </book-list>

실제로 NewBookListServlet은 다음과 같다.

  import javax.servlet.*;
  import javax.servlet.http.*;
  import java.io.*;
  
  import com.pub.book.BookMgr;
  import com.pub.book.BookMgrException;
  import com.pub.book.BookBean;
  
  public class NewBookListServlet extends HttpServlet {
  
     public void doGet(HttpServletRequest request,
                       HttpServletResponse response)
                       throws IOException, ServletException {
        response.setContentType("text/xml; charset=euc-kr");
        PrintWriter out = response.getWriter();
        
        out.println("<?xml version="1.0" encoding="euc-kr" ?>");
        out.println("<book-list>");
        
        try {
           BookMgr mgr = BookMgr.getInstance();
           BookBean[] newBookList = mgr.getNewBookList();
           for (int i = 0 ; i < newBookList.length ; i++) {
              out.println("    <book>");
              out.println("        <isbn>"+newBookList[i].getIsbn()+"</isbn>");
              out.println("        <title>"+newBookList[i].getTitle()+"</title>");
              out.println("        <publisher>"+
                          newBookList[i].getPublisher()+"</publisher>");
              out.println("        <author>"+newBookList[i].getAuthor()+"</author>");
              out.println("        <page>"+newBookList[i].getPage()+"</page>");
              out.println("        <price>"+newBookList[i].getPrice()+"</price>");
              out.println("    </book>");
           }
        } catch(BookMgrException ex) {
           // getInstance()나 getNewBookList()에서 예외가 발생한 경우
           // <error> 태그를 사용하여 에러 메시지를 출력한다.
           out.print("    <error>");
           out.print(ex.getMessage());
           out.println("</error>");
        }
        out.println("</book-list>");
        out.flush();
        out.close();
     }
  }

위 코드에서 BookMgr 클래스는 출판사의 서적을 관리해주는 역할을 맡고 있으며, BookBean 클래스는 책 정보를 저장할 때 사용되는 자바빈이다. BookMgr 클래스의 getNewBookList() 메소드는 어제 하루 동안 나온 신간 목록을 구해주는 메소드로서 NewBookListServlet은 이 메소드를 사용하여 신간 정보를 저장하고 있는 BookBean의 배열을 구한다. 일단 BookBean 배열을 구하게 되면, 그 배열의 정보를 이용하여 신간 목록 정보를 나타내는 XML 문서를 클라이언트에 출력한다. (여기서 BookMgr과 BookBean에 대한 내용은 언급하지 않겠다.)

XML 문서를 출력하기 위해서는 ServletResponse의 setContentType() 메소드를 사용하여 출력될 컨텐츠의 타입이 XML 문서라는 것을 알려주어야 한다. 위 코드를 보면 "text/xml; charset=euc-kr" 로 지정해준 것을 알 수 있다. 뒤에 있는 charset은 출력될 글자의 캐릭터셋이 EUC-KR 이라는 것을 나타낸다. XML 문서를 출력하는 것은 위 코드에서 볼 수 있듯이 서블릿의 Writer 객체에 HTML 문서를 출력하는 것과 완전히 같다.

여러분 중에는 아마도 DOM이나 JDOM을 사용하여 XML 문서를 나타내는 트리를 생성한 후 그 트리를 XML 문서 형태로 스트림에 출력하는 방법을 선택할 사람도 있을 것이다. 물론, 그 방법이 나쁘지는 않으며 또한 코드가 좀더 의미 있어지는 것이 사실이다. 하지만, 이 글은 DOM이나 JDOM을 사용하여 XML 문서를 생성하는 것에 대해서 살펴보는 것이 아니므로 위 코드에처럼 간단한 방법을 사용하기로 한다.

웹브라우저에서 NewBookListServlet으로 HTTP 요청을 보내면 다음 그림과 같은 결과가 출력된다. XML 문서가 요청에 대한 응답으로 출력되는 것을 알 수 있다.


그림2 출판사의 NewBookListServlet의 결과화면

신간 목록을 XML 문서로 출력해주는 출판사의 서블릿을 살펴보았으니 이제 인터넷 서점 부분을 살펴보자. 인터넷 서점은 출판사의 서블릿으로부터 신간 목록을 읽어와 XML 문서를 파싱한 후 알맞게 DB에 정보를 삽입하면 된다. 먼저 간단하게 인터넷 서점에서 신간 목록을 읽어오는 흐름을 살펴보자.

  1. HttpMessage를 이용하여 출판사의 http://host/servlet/NewBookListServlet 에 연결한다.
  2. 출판사 서블릿 NewBookListServlet으로부터의 응답 결과로부터 DOM 트리를 생성한다.
  3. DOM 트리로부터 서적 정보를 읽어와 자바빈에 저장한다.
  4. 인터넷 서점의 예비 신간 서적 DB에 자바빈 정보를 추가한다.
지금까지 살펴본 내용과 크게 다르지 않다. 다른 점이 있다면 프로토콜 부분이 XML 문서로 대체되었다는 것 뿐이다. 이를 구현하기 위해 이 글에서는 인턴넷 서점에서 사용될 세 개의 클래스 NewBookListCollector, NewBook, NewBookServlet을 살펴보기로 하자.

먼저 NewBook 클래스는 앞에서 출판사 부분에서 언급했던 BookBean과 마찬가지로 인터넷 서점의 신간 DB 테이블에 저장할 데이터를 담고 있는 자바빈이다. 그리고 NewBookListCollector는 출판사의 서블릿/JSP/CGI 등으로부터 신간 목록을 저장하고 있는 XML 문서를 읽어와 파싱하여 NewBook 자바빈 인스턴스를 생성해주는 역할을 한다. NewBookServlet은 NewBookListCollector 클래스를 사용하여 신간 목록을 사용자에게 보여주는 역할을 한다. 즉, 전체 구조는 다음과 같다.


그림3 인터넷 서점과 출판사 사이의 업무 흐름

XML 문서를 이용한 정보 교환에서 핵심적인 역할을 맡고 있는 NewBookListCollector 클래스를 살펴보자. 일단 이 클래스의 완전한 소스 코드부터 보도록 하자. 완전한 소스 코드는 다음과 같다. (코드가 좀 길긴 하지만 내부 로직은 복잡하지 않으므로 차분하게 살펴보기 바란다.)

  package com.interbook;
  
  import javax.xml.parsers.DocumentBuilderFactory;  import javax.xml.parsers.DocumentBuilder;  import javax.xml.parsers.ParserConfigurationException;  import org.xml.sax.SAXException;  import org.xml.sax.InputSource;  import org.w3c.dom.*;  import java.io.*;
  import java.util.*;
  import java.net.URL;
  import javacan.http.util.HttpMessage;  
  public class NewBookListCollector {
    /**
     * 지정한 URL로부터 신간 목록이 담긴 XML 문서를 읽어와
     * 파싱한 후 NewBook 배열을 리턴한다.
     */
    public static NewBook[] getNewBook(String url) {
      BufferedReader br = null; // 지정한 URL로부터
      try {
        HttpMessage httpMessage = new HttpMessage(new URL(url));
        InputStream is = httpMessage.sendGetMessage();
        br = new BufferedReader(new InputStreamReader(is));
        
        ArrayList newBookList = new ArrayList(5);
        
        InputSource source = new InputSource(br);        
        DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
        DocumentBuilder builder = dbFactory.newDocumentBuilder();
        Document document = builder.parse(source);        
        // document 로부터 ROOT 요소를 구한다.(<book-list>)
        Element rootElement = document.getDocumentElement();
        for (Node subNode = rootElement.getFirstChild() ;
             subNode != null ;
             subNode = subNode.getNextSibling() ) {
          
          if (subNode.getNodeType() == Node.ELEMENT_NODE && 
              subNode.getNodeName().compareTo("book") == 0) {
            // <book> 노드일 경우
            NewBook book = new NewBook();            // <book> 요소의 하위 요소를 처리한다.
            NodeList childNodeList = subNode.getChildNodes();
            int len = childNodeList.getLength();
            for (int i = 0; i < len ; i++) {
              Node childNode = childNodeList.item(i);
              // <book> 요소의 하위 노드의 타입이 ELEMENT 일 경우
              if (childNode.getNodeType() == Node.ELEMENT_NODE) {
                String childNodeName = childNode.getNodeName();
                String value = getSubTextNodeValue(childNode).trim();
                if (childNodeName.compareTo("isbn") == 0) {
                  book.setIsbn(value); 
               } else if (childNodeName.compareTo("title") == 0) {
                  book.setTitle(value);
                } else if (childNodeName.compareTo("publisher") == 0) {
                  book.setPublisher(value);
                } else if (childNodeName.compareTo("author") == 0) {
                  book.setAuthor(value);
                } else if (childNodeName.compareTo("page") == 0) {
                  try {
                    book.setPage(Integer.parseInt(value));
                  } catch(NumberFormatException ex) {
                    book.setPage(-1);
                  }
                } else if (childNodeName.compareTo("price") == 0) {
                  try {
                    book.setPrice(Integer.parseInt(value));
                  } catch(NumberFormatException ex) {
                    book.setPage(-1);
                  }
                }
              }
            }
            newBookList.add(book);
          } else if (subNode.getNodeType() == Node.ELEMENT_NODE && 
                       subNode.getNodeName().compareTo("error") == 0) {
            // <error> 요소인 경우 알맞은 처리
            // 이 예제에서는 처리하지 않는다.
            return null;
          }
        }
        
        NewBook[] bookList = new NewBook[newBookList.size()];
        newBookList.toArray(bookList);
        return bookList;      } catch(Exception ex) {
        // 처리하는 과정에서 예외가 발생한 경우 null을 리턴.
        ex.printStackTrace();
        return null;      } finally {
        if (br != null) try { br.close(); } catch(IOException ex) {}
      }
    }
    
    private static StringBuffer buff = new StringBuffer(512);
    
    private static String getSubTextNodeValue(Node node) {
      NodeList subNodeList = node.getChildNodes();
      int len = subNodeList.getLength();
      
      if (buff.length() > 0) buff.delete(0, buff.length());
      
      for (Node subNode = node.getFirstChild() ;
           subNode != null ;
           subNode = subNode.getNextSibling() ) {
        if (subNode.getNodeType() == Node.TEXT_NODE ) {
          buff.append(subNode.getNodeValue());
        } else if (subNode.getNodeType() == Node.ENTITY_REFERENCE_NODE) {
          buff.append(subNode.getFirstChild().getNodeValue().trim());
        }
      }
      return buff.toString();
    }
  
  }

위 코드를 보면 HttpMessage.sendGetMessage() 메소드를 사용하여 출판사의 /serlvet/NewBookListSerlvet에 접속한 후, NewBookListServlet이 전송하는 XML 문서를 Reader를 통해서 읽어들인다. XML 문서를 파싱하기 위해서 org.xml.sax.InputSource 클래스를 사용하고 있는데 InputSource는 DocumentBuilder가 다양한 자원으로부터 XML 문서를 읽어와 DOM 트리인 Document 객체를 생성할 수 있도록 해 준다. 일단 Document 객체가 생성되면 이제 그 Document 객체로부터 필요한 정보를 추출해내기만 하면 된다.

위 코드에서 XML 문서를 처리하는 부분이 있는데 조금 복잡해 보이지만 JAXP API와 DOM API를 사용하는 것에 대해서 조금만 공부한다면 쉽게 이해할 수 있을 것이다. 자바에서 DOM API를 사용하는 것에 대한 내용은 자바와 XML(한빛출판사)을 참고하기 바란다. 또는, 자바캔의 일반 강좌 게시판에 김인희씨가 올린 글을 참조해도 된다.

이제 인터넷 서점은 출판사로부터 XML 문서로 된 신간 목록을 읽어올 수 있게 되었다. 남은 작업은 서블릿을 사용하여 인터넷 서점 관리자에게 인터페이스를 제공하는 것이다. 이는 그림 3에서 보듯이 인터넷 서점의 NewBookServlet을 통해서 이루어진다. 이 글에서는 간단하게 출판사로부터 읽어온 신간 목록을 화면에 보여주도록 NewBookSerlvet을 구현해보았다. 다음은 NewBookServlet의 코드이다.

  import javax.servlet.*;
  import javax.servlet.http.*;
  import java.io.*;
  
  import com.interbook.NewBook;
  import com.interbook.NewBookListCollector;
  
  public class NewBookServlet extends HttpServlet {
  
     public void doGet(HttpServletRequest request,
                       HttpServletResponse response)
                       throws IOException, ServletException {
        
        NewBook[] list = NewBookListCollector.getNewBook(
                 "http://localhost:8080/servlet/NewBookListServlet");        
        response.setContentType("text/html; charset=euc-kr");
        PrintWriter out = response.getWriter();
        
        out.println("<html><head><title>신간 목록</title></head>");
        out.println("<body>");
        if (list == null) {
           out.println("신간 목록을 읽어오는 과정에서 에러가 발생하였습니다.");
        } else {
           out.println("신간 목록");
           
           for (int i = 0 ; i < list.length ; i++) {
              out.println("<hr>");
              out.println("도서명:"+list[i].getTitle());
              out.println("출판사:"+list[i].getPublisher());
              out.println("저자:"+list[i].getAuthor());
              out.println("가격:"+list[i].getPrice());
           }
        }
        out.println("</body></html>");
        out.flush();
        out.close();
     }
  }

물론, 실제로 인터넷 서점에서 사용되는 NewBookServlet은 위와 같이 단순하진 않을 것이며 다양한 기능을 제공할 것이다. 하지만 이 글는 여러분에게 XML과 웹-투-웹을 이용한 정보 교환을 보여주는 것에 중점을 두고 있으므로 다른 부차적인 기능은 살펴보지 않기로 한다.

실제로 웹브라우저를 통해서 NewBookServlet에 접근하면 다음과 같은 결과가 출력된다.


그림4를 보면 인터넷 서점의 관리자가 출판사로부터 읽어온 신간 목록을 간단하게 웹 브라우저를 통해서 읽어올 수 있다는 것을 알 수 있다. 이런 방법으로 인터넷 서점은 여러 출판사로부터 동시에 신간 목록을 읽어올 수 도 있을 것이다.(물론, 모든 출판사는 동일한 DTD/스키마를 사용하여 XML 문서를 생성한다.) 기존의 어떤 방법보다도 간편하게 신간 목록을 추가할 수 있다는 것을 알 수 있다.

Web-to-Web의 장단점

이제야 비로서 웹-투-웹 프로그래밍의 장단점에 대해서 알아볼 때가 되었다. 1주에서 장단점에 대해서 언급할 수도 있었으나 간단하게나마 예제를 살펴본 후에 논하는 것이 더 알맞을 것 같아서 이렇게 2부에서 웹-투-웹의 장단점에 대해서 언급하도록 한다. 먼저 장점부터 살펴보도록 하자.

  • 구현이 비교적 쉽다.
    앞의 예제에서 보았듯이 웹을 기반으로 데이터를 주고 받는 것은 매우 간단하게 구현된다. 일종의 웹 프로그래밍이기 때문에 JSP/서블릿, ASP, PHP 등과 같은 것으로 웹 사이트를 구축하듯 구현하면 된다. 게다가 JSP나 ASP와 같은 스크립트 언어는 CGI나 펄에 비해 쉽게 구현할 수 있다는 장점도 갖고 있다.

  • 복잡한 프로토콜이 필요없다.
    데이터의 전달은 파라미터를 통해서 이루어지며 결과는 HTTP 프로토콜을 응답을 통해서 이루어지기 때문에 데이터 송수신과 관련된 복잡한 프로토콜을 정의할 필요가 없다. 특히 XML을 응답 결과로서 사용할 경우 XML 문서와 관련된 DTD와 스키마만 서로 공유하면 되며 또한 XSL/T를 사용하여 응답 결과를 알맞은 형태로 어렵지 않게 변경할 수 있다.

  • 특정한 프로그래밍 언어에 구애받지 않아도 된다.
    웹을 기반으로 하는 통신에서는 서로 HTTP 프로토콜을 사용하여 정보를 주고 받는다. 따라서 자바든 VB 스크립트든 아니면 C 언어이건간에 서비스를 제공할 웹 페이지를 구현할 때 사용한 언어는 아무런 문제가 되지 않는다. 단지, 정해진 프로토콜에 따라 알맞게 정보만 주고 받으면 된다.

  • HTTPS를 사용하여 주고 받는 데이터를 쉽게 암호화할 수 있다.
    HTTPS는 HTTP 프로토콜에서 주고 받는 데이터를 암호화한 것이다. 비록 1부에서 작성한 HttpMessage 클래스가 HTTPS를 지원하고 있진 않지만 자바와 관련된 보안 관련 확장 API를 사용하면 어렵지 않게 구현할 수 있다. 또한, JDK 1.4 부터는 보안 관련 API가 기본적으로 포함되어 있기 때문에 별도의 라이브러리를 설치할 필요도 없어진다.

장점에 대해서 살펴봤는데 모든 게 그렇듯이 장점만 있을리가 없다. 이제 단점에 대해서 살펴보자.

  • 복잡한 흐름제어는 불가능하다.
    HTTP 프로토콜은 기본적으로 '연결 -> 요청전송 -> 응답받음 -> 연결해제'의 간단한 과정을 거친다. 몇번에 걸쳐서 서로 요청/응답을 반복적으로 수행하는 SMTP나 FTP와 같은 프로토콜과 달리 HTTP 프로토콜은 한번의 요청과 그 요청에 대한 한번의 응답으로 구성되어 있다. 따라서, 흐름제어와 같이 여러번의 요청/응답을 필요로 하는 업무에는 적용하기 힘들다.

한번의 연결에서 여러 차례의 요청/응답이 이루어지는 구조는 단순한 HTTP 프로토콜로는 구현이 불가능하다. 물론, 여러번에 걸쳐서 연결을 할 경우 이 단점이 극복될 수는 있지만 연결을 열었다 끊었다를 반복하는 것이 그리 좋은 방법은 아닐 것이다.

결론

이번 2부에서는 웹 기반의 정보 교환이 실제로 어떻게 적용될 수 있는 지 간단한 예를 통해서 알아보았으며, 웹 기반 통신의 장/단점에 대해서 알아보았다. 비록 여기서 살펴본 것이 웹 기반의 정보 교환을 위한 유일한 방법은 아니지만, HTTP 프로토콜을 이용하여 기업 사이에 정보를 주고 받는 것이 현실적으로 사용할 수 있는 알맞은 방법이라는 것은 확실하다. 물론 엔터프라즈 환경에 알맞은 자바 메시지 서비스(Java Message Service; JMS)를 사용하여 정보 교환을 할 수도 있겠지만, 여전히 많은 기업들은 아파치(또는 IIS) 웹서버와 JSP/ASP/PHP/펄CGI 등을 사용하여 웹 사이트를 구축하고 있으며 이러한 상황에서 기업간 정보 교환을 손쉽게 하기 위한 대안으로서 HTTP 프로토콜을 이용한 웹 기반의 통신을 제시해 보았다. (필자 역시 현재 진행하고 있는 프로젝트에서 다른 기업과의 공유를 위해서 웹 기반의 통신을 하고 있다.)

관련링크:
Posted by 최범균 madvirus

댓글을 달아 주세요

  1. 황타 2011.11.21 10:49 신고  댓글주소  수정/삭제  댓글쓰기

    OpenAPI를 사용하면서 제대로 이해도 못하고 openConnection을 사용했었는데,
    1부, 2부 강의를 보니 이해가되네요~ㅎ
    좋은글 감사합니다~

  2. 자바초보 2013.07.15 19:09 신고  댓글주소  수정/삭제  댓글쓰기

    님완전 개쩌는듯 .... 감사해요

  3. 오우 2017.11.21 09:29 신고  댓글주소  수정/삭제  댓글쓰기

    쓸일이 있었는데 유용하게 보았습니다 ^^
    감사합니다.

웹을 기반으로 정보를 주고 받는 웹-투-웹 프로그래밍에 대해서 살펴본다.

URL과 URLConnection을 이용한 데이터 전송

오늘날 B2B가 IT 업계의 대세로 자리 잡으면서 B2B 기업간에 정보를 주고 받아야 하는 상황이 종종 발생하고 있다. 각 기업간에 정보를 주고 받는 방법에는 데이터베이스를 공유하는 방법, RPC나 RMI 또는 코바를 사용하는 방법 또는 직접 프로토콜을 제작하여 TCP 기반의 통신을 하는 방법 등 다양한 형태가 존재한다.

하지만 이러한 방법들보다 더욱 간단하게 정보를 주고 받을 수 있는 방법이 있다. 바로 웹을 기반으로 해서 정보를 주고 받는 것이다. 웹 프로그래밍은 RMI/RPC/코바 프로그래밍에 비해서 간단하며 보안의 경우도 HTTPS 프로토콜을 사용하여 비교적 간단하게 해결할 수 있다. 더군다나 자바의 경우는 java.net.URLConnection 클래스를 통해서 소켓 프로그래밍을 직접 할 필요 없이 매우 간단하게 웹 페이지를 호출할 수 있도록 하고 있다.

이 글에서는 먼저 URL 클래스와 URLConnection 클래스를 사용하여 웹 기반의 통신을 하는 방법에 대해서 살펴보고, 그 다음으로 웹 기반의 통신 기능을 캡슐화한 HttpMessage 클래스를 작성해볼 것이다.

java.net.URL과 java.net.URLConnection 클래스

앞에서도 언급했듯이 자바는 이미 URLConnection 클래스를 통해서 매우 간단하게 HTTP 프로토콜 기반의 소켓 통신을 할 수 있도록 하고 있다. URLConnection 클래스는 실제로 추상클래스로서 URLConnection 타입을 갖는 인스턴스는 java.net.URL 클래스의 openConnection() 메소드를 통해서 구할 수 있다.

java.net.URL 클래스는 URL을 나타낼 때 사용되며, 프로토콜, 포트 번호, 호스트 이름, 자원의 경로 등의 정보를 포함하고 있다. 예를 들어, 자바캔 웹 사이트를 나타내는 URL 인스턴스를 생성하고자 할 경우 다음과 같이 하면 된다.

  URL url = new URL("http", "www.javacan.com", 80, "index.html");
  URL url = new URL("http://www.javacan.com/index.html");

위의 두 줄은 모두 http://www.javacan.com/index.html 을 URL 클래스를 사용하여 표현한 것이다. java.net.URL 클래스는 FTP, HTTP 프로토콜을 포함하여 대부분의 URL을 표현할 수 있도록 해 주고 있다. 이 글에서는 HTTP 프로토콜에 대한 내용만 언급할 것이므로, java.net.URL 클래스에 대해서 보다 자세한 내용을 알고 싶다면 JDK API 문서나 자바 관련 서적을 참고하기 바란다.

위 코드와 같이 URL 클래스의 인스턴스를 생성하게 되면 openConnection() 메소드를 사용하여 해당하는 URL에 대해 연결된 클래스인 URLConnection 을 얻을 수 있게 된다. 예를 들면 다음과 같다.

  URL url = new URL("http", "www.javacan.com", 80, "index.html");
  URLConnection conn = url.openConnection();

URL 클래스의 openConnection()을 호출하게 되면, URL 클래스가 사용하는 프로토콜에 따라 URLConnection을 상속한 알맞은 하위 클래스의 인스턴스를 얻게 된다. 예를 들어, HTTP 프로토콜을 사용할 경우 openConnection() 메소드는 java.net.HttpURLConnection 클래스의 인스턴스를 리턴하게 된다.

일단 URL 클래스로부터 URLConnection 인스턴스를 구하게 되면 URLConnection.getInputStream() 메소드를 사용하여 원격 자원으로부터 데이터를 읽어올 수 있게 된다. 예를 들어, 다음은 간단하게 파라미터로 입력받은 URL이 나타내는 사이트로부터 데이터를 읽어오는 자바 프로그램이다.

  import java.net.*;
  import java.io.*;
  
  public class ReadDataFromURL {
  
     public static void main(String[] args) throws IOException {
        if (args.length == 0) {
           System.out.println("java ReadDataFromURL URL");
           System.exit(0);
        }
        URL url = new URL(args[0]);
        URLConnection conn = url.openConnection();
        InputStream is = conn.getInputStream();
        BufferedReader br = new BufferedReader(new InputStreamReader(is));
        char[] buff = new char[512];
        int len = -1;
        
        while( (len = br.read(buff)) != -1) {
           System.out.print(new String(buff, 0, len));
        }
        
        br.close();
     }
  }

위 코드를 보면 특별히 어려운 부분이 느껴지지 않을 것이다. 단순히 URL 클래스의 openConnection() 메소드와 URLConnection 클래스의 getInputStream() 메소드를 차례대로 호출하기만 하면 URL 클래스가 나타내는 자원으로부터 데이터를 읽어올 수 있다. 실제로 다음과 같이 실행함으로써 특정 사이트의 내용을 원하는 파일에 저장할 수도 있다.

  java ReadDataFromURL http://www.javacan.com > javacanindex.html

위 코드에서 조심해야 할 부분이라면 URLConnection.getInputStream() 메소드로부터 얻어진 InputStream으로부터 직접 데이터를 읽어오기 보다는 Reader를 거쳐서 데이터를 읽어온다는 점이다. 이는 InputStream을 사용할 경우 바이트 단위로 데이터를 읽어오기 때문에 한글과 같이 아스키 코드 이외에 글자들이 깨지기 때문이다.

서버에 데이터 전송하기

이제 URL 클래스와 URLConnection 클래스를 사용하여 매우 간단하게 웹 사이트의 내용을 읽어올 수 있다는 것을 알았을 것이다. 하지만, 특정 URL에 있는 데이터를 읽어오는 것만으로는 웹 기반의 데이터 교환을 할 수 없게 된다. 진정으로 웹-투-웹 프로그래밍이 가능하려면 상대방의 웹 사이트에 있는 JSP/PHP/ASP/CGI와 같은 웹 프로그램에 데이터를 전송할 수 있어야 한다.

HTTP 프로토콜은 GET 방식과 POST 방식을 사용하여 데이터를 전송할 수 있도록 하고 있다. 간단히 이 두 방식의 차이점을 설명한다면 GET 방식은 URL을 통해서 서버에 전달되며 POST 방식의 경우는 스트림을 통해서 서버에 전달된다. 전통적인 CGI의 경우 GET 방식의 데이터는 환경 변수를 통해서, POST 방식으로 전달된 데이터는 입력 스트림을 통해서 구할 수 있었으나 자바의 서블릿이나 JSP에서는 이런 하위 레벨은 알 필요 없이 단순히 HttpServletRequest의 getParameter() 메소드를 사용하여 매우 간단하게 클라이언트가 전송한 데이터를 읽어올 수 있다.

이처럼 GET 방식과 POST 방식 사이의 차이점 때문에 서버에 데이터를 전송하는 클래스 역시 이 차이점에 맞춰서 각각의 방식을 알맞게 구현해주어야 한다.

GET 방식으로 데이터 전송하기

먼저 GET 방식으로 데이터를 전송하는 것에 대해서 살펴보자. GET 방식은 앞에서도 언급했듯이 다음과 같이 요청 URL과 함께 전송된다.

  http://somhost.com/jsp/write.jsp?name=...&email=...&....

여기서 물음표 다음에 있는 부분은 서버에 전송될 파라미터의 이름과 데이터를 나타낸다. 각각의 파라미터는 앰퍼샌드 기호(&)를 통해서 분리되며, 각 파라미터의 값은 인코딩된 상태이어야 한다. 파라미터 값의 인코딩은 java.net.URLEncoder 클래스의 static 메소드인 encode() 메소드를 통해서 처리할 수 있다. 예를 들어, 파라미터의 값이 '최범균' 일 경우 이를 인코딩하면 다음과 같은 문장으로 파라미터 값이 변경된다.

  %C3%D6%B9%FC%B1%D5

이처럼 인코딩된 데이터를 GET 방식을 사용하여 파라미터 값으로 전달하고자 할 경우 단순히 URL 뒤에 인코딩된 파라미터 값을 알맞게 추가하기만 하면 된다. 예를 들어, 다음은 간단히 파라미터 값을 인코딩한 형태로 변환해주는 메소드이다.

   public static String encodeString(Properties params) {
      StringBuffer sb = new StringBuffer(256);
      Enumeration names = params.propertyNames();
      
      while (names.hasMoreElements()) {
         String name = (String) names.nextElement();
         String value = params.getProperty(name);
         sb.append(URLEncoder.encode(name) + "=" + URLEncoder.encode(value) );
         
         if (names.hasMoreElements()) sb.append("&");
      }
      return sb.toString();
   }

위 코드에 있는 encodeString() 메소드를 보면 java.util.Properties를 파라미터로 전달받는다. java.util.Properties 클래스는 <이름, 값> 쌍을 저장할 수 있는 클래스로서 위 메소드는 파라미터의 이름과 값을 저장하고 있다. 위 코드를 보면 파라미터의 이름과 값 모두 URLEncoder.encode() 메소드를 사용하여 알맞게 인코딩하는 것을 알 수 있다.

encodeString() 메소드를 사용하여 파라미터의 이름과 값을 알맞게 인코딩할 수 있게 되었으므로 이제 GET 방식으로 전송하는 것에 대해 살펴보자. GET 방식으로 파라미터를 전달하기 위해서는 단순히 물음표 뒤에 encodeString() 메소드의 결과값을 연결하기만 하면 된다. 즉, 다음과 같은 형태의 코드를 사용하여 GET 방식의 파라미터를 전송할 수 있다.

   Properties prop = new Properties();
   prop.setProperty(paramName1, paramValue1);
   prop.setProperty(paramName2, paramValue2);
   ...
   String encodedString = encodeString(prop);
   
   URL url = new URL("http://www.host.com/index.jsp" + "?" + encodedString);
   URLConnection conn = url.openConnection();
   conn.setUseCaches(false);
   is = conn.getInputStream();
   ...

앞에서 살펴봤던 ReadDataFromURL 클래스의 main() 메소드와 크게 다르지 않으며, 단지 Properties를 이용하여 인코딩된 문자열을 알맞게 URL에 넣어준 것 밖에 차이점이 없다. 위 코드에서 URLConnection.setUseCaches() 메소드에 파라미터 값을 false를 전달한 것을 알 수 있는데, 이렇게 함으로써 캐시에 저장된 결과가 아닌 동적으로 그 순간에 생성된 결과를 읽어올 수 있게 된다. 이처럼 캐시로부터 값을 읽어오지 않는 이유는 파라미터를 전송하는 경우 대부분 웹 페이지의 결과가 그 순간 순간 파라미터의 값에 따라 달라지기 때문이다.

POST 방식으로 데이터 전송하기

GET 방식으로 파라미터를 전송하는 것은 단순히 URL 뒤에 물음표와 인코딩된 문자열을 연결해주기만 하면 될 정도로 비교적 간단했다. POST 방식의 경우는 스트림을 통해서 파라미터를 전송해야 하기 때문에 URLConnection으로부터 OutputStream을 구해야 한다. 먼저 POST 방식으로 파라미터를 전송해주는 코드부터 살펴보도록 하자.

   URLConnection conn = targetURL.openConnection();
   
   conn.setDoInput(true);
   conn.setDoOutput(true);
   
   conn.setUseCaches(false);
   
   conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
   
   out = null;
   try {
      out = new DataOutputStream(conn.getOutputStream());
      out.writeBytes(encodedParamString);
      out.flush();
   } finally {
      if (out != null) out.close();
   }
   is = conn.getInputStream();
   ...

위 코드를 보면 conn.setDoOutput(ture) 부분을 볼 수 있는데, URLConnection 클래스 setDoOutput() 메소드는 URLConnection의 출력 스트림을 사용할지의 여부를 나타낸다. POST 방식은 스트림 기반의 데이터 전송 방식이기 때문에 setDoOutput(true)를 실행하여 URLConnection의 출력 스트림을 사용하도록 지정해야 한다. 또 setRequestProperty() 메소드를 사용하여 "Content-Type" 프로퍼티를 "application/x-www-form-urlencoded"로 변경해주는 것을 알 수 있는 데, 이렇게 함으로써 웹 서버는 POST 방식의 데이터가 인코딩된 데이터라는 것을 알 수 있게 된다.

URLConnection에 대한 모든 설정이 끝나면 URLConnection.getOutputStream() 메소드를 사용하여 해당 URL에 대한 출력 스트림을 구할 수 있게 되고, 그 출력 스트림에 파라미터를 인코딩한 문자열을 전송하면 된다. 위 코드에서는 DataOutputStream을 사용하여 데이터를 전송하였다.

웹 기반의 통신 기능을 제공하는 HttpMessage 클래스

지금까지 살펴본 내용들은 웹기반 통신을 할 때 필요한 기능들을 부분적으로 설명한 것에 불과하다. 실제로 웹기반 통신을 할 때 마다 앞에서 설명한 코드들을 작성하는 것은 자바의 객체 지향적 특징을 무시(?)하는 처사라 할 수 있다. 이 글에서는 웹 기반의 통신을 쉽게 할 수 있도록 필요한 기능을 캡슐화한 HTTPMessage 클래스를 작성해보도록 하자.

HttpMessage 클래스의 소스 코드

먼저 여러분이 쉽게 접근할 수 있도록 HttpMessage 클래스의 소스 코드부터 살펴보자. HttpMessage 클래스의 소스 코드는 다음과 같다.

  package javacan.http.util;
  
  import java.io.*;
  import java.net.*;
  import java.util.*;
  
  /**
   * HTTP 요청 메시지를 웹서버에 전송한다.
   *
   * @author 최범균, era13@hanmail.net
   */
  public class HttpMessage {
     
     /**
      * HTTP 프로토콜을 사용하여 연결할 URL
      */
     private URL targetURL;
     
     /**
      * POST 방식으로 데이터를 전송할 때 사용되는 출력 스트림
      */
     private DataOutputStream out;
     
     public HttpMessage(URL targetURL) {
        this.targetURL = targetURL;
     }
     
     public InputStream sendGetMessage() throws IOException {
        return sendGetMessage(null);
     }
     
     public InputStream sendGetMessage(Properties params)     throws IOException {
        String paramString = "";
        
        if (params != null) {
           paramString = "?"+encodeString(params);
        }
        URL url = new URL(targetURL.toExternalForm() + paramString);
        
        URLConnection conn = url.openConnection();
        conn.setUseCaches(false);
        return conn.getInputStream();
     }
     
     public InputStream sendPostMessage() throws IOException {
        return sendPostMessage("");
     }
     
     public InputStream sendPostMessage(Properties params)     throws IOException {
        String paramString = "";
        
        if (params != null) {
           paramString = encodeString(params);
        }
        return sendPostMessage(paramString);
     }
  
     private InputStream sendPostMessage(String encodedParamString)     throws IOException {
        URLConnection conn = targetURL.openConnection();
        
        conn.setDoInput(true);
        conn.setDoOutput(true);
        
        conn.setUseCaches(false);
        
        conn.setRequestProperty("Content-Type",
                                   "application/x-www-form-urlencoded")
;
        
        out = null;
        try {
           out = new DataOutputStream(conn.getOutputStream());
           out.writeBytes(encodedParamString);
           out.flush();
        } finally {
           if (out != null) out.close();
        }
        
        return conn.getInputStream();
     }
     
     public static String encodeString(Properties params) {
        StringBuffer sb = new StringBuffer(256);
        Enumeration names = params.propertyNames();
        
        while (names.hasMoreElements()) {
           String name = (String) names.nextElement();
           String value = params.getProperty(name);
           sb.append(URLEncoder.encode(name) + "=" + URLEncoder.encode(value) );
           
           if (names.hasMoreElements()) sb.append("&");
        }
        return sb.toString();
     }
  }

HttpMessage 클래스는 생성자로 데이터를 주고 받을 CGI나 JSP 또는 단순한 웹 페이지를 나타내는 URL을 입력받는다. 일단 HttpMessage 객체를 생성하면 sendPostMessage(Properties) 메소드나 sendGetMessage(Properties) 메소드(전송할 파라미터가 없는 경우에는 sendPostMessage() 메소드와 sendGetMessage() 메소드)를 사용하여 지정한 URL로부터 데이터를 읽어올 수 있는 InputStream을 구할 수 있게 된다.

HttpMessage 클래스 사용하기

HttpMessage 클래스는 다음과 같은 순서로 사용하면 된다.

  1. 접속할 URL에 해당하는 java.net.URL 클래스의 인스턴스를 생성한다.
  2. URL 인스턴스를 HttpMessage 클래스의 생성자에 전달하여 HttpMessage 객체를 생성한다.
  3. 전송할 파라미터가 존재할 경우 java.util.Properties 클래스를 사용하여 파라미터의 이름/값 쌍을 저장한다.
  4. sendGetMessage() 메소드나 sendPostMessage() 메소드를 사용하여 원하는 사이트에 데이터를 전송한다.
  5. 단계 4에서 구해진 InputStream으로부터 데이터를 읽어온다.
예를 들어, 다음은 검색엔진 엠파스로부터 특정 단어에 대한 검색 결과를 읽어오는 SearchFromEmpas 클래스를 HttpMessage 클래스를 사용하여 작성한 것이다.

  import java.net.*;
  import java.io.*;
  import javacan.http.util.HttpMessage;
  import java.util.Properties;
  
  public class SearchFromEmpas {
  
     public static void main(String[] args) throws IOException {
        if (args.length == 0) {
           System.out.println("java SearchFromEmpas 검색어");
           System.exit(0);
        }
        
        URL url = new URL("http://search.empas.com/search/all.html");
        HttpMessage httpMessage = new HttpMessage(url);        
        Properties prop = new Properties();
        prop.setProperty("q", args[0]);
        prop.setProperty("m", "X");
        prop.setProperty("s", "s");
        prop.setProperty("e", "1");
        prop.setProperty("n", "10");
        
        InputStream is = httpMessage.sendGetMessage(prop);
        BufferedReader br = new BufferedReader(new InputStreamReader(is));
        char[] buff = new char[512];
        int len = -1;
        
        while( (len = br.read(buff)) != -1) {
           System.out.print(new String(buff, 0, len));
        }
        br.close();
     }
  }

다음과 같이 실행하면 SearchFromEmpas 클래스를 사용하여 엠파스로부터 검색 결과가 출력될 것이다. 만약 검색 결과를 파일로 저장하고 싶다면 명령어 뒤에 리디렉션( '>' )을 사용하면 된다.

  java SearchFromEmpas 자바캔

실제로 메타 검색 엔진의 구현은 SearchFromEmpas 클래스와 비슷한 형태로부터 시작된다.

HttpMessage 클래스를 사용할 때의 주의점

한국에 있는 자바 개발자들이 자바를 사용하여 어플리케이션을 개발하면서 겪게 되는 고통중의 하나를 꼽으라면 바로 한글 문제이다. 하지만 한글 문제는 자바에서 사용하는 유니코드와 각 규약에 정의되어 있는 내용을 이해하지 못한 되서 비롯되는 경우가 많으며 올바르게 캐릭터셋에 대한 내용을 이해하고 있다면 매우 간단하게 해결할 수 있는 부분이다.

HttpMessage 클래스 역시 아무 생각없이 사용한다면 한글 문제를 발생시키는 범인이 될 수도 있다. HttpMessage 클래스는 자바 어플리케이션에서 사용될 수도 있겠지만 웹 기반의 통신의 경우에는 서블릿이나 JSP 내에서 사용될 수도 있을 것이다. 이 경우 HttpMessage 클래스에 전달되는 Properties의 값들은 JSP나 서블릿의 HttpServletRequest 객체로부터 읽어온 파라미터가 될 수도 있다.

예를 들어, 다음과 같이 JSP 페이지에서 HttpMessage 클래스를 사용한다고 해 보자.

  <%@ page import = "javacan.http.util.HttpMessage" %>
  <%@ page import = "java.io.*" %>
  <%
     URL url = new URL("http://some.host.com");
     HttpMessage httpMessage = new HttpMessage(url);
     Propertie param = new Properties();
     param.setProperty("name", request.getParameter("name"));
     param.setProperty("address", request.getParameter("address"));
     
     InputStream is = httpMessage.sendGetMessage(param);
     // is 으로부터 값을 읽어와 알맞은 처리를 한다.
     ...
  %>
  ...

별다른 문제가 없어 보이지만, 서블릿/JSP 규약에 따라 사용자의 요청 데이터는 자동적으로 iso-8859-1 캐릭터 셋으로 읽허진다. 즉, request.getParameter() 메소드의 결과값은 iso-8859-1 캐릭터 셋으로 되어 있으며, 따라서 httpMessage 클래스의 sendGetMessage() 메소드는 euc-kr이 아닌 iso-8859-1을 전송하게 되며 따라서 HttpMessage가 연결하는 URL은 깨진 글자를 파라미터로 전달받게 된다.

서블릿에서도 이와 같은 문제가 발생한다. 이 문제의 해결방법은 의외로 간단한다. JSP나 서블릿에서 오라클을 사용할 때와 비슷하게 request.getParameter() 메소드를 사용하여 읽어온 값의 캐릭터 셋을 변환해주는 것이다. 이때 변환은 iso-8859-1 캐릭터 셋에서 euc-kr 로 일어난다. 반대로 HttpMessage 로부터 읽어온 값을 JSP 페이지나 서블릿을 통해서 웹브라우저에 출력할 때에도 캐릭터 셋과 관련된 문제가 발생할 것이다. 이 경우에는 앞에서 살펴본 것과 반대로 캐릭터 셋을 변환해주면 될 것이다.

결론

Part 1에서는 웹기반의 통신(웹-투-웹 통신) 기능을 제공하는 HttpMessage 클래스를 작성해보았다. 이 클래스를 통해서 우리는 GET 방식과 POST 방식을 사용하여 웹 서버에 데이터를 전송할 수 있게 되었으며, 또한 서버로부터 데이터를 전송받을 수 있게 되었다. 다음 Part 2에서는 실제로 웹-투-웹 프로그래밍을 응용하여 XML 기반의 데이터를 전송받는 것에 대해서 알아보도록 하자.

Posted by 최범균 madvirus

댓글을 달아 주세요

  1. 자바초보 2013.07.15 19:10 신고  댓글주소  수정/삭제  댓글쓰기

    님완전 멋져요 잘봤어여