CookieHandler를 이용한 쿠키 관리



자바 플랫폼의 경우, URL을 통한 오브젝트 액세스는 일련의 프로토콜 핸들러에 의해 관리된다. URL의 첫 부분은 사용되는 프로토콜을 알려주는데, 예를 들어 URL이 file:로 시작되면 로컬 파일 시스템 상에서 리소스를 액세스할 수 있다. 또, URL이 http:로 시작되면 인터넷을 통해 리소스 액세스가 이루어진다. 한편, J2SE 5.0은 시스템 내에 반드시 존재해야 하는 프로토콜 핸들러(http, https, file, ftp, jar 등)를 정의한다.

J2SE 5.0은 http 프로토콜 핸들러 구현의 일부로 CookieHandler를 추가하는데, 이 클래스는 쿠키를 통해 시스템 내에서 상태(state)가 어떻게 관리될 수 있는지를 보여준다. 쿠키는 브라우저의 캐시에 저장된 데이터의 단편이며, 한번 방문한 웹 사이트를 다시 방문할 경우 쿠키 데이터를 이용하여 재방문자임을 식별한다. 쿠키는 가령 온라인 쇼핑 카트 같은 상태 정보를 기억할 수 있게 해준다. 쿠키에는 브라우저를 종료할 때까지 단일 웹 세션 동안 데이터를 보유하는 단기 쿠키와 1주 또는 1년 동안 데이터를 보유하는 장기 쿠키가 있다.

J2SE 5.0에서 기본값으로 설치되는 핸들러는 없으나, 핸들러를 등록하여 애플리케이션이 쿠키를 기억했다가 http 접속 시에 이를 반송하도록 할 수는 있다.

CookieHandler 클래스는 두 쌍의 관련 메소드를 가지는 추상 클래스이다. 첫 번째 쌍의 메소드는 현재 설치된 핸들러를 찾아내고 각자의 핸들러를 설치할 수 있게 한다.

  • getDefault()
  • setDefault(CookieHandler)

보안 매니저가 설치된 애플리케이션의 경우, 핸들러를 얻고 이를 설정하려면 특별 허가를 받아야 한다. 현재의 핸들러를 제거하려면 핸들러로 null을 입력한다. 또한 앞서 얘기했듯이 기본값으로 설정되어 있는 핸들러는 없다.

두 번째 쌍의 메소드는 각자가 관리하는 쿠키 캐시로부터 쿠키를 얻고 이를 설정할 수 있게 한다.

  • get(URI uri, Map<String, List<String>> requestHeaders)
  • put(URI uri, Map<String, List<String>> responseHeaders)

get() 메소드는 캐시에서 저장된 쿠기를 검색하여 requestHeaders를 추가하고, put() 메소드는 응답 헤더에서 쿠키를 찾아내어 캐시에 저장한다.

여기서 보듯이 핸들러를 작성하는 일은 실제로는 간단하다. 그러나 캐시를 정의하는 데는 약간의 추가 작업이 더 필요하다. 일례로, 커스텀 CookieHandler, 쿠키 캐시, 테스트 프로그램을 사용해 보기로 하자. 테스트 프로그램은 아래와 같은 형태를 띠고 있다.


  1.    import java.io.*;
  2.    import java.net.*;
  3.    import java.util.*;
  4.  
  5.    public class Fetch {
  6.      public static void main(String args[]) throws Exception {
  7.        if (args.length == 0) {
  8.          System.err.println("URL missing");
  9.          System.exit(-1);
  10.        }
  11.        String urlString = args[0];
  12.        CookieHandler.setDefault(new ListCookieHandler());
  13.        URL url = new URL(urlString);
  14.        URLConnection connection = url.openConnection();
  15.        Object obj = connection.getContent();
  16.        url = new URL(urlString);
  17.        connection = url.openConnection();
  18.        obj = connection.getContent();
  19.      }
  20.    }


먼저 이 프로그램은 간략하게 정의될 ListCookieHandler를 작성하고 설치한다. 그런 다음 URL(명령어 라인에서 입력)의 접속을 열어 내용을 읽는다. 이어서 프로그램은 또 다른 URL의 접속을 열고 동일한 내용을 읽는다. 첫 번째 내용을 읽을 때 응답에는 저장될 쿠키가, 두 번째 요청에는 앞서 저장된 쿠키가 포함된다.

이제 이것을 관리하는 방법에 대해 알아보기로 하자. 처음에는 URLConnection 클래스를 이용한다. 웹 상의 리소스는 URL을 통해 액세스할 수 있으며, URL 작성 후에는 URLConnection 클래스의 도움을 받아 사이트와의 통신을 위한 인풋 또는 아웃풋 스트림을 얻을 수 있다.


  1.    String urlString = ...;
  2.    URL url = new URL(urlString);
  3.    URLConnection connection = url.openConnection();
  4.    InputStream is = connection.getInputStream();
  5.    // .. read content from stream


접속으로부터 이용 가능한 정보에는 일련의 헤더들이 포함될 수 있는데, 이는 사용중인 프로토콜에 의해 결정된다. 헤더를 찾으려면 URLConnection 클래스를 사용하면 된다. 한편, 클래스는 헤더 정보 검색을 위한 다양한 메소드를 가지는데, 여기에는 다음 사항들이 포함된다.

  • getHeaderFields() - 가용한 필드의 Map을 얻는다.
  • getHeaderField(String name) - 이름 별로 헤더 필드를 얻는다.
  • getHeaderFieldDate(String name, long default) - 날짜로 된 헤더 필드를 얻는다.
  • getHeaderFieldInt(String name, int default) - 숫자로 된 헤더 필드를 얻는다.
  • getHeaderFieldKey(int n) or getHeaderField(int n) - 위치 별로 헤더 필드를 얻는다.

일례로, 다음 프로그램은 주어진 URL의 모든 헤더를 열거한다


  1.    import java.net.*;
  2.    import java.util.*;
  3.  
  4.    public class ListHeaders {
  5.      public static void main(String args[]) throws Exception {
  6.        if (args.length == 0) {
  7.          System.err.println("URL missing");
  8.        }
  9.        String urlString = args[0];
  10.        URL url = new URL(urlString);
  11.        URLConnection connection = url.openConnection();
  12.        Map<String,List<String>> headerFields =
  13.          connection.getHeaderFields();
  14.        Set<String> set = headerFields.keySet();
  15.        Iterator<String> itor = set.iterator();
  16.        while (itor.hasNext()) {
  17.          String key = itor.next();
  18.          System.out.println("Key: " + key + " / " +
  19.            headerFields.get(key));
  20.        }
  21.      }
  22.    }


ListHeaders 프로그램은 가령 http://java.sun.com 같은 URL을 아규먼트로 취하고 사이트로부터 수신한 모든 헤더를 표시한다. 각 헤더는 아래의 형태로 표시된다.

   Key: <key> / [<value>]

따라서 다음을 입력하면,

  >> java ListHeaders http://java.sun.com

다음과 유사한 내용이 표시되어야 한다.

   Key: Set-Cookie / [SUN_ID=192.168.0.1:269421125489956; 
   EXPIRES=Wednesday, 31- Dec-2025 23:59:59 GMT; 
   DOMAIN=.sun.com; PATH=/]
   Key: Set-cookie / 
   [JSESSIONID=688047FA45065E07D8792CF650B8F0EA;Path=/]
   Key: null / [HTTP/1.1 200 OK]
   Key: Transfer-encoding / [chunked]
   Key: Date / [Wed, 31 Aug 2005 12:05:56 GMT]
   Key: Server / [Sun-ONE-Web-Server/6.1]
   Key: Content-type / [text/html;charset=ISO-8859-1]   

(위에 표시된 결과에서 긴 행은 수동으로 줄바꿈한 것임)

이는 해당 URL에 대한 헤더들만을 표시하며, 그곳에 위치한 HTML 페이지는 표시하지 않는다. 표시되는 정보에는 사이트에서 사용하는 웹 서버와 로컬 시스템의 날짜 및 시간이 포함되는 사실에 유의할 것. 아울러 2개의 ‘Set-Cookie’ 행에도 유의해야 한다. 이들은 쿠키와 관련된 헤더들이며, 쿠키는 헤더로부터 저장된 뒤 다음의 요청과 함께 전송될 수 있다.

이제 CookieHandler를 작성해 보자. 이를 위해서는 두 추상 메소드 CookieHandler: get() 과ㅓ put()을 구현해야 한다.

  •   public void put(
        URI uri,
        Map<String, List<String>> responseHeaders)
          throws IOException
    
  •   public Map<String, List<String>> get(
        URI uri,
        Map<String, List<String>> requestHeaders)
          throws IOException
    

우선 put() 메소드로 시작한다. 이 경우 응답 헤더에 포함된 모든 쿠키가 캐시에 저장된다.put()을 구현하기 위해서는 먼저 ‘Set-Cookie’ 헤더의 List를 얻어야한다. 이는 Set-cookieSet-Cookie2 같은 다른 해당 헤더로 확장될 수 있다.

   List<String> setCookieList =
     responseHeaders.get("Set-Cookie");


쿠키의 리스트를 확보한 후 각 쿠키를 반복(loop)하고 저장한다. 쿠키가 이미 존재할 경우에는 기존의 것을 교체하도록 한다.


  1.     if (setCookieList != null) {
  2.       for (String item : setCookieList) {
  3.         Cookie cookie = new Cookie(uri, item);
  4.         // Remove cookie if it already exists in cache
  5.         // New one will replace it
  6.         for (Cookie existingCookie : cache) {
  7.           ...
  8.         }
  9.         System.out.println("Adding to cache: " + cookie);
  10.         cache.add(cookie);
  11.       }
  12.     }


여기서 ‘캐시’는 데이터베이스에서 Collections Framework에서 List에 이르기까지 어떤 것이든 될 수 있다. Cookie 클래스는 나중에 정의되는데, 이는 사전 정의되는 클래스에 속하지 않는다.

본질적으로, 그것이 put() 메소드에 대해 주어진 전부이며, 응답 헤더 내의 각 쿠키에 대해 메소드는 쿠키를 캐시에 저장한다.

get() 메소드는 정반대로 작동한다. URI에 해당되는 캐시 내의 각 쿠키에 대해, get() 메소드는 이를 요청 헤더에 추가한다. 복수의 쿠키에 대해서는 콤마로 구분된(comma-delimited) 리스트를 작성한다. get() 메소드는 맵을 반환하며, 따라서 메소드는 기존의 헤더 세트로 Map 아규먼트를 취하게 된다. 그 아규먼트에 캐시 내의 해당 쿠키를 추가해야 하지만 아규먼트는 불변의 맵이며, 또 다른 불변의 맵을 반환해야만 한다. 따라서 기존의 맵을 유효한 카피에 복사한 다음 추가를 마친 후 불변의 맵을 반환해야 한다.

get() 메소드를 구현하기 위해서는 먼저 캐시를 살펴보고 일치하는 쿠키를 얻은 다음 만료된 쿠키를 모두 제거하도록 한다.


  1.     // Retrieve all the cookies for matching URI
  2.     // Put in comma-separated list
  3.     StringBuilder cookies = new StringBuilder();
  4.     for (Cookie cookie : cache) {
  5.       // Remove cookies that have expired
  6.       if (cookie.hasExpired()) {
  7.         cache.remove(cookie);
  8.       } else if (cookie.matches(uri)) {
  9.         if (cookies.length() > 0) {
  10.           cookies.append(", ");
  11.         }
  12.         cookies.append(cookie.toString());
  13.       }
  14.     }


이 경우에도 Cookie 클래스는 간략하게 정의되는데, 여기에는 hasExpired()matches() 등 2개의 요청된 메소드가 표시되어 있다. hasExpired() 메소드는 특정 쿠키의 만료 여부를 보고하고, matches() 메소드는 쿠키가 메소드에 패스된 URI에 적합한지 여부를 보고한다.

get() 메소드의 다음 부분은 작성된 StringBuilder 오브젝트를 취하고 그 스트링필드 버전을 수정 불가능한 Map에 put한다(이 경우에는 해당 키 ‘Cookie’를 이용).


  1.     // Map to return
  2.     Map<String, List<String>> cookieMap =
  3.       new HashMap<String, List<String>>(requestHeaders);
  4.  
  5.     // Convert StringBuilder to List, store in map
  6.     if (cookies.length() > 0) {
  7.       List<String> list =
  8.         Collections.singletonList(cookies.toString());
  9.       cookieMap.put("Cookie", list);
  10.     }
  11.     return Collections.unmodifiableMap(cookieMap);



다음은 런타임의 정보 표시를 위해 println이 일부 추가되어 완성된 CookieHandler 정의이다.


  1.    import java.io.*;
  2.    import java.net.*;
  3.    import java.util.*;
  4.  
  5.    public class ListCookieHandler extends CookieHandler {
  6.  
  7.      // "Long" term storage for cookies, not serialized so only
  8.      // for current JVM instance
  9.      private List<Cookie> cache = new LinkedList<Cookie>();
  10.  
  11.      /**
  12.       * Saves all applicable cookies present in the response
  13.       * headers into cache.
  14.       * @param uri URI source of cookies
  15.       * @param responseHeaders Immutable map from field names to
  16.       * lists of field
  17.       *   values representing the response header fields returned
  18.       */
  19.  
  20.      public void put(
  21.          URI uri,
  22.          Map<String, List<String>> responseHeaders)
  23.            throws IOException {
  24.  
  25.        System.out.println("Cache: " + cache);
  26.        List<String> setCookieList =
  27.          responseHeaders.get("Set-Cookie");
  28.        if (setCookieList != null) {
  29.          for (String item : setCookieList) {
  30.            Cookie cookie = new Cookie(uri, item);
  31.            // Remove cookie if it already exists
  32.            // New one will replace
  33.            for (Cookie existingCookie : cache) {
  34.              if((cookie.getURI().equals(
  35.                existingCookie.getURI())) &&
  36.                 (cookie.getName().equals(
  37.                   existingCookie.getName()))) {
  38.               cache.remove(existingCookie);
  39.               break;
  40.             }
  41.           }
  42.           System.out.println("Adding to cache: " + cookie);
  43.           cache.add(cookie);
  44.         }
  45.       }
  46.     }
  47.  
  48.     /**
  49.      * Gets all the applicable cookies from a cookie cache for
  50.      * the specified uri in the request header.
  51.      *
  52.      * @param uri URI to send cookies to in a request
  53.      * @param requestHeaders Map from request header field names
  54.      * to lists of field values representing the current request
  55.      * headers
  56.      * @return Immutable map, with field name "Cookie" to a list
  57.      * of cookies
  58.      */
  59.  
  60.     public Map<String, List<String>> get(
  61.         URI uri,
  62.         Map<String, List<String>> requestHeaders)
  63.           throws IOException {
  64.  
  65.       // Retrieve all the cookies for matching URI
  66.       // Put in comma-separated list
  67.       StringBuilder cookies = new StringBuilder();
  68.       for (Cookie cookie : cache) {
  69.         // Remove cookies that have expired
  70.         if (cookie.hasExpired()) {
  71.           cache.remove(cookie);
  72.         } else if (cookie.matches(uri)) {
  73.           if (cookies.length() > 0) {
  74.             cookies.append(", ");
  75.           }
  76.           cookies.append(cookie.toString());
  77.         }
  78.       }
  79.  
  80.       // Map to return
  81.       Map<String, List<String>> cookieMap =
  82.         new HashMap<String, List<String>>(requestHeaders);
  83.  
  84.       // Convert StringBuilder to List, store in map
  85.       if (cookies.length() > 0) {
  86.         List<String> list =
  87.           Collections.singletonList(cookies.toString());
  88.         cookieMap.put("Cookie", list);
  89.       }
  90.         System.out.println("Cookies: " + cookieMap);
  91.     return Collections.unmodifiableMap(cookieMap);
  92.     }
  93.   }



퍼즐의 마지막 조각은 Cookie 클래스 그 자체이며, 대부분의 정보는 생성자(constructor) 내에 존재한다. 생성자 내의 정보 조각(비트)들을 uri 및 헤더 필드로부터 파싱해야 한다. 만료일에는 하나의 포맷이 사용되어야 하지만 인기 있는 웹 사이트에서는 복수의 포맷이 사용되는 경우를 볼 수 있다. 여기서는 그다지 까다로운 점은 없고, 쿠키 경로, 만료일, 도메인 등과 같은 다양한 정보 조각을 저장하기만 하면 된다.


  1.    public Cookie(URI uri, String header) {
  2.      String attributes[] = header.split(";");
  3.      String nameValue = attributes[0].trim();
  4.      this.uri = uri;
  5.      this.name = nameValue.substring(0, nameValue.indexOf('='));
  6.      this.value = nameValue.substring(nameValue.indexOf('=')+1);
  7.      this.path = "/";
  8.      this.domain = uri.getHost();
  9.  
  10.      for (int i=1; i < attributes.length; i++) {
  11.        nameValue = attributes[i].trim();
  12.        int equals = nameValue.indexOf('=');
  13.        if (equals == -1) {
  14.          continue;
  15.        }
  16.        String name = nameValue.substring(0, equals);
  17.        String value = nameValue.substring(equals+1);
  18.        if (name.equalsIgnoreCase("domain")) {
  19.          String uriDomain = uri.getHost();
  20.          if (uriDomain.equals(value)) {
  21.            this.domain = value;
  22.          } else {
  23.            if (!value.startsWith(".")) {
  24.              value = "." + value;
  25.            }
  26.            uriDomain =
  27.              uriDomain.substring(uriDomain.indexOf('.'));
  28.            if (!uriDomain.equals(value)) {
  29.              throw new IllegalArgumentException(
  30.                "Trying to set foreign cookie");
  31.            }
  32.            this.domain = value;
  33.          }
  34.        } else if (name.equalsIgnoreCase("path")) {
  35.          this.path = value;
  36.        } else if (name.equalsIgnoreCase("expires")) {
  37.          try {
  38.            this.expires = expiresFormat1.parse(value);
  39.          } catch (ParseException e) {
  40.            try {
  41.              this.expires = expiresFormat2.parse(value);
  42.            } catch (ParseException e2) {
  43.              throw new IllegalArgumentException(
  44.                "Bad date format in header: " + value);
  45.            }
  46.          }
  47.        }
  48.      }
  49.   }



클래스 내의 다른 메소드들은 단지 저장된 데이터를 반환하거나 만료 여부를 확인한다.

  1.    public boolean hasExpired() {
  2.      if (expires == null) {
  3.        return false;
  4.      }
  5.      Date now = new Date();
  6.      return now.after(expires);
  7.    }
  8.  
  9.    public String toString() {
  10.      StringBuilder result = new StringBuilder(name);
  11.      result.append("=");
  12.      result.append(value);
  13.      return result.toString();
  14.    }



쿠키가 만료된 경우에는 ‘match’가 표시되면 안 된다.

  1.    public boolean matches(URI uri) {
  2.  
  3.      if (hasExpired()) {
  4.        return false;
  5.      }
  6.  
  7.      String path = uri.getPath();
  8.      if (path == null) {
  9.        path = "/";
  10.      }
  11.  
  12.      return path.startsWith(this.path);
  13.    }



Cookie
스펙이 도메인과 경로 양쪽에 대해 매치를 수행할 것을 요구한다는 점에 유의해야 한다. 단순성을 위해 여기서는 경로 매치만을 확인한다.

아래는 전체 Cookie 클래스의 정의이다.

  1.    import java.net.*;
  2.    import java.text.*;
  3.    import java.util.*;
  4.  
  5.    public class Cookie {
  6.  
  7.      String name;
  8.      String value;
  9.      URI uri;
  10.      String domain;
  11.      Date expires;
  12.      String path;
  13.  
  14.      private static DateFormat expiresFormat1
  15.          = new SimpleDateFormat("E, dd MMM yyyy k:m:s 'GMT'", Locale.US);
  16.  
  17.      private static DateFormat expiresFormat2
  18.         = new SimpleDateFormat("E, dd-MMM-yyyy k:m:s 'GMT'", Local.US);
  19.        
  20.  
  21.      /**
  22.       * Construct a cookie from the URI and header fields
  23.       *
  24.       * @param uri URI for cookie
  25.       * @param header Set of attributes in header
  26.       */
  27.      public Cookie(URI uri, String header) {
  28.        String attributes[] = header.split(";");
  29.        String nameValue = attributes[0].trim();
  30.        this.uri = uri;
  31.        this.name =
  32.          nameValue.substring(0, nameValue.indexOf('='));
  33.        this.value =
  34.          nameValue.substring(nameValue.indexOf('=')+1);
  35.        this.path = "/";
  36.        this.domain = uri.getHost();
  37.  
  38.        for (int i=1; i < attributes.length; i++) {
  39.          nameValue = attributes[i].trim();
  40.          int equals = nameValue.indexOf('=');
  41.          if (equals == -1) {
  42.            continue;
  43.          }
  44.          String name = nameValue.substring(0, equals);
  45.          String value = nameValue.substring(equals+1);
  46.          if (name.equalsIgnoreCase("domain")) {
  47.            String uriDomain = uri.getHost();
  48.            if (uriDomain.equals(value)) {
  49.              this.domain = value;
  50.            } else {
  51.              if (!value.startsWith(".")) {
  52.                value = "." + value;
  53.              }
  54.              uriDomain = uriDomain.substring(
  55.                uriDomain.indexOf('.'));
  56.              if (!uriDomain.equals(value)) {
  57.                throw new IllegalArgumentException(
  58.                  "Trying to set foreign cookie");
  59.              }
  60.              this.domain = value;
  61.            }
  62.          } else if (name.equalsIgnoreCase("path")) {
  63.            this.path = value;
  64.          } else if (name.equalsIgnoreCase("expires")) {
  65.            try {
  66.              this.expires = expiresFormat1.parse(value);
  67.            } catch (ParseException e) {
  68.              try {
  69.                this.expires = expiresFormat2.parse(value);
  70.              } catch (ParseException e2) {
  71.                throw new IllegalArgumentException(
  72.                  "Bad date format in header: " + value);
  73.              }
  74.            }
  75.          }
  76.        }
  77.      }
  78.  
  79.      public boolean hasExpired() {
  80.        if (expires == null) {
  81.          return false;
  82.        }
  83.        Date now = new Date();
  84.        return now.after(expires);
  85.      }
  86.  
  87.      public String getName() {
  88.        return name;
  89.      }
  90.  
  91.      public URI getURI() {
  92.        return uri;
  93.      }
  94.  
  95.      /**
  96.       * Check if cookie isn't expired and if URI matches,
  97.       * should cookie be included in response.
  98.       *
  99.       * @param uri URI to check against
  100.       * @return true if match, false otherwise
  101.       */
  102.      public boolean matches(URI uri) {
  103.  
  104.        if (hasExpired()) {
  105.          return false;
  106.        }
  107.  
  108.       String path = uri.getPath();
  109.        if (path == null) {
  110.          path = "/";
  111.        }
  112.  
  113.        return path.startsWith(this.path);
  114.      }
  115.  
  116.      public String toString() {
  117.        StringBuilder result = new StringBuilder(name);
  118.        result.append("=");
  119.        result.append(value);
  120.        return result.toString();
  121.      }
  122.    }



이제 조각들이 모두 확보되었으므로 앞의 Fetch 예제를 실행할 수 있다.

   >> java Fetch http://java.sun.com

   Cookies: {Connection=[keep-alive], Host=[java.sun.com], 
    User-Agent=[Java/1.5.0_04], GET / HTTP/1.1=[null], 
    Content-type=[application/x-www-form-urlencoded], 
    Accept=[text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2]}
   Cache: []
   Adding to cache: SUN_ID=192.168.0.1:235411125667328
   Cookies: {Connection=[keep-alive], Host=[java.sun.com], 
    User-Agent=[Java/1.5.0_04], GET / HTTP/1.1=[null], 
    Cookie=[SUN_ID=192.168.0.1:235411125667328], 
    Content-type=[application/x-www-form-urlencoded], 
    Accept=[text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2]}
   Cache: [SUN_ID=192.168.0.1:235411125667328]

(위에 표시된 결과에서 긴 행은 수동으로 줄바꿈한 것임)

‘Cache’로 시작되는 행은 저장된 캐시를 나타낸다. 저장된 쿠키가 즉시 반환되지 않도록 put() 메소드 전에 get() 메소드가 어떻게 호출되는지에 대해 유의하도록 할 것.

쿠키와 URL 접속을 이용한 작업에 관해 자세히 알고 싶으면 자바 튜토리얼의 Custom Networking trail(영문)을 참조할 것. 이는 J2SE 1.4에 기반을 두고 있으므로 튜토리얼에는 아직 여기서 설명한 CookieHandler에 관한 정보가 실려 있지 않다. Java SE 6 ("Mustang")(영문) 릴리즈에서도 기본 CookieHandler 구현에 관한 내용을 찾아볼 수 있다.

2007/07/03 18:41 2007/07/03 18:41
Trackback Address:이 글에는 트랙백을 보낼 수 없습니다

Redirection When Session Times Out :: 서블릿 필터를 이용한 리다이렉션 코드

세션 타임아웃이 일어날경우 특정 페이지로 리다이렉션을 수행하는 필터클래스 코드

  1. public class SessionCheckFilter implements Filter {
  2.  private static int firstRequest = 0;
  3.  public void doFilter(ServletRequest request, ServletResponse response,
  4.                     FilterChain chain) throws IOException, ServletException {
  5.       HttpServletRequest hreq = (HttpServletRequest)request;
  6.       HttpServletResponse hres = (HttpServletResponse)response;
  7.       HttpSession session = hreq.getSession();
  8.       if (session.isNew()) {
  9.           if(firstRequest == 0){
  10.                firstRequest++;
  11.           } else {
  12.                hres.sendRedirect("faces/ErrorPage.jsp");
  13.                return;
  14.           }
  15.       }
  16.       chain.doFilter(request, response);
  17.   }
  18.   public void init(FilterConfig filterConfig) throws ServletException {}
  19.   public void destroy() {}
  20. }

2007/06/30 14:58 2007/06/30 14:58
Trackback Address:이 글에는 트랙백을 보낼 수 없습니다

ZIP/JAR 엔트리의 생성 제어하기

2007/06/28 15:36

서비 JAVA , ,

JAR 관련 클래스는 ZIP 관련 클래스의 하위 클래스이므로 이 팁에서는 ZipOutputStreamjava.util.zip 패키지에 대해 구체적으로 다룬다.

ZIP 또는 JAR 파일의 생성을 살펴보기 전에 그 목적을 언급하는 것이 중요하다. ZIP 파일은 패키징 메커니즘을 제공하여 여러 파일을 하나의 파일로 묶을 수 있게 한다. 따라서 파일 그룹을 웹에서 다운로드해야 할 경우 이 파일들을 하나의 ZIP 파일로 패키지하여 단일 파일로 쉽게 전송할 수 있다. 이 패키지 파일은 디렉토리 계층구조 같은 추가 정보를 포함할 수 있으므로 압축을 풀 때 애플리케이션 또는 시리즈의 자원에 필요한 경로를 유지할 수 있다.

이 팁에서는 ZIP 파일 사용의 세 가지 측면을 다룬다. 즉, ZIP 파일 생성, ZIP 파일에 파일 추가 및 해당 추가 파일 압축에 대해 다룬다.

우선 ZIP 파일을 생성할 때는 ZIP 스트림이 생성된다. ZipOutputStream은 송신 바이트 압축을 위한 스트림을 제공한다. 다른 OutputStream을 받는 ZipOutputStream의 단일 구성자가 있다.

public ZipOutputStream(OutputStream out)

구성자 인수가 FileOutputStream 유형이면 ZipOutputStream이 작성한 압축 바이트는 파일에 저장된다. 하지만 ZipOutputStream을 파일과 관련해서만 사용할 수 있는 것은 아니다. 소켓 연결 또는 기타 일부 비파일 중심 스트림으로부터 나오는 OutputStream을 사용할 수도 있다. 따라서 파일 중심 ZipOutputStream의 경우 일반적인 사용법은 다음과 같다.

String path = "afile.zip";
FileOutputStream fos = new FileOutputStream(path);
ZipOutputStream zos = new ZipOutputStream(fos);

생성된 후에는 ZipOutputStream에 바이트를 작성하는 것으로 충분하지 않다. 대신 출력 스트림을 구성요소의 컬렉션으로 다루어야 한다. ZipOutputStream의 각 구성요소는 ZipEntry와 쌍을 이룬다. 이 ZipEntry를 생성하고 ZipOutputStream에 추가한 후에 해당 컨텐츠를 스트림에 실제로 작성해야 한다.

String name = ...;
ZipEntry entry = new ZipEntry(name);
zos.putNextEntry(entry);
zos.write(<< all the bytes for entry >>);

각 엔트리는 전체 스트림에서 마커 역할을 하며 라이브러리 파일에서 엔트리와 관련된 바이트를 찾을 수 있다. ZIP 파일이 생성된 후 엔트리 컨텐츠를 도로 가져와야 할 경우 관련 입력 스트림을 요청하기만 하면 된다.

ZipFile zip = "afile.zip";
ZipEntry entry = zip.getEntry("anEntry.name");
InputStream is = zip.getInputStream(entry);

ZIP 파일을 생성하고 해당 파일에 엔트리를 추가하는 방법을 살펴본 이 시점에서 java.util.zip 라이브러리가 ZipOutputStream의 추가 엔트리에 대해 일정 수준의 제어를 제공한다는 것을 지적할 필요가 있다. 먼저, 엔트리를 ZipOutputStream에 추가하는 순서는 엔트리가 .zip 파일에 실제로 위치하는 순서이다. ZipFileentries() 메소드에 의해 반환된 엔트리의 열거를 조작하여 영문자 또는 크기 순서대로 목록을 생성할 수도 있지만 엔트리는 출력 스트림에 작성된 순서대로 저장되어 있다.

ZIP/JAR 파일에 추가된 파일은 개별적으로 압축된다. 라이브러리 패키지를 전체적으로 압축하는 Microsoft CAB 파일과 달리, ZIP/JAR 파일 내의 파일들은 각각 별도로 압축되거나 압축되지 않는다. ZipOutputStreamZipEntry를 추가하기 전에 해당 연관 바이트의 압축 여부를 결정해야 한다. ZipEntrysetMethod 메소드를 사용하면 두 가지 사용 가능 압축 형식 중에서 어떤 형식을 사용할지 지정할 수 있다. 압축되지 않은 파일을 원하면 ZipEntry의 STORED 상수를 사용하고, 압축된 버전을 원하면 DEFLATED 설정을 사용한다. 압축 효율성은 제어할 수 없다. 이는 연관 엔트리의 데이터 유형에 좌우된다. 일반 텍스트는 원래 크기의 80% 정도까지 쉽게 압축할 수 있지만 MP3 또는 JPEG 데이터의 압축률은 이보다 현저히 떨어진다.

모든 파일을 압축하는 게 당연하다고 생각할 수도 있지만 파일을 압축하고 푸는 데에는 시간이 소요된다. 생성 시점에서 압축 작업에 너무 많은 시간과 자원이 소요될 경우 전체 파일 데이터를 STORED 형식으로 저장하여 원시 바이트를 저장하는 것이 나은 경우가 있다. 압축 해제의 경우에도 마찬가지이다. 물론, 압축되지 않은 파일은 용량이 더 크므로 파일 전송 시 더 많은 대역폭을 사용하고 더 많은 디스크 공간을 차지하므로 이에 따른 비용을 지불해야 한다. ZipFile 전체가 아니라 각 엔트리에 대한 설정을 변경해야 한다는 것을 유념한다. 하지만 각 엔트리에 대해 각기 다른 설정을 사용하는 것보다 전체 ZipFile을 압축 또는 비압축하는 것이 보다 일반적이다.

압축 방법으로 STORED 상수를 사용할 경우 알아 두어야 할 한 가지 사항이 있다. 엔트리가 압축될 때 자동으로 설정되는 ZipEntry의 특정 속성을 명시적으로 설정해야 한다. 해당 속성은 엔트리의 입력 스트림에 대한 체크섬, 압축 크기 및 크기이다. 입력 파일인 경우 크기 및 압축 크기는 바로 파일 크기가 될 수 있다. 체크섬을 계산하려면 ava.util.zip 패키지의 CRC32 클래스를 사용한다. checksum 값을 무시하기 위해 단순히 0 또는 -1을 전달할 수는 없다. ZIP 파일을 작성하고 나중에 해당 파일을 읽을 때 입력사항을 검증하기 위해 CRC 값이 사용된다.

ZipEntry entry = new ZipEntry(name);
entry.setMethod(ZipEntry.STORED);
entry.setCompressedSize(file.length());
entry.setSize(file.length());
CRC32 crc = new CRC32();
crc.update(<< all the bytes for entry >>);
entry.setCrc(crc.getValue());
zos.putNextEntry(entry);

예 를 들어, 다음 프로그램은 STORED 압축 메소드를 사용하여 일련의 파일을 결합한다. 프로그램의 첫 번째 인수는 생성할 ZIP 파일이 된다. 나머지 인수는 추가할 파일을 나타낸다. 생성할 ZIP 파일이 이미 존재하면 프로그램은 파일을 수정하지 않고 종료된다. 존재하지 않는 파일을 ZIP 파일에 추가하는 경우 프로그램은 존재하지 않는 파일을 건너뛰고 나머지 명령줄 인수를 생성된 ZIP에 추가한다.

  1. import java.util.zip.*;
  2. import java.io.*;
  3. public class ZipIt {
  4.     public static void main(String args[]) throws IOException {
  5.         if (args.length < 2) {
  6.             System.err.println("usage: java ZipIt Zip.zip file1 file2 file3");
  7.             System.exit(-1);
  8.         }
  9.         File zipFile = new File(args[0]);
  10.         if (zipFile.exists()) {
  11.             System.err.println("Zip file already exists, please try another");
  12.             System.exit(-2);
  13.         }
  14.         FileOutputStream fos = new FileOutputStream(zipFile);
  15.         ZipOutputStream zos = new ZipOutputStream(fos);
  16.         int bytesRead;
  17.         byte[] buffer = new byte[1024];
  18.         CRC32 crc = new CRC32();
  19.         for (int i=1, n=args.length; i < n; i++) {
  20.             String name = args[i];
  21.             File file = new File(name);
  22.             if (!file.exists()) {
  23.                 System.err.println("Skipping: " + name);
  24.                 continue;
  25.             }
  26.             BufferedInputStream bis = new BufferedInputStream(
  27.                 new FileInputStream(file));
  28.             crc.reset();
  29.             while ((bytesRead = bis.read(buffer)) != -1) {
  30.                 crc.update(buffer, 0, bytesRead);
  31.             }
  32.             bis.close();
  33.             // Reset to beginning of input stream
  34.             bis = new BufferedInputStream(
  35.                 new FileInputStream(file));
  36.             ZipEntry entry = new ZipEntry(name);
  37.             entry.setMethod(ZipEntry.STORED);
  38.             entry.setCompressedSize(file.length());
  39.             entry.setSize(file.length());
  40.             entry.setCrc(crc.getValue());
  41.             zos.putNextEntry(entry);
  42.             while ((bytesRead = bis.read(buffer)) != -1) {
  43.                 zos.write(buffer, 0, bytesRead);
  44.             }
  45.             bis.close();
  46.         }
  47.         zos.close();
  48.     }
  49. }


JAR 파일의 봉합 및 버전 지정을 비롯한 자세한 내용을 보려면 The Java Tutorial의 JAR Files 단원에서 Packing Programs를 참조한다.

원문 출처 : http://www.sdnkorea.com/blog/398

2007/06/28 15:36 2007/06/28 15:36
Trackback Address:이 글에는 트랙백을 보낼 수 없습니다

BEA WebLogic Documentation :: 웹로직 설정문서

웹로직 설정 온라인 참고 문서

BEA WebLogic Server and WebLogic Express 8.1 Documentation
http://edocs.bea.com/wls/docs81/

BEA WebLogic Server and WebLogic Express 6.1 Documentation
http://edocs.bea.com/wls/docs61/

2007/06/28 03:21 2007/06/28 03:21
Trackback Address:이 글에는 트랙백을 보낼 수 없습니다

JMS ( Java Messaging System ) :: 자바 메세징 시스템

2007/06/27 22:22

서비 JAVA ,

BEA 웹로직에서 JMS 서비스 구현에관한 Dev2Dev 링크:
http://www.dev2dev.co.kr/devcenters/category.jsp?category=jms

BMT Message-Driven Bean 과 Spring을 이용한 고성능 메세지 프로세싱
http://www.dev2dev.co.kr/pub/a/2006/01/custom-mdb-processing.jsp

WebLogic JMS 프로그래밍 하기
http://edocs.bea.com/wls/docs81/jms/index.html (weblogic 8.1 )
http://edocs.bea.com/wls/docs61/jms/index.html (weblogic 6.1 )

BEA 웹로직8.1 JMS설정하기 :: JMS and WebLogic Server
http://edocs.bea.com/wls/docs81/ConsoleHelp/jms_config.html (weblogic 8.1 )
http://edocs.bea.com/wls/docs61/adminguide/jms.html (weblogic 6.1 )

2007/06/27 22:22 2007/06/27 22:22
Trackback Address:이 글에는 트랙백을 보낼 수 없습니다

JAVA.UTIL.SCANNER로 텍스트 스캔하기

JAVA.UTIL.SCANNER로 텍스트 스캔하기

J2SE 5.0에는 일상적 태스크를 좀 더 쉽게 구현할 수 있도록 하는 클래스와 메소드들이 추가되었다. 이번 팁에서는 새로 추가된 java.util.Scanner클래스를 이용함으로써 일반 표현문을 사용하는 스트링과 프리미티브 타입을 읽고 파싱(parsing)하는 것이 어떻게 좀 더 쉬워졌는지 알아보도록 하자.

J2SE 5.0의 출시 이전에는 파일에서 텍스트를 읽으려면 다음의 TextReader 클래스 같은 코드를 작성해야했다.


  1. import java.io.BufferedReader;
  2. import java.io.FileReader;
  3. import java.io.IOException;
  4. import java.io.File;
  5.  
  6.  public class TextReader {
  7.   private static void readFile(String fileName) {
  8.     try {
  9.       File file = new File(fileName);
  10.       FileReader reader = new FileReader(file);
  11.       BufferedReader in = new BufferedReader(reader);
  12.       String string;
  13.       while ((string = in.readLine()) != null) {
  14.         System.out.println(string);
  15.       }
  16.       in.close();
  17.     } catch (IOException e) {
  18.       e.printStackTrace();
  19.     }
  20.   }
  21.  
  22.   public static void main(String[] args) {
  23.     if (args.length != 1) {
  24.       System.err.println("usage: java TextReader "
  25.         + "file location");
  26.       System.exit(0);
  27.     }
  28.     readFile(args[0]);
  29.   }
  30. }

이와 같은 클래스에서의 기본적인 접근법은 하드 드라이브의 실제 파일과 일치하는 File 오브젝트를 생성하는 것이다. 그리고 나서 그 파일과 관련된 FileReader와 그 FileReaderBufferedReader를 생성하고, 그 후 BufferedFile 리더를 사용하여 한번에 한 줄씩 읽는다.

실행되는 TextReader클래스를 보기위해서는 클래스에 대한 문서를 생성하여 읽고 파싱해야한다. 문서를 생성하기 위해서는 TextReader와 같은 디렉토리 안에 있는 TextSample.txt라는 파일에 다음과 같은 두 줄의 텍스트를 저장해야 한다.

   Here is a small text file that you will
   use to test java.util.scanner.

TextReader를 컴파일하고 다음을 입력하여 구동시켜보자.

   java TextReader TextSample.txt

표준 출력으로 되돌아온 원본 파일을 보게 될 것이다.

프리미티브 타입과 스트링을 파싱하는 클래스인 java.util.Scanner를 이용하여 TextReader의 코드를 간단하게 할 수 있다.


  1. import java.io.File;
  2. import java.io.FileNotFoundException;
  3. import java.util.Scanner;
  4.  
  5. public class TextScanner {
  6.  
  7.   private static void readFile(String fileName) {
  8.     try {
  9.       File file = new File(fileName);
  10.       Scanner scanner = new Scanner(file);
  11.       while (scanner.hasNext()) {
  12.         System.out.println(scanner.next());
  13.       }
  14.       scanner.close();
  15.     } catch (FileNotFoundException e) {
  16.       e.printStackTrace();
  17.     }
  18.   }
  19.  
  20.   public static void main(String[] args) {
  21.     if (args.length != 1) {
  22.       System.err.println("usage: java TextScanner1"
  23.         + "file location");
  24.       System.exit(0);
  25.     }
  26.     readFile(args[0]);
  27.   }
  28. }

TextScanner를 컴파일하고 다음과 같이 구동하자.

   java TextScanner TextSample.txt

다음과 같은 결과가 나타난다.

   Here
   is
   a
   small
   text
   file
   that
   you
   will
   use
   to
   test
   java.util.scanner.

TextScanner 는 파일로부터 Scanner 오브젝트를 생성한다. Scanner는 파일의 컨텐츠를 구획자 패턴을 이용하여 분해한다. 구획자 패턴의 디폴트 값은 흰 여백이다. 그 후 TextScannerScannerhasNext() 메소드를 호출한다. 이 메소드는 Scanner 입력값에 파일의 마지막 부분에 이를 때까지 다른 token이 있으면 'true'를 리턴한다. next() 메소드는 다음 token을 나타내는 스트링을 리턴한다. 따라서 TextScanner는 파일의 마지막부분에 이를 때까지 각 라인에서 next()에 의해 리턴되는 스트링을 프린트한다.

ScanneruseDelimiter 를 이용해 입력물을 토큰화하는 데 이용하는 구획자를 변경시킬 수도 있다. 메소드에 스트링 또는 java.util.regex.Pattern에 전달해주면 된다. 어떤 패턴들이 적절한 지에 대해서는 JavaDocs page for Pattern를 참조하기 바란다. 예를 들어 newline(\n)을 구획자로 이용하여 한번에 한 줄의 입력물을 읽을 수 있다. 다음은 새줄 문자를 구획자로 이용하는 수정된 readFile() 메소드이다.


  1.    private static void readFile(String fileName) {
  2.      try {
  3.        Scanner scanner = new Scanner(new File(fileName));
  4.        scanner.useDelimiter
  5.          (System.getProperty("line.separator"));
  6.        while (scanner.hasNext()) {
  7.          System.out.println(scanner.next());
  8.        scanner.close();
  9.      } catch (FileNotFoundException e) {
  10.        e.printStackTrace();
  11.      }
  12.    }

마지막 줄을 찾는 다른 옵션들도 있다. 예를 들어 새줄 문자로 끝나는 라인이나 캐리지 리턴(enter키)과 newline으로 끝나는 라인들을 조사할 수 있다. "\r\n|\n" 일반 표현문을 이용하여 이를 실행할 수 있다. java.util.regex.Pattern의 JavaDocs는 또다른 라인 종결기들을 보여주므로 좀 더 복잡한 분석은 "\r\n|[\r\n\u2028\u2029\u0085]"표현문을 이용한다. 또한 Scanner 클래스의 hasNextLine()nextLine() 메소드를 이용할 수 있다. 어느 경우이던 수정된 TextScanner를 사용하면 결과물은 TextSample.txt의 컨텐츠와 레이아웃에 부합될 것이다. 다음을 참고하기 바란다.

   Here is a small text file that you will
   use to test java.util.scanner.

Scanner에 의해 사용된 구획자의 패턴을 간단하게 변경하여 큰 효과와 유연성을 얻을 수 있다. 예를 들어 다음의 구획자를 지정하면,

   scanner.useDelimiter("\\z");

한번에 전체 파일을 읽는다. 이는 Pat Niemeyer가 java.net blog에서 제안하고 있는 요령과도 비슷하다. 몇 개의 중간 오브젝트를 생성하지 않고도 웹페이지의 모든 컨텐츠를 읽을 수 있는 것이다. 다음 WebPageScanner클래스의 코드는 java.net homepage의 현재 컨텐츠를 읽고 있다.


  1.    import java.net.URL;
  2.    import java.net.URLConnection;
  3.    import java.io.IOException;
  4.    import java.util.Scanner;
  5.  
  6.    public class WebPageScanner {
  7.      public static void main(String[] args) {
  8.        try {
  9.          URLConnection connection =
  10.            new URL("http://java.net").openConnection();
  11.          String text = new Scanner(
  12.            connection.getInputStream()).
  13.            useDelimiter("\\Z").next();
  14.        } catch (IOException e) {
  15.          e.printStackTrace();
  16.        }
  17.      }
  18.    }

Scanner 클래스로 스트링 이외의 것들도 다룰 수 있다. 프리미티브 타입으로 이루어진 데이터를 파싱하는 데에도 사용할 수 있다. 이에 대한 예제로, 다음의 세 라인을 Employee.data라는 이름의 파일(TextSample와 같은 디렉토리 안)에 저장하자.

   Joe, 38, true
   Kay, 27, true
   Lou, 33, false

이를 하나의 큰 스트링으로 취급하여 이 스트링을 파싱한 후에 대화문을 실행할 수도 있지만, 대신에 이 파일을 두가지 단계로 파싱해보자. 이는 다음의 클래스 DataScanner에 설명되어 있다.


  1.    import java.util.Scanner;
  2.    import java.io.File;
  3.    import java.io.FileNotFoundException;
  4.  
  5.    public class DataScanner {
  6.  
  7.      private static void readFile(String fileName) {
  8.        try {
  9.          Scanner scanner =
  10.            new Scanner(new File(fileName));
  11.          scanner.useDelimiter
  12.            (System.getProperty("line.separator"));
  13.          while (scanner.hasNext()) {
  14.            parseLine(scanner.next());
  15.          }
  16.          scanner.close();
  17.        } catch (FileNotFoundException e) {
  18.          e.printStackTrace();
  19.        }
  20.      }
  21.  
  22.      private static void parseLine(String line) {
  23.        Scanner lineScanner = new Scanner(line);
  24.       lineScanner.useDelimiter("\\s*,\\s*");
  25.        String name = lineScanner.next();
  26.        int age = lineScanner.nextInt();
  27.        boolean isCertified = lineScanner.nextBoolean();
  28.        System.out.println("It is " + isCertified +
  29.          " that " + name + ", age "
  30.          + age + ", is certified.");
  31.      }
  32.  
  33.      public static void main(String[] args) {
  34.        if (args.length != 1) {
  35.          System.err.println("usage: java TextScanner2"
  36.            + "file location");
  37.          System.exit(0);
  38.        }
  39.        readFile(args[0]);
  40.      }
  41.    }


DataScanner의 바깥쪽 Scanner 오브젝트는 한번에 한 라인씩 파일을 읽는다. readFile() 메소드는 각 라인을 두번째 스캐너에 전달하고, 이 두번째 스캐너는 콤마로 ,구획된 데이터를 파싱하고 콤마 양쪽의 흰 여백을 삭제한다. 다음 token이 특정타입의 token인지 아닌지 분석하여 다음 token을 그 타입의 인스턴스로 취급하도록 하는 hasNext()next()메소드들도 있다. 예를 들어 nextBoolean()은 다음 token을 boolean으로 취급하여 "true" 또는 "false" 스트링과 매치시킨다. 매칭이 이뤄지지 않으면 java.util.InputMismatchException이 던져진다. DataScannerparseLine() 메소드는 각 라인이 어떻게 String, int, boolean으로 파싱되는지 보여준다.

Compile DataScanner. Then run it as follows:

DataScanner를 컴파일하고 다음과 같이 구동시키자.

   java DataScanner Employee.data

다음과 같은 결과가 나타난다.

   It is true that Joe, age 38, is certified.
   It is true that Kay, age 27, is certified.
   It is false that Lou, age 33, is certified.

콤마를 구획문자로 사용하고 싶을 것이다. 다시 말해 다음과 같이 시도해보려고 할 것이다.

   lineScanner.useDelimiter(",");

이는 결국 InputMismatchException로 끝날 것이다. 이는 boolean으로 변경하려는 token에 여분의 공간이 포함되며 이 공간은 "true"나 "false" 에 매치되지 않기 때문이다. 일반적인 표현문의 모든 애플리케이션에 해당되는 케이스이므로 패턴을 구축하는 데 있어서 특히 세심한 주의가 필요할 것이다.

Scanner에 대한 좀 더 자세한 정보는 formal documentation를 참고하기 바란다.

저자 : Daniel H. Steinberg
원문 출처 : http://kr.sun.com/developers/techtips/c2004_1201.html

2007/06/24 11:28 2007/06/24 11:28
Trackback Address:이 글에는 트랙백을 보낼 수 없습니다

Vector에 대한 소고

이 기사에서는 다른 알고리즘 관련 도서와 마찬가지로 알고리즘(설계)가 중요하다고 말한다. 전적으로 동의하는 바이다. 알고리즘은 중요하다.

하지만, 전산 경력이 6년이 넘어가도록 알고리즘이 문제가 되었던 적은 알고리즘 관련 수업에서 나오는 과제와 소프트웨어 엔지니어링 수업에서 나온 이상한 과제를 할 때 뿐이었다. 그렇게 중요한 것이라면, 경력의 대부분을 차지하는 현장 경험에서는 왜 알고리즘이 중요하는 사실을 몰랐을까? 적어도 알고리즘을 사용하지 않았을리는 없고, 그렇다면 남들이 해서 숨겨 놓은 알고리즘을 나도 모르게 사용한 것이 아닐까 생각된다. 이런 일들이 가장 잘 일어나는 곳은 데이터베이스와 자바 패키지가 아닐까 생각된다. 데이터베이스는 정렬과 Search를 단 몇 단어로 가능하게 만들지만, 정렬과 Search는 알고리즘에서 가장 중요한 분야 중에 하나이며, 아직도 연구가 끝나지 않은 분야이다. 자바에서 제공하는 표준 API중에도 알고리즘을 제공하는 것이 많다.

자바 표준 API 중에서 java.util 패키지는 알고리즘과 항상 단짝이 되어 나오는 자료 구조(Data Structure)의 Implementation들을 제공하는데, 그 중에 하나가 Vector이다. 실제 얘기에 들어가기 전에 몇 가지 재미난 데이터를 살펴보자.

그림1

위 그래프는 VectorTest.java의 실행결과를 보여주고 있다. 아래 데이터는 VectorInd.java의 실행결과를 보여주고 있다.

> java VectorInd 1000000
Insert Time [749] : 11
Insert Time [61480] : 6
Insert Time [122880] : 48
Insert Time [245760] : 70
Insert Time [491520] : 113
Insert Time [983040] : 215
Insert Time [983917] : 18

소스에서도 알 수 있듯이, 그래프는 각 데이터 크기만큼 Vector에 add를 한 것이고, 변화 폭이 큰 것은 입력되는 위치가 Vector의 처음이고, 아래 그래프는 입력되는 위치가 Vector의 마지막이다. 똑같은 데이터를 입력하지만, 실행 속도는 엄청나게 차이가 난다. 이것을 알고리즘에서 사용하는 용어로 표현하면, 전자의 수행속도는 O(n*n), 후자는 O(n)로 표현된다. 루프를 제외하면, 각각 add에 걸리는 시간은 O(n), O(1)이다. 아래 두 그림은 그 차이를 보여준다.

그림2

위의 그림은 마지막에 입력되는 경우를 보여주고, 아래 그림은 첫번째 위치에 입력되는 경우이다. 아래의 경우에는 입력되어 있는 데이터의 수만큼 데이터가 이동해야 한다. 만약, 10번의 입력이 일어나면, 실제로는 10 + 9 + 8 + … + 2 + 1 = 10 * 9 / 2 번의 시간이 걸니는 것이다. 데이터가 커지면 커질수록 치루어야 하는 대가는 급속하게 커진다.

정말 모든 addLast가 일정한 시간이 들어가는지를 보여주는 것이 VectorInd.java이다. 데이터 입력 시간이 3 millisecond 이상 들어가는 경우에 소요된 시간을 프린트한 것이다. 백만번 중에 7번 발생했다. 처음과 마지막 데이터를 제외하면, 데이터가 두 배가 되어질 때 시간이 많이 걸리는 현상이 발생했다. 그 이유를 그림으로 설명하면 다음과 같다.

그림3

사용자에게 보이지는 않지만, Vector는 데이터를 저장하기 위해 Array를 만들었다. 만든 Array의 사이즈는 무한하지 않기 때문에 데이터를 계속 입력하면, 공간이 부족하게 되고, Vector는 이 때 원래 사이즈의 두 배의 크기의 Array를 만들고, 가지고 있던 데이터를 새로운 Array로 이동시킨다. 즉, Array에 데이터가 입력될 때, Array가 부족하면 기존에 저장된 데이터의 크기(Array 사이즈) 만큼의 데이터 이동이 일어나게 된다. 위 자료에서 보듯이, 데이터 사이즈가 두 배에 도달할 때마다 약 2 배 가량의 시간이 더 걸리는 것을 볼 수 있다.

이때가지의 관찰결과에 따르면, Vector의 인덱스가 작은 쪽에는 입력하는 것도, 삭제하는 것도 좋지 못하다. 그만큼의 댓가(실제 데이터 사이즈 - Index만큼의 데이터 이동)를 치루어야 하기 때문이다. 그리고, 데이터가 입력되는데 걸리는 시간을 반드시 일정한 시간이하로 낮추어야 하는 경우에는 일반적인 Vector를 사용할 수 없다.

실제로 Vector는 인덱스 관리가 편리한 Array에 불과한 것을 알 수 있다. 하지만 Array 작업시 문제가 되는 것들은 여전히 Vector에서도 문제가 된다. 그러므로 처음 넣은 데이터를 가장 먼저 지워야 되는 일에는 Vector를 사용하지 않는 것이 좋다. 이 작은 사실 하나가 위 그래프에서 볼 수 있듯이 데이터가 많은 경우, 실제 프로그램의 실행속도에는 엄청난 영향을 미치게 된다.

Note : VectorInd의 작업결과는 리눅스에서 실행한 것이다. 필자의 윈도우에서는 다른 실행결과가 나왔다. 출력되는 데이터의 모든 시간은 10이였다.

[VectorTest.java]

  1. import java.util.Vector;
  2. import java.util.Calendar;
  3.  
  4. public class VectorTest {
  5.  
  6.     public static void main(String[] args) {
  7.  
  8.          Vector target      = new Vector();
  9.          String obj          = new String("Vector Data");
  10.          Calendar   start, end;
  11.          long    interval   = 0;
  12.          long    LIMIT       = 0;
  13.          int      index       = 0;
  14.  
  15.          long    MAX = Long.parseLong(args[0]);
  16.          
  17.          for ( LIMIT = MAX / 10 ; LIMIT <= MAX ; LIMIT = LIMIT + MAX/10 ) {
  18.              start = Calendar.getInstance();
  19.              for ( index=0 ; index < LIMIT ; index++) {
  20.                   target.add(index, obj);
  21.              }
  22.              end    = Calendar.getInstance();
  23.  
  24.              interval = end.getTimeInMillis() - start.getTimeInMillis();
  25.              System.out.println(index + " " + interval);
  26.              target = new Vector();
  27.          }
  28.  
  29.          System.out.println("===========================");
  30.  
  31.          for ( LIMIT = MAX / 10 ; LIMIT <= MAX ; LIMIT = LIMIT + MAX/10 ) {
  32.              start = Calendar.getInstance();
  33.              for ( index=0 ; index < LIMIT ; index++) {
  34.                   target.add(0, obj);
  35.              }
  36.              end    = Calendar.getInstance();
  37.  
  38.              interval = end.getTimeInMillis() - start.getTimeInMillis();
  39.              System.out.println(index + " " + interval);
  40.              target = new Vector();
  41.          }
  42.     }
  43.  
  44.    
  45. }


[VectorInd.java]


  1. import java.util.Vector;
  2. import java.util.Calendar;
  3.  
  4. public class VectorInd {
  5.  
  6.     public static void main(String[] args) {
  7.    
  8.          Vector target = new Vector(30);
  9.          Object obj      = new Object();
  10.  
  11.          Calendar   start, end;
  12.          long         interval = 0;
  13.  
  14.          int LIMIT = Integer.parseInt(args[0]);
  15.  
  16.          for ( int index = 0 ; index <= LIMIT ; index++ ) {
  17.              start = Calendar.getInstance();
  18.              target.add(obj);
  19.              end    = Calendar.getInstance();
  20.              interval = end.getTimeInMillis() - start.getTimeInMillis();
  21.              if ( interval > 3 ) {
  22.                   System.out.println("Insert Time : " + interval);
  23.              }
  24.          }
  25.  
  26.     }
  27. }



저자 : 김대곤
원문 출처 : http://network.hanb.co.kr/view.php?bi_id=964
2007/06/24 11:17 2007/06/24 11:17
Trackback Address:이 글에는 트랙백을 보낼 수 없습니다

자바 API 항해지도

2007/06/18 01:57

서비 JAVA ,

이것은 개발 플랫폼에 있는 중요한 Java API를 정리한 Java API 지도이다.
여기에는 Java 2SE, Java 2EE, Java 2ME 등이 포함된다.
CORBA와 COM에서 상호 운용될 수 있는 Jini나 다른 API도 포함하였다.
이 도해는 Java API에 대한 빠른 레퍼런스이다.

자바 api flow

2007/06/18 01:57 2007/06/18 01:57
Trackback Address:이 글에는 트랙백을 보낼 수 없습니다

CLASSPATH에 없는 클래스 로딩

2007/06/18 01:43

서비 JAVA , , ,

java.lang.reflect를 이용하면 우리가 원하는 클래스에 대한 invoke가 가능하다는 것은 알고 있을 것이다.
하지만 classpath에 등록안되어진 클래스들에 대해서는 어떻게 할 것인가?

일일이 사용자에게 클래스 패스를 설정하게 할수만은 없는 일이다.

보통의 엔진들을 보게 되면 install되어진 디렉토리의 위치만을 세팅하도록 하고 있다.
set JAVA_HOME이라던지
set ANT_HOME이라던지..

쉘스크립트에 의하여 그러한 것들을 정의하여 java process를 띄우곤 하는데 그러면
내가 ant.jar등을 등록하지 않았음에도 불구하고 해당 애플리케이션들이 잘 작동하는 이유는 무엇일까?
그것은 바로 ClassLoader에 숨겨져 있다.
아래에서 보여지는 샘플코드는 classpath 프로퍼티에 등록이 되어지지 않은 클래스들에 대한 조작을 할 것이다.

그렇게 함으로서 이 글을 읽는 당신이 만든 애플리케이션이 별다른 클래스로딩 정책 없이도 작동이 될수 있겠다.
그러려면 또한 잘 알아야 하는것이 reflection API이거늘...
이부분에서는 그러한 것을 생략하고 URLClassLoader를 이용하여 디렉토리나 jar파일을 등록하여 가져오는
방법을 설명하도록 하겠다.

ClassLoader클래스는 이미 1.0API부터 존재해왔으면 URLClassLoader는 1.2에 새롭게 추가된 클래스이다.
우리가 사용하는 파일시스템이 URL이란 이름하에 조작이 될 수 있다는 것을 우선 명심하기 바란다.
왜냐면 file:/// 이란 URI를 사용하기 때문이다.

아래에서는 특정한 디렉토리 안의 jar파일에 대한 class loading샘플을 보여준다..

  1. import java.io.*;
  2. import java.net.*;
  3. public class ClassLoading {
  4.   public static void main(String [] args) throws Exception {
  5.     // Create a File object on the root of the directory containing the class file
  6.     File file = new File("D:/_Develop/jmxSamples/customMBean/log4j-1.2.8.jar");
  7.      
  8.     try {
  9.       // Convert File to a URL
  10.       URL url = file.toURL();          // file:/D:/_Develop/jmxSamples/customMBean/log4j-1.2.8.jar
  11.       URL[] urls = new URL[]{ url };
  12.       System.out.println(urls);
  13.        
  14.       // Create a new class loader with the directory
  15.       ClassLoader cl = new URLClassLoader(urls);
  16.       System.out.println(cl);
  17.        
  18.       // Load in the class; Logger.class should be located in
  19.       // the directory file:/D:/_Develop/jmxSamples/customMBean/log4j-1.2.8.jar
  20.       Class cls = cl.loadClass("org.apache.log4j.Logger");
  21.       System.out.println(cls);
  22.      
  23.     } catch (MalformedURLException e) {
  24.       e.printStackTrace();
  25.     } catch (ClassNotFoundException e2) {
  26.       e2.printStackTrace();
  27.     }
  28.    
  29.   }
  30. }

위에서 보는 것처럼 디렉토리를 설정하거나 특정 jar파일을 사용할 수 있도록 작성한다.
특정파일이 가르키지 않으면 해당 디렉토리의 class파일들을 package형태로 참조하도록 할 수 있는데
해당 디렉토리에 대한 클래스 로딩 샘플을 아래와 같다.

  1. import java.io.*;
  2. import java.net.*;
  3. public class ClassLoading {
  4.   public static void main(String [] args) throws Exception {
  5.     // Create a File object on the root of the directory containing the class file
  6.     File file = new File("D:/_CVSDevelop/jca_hello_adapter/build/classes");
  7.      
  8.     try {
  9.       // Convert File to a URL
  10.       URL url = file.toURL();          // file:/D:/_CVSDevelop/jca_hello_adapter/build
  11.       URL[] urls = new URL[]{ url };
  12.       System.out.println(urls);
  13.        
  14.       // Create a new class loader with the directory
  15.       ClassLoader cl = new URLClassLoader(urls);
  16.       System.out.println(cl);
  17.        
  18.       // Load in the class; Test.class should be located in
  19.       // the directory file:/D:/_CVSDevelop/jca_hello_adapter/build/classes/com/bea/jca/test/Test
  20.       Class cls = cl.loadClass("com.bea.jca.test.Test");
  21.       System.out.println(cls);
  22.      
  23.     } catch (MalformedURLException e) {
  24.       e.printStackTrace();
  25.     } catch (ClassNotFoundException e2) {
  26.       e2.printStackTrace();
  27.     }
  28.    
  29.   }
  30. }

위와 같은 경우에는 classpath의 루트로 잡은 디렉토리를 기준의 package형태로 설정되 파일을
로딩하여 사용할수 있도록 한다.

이 이후의 코딩에는 class가 newInstance를 취한 후 method를 invoking해야 하는 과정을 거치게 되는데
한 가지 주의할 점은 해당 클래스를 반드시 reflection API를 이용하여 호출해야 한다는 점이다.

대략 아래의 코드정도를 이용하여 main 메소드등을 호출하는 클래스를 작성할 수 있을 것이다.

  1. public void invokeClass(String name, String[] args)
  2.     throws ClassNotFoundException,
  3.           NoSuchMethodException,
  4. {
  5.     Class c = loadClass(name);
  6.     Method m = c.getMethod("main", new Class[] { args.getClass() });
  7.     m.setAccessible(true);
  8.     int mods = m.getModifiers();
  9.     if (m.getReturnType() != void.class || !Modifier.isStatic(mods) ||
  10.         !Modifier.isPublic(mods)) {
  11.         throw new NoSuchMethodException("main");
  12.     }
  13.     try {
  14.         m.invoke(null, new Object[] { args });
  15.     } catch (IllegalAccessException e) {
  16.         // This should not happen, as we have disabled access checks
  17.     }
  18. }

reflection에 대한 샘플은 몇가지에 대하여 놀새~ 사이트에 이미 올려져 있으므로 참조하기 바란다.
위와 같은 샘플을 이용하게 되면 서버측 프로그램에 대한 작성을 해볼 수 있는 좋은 기회가 아닐까 한다.

원문 출처 : http://ienvyou.egloos.com/?doc=bbs/gnuboard.php&bo_table=sample_code&page=1&wr_id=68


2007/06/18 01:43 2007/06/18 01:43
Trackback Address:이 글에는 트랙백을 보낼 수 없습니다

자바 프로그래밍으로 온라인 XML 데이터 검색하기

소스 파일 :



인터넷에 정보를 퍼블리싱하는 것이 보편화 되어 가면서 이러한 정보를 발견하고 요청하는 것 또한 자연스러운 일이 되었다. 이 글은 자바 프로그래밍을 사용하여 웹 기반 XML 데이터를 얻고 데이터를 파싱하여 필요한 엘리먼트와 속성들을 필터링하고 요청된 정보를 이용하여 작업을 수행하는 방법을 설명한다.

인터넷에 정보를 내보내는 것이 자연스러운 현상인 만큼 정보를 발견하고 요청하는 것도 자연스러운 일이 되었다. XML은 비즈니스와 소비자가 정보를 더욱 쉽게 공유할 수 있도록 데이터를 설명하는데 사용할 수 있는 기술 중 하나이다. 웹 상의 XML 정보의 예제에는 날씨 정보, 증권 시세표, 선적표, 항공요금표, 경매가격, 오늘의 유머 같은 정보들을 포함하고 있다. 이 데이터에 왜 액세스하려 하는가? 아마도 자기 지역의 날씨 데이터를 저장하고 검색하거나, 선적 과정을 조사 할 작은 유틸리티를 작성해야 할 이유에서 일 것이다. 여러분이 자바 개발자라고 가정한다면 파싱과 조작이 쉬운 XML 정보를 찾을것이다.

물론 HTML 페이지가 있지만 대부분의 데이터는 XML 포맷에서 비롯되고 웹 서버에는 HTML로 변환된다. 많은 웹 사이트들은 두 포맷으로 정보를 제공한다. 구 브라우저와 데이터를 단순히 검색하고자 하는 사람들에게는 HTML로 제공되고, 데이터 모음과 분석을 원하는 넷 정통 프로그래머에게는 XML 포맷으로 제공된다. HTML 페이지에서 데이터를 가져오는 것 보다는 자바 프로그래밍으로 XML 정보를 파싱하고 모으는 것이 훨씬 더 쉽다. 우선 표준 XML 파서가 있으면 다운로드하기가 쉽다. 둘째, 문서 구조는 시간이 흐르면서 변하기 때문에 HTML 태그 보다는 XML 엘리먼트와 속성으로 나타내는 것이 더욱 쉽다.

시작하기

XML 데이터를 파싱하기 위해서는 XML 파서가 있어야 한다. 물론 여러분 정도라면 각자의 파서를 작성할 수 있겠지만 완벽한 기능의 표준 파서들이 무료로 나와있다. Java 2 Platform, version 1.3 Enterprise Edition (J2EE)도 유용할 것이다. 파서는 개발자의 시각에서 볼 때 속도나 신뢰성에서 차이가 나지만 프로그래밍 모델과 인터페이스는 동일하다. 게다가 무료이다.

자바 개발자라면 javax.xml 패키지에 주의를 기울여야 한다. 이 패키지에는 XML 데이터를 파싱하는데 필요한 모든 코드가 있다. XML 문서를 다른 형식으로 변형하는 패키지와 함께 파서 구현 패키지들이 있다.






DOM vs SAX 파서

두 가지 유형의 XML 문서 파서가 있다. 이들은 XML 문서에 액세스하는 방식부터 다르다:

  • Document Object Model (DOM). XML 문서로의 랜덤 액세스(random access)에 사용된다. DOM의 장점은 메모리 안에 문서의 전체 모델을 갖고 있다는 점이다. 이는 모든 XML 엘리먼트에 어떤 순서로든지 액세스가 가능하다는 것을 의미한다. 하지만 큰 문서의 경우 둔해 질 수 있다. 메모리에서 실행시키지 않으면 시스템이 한계에 다다를 때 퍼포먼스가 느려진다.
  • Simple API for XML (SAX). 순차적 액세스(sequential access)에 사용된다. SAX의 장점은 문서의 한 부분이 메모리에서 사용될 수 있기 때문에 보다 큰 문서를 핸들할 수 있다는 점이다. SAX의 단점은 엘리먼트를 순서대로 처리해야 하며 한번에 볼 수 있는 문서 부분도 작다. SAX를 이용하여 문서를 파싱할 때 XML 부분을 저장할 수 있다.

온라인 정보 사이트(날씨 데이터 또는 주식 현황)에 액세스하는 XML 문서는 작고 집중되어 있는 경향이 있다. 일반적으로 특정 사이트나 주식 데이터를 검색하고 정보를 처리하고 다음 쿼리 또는 문서가 고려된다. 이러한 유형의 액세스의 경우 DOM은 선호되는 파싱 방식이다. 반면 SAX 파싱은 이 분야가 아니다.






XML 문서 얻기

파서가 있으므로 웹에서 XML 문서에 액세스 할 수 있다. Listing 1은 인터넷에서 날씨 데이터를 가져오는 예제이다:

Listing 1. XML 날씨 데이터 예제

<forexml site="Austin">
<observation
    city="Austin, Texas"
    temp.F="75"
    rel_hum.percent="31"
    wind_speed.knt="8"
    skies="partly cloudy"
/>
<almanac
    sunrise="7:08 AM"
    sunset="6:21 PM"
/>
<forecast
    type="nearterm"
    source="NWS"
    day="THIS AFTERNOON"
    weather="SU"
    high_temp="77"
    text="HIGHS 75 TO 80. WEST 15 TO 20 MPH."
/>
</forexml>

Unisys Corporation은 National Oceanic and Atmospheric Administration와 수백개의 날씨 관측 사이트와 협력하여 www.weather.unisys.com/forexml.cgi에 XML 포맷으로 기상 관측을 내보내고 있다. URL (예를 들어, www.weather.unisys.com/forexml.cgi?Austin) 다음에 요청 필드에 도시 이름 같은 관측 지역의 이름을 입력하라. 물음표는 요청 필드를 URL에 추가하는데 필요한 경계기호 이다. 읽을 수 있는 관측 결과가 XML 포맷으로 리턴된다. (Listing 1). J2EE에 제공된 XML 파서를 사용해보자.

개념상으로 XML 문서를 DOM으로 볼 수 있다. forexml 노드는 문서의 노드 트리에서 뿌리 노드 (root node)로서 보여진다. 뿌리 노드는 observation, almanac, forecast 같은 자식 노드를 갖고 있다. 이들 노드 각각 observation 이나 0 또는 그 이상의 엘리먼트를 갖고 있다. 예를들어 observation 엘리먼트는 75 값을 가진 temp.F 속성을 갖고 있다. 그림 1은 문서 노드의 계층을 나타낸다:

그림1. 문서 노드 계층(맑은 날!)
Hierarchy of document nodes (A pleasant day!)

주어진 XML 문서에서 관심있는 엘리먼트와 속성을 어떻게 필터링 할까? 우선 파싱하고자 하는 인자를 모아야 한다. Listing 2는 명령행에서 프로그램 인자를 모으고 그들을 processDocument라고 하는 메인 프로세싱 메소드로 전달하는 표준 main 메소드를 보여주고 있다. Java applet과 HTML 매개변수로 비슷한 작동을 수행할 수 있다. Listing 2에서 코드는 표준이고 XML 프로그래밍이 없다. 다음에 온다.

Listing 2. 날씨 애플리케이션 메인 메소드

// Given a list of arguments, parse a city
// name and a list of attributes to filter.
// Alternatively, accept "all" as an indication
// to get all attributes

public static void main( String [] args ) {
   if ( args.length == 0) {
     System.out.println
       ( "Usage: java Weather city"
           +" [all|attributes]" );
     System.out.println
       ( "Example:"
           +" java Weather Austin temp.F" );
     System.exit( 0 );
   }
   // Gather arguments
   String city = args[ 0 ];
   String [] attrNames = null;
   if ( args.length == 1) {
     attrNames = new String [] { "all" };
   } else {
     // args.length > 1
     attrNames =
       new String [ args.length - 1 ]; 
     for ( int i = 1; i < args.length; i++)
       attrNames[ i - 1 ] = args[ i ];
   } // if
	
   // Process document
   processDocument( city, observationTagName,
     attrNames );
} // main

Listing 3은 XML 문서를 얻어 이를 파싱하는데 쓰이는 전형적인 구조이다:

Listing 3. DocumentBuilder 요청하기

// Given a city string, an element name, and
// a list of attributes, print the values of
// the attributes found in the XML document.
public static void processDocument(
   String city,
   String elementName, String [] attrNames ) {
   DocumentBuilderFactory dbf =
      DocumentBuilderFactory.newInstance ( ) ;
      
   try {
         DocumentBuilder db =
            dbf.newDocumentBuilder ( ) ;
            
         if ( city != null ) {
             String urlString =
                 weatherSite + city;
             Document document = getDocument
                ( db, urlString );
             if ( document != null ) {
                Node [] matchNodes =
                   getAttributes( document,
                      elementName, attrNames );
                 if (null != matchNodes ) {
                   if ( matchNodes.length > 6 )
                       printNodes( "City=" + city,
                         matchNodes, true );
                   else  
                       printNodes( "City=" + city,
                         matchNodes, false );
                 } else 
                     System.out.println (  
                        "Element \"" +
                        elementName +
                   "\" not found in document." );
         } else
             System.out.println 
                ( "No XML created from URL="
                   + urlString );
   } // if         
  } catch (  
      ParserConfigurationException e ) { 
      e.printStackTrace ( ) ; 
  }
} // processDocument

먼저 DocumentBuilderFactory의 인스턴스를 얻는다. 일단 팩토리 객체를 얻으면 새로운 Document Builder 메소드를 사용하여 DocumentBuilder를 얻는다. 이것은 XML 인풋 스트림을 파싱할 수 있는 객체이다. 리턴되는 실제 DocumentBuilderFactory 객체는 다음 설정에 의존한다:

  • javax.xml.parsers.DocumentBuilder Factory system property
  • JAVA_HOME/lib/jaxp.properties
  • META-INF/services/javax.xml.parsers.DocumentBuilderFactory 서비스(jar file)
  • platform default

J2EE를 설치할 때 디폴트 설정을 사용한다면 이 예제는 올바르게 작동한다 특별한 파서 기능을 이용하고자 할 때에는 다른 파서를 지정한다. 여기서 얻을 수 있는 교훈은 많은 자바 XML 프로세서를 가지고 작업하라는 것이다.

Listing 3 의 코드 나머지 부분은 속성과 일치하는 XML 문서를 찾아 프린트한다. 이것은 고급 메소드 이다. URL에서 문서를 얻고 필터링하고 속성을 프린트하는 것에 대한 설명은 다음 섹션에 있다.

Listing 4는 웹 에서 XML 문서를 얻고 Listing 3에서 getDocument 메소드를 검색하는 것을 보여주고 있다:

Listing 4. 요청된 URL에서 XML 문서 파싱하기

// Using the given document builder object,
// construct and return an XML DOM
// from the given URL.

public static Document getDocument
  ( DocumentBuilder db, String urlString ) {
   try {
     URL url = new URL( urlString );
     
     try {
       URLConnection URLconnection =
          url.openConnection ( ) ;
       HttpURLConnection httpConnection =
          (HttpURLConnection)
         URLconnection;
         
       int responseCode =
        httpConnection.getResponseCode ( ) ;
       if ( responseCode ==
        HttpURLConnection.HTTP_OK) {
           InputStream in =
            httpConnection.getInputStream ( ) ;
            
           try {
             Document doc = db.parse( in );
             return doc;
           } catch(
            org.xml.sax.SAXException e ) {
                e.printStackTrace ( ) ;
           }
        } else {
            System.out.println
               ( "HTTP connection response !=
               HTTP_OK" );
               
        } 
     } catch ( IOException e ) { 
          e.printStackTrace ( ) ;
     } // Catch
  } catch ( MalformedURLException e ) {  
      e.printStackTrace ( ) ;
  } // Catch
  return null;
} // getDocument

우선 URLConnection 은 URL 스트링에 열려있다. 날씨 사이트가 HTTP 요청을 통해서 작동하는 것을 알고 있기 때문에 단순한 URLConnectionHttpURLConnection에 캐스팅할 수 있다. 그런 다음, 서버에서 온 응답 코드를 테스트한다. 요청에 에러가 없다는 것을 서버가 표시하면 커넥션 스트림을 열고 응답을 XML 스트림으로 취급한다. DocumentBuilder 파스 메소드는 파싱하고 데이터 스트림을 javax.xml. 문서로 돌린다. 이 과정 중 어떤 것이라도 실패하면 메소드는 XML 문서 객체를 만들 수 없다는 것을 나타내는 null을 리턴한다.






원하는 엘리먼트를 필터링하기

여러 엘리먼트 태그들이 root 태그인 forexml 다음에 나왔다는 것을 기억하라:

  • observation. 날씨 관측 지역 및 장소, 온도, 습도, 풍속 등의 데이터 포함.
  • almanac. 관측 장소의 일출과 일몰 등의 데이터 포함.
  • forecast. 앞으로의 날씨 예상 포함.

Listing 5는 observation 태그를 사용하고 다른 태그들은 무시된다:

Listing 5. XML 엘리먼트와 속성 맞추기

// Given an XML document,
// return the values of all attributes
// for the given element.

public static Node [] getAttributes
   ( Document document,
    String elementName, String [] attrNames ) {
    
    // Get elements with the given tag name
    // (matches on * too)
    NodeList nodes = document.getElementsByTagName
       ( elementName );
    if ( nodes.getLength() < 1) {
      return null;
    }
    
    Node firstElement = nodes.item( 0 );
    NamedNodeMap nnm =
       firstElement.getAttributes ( ) ;
   
    if (nnm != null) {
      // Test the value of each attribute
      Node [] matchNodes = new Node
         [ attrNames.length ];
        
       for (int i = 0; i < attrNames.length; i++){
          boolean all =
            attrNames[ i ].equalsIgnoreCase("all");
           if (all) {
             // named node map
             int nnmLength = nnm.getLength();
             matchNodes = new Node[ nnmLength ];
             
             for ( int j = 0; j < nnmLength; j++){
                matchNodes[ j ] = nnm.item( j );
             }
             return matchNodes;
           } else {
               matchNodes[ i ] = nnm.getNamedItem
                 ( attrNames[ i ] );
               if ( matchNodes[ i ] == null ) {
                 matchNodes[ i ] =
                     document.createAttribute
                     ( attrNames[ i ] );
                  ((Attr)matchNodes[ i ]).setValue
                    ( unknownAttribute );
               }
           } // if
      } // for
      
      return matchNodes;
   } // if
   
   return null;
} // printDocumentAttrs

Document 클래스가 getElementsByTagName 메소드를 가지고 있는 것에 주목하라. observation 엘리먼트 이름을 사용하여 메소드는 문서의 다른 엘리먼트에서 요청된 태그를 필터링하고 NodeList 객체를 리턴한다.

코드는 첫 번째 엘리먼트를 선택하고 getAttributes을 사용하여 그 엘리먼트용 XML 속성 리스트를 얻는다. 속성은 NamedNodeMap 객체에 리턴된다. 이러한 각각의 노드들은 요청된 속성 이름이 맞는지 또는 와일드 카드가 모두와 맞는지를 확인하기 위해 테스트된다. 이 예제는 노드 객체들의 어레이를 구현하고 이것을 caller에게 리턴한다. 엘리먼트나 속성을 찾지 못하면 메소드는 null을 리턴한다.

자바 프로그래밍으로 온라인 XML 데이터를 조작하는 주요 단계는 다음으로 요약될 수 있다:

  1. DocumentBuilderFactory를 이용하여 XML 파서 요청하기
  2. DocumentBuilder를 이용하여 XML 파서 및 구현하기
  3. 요청된 엘리먼트와 속성 찾기

발견한 데이터를 어떻게 이용할 것인가는 이제 여러분에게 달려있다. 다음 섹션에서는 XML 데이터를 핸들링하는 방법을 설명하겠다.






XML 데이터의 프린팅

Listing 6은 노드들을 통해 반복하고 속성 데이터를 프린트하는 방법이다:

Listing 6. XML 속성 데이터를 프린트하는 메소드 예제

// Given a set of nodes,
// print the values of the nodes.
// The nodes may be given a title.
// The nodes may be printed on one line
// (grouped) or many.

public static void printNodes
  ( String title, Node [] nodes,
       boolean grouped ) {
       
       // Report the name value of each node
       System.out.print( title );
       
       if (grouped){
          System.out.println ( ) ;
       }
       // Walk through the nodes.
       for ( int i = 0; i < nodes.length; i++ ) {
           Node node = nodes[ i ];
           System.out.print
              ( grouped ? " " : ", " );
           System.out.print
              ( node.getNodeName() + "=" +
              node.getNodeValue() );
              
            if ( grouped ) {
               System.out.println ( ) ;
            }  
    } // for
    
    if (!grouped) {
        System.out.println ( ) ;
    }  
} // printAttributeValues

Listing 5 에서는 캡쳐하고자하는 XML 엘리먼트와 속성 데이터를 맞춰 노드 어레이를 만든다. 이러한 특정한 프린트 루틴은 한 라인 또는 여러 라인에 걸쳐 모든 속성들을 프린트할 수 있다.

이제 여행의 끝에 와 있다. Listing 7은 파싱하고 선택한 XML 속성 데이터 아웃풋 예제이다:

Listing 7. 자바 날씨 프로그램 실행에 따른 아웃풋 예제

City=Austin
   city=Austin, Texas
   latitude=30.30
   longitude=-97.70
   time=3 PM CDT ...
   temp.F=84
   temp.C=28
   rel_hum.percent=58
   wind.string=S at 11 knt
   skies=clear    

이 경우 데이터를 System.out 으로 프린트했지만 각자의 애플리케이션에서는 데이터를 데이터베이스에 저장할 수 있고 트랜드 분석이나 그래프 그리기도 가능하다. 이러한 날씨 관측은 매 시간 업데이트 되어 주기적인 점검이 가능하다.

이 프로그램을 사용하여 모든 관측 속성들을 찾거나 온도 또는 상대 습도, 위도와 경도 같은 서브셋도 요청할 수 있다!






결론

자바 프로그래밍을 이용하여 온라인 XML 데이터를 검색하는 방법을 설명했다. 가장 중요한 단계는 관심있는 내용이 수록되어있는 사이트를 찾는 것이다. 일단 사이트를 발견하면 데이터 추출 작업은 모든 XML 문서의 경우 같다. 우선 문서를 요청하고 그런 다음 파싱한다. 마지막으로 원하는 엘리먼트와 속성을 필터링한다. 표준 XML 파서를 이용하면 직접 작성하는 것 보다 강력한 툴을 가질 수 있다.



원문 출처 : http://www.ibm.com/developerworks/kr/library/j-xmljava/
2007/06/07 12:42 2007/06/07 12:42
Trackback Address:이 글에는 트랙백을 보낼 수 없습니다

JDOM과 XML 파싱

2007/05/30 10:29

서비 JAVA , ,

Java에서의 XML 가공을 간소화 해주는 JDOM

대부분의 개발자는 과거 XML 데이타 구조를 가공하기 위해 수많은 Java 라이브러리 중 하나를 이용해본 적이 있을 것이다. 그렇다면 JDOM(Java Document Object Model)은 무엇이며 개발자는 왜 JDOM을 필요로 할까?

JDOM은 Java에 최적화된 XML 데이타 가공을 위한 개방 소스 라이브러리이다. JDOM은 W3C(World Wide Web Consortium) DOM과 유사하기는 하지만, DOM을 기반으로 설계되거나 DOM을 모델링하지 않은 대안적인 문서 객체 모델이다. 가장 큰 차이점은 DOM은 언어 중립적으로 설계되었고 초기에 HTML 페이지의 JavaScript 가공에 주로 이용되었던 반면, JDOM은 Java 전용으로 설계됐기 때문에 메소드 오버로딩(method overloading), 컬렉션(collections), 리플렉션(reflection) 및 친숙한 프로그래밍 환경 등 Java의 기본 기능들을 활용한다는 데 있다. Java 프로그래머에게는 JDOM이 보다 자연스럽고 ‘알맞게’ 느껴질 것이다. 이는 언어 중립적인 CORBA(Common Object Request Broker Architecture)에 비해 Java에 최적화된 RMI(Remote Method Invocation) 라이브러리가 보다 더 자연스럽게 느껴지는 것과 유사하다고 할 수 있다.

JDOM은 jdom.org에서 구할 수 있으며, 개방 소스로 Apache 스타일(상용 친화적) 라이선스로 제공된다. JDOM은 공동 협력을 통해 설계 및 개발됐으며, 메일링 리스트에 등록된 가입자만도 3,000여 명에 이른다. 또한 이 라이브러리는 Sun의 JCP(Java Community Process)에 Java Specification Request(JSR-102)로 채택됐으며, 곧 공식 Java 사양으로 채택될 것으로 전망된다.

이 글에서는 JDOM의 기술적 측면에 대해 다룰 것이다. 먼저, 주요 클래스에 대한 정보를 소개하고, 이어 Java 프로그램에서 JDOM을 이용하는 방법에 대해 설명할 것이다.

ORACLE XML TOOLS

이 XDK (XML Developer Kit)는 오라클이 개발자를 위해 제공하는 무료 XML 툴 라이브러리입니다. 이 라이브러리에서는 JDOM과 함께 이용할 수 있는 XML 파서와 XSLT 변환 엔진이 제공됩니다. 이들 툴에 대한 자세한 정보는 Oracle XML 홈페이지인 oracle.com/xml에서 제공됩니다.

파서를 다운로드받으려면 "XDK for Java"로 명명된 XML Developer Kit를 찾은 뒤 좌측의 "소프트웨어" 항목을 클릭해 다운로드를 시작합니다. 다운로드 받은 파일을 열면 xalparserv2.jar 파일에 파서가 들어있습니다.

JDOM 및 기타 소프트웨어가 기본값으로 오라클 파서를 이용하도록 구성하려면 oracle.xml.jax.JXSAXParserFactory에 JAXP javax.xml.parsers.SAXParserFactory 시스템 속성을 설정해야 합니다. 이는 JAXP에 오라클 파서를 이용하겠다고 밝히는 것입니다. 가장 쉬운 방법은 다음의 명령줄을 이용하는 것입니다.

java -Djavax.xml.parsers.SAXParserFactory=
oracle.xml.jaxp.JXSAXParserFactory

혹은 아래와 같이 프로그래밍 방식을 이용할 수도 있습니다.

System.setProperty("jaxax.xml.parsers
.SAXParserFactory",
"oracle.xml.jaxp.JXSAXParserFactory");

오라클은 XDK외에도 Oracle9i Database Release 2에 고유 XML 리포지토리를 제공하고 있습니다. Oracle9i XML Database (XDB)는 고성능을 지닌 고유 XML 스토리지 및 검색 기술입니다. XDB는 W3C XML 데이타 모델을 Oracle9i Database로 완벽하게 수용하며 XML의 네비게이션 및 질의를 위한 새로운 표준 액세스 방법을 제공합니다. XDB를 이용할 경우 관계형 데이타베이스 기술의 모든 이점과 함께 XML 기술의 장점을 활용할 수 있습니다.

JDOM 패키지의 구조

JDOM 라이브러리는 6개 패키지로 구성되어 있다. 첫째, org.jdom 패키지에는 Attribute, CDATA, Comment, DocType, Document, Element, EntityRef, Namespace, ProcessingInstruction, Text 등 XML 문서와 그 컴포넌트를 나타내는 클래스들이 포함돼 있다. XML에 익숙한 개발자라면 클래스 이름만 봐도 이해가 될 것이다.

다음은 XML 문서를 생성하는 클래스를 담고 있는 org.jdom.input 패키지이다. 가장 중심적이고 중요한 클래스는 SAXBuilder이다. SAXBuilder는 수신되는 SAX(Simple API for XML) 이벤트를 참조해 이에 대응하는 문서를 구성함으로써 문서를 생성한다. 파일이나 다른 스트림으로부터 문서를 생성하고자 한다면 SAXBuilder를 이용해야 한다. SAXBuilder는 SAX 파서를 이용해 스트림을 읽은 뒤 SAX 파서 콜백에 따라 문서를 생성한다. 이 설계의 좋은 점은 SAX 파서의 속도가 빨라질수록 SAXBuilder도 빨라진다는 것이다. 그밖에 주요 입력 클래스는 DOMBuilder이다. DOMBuilder는 DOM 트리를 통해 문서를 생성한다. 이 클래스는 이미 존재하는 DOM 트리를 JDOM 버전으로 대신 사용하고자 할 경우 편리하다.

이러한 빌더의 잠재성에는 아무런 제한이 없다. 예를 들어, Xerces에는 SAX보다 더 낮은 수준에서 운용되는 XNI(Xerces Native Interface)가 있으므로 SAX를 통해 노출되지 않는 일부 파서 정보를 다루기 위해서 XNIBuilder를 사용하는 것이 적합할 수도 있다. JDOM 프로젝트를 지원해온 한 가지 대중적인 빌더는 ResultSetBuilder이다. 이 빌더는 JDBC ResultSet을 통해 SQL 결과를 다양한 구성의 요소(element)와 속성(attribute)을 가지는 XML 문서를 표현한다.

org.jdom.output 패키지에는 XML 문서를 출력하는 클래스가 포함돼 있다. 가장 중요한 클래스는 XMLOutputter이다. XMLOutputter는 파일, 스트림, 소켓으로 출력할 수 있도록 문서를 바이트 스트림으로 변환한다. XMLOutputter는 원시 출력, 가공 출력, 압축 출력 등을 지원하는 다수의 특별 구성 옵션을 가지고 있다. 이 클래스는 상당히 복잡하다. DOM Level 2에 아직도 이런 기능이 없는 것은 바로 이런 이유 때문일 것이다.

그 밖에 문서의 컨텐트를 기반으로 SAX 이벤트를 생성하는 SAXOutputter가 있다. 이 클래스는 모호해 보이기는 하지만 XSLT 변환시 매우 유용한데, 이는 문서 데이타를 엔진으로 전송하는 데 있어 SAX 이벤트가 바이트 스트림보다 훨씬 효율적인 방식이기 때문이다. 또한 문서를 DOM 트리 형식으로 표현하는 DOMOutputter도 있다. 그 밖에 수십 라인의 코드만으로 문서를 JTree로 보여주는 JTreeOutputter도 있는데, JTreeOutputter를 ResultSetBuilder와 함께 사용할 경우 코드 몇 라인만 추가하는 것만으로도 SQL 질의 결과를 트리 뷰로 나타낼 수 있다.

DOM과는 달리, JDOM에서는 해당 문서가 빌더에 구속되지 않는다는 점에 주목해야 한다. 따라서 데이타를 담는 클래스와 데이타를 구조화하는 다양한 클래스, 이 데이타를 사용하는 그 밖의 여러 클래스가 포함된 세련된 모델이 생성된다. 원하는 만큼 자유롭게 혼합해 사용할 수 있다.

org.jdom.transform 및 org.jdom.XPath 패키지에는 기본 XSLT 변환과 XPath 조회를 지원하는 클래스가 포함돼 있다.

마지막으로, org.jdom.adapters 패키지는 DOM 상호작용의 라이브러리를 지원하는 클래스를 포함하고 있는데, 이 패키지의 클래스를 호출할 필요가 전혀 없다. 이들 클래스가 존재하는 이유는 각 DOM의 구현 방식이 각각의 부트 스트래핑 작업 방식별로 서로 다른 함수 이름을 사용하기 때문이며, 이에 따라서 각 어댑터 클래스가 표준 콜을 파서 전용 콜로 번역한다. JAXP(Java API for XML Processing)는 어댑터 클래스가 과도하게 사용될 때의 문제점에 대한 대안으로서, 실제로 이들 클래스에 대한 요구를 감소시키는 역할을 한다. 그러나 모든 파서가 JAXP를 지원하는 것은 아니고, 또한 라이선스 문제 때문에 어디나 JAXP가 설치돼 있는 것도 아니기 때문에, 이러한 클래스들에 대한 필요성은 여전히 남아 있다.

문서의 생성

문서는 org.jdom.Documentclass에 의해 표현된다. 다음은 완전히 새로운 문서를 생성하는 경우이다.

// This builds: <root/>

Document doc = new Document(new Element("root"));

또한 파일이나 스트림, 시스템 ID, URL 등을 통해 문서를 생성할 수도 있다.

// This builds a document of whatever's in the given resource
SAXBuilder builder = new SAXBuilder();
Document doc = builder.build(url);

소수의 콜을 조합함으로써 간단한 JDOM 문서를 생성할 수도 있다.

// This builds: <root>This is the root</root>

Document doc = new Document();
Element e = new Element("root");
e.setText("This is the root");
doc.addContent(e);

파워유저라면 다양한 방법을 연속적으로 호출하는 'method chaining'선호할 것이다. 이 방식을 통해 여러 개의 메소드를 한 번에 호출할 수 있다. 다음은 method chaining의 예이다.

Document doc = new Document(
  new Element("root").setText("This is the root"));

다음은 JAXP/DOM를 이용해 동일한 문서를 생성하는 예이다.

// JAXP/DOM
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
Document doc = builder.newDocument();
Element root = doc.createElement("root");
Text text = doc.createText("This is the root");
root.appendChild(text);
doc.appendChild(root);

SAXBuilder 이용하기

앞서 설명했듯이 SAXBuilder는 모든 바이트 근간 자원으로부터 문서를 생성하는 간단한 메커니즘을 제공한다. 입력 변수가 없는 기본 SAXBuilder() 생성자는 내부적으로 JAXP를 이용하여 SAX 파서를 선택한다. 파서를 변경하고자 할 때는 javax.xml.parsers.SAXParserFactory 시스템 속성을 파서가 제공하는 SAXParser Factory를 가리키도록 설정하면 된다. Oracle9i Release2 XML 파서의 경우 다음과 같이 실행하면 된다.

java -Djavax.xml.parsers.SAXParserFactory=

oracle.xml.jaxp.JXSAXParserFactory YourApp

Xerces 파서의 경우 다음과 같이 실행한다.

java -Djavax.xml.parsers.SAXParserFactory=org.apache.xerces.jaxp
.SAXParserFactoryImpl YourApp

만약 JAXP가 설치돼 있지 않다면 SAXBuilder는 Apache Xerces를 기본값으로 이용한다. SAXBuilder 인스턴스가 생성된 뒤에는 다음과 같은 몇 가지 속성을 빌더에 설정할 수 있다.

setValidation(boolean validate)
WebLOCATOR

개방 소스 JDOM 라이브러리: jdom.org

Java Servlet Programming (second edition), (제이슨 헌터(Jason Hunter) 저) (2001년 O'Reilly & Associates 출간): www.oreilly.com

이 메소드는 문서 생성 중 DTD(Document Type Definition)에 대해 검증할 것인지 여부를 파서에 알려준다. 기본으로 설정된 값은 false이다. 사용된 DTD는 문서의 DocType 내에서 참조된 것이다. 다른 DTD에 대해 검증하는 것은 아직 불가능한데, 이 기능을 지원하는 파서가 아직 없기 때문이다.

setIgnoringElementContentWhitespace(boolean ignoring)

위 메소드는 요소 컨텐트에서 ‘무시할 수 있는 여백(ignorable whitespace)’을 무시할 것인지 여부를 파서에 알려준다. XML 1.0 사양에 의하면, 요소 컨텐트의 여백은 파서에 의해 보존돼야 하지만 DTD에 대해 검증할 경우 문서의 특정 부분이 여백을 지원하지 않는다는 사실을 파서가 인식할 수 있기 때문에 이 영역의 여백은 ‘무시할 수’ 있다. 기본값으로는 해제 상태이다. 문서를 입력 때와 동일한 컨텐트를 출력하고자 할 때가 아니라면 ‘무시 가능’으로 사용하는 것이 일반적으로 성능상 바람직하다. 단, 이 플래그는 DTD 검증이 수행될 때만 유효하며 이때는 이미 검증 과정을 통한 성능 저하가 발생한 것이기 때문에 결국 이 메소드는 검증이 이미 이용되고 있을 경우에만 유용하다는 점을 유의해야 한다.

setFeature(String name, String value)

위 메소드는 기본 SAX 파서상에 기능을 설정하는 방법이다. 이 방법은 원시적인 호출 방식이기 때문에 이 방법을 이용할 때는 매우 신중해야 한다. 왜냐하면 특정 기능(예 : 네임스페이스 변경)을 잘못 설정할 경우 JDOM 작업이 중단될 수도 있기 때문이다. 게다가 파서 전용 기능에 의존할 경우 이식성을 제한할 위험이 있다. 이 콜은 스키마 검증을 선택할 때 가장 유용하다.

setProperty(String name, Object value)

위 메소드는 기본 SAX 파서상에 속성을 설정하는 방법이다. 이 방법 역시 원시 호출 방식으로, 위험한 동시에 특히 스키마 검증시 파워유저에게 유용한 방법이다. 다음 코드는 이 방법들을 조합해 검증 기능을 선택하고 여백 무시 가능 기능으로 설정한 뒤 JAXP 선택 파서를 이용해 로컬 파일을 읽게 된다.

SAXBuilder builder = new SAXBuilder();
builder.setValidation(true);
builder.setIgnoringElementContentWhitespace(true);
Document doc = builder.build(new File("/tmp/foo.xml"));

XMLOutputter를 이용한 문서 출력

문서는 다양한 포맷으로 출력될 수 있지만 가장 흔한 포맷은 바이트 스트림이다. JDOM에서는 XMLOutputter 클래스가 이 기능을 제공한다. 이 클래스의 기본 생성자는 문서에 저장된 원문 그대로 문서를 출력하려 한다. 아래 코드는 원문 그대로 문서의 내용을 파일에 출력하는 코드이다.

// Raw output
XMLOutputter outp = new XMLOutputter();
outp.output(doc, fileStream);

여백에 신경 쓰지 않아도 된다면 텍스트 트리밍을 선택해 약간의 공간을 절약할 수 있다.

// Compressed output
outp.setTextTrim(true);
outp.output(doc, socketStream);

사람 눈에 맞춰 문서의 인쇄 상태를 보기 좋게 만들려면 들여쓰기와 줄 바꿔쓰기를 추가하면 된다.

outp.setTextTrim(true);
outp.setIndent("  ");
outp.setNewlines(true);
outp.output(doc, System.out);

이미 여백을 통해 포맷된 문서에 위의 가공 기능을 다시 적용할 경우 트리밍을 선택해야 한다. 그렇지 않으면 이미 포맷된 상태에서 또다른 포매팅을 가하는 것이 돼 최종 출력 상태가 보기 흉하게 된다.

요소 트리의 네비게이션

JDOM은 요소 트리(element tree)의 네비게이션을 간편하게 해준다. 루트 요소를 호출하려면 다음 코드를 이용한다.

Element root = doc.getRootElement();
모든 자식 요소 리스트를 불러오는 방법은 다음과 같다.

List allChildren = root.getChildren();

주어진 이름의 요소만을 호출하려면,

List namedChildren = root.getChildren("name");

주어진 이름의 요소 중 첫 번째 요소만을 호출하려면 다음을 이용한다.

Element child = root.getChild("name");

getChildren() 콜을 통해 반환된 리스트는 모든 Java 프로그래머가 알고 있는 리스트 인터페이스의 구현인 java.util.List이다. 이 리스트에서 특기할 만한 것은 이것이 라이브 리스트라는 점이다. 리스트에 가해진 모든 변경사항은 원본 문서 객체에도 반영된다.

// Remove the fourth child
allChildren.remove(3);
// Remove children named "jack"
allChildren.removeAll(root.getChildren("jack"));
// Add a new child, at the tail or at the head
allChildren.add(new Element("jane"));

allChildren.add(0, new Element("jill"));

이러한 리스트를 통한 대치 방법을 이용하면, 수많은 별도의 방법들을 과도하게 사용하지 않고도 요소를 다양하게 가공할 수 있다. 그러나, 편의상 주로 이용하는 작업인, 마지막에 요소를 추가하거나 이름이 있는 요소들을 삭제하는 경우 요소 자체에 이미 동일한 메소드가 포함돼 있기 때문에 이 작업을 실행할 때는 리스트를 우선 호출할 필요가 없다.

root.removeChildren("jill");
root.addContent(new Element("jenny"));

JDOM의 또 다른 장점은 한 문서 내에서 혹은 여러 문서 사이에서 요소들을 이동하는 작업이 간편하다는 것이다. 이 때 몇 개의 문서간에 이동하든 관계없이 동일한 코드를 사용할 수 있다.

Element movable = new Element("movable");
parent1.addContent(movable);    // place
parent1.removeContent(movable); // remove
parent2.addContent(movable);    // add

DOM의 경우 요소의 이동이 JDOM에서만큼 쉽지 않은데, 이는 DOM에서는 요소들이 그들을 생성한 객체에 강하게 묶여 있기 때문이다. 따라서 문서간 이동시에는 DOM 요소가 직접 '임포트' 되어야 한다.

JDOM에서 한 가지 유념할 사항은, 요소를 다른 데 추가하기 전에 제거해야 한다는 점이다. 이렇게 해야 트리 내에서 순환이 발생하는 것을 막을 수 있다. detach() 메소드를 이용하면 분리/추가 작업을 라인 하나로 처리할 수 있다.

parent3.addContent(movable.detach());

요소를 다른 부모에 추가하기 전에 먼저 분리하지 않았을 경우, 해당 라이브러리는 Exception을 떨어트릴 것이다(정확하고 도움이 되는 오류 메시지와 함께). 또한 라이브러리는 요소에 스페이스와 같은 부적절한 문자가 포함되지 않도록 요소의 이름과 컨텐트를 확인한다. 또한 단일 루트 요소의 포함 여부, 일관적인 네임스페이스 선언 여부 및 주석과 CDATA 섹션에 금지된 문자열이 없는지 등 기타 여러 규칙도 검증한다. 이를 통해 가능한 한 프로세스 초기 단계에서 문서가 'well-formed' 인지 확인하는 과정이 이루어지게 되는 것이다.

요소 속성의 처리

요소 속성의 예를 들면 다음과 같다.

<table width="100%" border="0"> ... </table>

요소 참조를 통해, 어떤 이름의 속성 값이든 요소에 요청할 수 있다.

String val = table.getAttributeValue("width");

또한 타입 변환과 같은 특별 가공 작업을 위해 속성을 객체로 불러올 수도 있다.

Attribute border = table.getAttribute("border");
int size = border.getIntValue();

속성을 설정하거나 변경하려면 setAttribute()를 사용한다.

table.setAttribute("vspace", "0");

속성을 삭제하려면 removeAttribute()를 사용한다.

table.removeAttribute("vspace");

텍스트 컨텐트를 가진 요소의 예를 들면 다음과 같다.

<description>
  A cool demo
</description>

JDOM에서는 호출을 통해 텍스트를 직접 이용할 수 있다.

String desc = description.getText();

한 가지 유의할 점은, XML 1.0 사양에서는 여백의 보존이 요구되기 때문에 이 경우 '\n A cool demo\n'반환된다는 것이다. 그러나 실제 환경에서는 여백의 포매팅에 대해 크게 유념할 필요가 없으므로 가장자리의 여백을 무시하고 텍스트를 불러오는 편리한 방법이 있다.

String betterDesc = description.getTextTrim();

여백을 아예 없애고 싶다면 스페이스를 이용해 내부의 여백을 표준화하는 getTextNormalize() 메소드를 이용하면 된다. 이 메소드는 다음과 같은 텍스트 컨텐트에 이용할 때 편리한다.

<description>
  Sometimes you have text content with formatting
  space within the string.
</description>

텍스트 컨텐트를 변경하고자 할 때는 setText() 메소드를 이용한다.

description.setText("A new description");

텍스트에 포함된 특수 문자는 모두 문자로서 올바르게 해석되어 출력시에 적절한 의미를 유지하게 된다. 다음 콜을 예로 들어보자.

element.setText("<xml/> content");

내부 저장 영역은 이 문자열을 그대로 문자로 저장할 것이다. 이 컨텐트에 대한 함축적 파싱은 이루어지지 않는다. 출력시에는 다음과 같이 표현된다.

&lt;xml/&gt; content<elt>

이는 이전 setText() 콜의 의미론적 내용을 보존하기 위한 것이다. 따라서, 요소 내에 XML 컨텐트를 포함하고자 한다면 적절한 JDOM 자식 요소 객체를 추가해야 할 것이다.

JDOM에서는 CDATA 섹션을 처리할 수도 있다. CDATA 섹션은 파싱되어서는 안 될 텍스트 블록을 지시한다. 원래 CDATA 섹션은 &lt;와 &gt; 같은 에스케이프 문자열을 과도하게 사용하지 않고도 HTML이나 XML을 손쉽게 포함시킬 수 있게 해주는 문법적인 용어이다. 그러나 JDOM에서는 이를 객체화하여 사용한다. CDATA 섹션을 생성하려면 이 문자열을 CDATA 객체로 래핑하면 된다.

element.addContent(new CDATA("<xml/> content"));

JDOM의 장점은 getText() 콜이 문자열을 CDATA 섹션으로 나타낼 것인지 일일이 물어보지 않고 문자열을 반환한다는 점이다.

혼합 컨텐트의 처리

어떤 요소에는 여백, 주석, 텍스트, 자식 요소 등 수많은 항목들이 포함되어 있다.

<table>

  <!-- Some comment -->
  Some text
  <tr>Some child element</tr>
</table>

어떤 요소에 텍스트와 자식 요소가 모두 들어 있을 경우 ‘혼합 컨텐트’를 포함하고 있다고 한다. 혼합 컨텐트를 처리하는 것은 어려울 수도 있지만, JDOM을 이용하면 쉽게 처리할 수 있다. 텍스트 컨텐트를 불러오고 자식 요소를 네비게이션하는 기본 이용 사례는 간단하게 처리할 수 있다.

String text = table.getTextTrim();  // "Some text"
Element tr = table.getChild("tr");  // A straight reference

주석, 여백 블록, 프로세싱 명령어, 엔티티 참조 등이 필요한 고급 응용의 경우, 혼합 컨텐트를 리스트로서 직접 불러올 수 있다.

List mixedCo = table.getContent();
Iterator itr = mixedCo.iterator();
while (itr.hasNext()) {
  Object o = i.next();
  if (o instanceof Comment) {
    ...
  }
  // Types include Comment, Element, CDATA, DocType,
  // ProcessingInstruction, EntityRef, and Text
}

자식 요소 리스트와 마찬가지로 혼합 컨텐트 리스트를 변경할 경우 원래 문서에도 영향을 미치게 된다.

// Remove the Comment.  It's "1" because "0" is a whitespace block.
mixedCo.remove(1);

자세히 살펴보면, 여기에 Text 클래스가 포함돼 있다는 사실을 알 수 있다. JDOM은 내부적으로 Text 클래스를 이용해 문자열 컨텐트를 저장하는데, 이는 이 문자열이 부모를 갖도록 하고 XPath 액세스를 보다 쉽게 지원하도록 하기 위한 것이다. 원시 컨텐트 리스트에 액세스할 때 텍스트만을 불러오거나 설정할 경우라면 이 클래스에 대해 염려할 필요가 없다.

DocType, ProcessingInstruction, EntityRef 클래스에 대해 보다 자세한 정보는 jdom.org의 API 설명서를 참조하기 바란다.



원문 출처 : http://www.oracle.com/global/kr/magazine/webcolumns/2002/o52jdom.html

2007/05/30 10:29 2007/05/30 10:29
Trackback Address:이 글에는 트랙백을 보낼 수 없습니다

대칭키를 이용한 암호화와 복호화

지난글 에서는 서로 다른 키를 사용하는 암호화와 복호화, 즉 비대칭 암호화(asymmetric encryption)에 관해 설명했다. 하지만 암호화와 복호화는 대칭적으로 작업할 수도 있다. 이 글에서 데이터를 암호화하고 복호화하는데 같은 키를 사용하고 있는 예를 볼 수 있다. 양쪽 다 동일한 키를 사용하기 때문에 복호화는 암호화 과정의 일부를 역으로 적용하면 된다. Blowfish 알고리즘이 대칭 키의 한 예이다. Blowfish 알고리즘은 Java Cryptography Extension (JCE)에 의해 지원되고 javax.crypto.* packages에서 적절한 APIs를 찾을 수가 있다. 현재 JCE에 의해 지원되는 암호 알고리즘(cipher algorithm)은 Blowfish뿐만 아니라, Digital Encryption Standard (DES), Triple DES Encryption (DESede), Password-based encryption algorithm (PBEWithMD5AndDES)이 있다.

대칭 키 알고리즘은 비대칭 키 알고리즘보다 훨씬 빠른 경향이 있다. 게다가 첫번째 글에서 본 것과 같이 암호화될 수 있는 텍스트의 사이즈가 공개 키와 비밀 키를 생성할 때 사용되었던 두개의 소인수 곱의 크기에 의해 좌우된다. 하지만 대칭 키 알고리즘을 사용하면 암호화하고자 하는 대상의 전체 크기에 전혀 제한을 받지 않는다. 대칭 암호 알고리즘(symmetric cipher algorithms)의 종류에 따라 다르지만, 전제 입력 사이즈는 블록 사이즈의 배수여야하고 패딩(padding)이 요구될 수도 있다. 대칭 키와 관련한 문제는 이 키들이 암호화나 복호화에 관련된 파티내에서는 공유되어야만 한다는 데에 있다. 그렇기 때문에 차단이 되거나 공인되지 않은 사용자가 공유하는 등의 문제가 생길 수 있다.

대칭 키를 생성하는 것은 키 쌍을 생성했던 것과 매우 흡사하다. KeyGenerator 클래스의 factory 메소드를 사용하고 스트링값을 알고리즘에 대입하자. generateKey() 메소드를 호출하면 KeyPair 인터페이스 대신 Key 인터페이스를 구현하는 객체를 되받을 수 있다.

  SecretKey key =
         KeyGenerator.getInstance("DES").generateKey();

다음으로 Cipher를 생성하는데, 이는 JCE를 이용할 때 편리하다. 애플리케이션을 바꿀 필요 없이 서로 다른 제공자의 이점을 취하기 위해서 Cipher 클래스의 factory 메소드를 재사용하자. 이하의 방법으로 Cipher를 생성한다.

   Cipher cipher = Cipher.getInstance("DES");

Cipher
는 바이트 어레이 형태로 넘겨진 데이터를 암호화 혹은 복호화하는데 사용된다. 어떤 동작이 호출될지를 지정하는 init()과 그 연산을 실행하는 doFinal() 메소드가 꼭 사용해야만 하는 필수 메소드들이다. 가령, 다음 2줄의 코딩은 바이트 어레이를 암호화하기 위해 생성한 textBytes라고 불리는 cipherkey 인스턴스를 사용하고 있다. 결과는 encryptedBytes라고 불리는 바이트 어레이 내에 저장된다.

   cipher.init(Cipher.ENCRYPT_MODE, key);
   byte[] encryptedBytes =
      cipher.doFinal( textBytes );

다음 프로그램은 위의 값을 모으면서 입력 스트링을 받은 후에 그것을 암호화한다. 그리고나서 암호화된 스트링은 다시 복호화된다.

  1.    import javax.crypto.Cipher;
  2.    import javax.crypto.BadPaddingException;
  3.    import javax.crypto.IllegalBlockSizeException;
  4.    import javax.crypto.KeyGenerator;
  5.    import java.security.Key;
  6.    import java.security.InvalidKeyException;
  7.  
  8.    public class LocalEncrypter {
  9.  
  10.         private static String algorithm = "DESede";
  11.         private static Key key = null;
  12.         private static Cipher cipher = null;
  13.  
  14.         private static void setUp() throws Exception {
  15.             key = KeyGenerator.getInstance(algorithm).generateKey();
  16.             cipher = Cipher.getInstance(algorithm);
  17.         }
  18.  
  19.         public static void main(String[] args)
  20.            throws Exception {
  21.             setUp();
  22.             if (args.length !=1) {
  23.                 System.out.println(
  24.                   "USAGE: java LocalEncrypter " +
  25.                                          "[String]");
  26.                 System.exit(1);
  27.             }
  28.             byte[] encryptionBytes = null;
  29.             String input = args[0];
  30.             System.out.println("Entered: " + input);
  31.             encryptionBytes = encrypt(input);
  32.             System.out.println(
  33.               "Recovered: " + decrypt(encryptionBytes));
  34.         }
  35.  
  36.         private static byte[] encrypt(String input)
  37.             throws InvalidKeyException,
  38.                    BadPaddingException,
  39.                    IllegalBlockSizeException {
  40.             cipher.init(Cipher.ENCRYPT_MODE, key);
  41.             byte[] inputBytes = input.getBytes();
  42.             return cipher.doFinal(inputBytes);
  43.         }
  44.  
  45.         private static String decrypt(byte[] encryptionBytes)
  46.             throws InvalidKeyException,
  47.                    BadPaddingException,
  48.                    IllegalBlockSizeException {
  49.             cipher.init(Cipher.DECRYPT_MODE, key);
  50.             byte[] recoveredBytes =
  51.               cipher.doFinal(encryptionBytes);
  52.             String recovered =
  53.               new String(recoveredBytes);
  54.             return recovered;
  55.           }
  56.    }

커맨드 라인의 파라미터로 어떤 텍스트나 입력할 수 있다. 가령, 커맨드 라인에 다음을 입력하면,

   java LocalEncrypter "Whatever phrase we would like to
    input at this point"

이하의 출력값을 보게 된다.

   Entered: Whatever phrase we would like to
    input at this point
   Recovered: Whatever phrase we would like to
    input at this point

위의 예에서 암호화와 복호화 모두 동일한 Key 객체를 사용해서 이뤄졌다. 암호화와 복호화는 대게 서로 다른 버츄얼 머신에서 발생하기 때문에 안전하게 키를 전송할 수 있는 메소드가 필요하다.

첫번째 글에서 비대칭 암호 알고리즘을 위한 키 쌍을 생성하는 방법을 공부했고, 두번째 글에서는 대칭 키를 사용하는 방법을 살펴보았다. 하지만 비대칭 키와 대칭 키를 결합해서 사용하는 또 다른 테크닉이 있다. 이 기법은 임의대로 대칭 키를 선택해서 데이터를 암호화하는데 사용한다. 그리고는 다른 파티의 공개 키를 이용해서 대칭 키 그 자체가 암호화된다. 그러면 받는 사람은 대칭 키를 암호화하기 위해서 그들의 비밀 키를 이용하고 메시지를 복호화하기 위해 복호화된 키를 이용한다. 비대칭 기법에서 사용되는 모듈러스는 대칭 키를 암호화할 정도의 크기만 되면 된다. 대칭 키는 단일 전송을 위해서 사용된 후 제거된다. 이러한 방법으로 각각의 타입의 단점이 보완된다. 각 주제에 대한 자세한 정보는 다음을 참고하기 바란다.

DES Encryption

from http://kr.sun.com/developers/techtips/c2004_01_16.htm

2007/05/24 01:58 2007/05/24 01:58
Trackback Address:이 글에는 트랙백을 보낼 수 없습니다

KEYPAIRGENERATOR를 이용해서 비대칭 암호 키 구하기

공개 키 암호화(public key encryption)는 사용자가 암호화(encrypt)한 메시지를 복호화(decrypt)하거나 복호화만이 가능한 메시지를 암호화하는데 사용되는 공개 키(public key)를 제공할 수 있도록 해준다. Dr. Ronald L. Rivest, Dr. Adi Shamir, 그리고 Dr. Leonard M. Adleman가 RSA 암호화 코드의 R, S, A에 해당한다. 이들은 공개 키 암호기법(cryptography)으로 ACM's 2002 Turing Award Winners를 수상하였고, ACM Turing Award Winners웹페이지에서 "Early Days of RSA", "Cryptology: A Status Report", "Pre-RSA."에 관한 이들의 프리젠테이션을 볼 수 있다.

RSA 암호화 알고리즘(RSA encryption algorithm)은 임의적이고 상호 독립적인, 가령 p=11, q=31과 같은 크기가 큰 두 개의 소인수를 선택하는 것으로부터 시작한다. 다음으로 N=(p)(q)를 계산하는데, p=11, q=31인 경우에는 N=(11)(31)=341이다. 그리고는 정수 e를 하나 선택하는데 (p-1)(q-1)와 공통인수가 없는 3과 N-1 사이의 범위에서 선택한다. 수 300은 (2)(2)(3)(5)(5)으로 소인수분해 되기 때문에 2, 3, 혹은 5의 배수는 정수 e의 선택범위에서 제외된다. 이 알고리즘에 대입하는 수는 꼭 소인수가 아니어도 되기 때문에 49나 77과 같은 수도 가능하다. 간단하게 e= 7을 대입해보자.

다음으로 (d)(e)=1 (mod(p-1)(q-1))을 성립하는 정수 d가 필요하다. 여기서 mod는 나머지 연산이다. 위의 예제에서 e = 7 이고 (p -1)(q-1) = 300이기 때문에 d는 7d % 300 = 1을 통해 구한다. 연산의 방법은 300의 배수를 하나 이상 찾고 이 중, 7로 나누어 떨어지는 첫번째 숫자를 찾아내면 된다. 다시 말하면, 301, 601, 901 등이 가능한 값들이다. 이중 301은 7로 나누어 떨어지기 때문에(43*7은 301이다.) d는 43이다.

여기서 2쌍의 숫자들이 중요한데, RSA 모듈에 따르면 N = (p)(q)이고, 이 두 쌍의 숫자들이 이 식의 컴포넌트들이다. 첫번째 숫자쌍이 RSA 비밀 키(private key)인 (N,d)이고, 두 번째 숫자쌍이 RSA 공개키(public key)인 (N,e)이다. D는 RSA 비밀 지수(private exponent) (d=7)이고 e는 RSA 공개 지수(public exponent) (e=43)이다. 이미 알고 있는 공개 키는 공개하자. 그렇지만 비밀 키(특히 d)와 최초의 소인수들(p와 q)은 비밀로 유지해야 한다.

그렇다면 자바 프로그래밍 랭귀지가 RSA 구성의 어느 부분에서 쓰여지는 것일까? 이는 java.security 패키지를 통해 쓰여진다. 이 패키지를 이용하면 위에서 설명한 RSA 알고리즘에 필요한 쌍으로 이뤄진 키를 생성할 수 있다. 먼저 KeyPairGenerator의 인스턴스를 생성하고 그것을 비트형식의 원하는 키 사이즈로 초기화한다. 그리고 generateKeyPair()메소드를 호출하면 한 쌍의 RSA 키를 생성할 수 있다.

   KeyPairGenerator generator =
                   KeyPairGenerator.getInstance("RSA");
   generator.initialize(1024);
   KeyPair keyPair = generator.generateKeyPair();

이 알고리즘에서는 factory getInstance() 메소드에 스트링을 입력한다. 알고리즘이 인스톨된 제공자(provider)에 의해 지원되지 않으면 NoSuchAlgorithmException이 발생한다.

각각의 제공자는 반드시 디폴트 초기화를 공급하고 이를 문서화해야한다. 제공자 디폴트(provider default)가 사용자의 요구와 맞아 떨어지면, 중간 KeyPairGenerator 객체 (intermediate KeyPairGenerator object)를 저장할 필요가 없다. 한 줄의 코드로 키 쌍을 간단하게 생성할 수는 있지만 하나 이상의 키 쌍을 생성해야 한다면, KeyPairGenerator 객체를 재사용하는 것이 낫다. 왜냐하면 이는 새로운 KeyPairGenerator 객체를 매번 생성하는 것보다 훨씬 나은 퍼포먼스를 주기 때문이다.

  1.    import java.security.KeyPairGenerator;
  2.    import java.security.NoSuchAlgorithmException;
  3.    import java.security.KeyPair;
  4.  
  5.    public class AsymmetricKeyMaker {
  6.  
  7.       public static void main(String[] args) {
  8.         String algorithm = "";
  9.         if (args.length == 1) algorithm = args[0];
  10.  
  11.         try {
  12.           KeyPair keyPair = KeyPairGenerator
  13.                                .getInstance(algorithm)
  14.                                .generateKeyPair();
  15.  
  16.           System.out.println(keyPair.getPublic());
  17.           System.out.println(keyPair.getPrivate());
  18.  
  19.         } catch (NoSuchAlgorithmException e) {
  20.           System.err.println(
  21.             "usage: java AsymmetricKeyMaker <RSA | DSA>");
  22.         }
  23.  
  24.       }
  25.    }
  26.  


스트링 RSA는 rsa, Rsa 혹은 어떤 형태이든지 상관이 없다. 다음과 같은 프로그램을 실행하면

   java AsymmetricKeyMaker RSA

출력값은 이하와 같다.

   SunJSSE RSA public key:
      public exponent:
        010001
      modulus:
        b24a9b5b ba01c0cd 65096370 0b5a1b92 08f8555e
        7c1b5017 ec444c58 422b4109
        59f2e15d 43714d92 031db66c 7f5d48cd 17ecd74c
        39b17be2 bf9677be d0a0f02d
        6b24aa14 ba827910 9b166847 8154a2fa 919e0a2a
        53a6e79e 7d2933d8 05fc023f
        bdc76eed aa306c5f 52ed3565 4b0ec8a7 12105637
        af11fa21 0e99fffa 8c658e6d

   SunJSSE RSA private CRT key:
      private exponent:
        78417240 9059965d f3843d99 d94e51c2 52628dd2
        490b731e 6fb2317c 66451e7c
        dc3ac25f 519a1ea4 198df4f9 817ebe17 f7c73c00
        a1f96082 348f9cfd 0b63421b
        7f45f131 c363475c c1b25f57 ee029f5e 0848ba74
        ba81b730 ac4c0135 ce46478c
        e462361a 650e3356 f9b7a0c4 b682557d 3655c052
        5e3554bd 970100bf 10dc1b51
      modulus:
        b24a9b5b ba01c0cd 65096370 0b5a1b92 08f8555e
        7c1b5017 ec444c58 422b4109
        59f2e15d 43714d92 031db66c 7f5d48cd 17ecd74c
        39b17be2 bf9677be d0a0f02d
        6b24aa14 ba827910 9b166847 8154a2fa 919e0a2a
        53a6e79e 7d2933d8 05fc023f
        bdc76eed aa306c5f 52ed3565 4b0ec8a7 12105637
        af11fa21 0e99fffa 8c658e6d
      public exponent:
        010001
      prime p:
        e768033e 21646824 7bd031a0 a2d9876d 79818f8f
        2d7a952e 559fd786 2993bd04
        7e4fdb56 f175d04b 003ae026 f6ab9e0b 2af4a8d7
        ffbe01eb 9b81c75f 0273e12b
      prime q:
        c53d78ab e6ab3e29 fd98d0a4 3e58ee48 45a366ac
        e94dbd60 ea24ffed 0c67c5fd
        3628ea74 88d1d1ad 58d7f067 20c1e3b3 db52adf3
        c421d88c 4c4127db d03592c7
      prime exponent p:
        e09942b4 76029755 f9da3ba0 d70edcf4 337fbdcf
        d0eb6e89 f74f5a07 7ca94947
        6835a805 3dfd047b 17310dc8 a39834a0 504400f1
        0ce6e5c4 413df83d 4e0b1cdb
      prime exponent q:
        829b8afd a1984168 c2d1df4e f32e2653 5b31b17a
        cc5ebb09 a2e26f4a 040def90
        15be104a ac92ebda 72db4308 b72b4ce1 bb58cb71
        80adbcdc 625e3ecb 92daf6df
      crt coefficient:
        4d8190c5 7730b729 00a8f1b4 ae526300 b22d3e7d
        d64df98a c1b19889 5240141b
        0e618ff4 be597979 95195c51 0866c142 30b37a86
        9f3ef519 a3ae6469 14075097

자바 프로그래밍이 처음이라면 위에서 적절하게 오버로딩된 toString() 메소드의 출력값을 주목할 필요가 있다. "SunJSSE RSA public key:"으로 시작하는 텍스트가 RSAPublicKey 클래스 내의 toString() 메소드를 호출한 결과값이다. 이는 e라고 불리는 공개 지수(public exponent)와 모듈러스(modulus) N으로 구성되어 있다. "SunJSSE RSA private key:"로 시작하는 텍스트는 RSAPrivateKey 클래스 내의 동일한 메소드를 호출한 결과이다. 이는 비밀 지수 d, 모듈러스 N, 공개 지수 e 뿐만 아니라 생성되는 소인수들에 대한 정보도 포함하고 있다. 만약 데이터를 암호화하고 있는 중이라면, 공개 키 값을 전송할 때 주의해야 한다. 하지만, 이 값들이 전송을 인증하는데 사용되는 중이라면, 다른 사람들이 파일을 조회 혹은 검증할 수 있도록 이를 공개로 해 놓아야 한다.

코드를 재실행하고 스트링 DSA 를 입력하면 공개 키와 비밀 키 모두에서 p, q 와g의 값을 찾을 수가 있다. y 값은 공개 키에서, x값은 비밀 키에서 제공된다. 하지만 알고리즘이 다르기 때문에 이러한 정보는 다른 방법으로 계산되고 공유되어야 한다.

그렇다면 암호화에서 이 키 쌍을 어떻게 사용할 것인가? RSA의 경우를 먼저 살펴보면, 암호화하고 싶은 텍스트를 정하고 그것을 모듈러스보다 작은 수 m으로 바꾼다. 이에 관해서는 후에 더 언급하도록 하겠다. 여기에서 크기가 큰 소인수를 선택하는 이유를 알 수 있는데, 이는 소인수의 곱이 암호화 될 수 있는 것들의 사이즈를 결정하기 때문이다. 예를 들면 m=2라고 가정하자. 그러면 사용자는 c=m^e (mod N)를 계산해서 암호화하기 시작한다. 바꾸어 말하자면, 암호화하는 수를 공개 지수의 거듭제곱까지 증가시키고 이를 모듈러스로 나눈 나머지를 구한다. 이 예에서 2^43 는 341의 배수보다 8이 더 크기 때문에 c = 8이라는 암호화된 메시지를 전송해야 한다.

복호화(decryption)는 비밀 키를 이용해서 프로세스를 반복함으로써 이뤄진다. c^d(mod N)를 이용하자. 예를 통해 살펴보면, 먼저 8^7 을 계산하고 이를 341으로 나눈다. 나머지를 구해보면 2라는 메시지를 받게 된다. 이것은 우연히 나온 값이 아니다. 이는 m^(e d) = m (mod N)를 나타내는 Fermat의 정리 애플리케이션을 따른 것이다. 보안을 위해서 각각의 파티는 다른 파티의 공개 키를 이용해서 암호화하게 된다.

RSA 알고리즘에 대한 정보는 RSA, DSA 알고리즘에 대한 정보는 DSA를 참고하기 바란다.

from http://kr.sun.com/developers/techtips/c2004_01_16.htm

2007/05/24 01:56 2007/05/24 01:56
Trackback Address:이 글에는 트랙백을 보낼 수 없습니다

Struts File Upload

Struts File Upload.

In this tutorial you will learn how to use Struts to write program to upload files. The interface org.apache.struts.upload.FormFile is the heart of the struts file upload application. This interface represents a file that has been uploaded by a client. It is the only interface or class in upload package which is typically referenced directly by a Struts application.

Creating Form Bean
Our form bean class contains only one property theFile,  which is of type org.apache.struts.upload.FormFile. Here is the code of FormBean (StrutsUploadForm.java):

package roseindia.net;
import org.apache.struts.action.*;
import org.apache.struts.upload.FormFile;

/**

* @author Deepak Kumar
* @Web http://www.roseindia.net
* @Email roseindia_net@yahoo.com
*/

/**
* Form bean for Struts File Upload.
* */
public class StrutsUploadForm extends ActionForm {


  private
FormFile theFile;
  /**  
 
* @return Returns the theFile.
 
 
*/

 
public FormFile getTheFile() {  
    
return theFile;

 
}
 
/**  
 
* @param theFile The FormFile to set.
 
 
*/

 
public void setTheFile(FormFile theFile) {  
    this.theFile = theFile;

 
}
}

Creating Action Class
Our action class simply calls the getTheFile() function on the FormBean object to retrieve the reference of the uploaded file. Then the reference of the FormFile is used to get the uploaded file and its information. Here is the code of our action class(StrutsUploadAction.java):
package roseindia.net;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.struts.action.Action;
import org.apache.struts.action.ActionForm;
import org.apache.struts.action.ActionForward;
import org.apache.struts.action.ActionMapping;
import org.apache.struts.upload.FormFile;

/**
* @author Deepak Kumar
* @Web http://www.roseindia.net
* @Email roseindia_net@yahoo.com
*/

/**
* Struts File Upload Action Form.
* */

public class StrutsUploadAction extends Action {


 
public ActionForward execute( ActionMapping mapping,
   
          ActionForm form,
 
         
HttpServletRequest request,
 
         
HttpServletResponse response) throws Exception{
 

     StrutsUploadForm myForm =
(StrutsUploadForm)form;
     
    
// Process the FormFile
     
    
FormFile myFile = myForm.getTheFile();
     
    
String contentType = myFile.getContentType();
     
    
String fileName    = myFile.getFileName();
     
    
int fileSize       = myFile.getFileSize();
     
    
byte[] fileData    = myFile.getFileData()
;
   
     System.out.println("contentType: " + contentType);
     
     System.out.println("File Name: " + fileName)
;
 
  System.out.println("File Size: " + fileSize)
;

     return mapping.findForward("success");

 
}
}

Defining form Bean in struts-config.xml file
Add the following entry in the struts-config.xml file for defining the form bean:
<form-bean name="FileUpload" type="roseindia.net.StrutsUploadForm"/>

Defining Action Mapping
Add the following action mapping entry in the struts-config.xml file:

<action path="/FileUpload" type="roseindia.net.StrutsUploadAction"
       name="FileUpload" scope="request" validate="true"
       input="/pages/FileUpload.jsp">    
    <forward name="success" path="/pages/uploadsuccess.jsp"/>
</action>

Developing jsp page
Code of the jsp (FileUpload.jsp) file to upload is as follows:

<%@ taglib uri="/tags/struts-bean" prefix="bean" %>
<%@ taglib uri="/tags/struts-html" prefix="html" %>

<html:html locale="true">
<head>
<title>Struts File Upload Example</title>
<html:base/>
</head>

<body bgcolor="white">
<html:form action="/FileUpload" method="post" enctype="multipart/form-data">

<table>
  <tr><td align="center" colspan="2">
  <font size="4">Please Enter the Following Details</font>
  </td></tr>
  <tr><td align="left" colspan="2">
  <font color="red"><html:errors/></font>
  </td></tr>
  <tr><td align="right"> File Name </td>
  <td align="left"> <html:file property="theFile"/> </td> </tr>
  <tr> <td align="center" colspan="2"> <html:submit>Upload File</html:submit>
  </td> </tr>
  </table>

</html:form>
</body>
</html:html>

Note that we are setting the encrypt property of the form to enctype="multipart/form-data".
code for the success page (uploadsuccess.jsp) is:

<html>
  <head>
  <title>Success</title>
  </head>

  <body>
  <p align="center"><font size="5" color="#000080">File Successfully Received</font></p>
  </body>
</html>

Add the following line in the index.jsp to call the form.

<li>
  <html:link page="/pages/FileUpload.jsp">Struts File Upload</html:link>
  <br> Example shows you how to Upload File with Struts.
</li>

Building Example and Testing

2007/05/24 01:50 2007/05/24 01:50
Trackback Address:이 글에는 트랙백을 보낼 수 없습니다

JAVA 5 한글 api

2007/05/15 09:54

서비 JAVA ,

JAVA SDK 1.5 API 를 강혜원님이 한글 번역 해 주신 문서입니다.

JAVA_1.5_API_KO_다운로드
2007/05/15 09:54 2007/05/15 09:54
Trackback Address:이 글에는 트랙백을 보낼 수 없습니다