자바 플랫폼의 경우, 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 , 쿠키 캐시, 테스트 프로그램을 사용해 보기로 하자. 테스트 프로그램은 아래와 같은 형태를 띠고 있다. import java.io.*; import java.net.*; import java.util.*; public class Fetch { public static void main(String args[]) throws Exception { if (args.length == 0) { System.err.println("URL missing"); System.exit(-1); } String urlString = args[0]; CookieHandler.setDefault(new ListCookieHandler()); URL url = new URL(urlString); URLConnection connection = url.openConnection(); Object obj = connection.getContent(); url = new URL(urlString); connection = url.openConnection(); obj = connection.getContent(); } } 먼저 이 프로그램은 간략하게 정의될 ListCookieHandler 를 작성하고 설치한다. 그런 다음 URL(명령어 라인에서 입력)의 접속을 열어 내용을 읽는다. 이어서 프로그램은 또 다른 URL의 접속을 열고 동일한 내용을 읽는다. 첫 번째 내용을 읽을 때 응답에는 저장될 쿠키가, 두 번째 요청에는 앞서 저장된 쿠키가 포함된다. 이제 이것을 관리하는 방법에 대해 알아보기로 하자. 처음에는 URLConnection 클래스를 이용한다. 웹 상의 리소스는 URL을 통해 액세스할 수 있으며, URL 작성 후에는 URLConnection 클래스의 도움을 받아 사이트와의 통신을 위한 인풋 또는 아웃풋 스트림을 얻을 수 있다. String urlString = ...; URL url = new URL(urlString); URLConnection connection = url.openConnection(); InputStream is = connection.getInputStream(); // .. 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의 모든 헤더를 열거한다 import java.net.*; import java.util.*; public class ListHeaders { public static void main(String args[]) throws Exception { if (args.length == 0) { System.err.println("URL missing"); } String urlString = args[0]; URL url = new URL(urlString); URLConnection connection = url.openConnection(); Map<String,List<String>> headerFields = connection.getHeaderFields(); Set<String> set = headerFields.keySet(); Iterator<String> itor = set.iterator(); while (itor.hasNext()) { String key = itor.next(); System.out.println("Key: " + key + " / " + headerFields.get(key)); } } } 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-cookie 나 Set-Cookie2 같은 다른 해당 헤더로 확장될 수 있다. List<String> setCookieList = responseHeaders.get("Set-Cookie"); 쿠키의 리스트를 확보한 후 각 쿠키를 반복(loop)하고 저장한다. 쿠키가 이미 존재할 경우에는 기존의 것을 교체하도록 한다. if (setCookieList != null) { for (String item : setCookieList) { Cookie cookie = new Cookie(uri, item); // Remove cookie if it already exists in cache // New one will replace it for (Cookie existingCookie : cache) { ... } System.out.println("Adding to cache: " + cookie); cache.add(cookie); } } 여기서 ‘캐시’는 데이터베이스에서 Collections Framework에서 List 에 이르기까지 어떤 것이든 될 수 있다. Cookie 클래스는 나중에 정의되는데, 이는 사전 정의되는 클래스에 속하지 않는다. 본질적으로, 그것이 put() 메소드에 대해 주어진 전부이며, 응답 헤더 내의 각 쿠키에 대해 메소드는 쿠키를 캐시에 저장한다. get() 메소드는 정반대로 작동한다. URI에 해당되는 캐시 내의 각 쿠키에 대해, get() 메소드는 이를 요청 헤더에 추가한다. 복수의 쿠키에 대해서는 콤마로 구분된(comma-delimited) 리스트를 작성한다. get() 메소드는 맵을 반환하며, 따라서 메소드는 기존의 헤더 세트로 Map 아규먼트를 취하게 된다. 그 아규먼트에 캐시 내의 해당 쿠키를 추가해야 하지만 아규먼트는 불변의 맵이며, 또 다른 불변의 맵을 반환해야만 한다. 따라서 기존의 맵을 유효한 카피에 복사한 다음 추가를 마친 후 불변의 맵을 반환해야 한다.
get() 메소드를 구현하기 위해서는 먼저 캐시를 살펴보고 일치하는 쿠키를 얻은 다음 만료된 쿠키를 모두 제거하도록 한다.
// Retrieve all the cookies for matching URI // Put in comma-separated list StringBuilder cookies = new StringBuilder(); for (Cookie cookie : cache) { // Remove cookies that have expired if (cookie.hasExpired()) { cache.remove(cookie); } else if (cookie.matches(uri)) { if (cookies.length() > 0) { cookies.append(", "); } cookies.append(cookie.toString()); } } 이 경우에도 Cookie 클래스는 간략하게 정의되는데, 여기에는 hasExpired() 와 matches() 등 2개의 요청된 메소드가 표시되어 있다. hasExpired() 메소드는 특정 쿠키의 만료 여부를 보고하고, matches() 메소드는 쿠키가 메소드에 패스된 URI에 적합한지 여부를 보고한다. get() 메소드의 다음 부분은 작성된 StringBuilder 오브젝트를 취하고 그 스트링필드 버전을 수정 불가능한 Map에 put한다(이 경우에는 해당 키 ‘Cookie’를 이용).
// Map to return Map<String, List<String>> cookieMap = new HashMap<String, List<String>>(requestHeaders); // Convert StringBuilder to List, store in map if (cookies.length() > 0) { List<String> list = Collections.singletonList(cookies.toString()); cookieMap.put("Cookie", list); } return Collections.unmodifiableMap(cookieMap); 다음은 런타임의 정보 표시를 위해 println 이 일부 추가되어 완성된 CookieHandler 정의이다. import java.io.*; import java.net.*; import java.util.*; public class ListCookieHandler extends CookieHandler { // "Long" term storage for cookies, not serialized so only // for current JVM instance private List<Cookie> cache = new LinkedList<Cookie>(); /** * Saves all applicable cookies present in the response * headers into cache. * @param uri URI source of cookies * @param responseHeaders Immutable map from field names to * lists of field * values representing the response header fields returned */ public void put( URI uri, Map<String, List<String>> responseHeaders) throws IOException { System.out.println("Cache: " + cache); List<String> setCookieList = responseHeaders.get("Set-Cookie"); if (setCookieList != null) { for (String item : setCookieList) { Cookie cookie = new Cookie(uri, item); // Remove cookie if it already exists // New one will replace for (Cookie existingCookie : cache) { if((cookie.getURI().equals( existingCookie.getURI())) && (cookie.getName().equals( existingCookie.getName()))) { cache.remove(existingCookie); break; } } System.out.println("Adding to cache: " + cookie); cache.add(cookie); } } } /** * Gets all the applicable cookies from a cookie cache for * the specified uri in the request header. * * @param uri URI to send cookies to in a request * @param requestHeaders Map from request header field names * to lists of field values representing the current request * headers * @return Immutable map, with field name "Cookie" to a list * of cookies */ public Map<String, List<String>> get( URI uri, Map<String, List<String>> requestHeaders) throws IOException { // Retrieve all the cookies for matching URI // Put in comma-separated list StringBuilder cookies = new StringBuilder(); for (Cookie cookie : cache) { // Remove cookies that have expired if (cookie.hasExpired()) { cache.remove(cookie); } else if (cookie.matches(uri)) { if (cookies.length() > 0) { cookies.append(", "); } cookies.append(cookie.toString()); } } // Map to return Map<String, List<String>> cookieMap = new HashMap<String, List<String>>(requestHeaders); // Convert StringBuilder to List, store in map if (cookies.length() > 0) { List<String> list = Collections.singletonList(cookies.toString()); cookieMap.put("Cookie", list); } System.out.println("Cookies: " + cookieMap); return Collections.unmodifiableMap(cookieMap); } } 퍼즐의 마지막 조각은 Cookie 클래스 그 자체이며, 대부분의 정보는 생성자(constructor) 내에 존재한다. 생성자 내의 정보 조각(비트)들을 uri 및 헤더 필드로부터 파싱해야 한다. 만료일에는 하나의 포맷이 사용되어야 하지만 인기 있는 웹 사이트에서는 복수의 포맷이 사용되는 경우를 볼 수 있다. 여기서는 그다지 까다로운 점은 없고, 쿠키 경로, 만료일, 도메인 등과 같은 다양한 정보 조각을 저장하기만 하면 된다. public Cookie(URI uri, String header) { String attributes[] = header.split(";"); String nameValue = attributes[0].trim(); this.uri = uri; this.name = nameValue.substring(0, nameValue.indexOf('=')); this.value = nameValue.substring(nameValue.indexOf('=')+1); this.path = "/"; this.domain = uri.getHost(); for (int i=1; i < attributes.length; i++) { nameValue = attributes[i].trim(); int equals = nameValue.indexOf('='); if (equals == -1) { continue; } String name = nameValue.substring(0, equals); String value = nameValue.substring(equals+1); if (name.equalsIgnoreCase("domain")) { String uriDomain = uri.getHost(); if (uriDomain.equals(value)) { this.domain = value; } else { if (!value.startsWith(".")) { value = "." + value; } uriDomain = uriDomain.substring(uriDomain.indexOf('.')); if (!uriDomain.equals(value)) { throw new IllegalArgumentException( "Trying to set foreign cookie"); } this.domain = value; } } else if (name.equalsIgnoreCase("path")) { this.path = value; } else if (name.equalsIgnoreCase("expires")) { try { this.expires = expiresFormat1.parse(value); } catch (ParseException e) { try { this.expires = expiresFormat2.parse(value); } catch (ParseException e2) { throw new IllegalArgumentException( "Bad date format in header: " + value); } } } } } 클래스 내의 다른 메소드들은 단지 저장된 데이터를 반환하거나 만료 여부를 확인한다. public boolean hasExpired() { if (expires == null) { return false; } Date now = new Date(); return now.after(expires); } public String toString() { StringBuilder result = new StringBuilder(name); result.append("="); result.append(value); return result.toString(); } 쿠키가 만료된 경우에는 ‘match’가 표시되면 안 된다. public boolean matches(URI uri) { if (hasExpired()) { return false; } String path = uri.getPath(); if (path == null) { path = "/"; } return path.startsWith(this.path); } Cookie 스펙이 도메인과 경로 양쪽에 대해 매치를 수행할 것을 요구한다는 점에 유의해야 한다. 단순성을 위해 여기서는 경로 매치만을 확인한다.
아래는 전체 Cookie 클래스의 정의이다. import java.net.*; import java.text.*; import java.util.*; public class Cookie { String name; String value; URI uri; String domain; Date expires; String path; private static DateFormat expiresFormat1 = new SimpleDateFormat("E, dd MMM yyyy k:m:s 'GMT'", Locale.US); private static DateFormat expiresFormat2 = new SimpleDateFormat("E, dd-MMM-yyyy k:m:s 'GMT'", Local.US); /** * Construct a cookie from the URI and header fields * * @param uri URI for cookie * @param header Set of attributes in header */ public Cookie(URI uri, String header) { String attributes[] = header.split(";"); String nameValue = attributes[0].trim(); this.uri = uri; this.name = nameValue.substring(0, nameValue.indexOf('=')); this.value = nameValue.substring(nameValue.indexOf('=')+1); this.path = "/"; this.domain = uri.getHost(); for (int i=1; i < attributes.length; i++) { nameValue = attributes[i].trim(); int equals = nameValue.indexOf('='); if (equals == -1) { continue; } String name = nameValue.substring(0, equals); String value = nameValue.substring(equals+1); if (name.equalsIgnoreCase("domain")) { String uriDomain = uri.getHost(); if (uriDomain.equals(value)) { this.domain = value; } else { if (!value.startsWith(".")) { value = "." + value; } uriDomain = uriDomain.substring( uriDomain.indexOf('.')); if (!uriDomain.equals(value)) { throw new IllegalArgumentException( "Trying to set foreign cookie"); } this.domain = value; } } else if (name.equalsIgnoreCase("path")) { this.path = value; } else if (name.equalsIgnoreCase("expires")) { try { this.expires = expiresFormat1.parse(value); } catch (ParseException e) { try { this.expires = expiresFormat2.parse(value); } catch (ParseException e2) { throw new IllegalArgumentException( "Bad date format in header: " + value); } } } } } public boolean hasExpired() { if (expires == null) { return false; } Date now = new Date(); return now.after(expires); } public String getName() { return name; } public URI getURI() { return uri; } /** * Check if cookie isn't expired and if URI matches, * should cookie be included in response. * * @param uri URI to check against * @return true if match, false otherwise */ public boolean matches(URI uri) { if (hasExpired()) { return false; } String path = uri.getPath(); if (path == null) { path = "/"; } return path.startsWith(this.path); } public String toString() { StringBuilder result = new StringBuilder(name); result.append("="); result.append(value); return result.toString(); } } 이제 조각들이 모두 확보되었으므로 앞의 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 구현에 관한 내용을 찾아볼 수 있다. Back to Top |