주요글: 도커 시작하기
반응형
웹을 기반으로 정보를 주고 받는 웹-투-웹 프로그래밍에 대해서 살펴본다.

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 기반의 데이터를 전송받는 것에 대해서 알아보도록 하자.

+ Recent posts