UTF-8로 설정한 MySQL 4.1 이상 Tomcat 5.0 이상에서 JDBC접속 URL설정

회사의 인턴에 대한 교육을 목적으로 정말 오랜만에  Tomcat , MySQL 조합으로 예제 코드를
작성 하고 있었다.

톰캣이랑 MySQL의 조합을 마지막으로 사용 해 본게 톰캣 3.X 시절의 일이다.

현재의 조합은 톰캣 5.0 + MySQL 4.1.22 에 utf-8 로 캐릭터 셋을 설정 한데다
서블릿 필터와 JDBC 드라이버 버전업으로  예전의 그 악몽같던 JDBC 한글문제는 사라졌을거라 믿었다..

그러나 왠걸.. 코드를 작성하고 돌려보니 java 클래스 단까지는 한글이 잘 전달 되는데
db에 인서트 하는데 아래와 같은 익셉션이  발생...

MysqlDataTruncation 발생..


이때의  connection url은
jdbc:mysql://localhost:3306/subby_test?useUnicode=true&characterEncoding=UTF8

이 문제는 꽤 간단하게 해결 할 수 있었다.
url에 Truncation 옵션을 false 로 주어서 해결 할 수 있었지만 여전히 DB에는 한글이 안들어가고 있다.

생 ㅈㄹ을 다 해 본다. JDBC버전 별로 봐꿔가며 컴파일과 실행.. 그옜날 썼던 new String() 하며
캐릭터셋 바꿔보기..  그래도 안된다..  그만큼 짜증도 밀려오고..

잠시 머리를 식히고 다시 차근차근 생각 해 봤다. 우선 톰캣의 JSP와 MYSQL의 캐릭터 셋은
분명히 utf-8인 상황이다.  그럼 순순히 안 될리가 없지않나?  역시나 JDBC 드라이버 문제인가?
JDBC드라이버를 최신의 안정버전인 5.0.6 으로 선택하고 JDBC 메뉴얼을 살피기 시작했다..

그러다 찾게 된 희망의 한 단락
Using the UTF-8 Character Encoding - Prior to MySQL server version 4.1, the UTF-8 character encoding was not supported by the
server, however the JDBC driver could use it, allowing storage of multiple character sets in latin1 tables on the server.
Starting with MySQL-4.1, this functionality is deprecated. If you have applications that rely on this functionality, and can not upgrade
them to use the official Unicode character support in MySQL server version 4.1 or newer, you should add the following property
to your connection URL:
useOldUTF8Behavior=true

쌍.. 장난하냐? 장난해? 이놈은 뻑하면 옵션을 바꿔...
답은 useOldUTF8Behavior 에 있는 듯했다.. 적용하고 돌려보니 깔끔..
이런 옵션이야 메뉴얼에서 찾는게 정석이긴 하지만, MySQL은 버전이나 서버 설정에 따른 옵션 변화가
너무 많아 난감한 경우가 한두번이 아니다..

아~ 내 3시간은 어딜가서 하소연 하나...

최종적으로 문제를 해결한 utf8 과 MySQL JDBC Driver 5.0 에서의 DB URL은
jdbc:mysql://localhost:3306/subby_test?useUnicode=true&characterEncoding=UTF8&jdbcCompliantTruncation=false&useOldUTF8Behavior=true
2007/07/10 11:49 2007/07/10 11:49
Trackback Address:이 글에는 트랙백을 보낼 수 없습니다
  1. 2008/05/12 16:56
    애드널의 생각 Tracked from addnull's me2DAY
  2. 2011/06/30 14:53
    Mysql 저장시 한글문제 Tracked from IT 잡동산
  1. Blog Icon
    카우보이

    헐 감사합니다. 저두 이것저것 삽질해보다 저위에 올리신 옵션쓰고나서야 제대로 출력이 되네요

    2시간 삽질

  2. Blog Icon
    Sean

    Wow 덕분에 잘 해결했습니다.

넷빈즈를 이용한 Open Office 플러그인 개발

아래는 넷빈즈 5.5와 Open Office 개발 플러그인을 통하여 Open Office플러그인의
개발 과정을 보여주는 스크린 캐스트입니다.

넷빈즈의 오픈오피스 플러그인은 OpenOffice.org APIs를 자바에서 이용할 수 있도록
idl파일과 Open Office 플러그인 개발에 필요한 클레스, 메소드를 생성 해 주며
플러그인 빌드시 오픈오피스 플러그인에 등록 절차까지 완료 해 줍니다.



스크린 캐스트 출처 : netbeans.org

2007/07/09 11:53 2007/07/09 11:53
Trackback Address:이 글에는 트랙백을 보낼 수 없습니다

스트리트 파이터 vs. 모탈컴벳 플래시 무비

플래시 무비의 용량이 상당하므로 로딩에 시간이 좀 걸릴 수 있습니다.

스트리트 파이터와 모탈컴벳의 전투시스템을 알고 계신다면 재미있게 보실 수 있는 영상입니다.
플래시로 이렇게 만들어 낸 제작자의 노력이 느껴질 정도로 화려한 이펙트와 모션을 감상 하실
수 있습니다.

 

원본 출처 : http://www.newgrounds.com/portal/view/377529

2007/07/08 02:01 2007/07/08 02:01
Trackback Address:이 글에는 트랙백을 보낼 수 없습니다

3류 B급 영화를 찾는사람 이라면.. :: 프로그맨

나는 잘 만들어진 대작영화 만큼이나 3류 B급 영화도 즐겨보는 편이다.
왠지 3류 B급 영화 하면. 그런 류의 영화만이 보여 줄 수 있는 엉성함이라든지
그 영화에서만 볼 수 있는 유니크한 요소들을 찾아 내는 재미가 쏠쏠하기 때문이다.

모 포탈 VOD 란에 프로그맨이라는 영화가 상영중이었다.

프로그맨 포스터


우선 이 영화에 달린 사람들의 평가를 보자..

현명해지자 2007-07-07 오후 6:01:58
리플 달다가 실수로 재밋다에 한표줌 -_-;;

현명해지자 2007-07-07 오후 6:01:23
밑에분들 재미없다해서 영화는안보고 엔딩만 봤는데 ㅎㅎㅎ 개구리 춤추네요 -_-;;
보고나서 뻥해짐 ! 영구와 땡칠이 같은 영화가 생각나는군요

쫄깃. 2007-07-07 오전 11:50:35
일단 거두절미하고 감동적이였다. 초반 녹색인형을보며 심각한연기를할수있는 연기자들에게 찬사를보낸다.
특히 여주인공의 마지막 대사는 일품이다. 엔딩크레딧의 프로그맨 춤은 따라하고 싶을 정도..

슈레기 2007-07-07 오전 9:27:01
어이가 없네...나참

세바` 2007-07-07 오전 6:40:22
**** 관계자 여러분 어디서 이런 4류 B급영화를 발굴하시는지.. 존경합니다;;
B급영화 매니아에겐 도전과제가 될 수 도 있겠네요

어이없당 2007-07-05 오후 1:07:31
어이없다. 예전의 더플라이의 한장면...

QnRn 2007-07-05 오전 10:52:57
진짜 장난이다.ㅎㅎ..참 로또하는 심정으로 영화찍었구나...

김선아 2007-07-04 오후 8:21:53
극장개봉 남들이보던거나 보는것은 취향없고 남들안보는것에주로취향..

비내린다 2007-07-04 오후 4:38:30
황당하지만...웃긴다. 개구리가 넘 허접하지만...발상은 잼난다...ㅋ

심한영화 2007-07-04 오후 2:22:28
우뢰매 수준보다 유치합니다. 아주 정말... 웃겨버려요

우와.. 2007-07-04 오후 1:58:22
너무심하다.. 언젠적 영화인지..할일없어 왠만하면 보는데..ㅎㅎㅎ 할말잃었음

꽃띠 2007-07-04 오후 1:02:39
아무문제많아~쑤레기야!



얼마나 황당한 영화기에 그 흔한 악플마저 보이지 않는다.. 그냥 허탈하단다..
이런 댓글을 보지 않았더라도 이 영화를 관람 했겠지만.. 댓글을 보고나니 궁금증이 더해만 간다.
그래서 열심히 봤다. 리플 단 사람들 이야기가 전부 맞더라..


영화 하나를 만들어내기위해 들어가는 자금, 인력.. 거기에 관계된 사람들의 노력마저 깍아내릴 생각은
추호도 없다.
하지만, 프로그맨은 상업영화로는 빵점짜리다. 시나리오, 배우, 연출, 촬영 뭐 하나 건질 만 한게 없다.
( 아니, 굳이 찾아내자면 눈요기용 서비스 씬 몇개..)
엉성한 시나리오와 엉성한 연출/연기가 합쳐지면 어떤 영화가 만들어지는지 궁금하신분이나
리플의 누구 말마따나 B급영화 섭렵에 도전 하고 있는사람이라면 강력 추천 한다.

안그래도 아무생각 없는 나지만 정말, 아무생각 없이 간만에 유쾌하게 본 영화.
2007/07/07 22:57 2007/07/07 22:57
Trackback Address:이 글에는 트랙백을 보낼 수 없습니다

브라우저별 DOM 과 innerHTML 수행 속도 비교

원문은 http://blog.naver.com/ungahah/40032248947
입니다. 블로그 원문에는 이미지가 보이지 않아서 부득이 편집 하게 되었습니다.

Gleb Lebedev 라는 사람이 아주 재미난 실험을 해서 소개 한다.
그는 간단한 페이지를 통해(구글메인같은) 자바스크립트의 성능테스트 를 하고 싶어했다.
그리고 그 결과를 오페라9, 파이어폭스 1.5, 익스플로러 6 에서 비교했다.

테스트는 이렇다.
그는 구글 메인페이지전체(라고해봐야 몇줄 안되지만..)를 innerHTML 속성과 document.createElement를 이용해 생성하는 함수를 짰다.(소스 )
그리고 그걸 1초동안 반복해서 실행한다.
결국 초당 구글 메인페이지를 몇번이나 만들어 낼수 있느냐를 테스트 한거다.

브라우저별 DOM 과 innerHTML 수행 속도 비교

이미지 출처 : http://ajaxian.com/archives/benchmark-dom-vs-innerhtml


결과가 아주 놀랍다.
오페가 9.01 는 innerHTML로 돌리리때 1초에 1320번이라는 경의적인 수치를 보여준다!
요즘 한참 잘나가는 불여우에 비해서도 3배가 넘는 수치다.
자바스크립트처리 성능만 놓고 볼때는 왜 오페라가 3브라우저중 밀리는지 이해불가다.
IE6은 초라하기 그지 없다. 더군다나 32비트, 64비트 버전이 별만 차이가 없다. (IE 7.0이 출시  됬으니 따로 한번 해봐야겠다. ^^)

브라우저별 성능은 이쯤하고..

중요한건 DOM과 innerHTML과의 비교다.
createElementinnerHTML은 모든 브라우저가 가각 3배 가까이 차이가 난다.
왜 이렇게 차이나 나는 것일까?
createElement는 W3C에서 정한 HTML DOM을 다루는 표준방식이며 HTML의 요소를 객체로 접근도록 해서 프로그램을 좀더 프로그램답게 무언가 '짜고 있다는' 느낌이 들게 하는 반면,  innerHTML은 HTML을 몽창 문자열로 넘겨서 무언가를 '쓰고 있다는'느낌들 들게하는데  말이다.
하지만 이건 조금만 생각해보면 당연한 결과다.
위의 소스를 보면 알겠지만, innerHTML은 이 속성을 한번 호출하는 것으로 끝나지만,
createElement는 그렇지 않다. 개별 HTML 요소를 모두 생성하여 해당 style과 property 값을 넣어주어야 하고, 그걸 형태에 맞게 append를 시켜야 한다.
위의 구글 메인을 만들어내는데 createElement는 자그마치 69번이나 호출된다.
느린게 당연한거다.

결국 용도에 맞게 쓰는것이 중요하단 무미건조한 결론을 내릴 수 밖에 없다.
이런 테스트가 별 근거없이 개발자가 자신의 기호에 따라 한쪽에 편향하기 보다, 성능과 객체 컨트롤의 용의함을 잘 저울질 해서 적절히 쓸 수 있었으면 좋겠다.

p.s. Ajaxian 에 가서 Comment를 보면 사람들이 자신의 PC에서 테스트 한 다양한 결과를 볼 수 있다. ^^
2007/07/07 01:32 2007/07/07 01:32
Trackback Address:이 글에는 트랙백을 보낼 수 없습니다

中島美嘉 - 見えない星 :: 나카시마 미카 - 보이지 않는 별

 
中島美嘉 나카시마 미카

영화 '나나'에서 나나역으로 원작의 분위기를 잘 살렸다는 평을 받은 가수 나카시마 미카.
박효신의 눈의꽃 원곡인 雪の華를 부른 가수로도 잘 알려져있다.

아래는 드라마 '파견의 품격' OST에 수록된 見えない星이다.

가사 more..


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

패트레이버 Patlabor OVA 13화~16화 完







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

패트레이버 Patlabor OVA 05화~08화







2007/07/05 16:51 2007/07/05 16:51
Trackback Address:이 글에는 트랙백을 보낼 수 없습니다

패트레이버 Patlabor OVA 09화~12화





2007/07/05 11:14 2007/07/05 11:14
Trackback Address:이 글에는 트랙백을 보낼 수 없습니다

패트레이버 Patlabor OVA 01화~04화

트랜스포머를 개봉당일 심야편으로 보고 왔다.

트랜스포머는 누가 봐도 헐리웃 오락물로 분류 하는것은에 동의 할 것이다.
하지만 이 영화가 단순 오락물로 그치는게 아니라 이제 스크린안으로
거대 로봇을 불러들이는게 기술적으로 문제가 되지 않으며,
관객에게도 스크린을 누비는 거대 로봇의 영상을 받아들일 기회
(거대 무생물체에서 드라마를 느낄 준비.. 같은)를 제공한데
더 큰 의의가 있지 않나 생각 한다.

트랜스포머를  본 후, 패트레이버 같은 리얼로봇 계열의 애니메이션이
영화화 될지도 모르겠다는 기대감이 생겼다.

패트레이버는 메카닉물이면서도 당시(80년대 말)까지의 메카물과는 다르게
메카닉이 이야기의 주도권을 쥐기보단 경시청 특과차량 2과라는 특수과 일원들의
이야기가 주. 메카가 부.를 이루는 구성으로 리얼메카물 중에서도 그 특징이
확연히 드러나는 작품으로 평가받고 있다.

극장판에서는 OVA나 TV판에서 보여준 코믹한 설정과는 완전히 다른
( 이건 오시이 마모루 라는 감독이기에 가능 했는지도 모르겠다. )
이야기를 들려주지만 그 나름대로 존재감이 있는 작품이라고 생각한다.

이런 의미를 생각 해 봤을 때도 패트레이버는 한번 쯤 실사 영화로
만들어 졌으면 좋겠다고 생각하는 작품 중 하나이다.








2007/07/04 23:50 2007/07/04 23:50
Trackback Address:이 글에는 트랙백을 보낼 수 없습니다

문자열 오브젝트의 길이는?

여러분의 텍스트 스트링의 길이는 얼마나 되는가? 사용자 입력이 데이터 필드 길이 제한 조건에 부합하는지 확인하기 위해선 그 답이 필요하다. 데이터베이스의 텍스트 필드는 입력을 일정한 길이로 제한하는 경우가 많으므로 텍스트를 제출하기 전에 길이를 확인할 필요가 있다. 이유야 어쨌든, 누구든지 이따금씩 텍스트 필드의 길이를 알아야 할 때가 있다. 많은 프로그래머들이 String 오브젝트의 length 메소드를 이용하여 그 정보를 얻으며, 대개의 경우 length 메소드는 올바른 솔루션을 제공한다. 하지만 이것이 String 오브젝트의 길이를 결정하는 유일한 방법은 아니며, 언제나 올바른 것도 아니다.

자바 플랫폼에서 텍스트 길이를 측정하는 데 세 개 이상의 일반적인 방법이 있다.

  1. char 코드 유닛의 수
  2. 문자 또는 코드 포인트의 수
  3. 바이트의 수


char
유닛 수 세기

자바 플랫폼은 유니코드 표준 을 사용하여 문자를 정의하는데, 이 유니코드 표준은 한때 고정 폭(U0000~UFFFF 범위 내의 16비트 값)으로 문자를 정의했다. U 접두사는 유효한 유니코드 문자 값을 16진수로 표시한다. 자바 언어는 편의상 char 타입에 대해 고정 폭 표준을 채택했고, 따라서 char 값은 어떠한 16비트 유니코드 문자라도 표현이 가능했다.

대부분의 프로그래머는 length 메소드에 대해 잘 알고 있고 있을 것이다. 아래의 코드는 예제 문자열에서 char 값의 수를 세는데, 예제 String 오브젝트에는 자바 언어의 \u 표기법으로 정의되는 몇 개의 단순 문자와 기타의 문자가 포함되어 있다는 점에 유의하기 바란다. \u 표기법은 16비트 char 값을 16진수로 정의하며 유니코드 표준에서 사용되는 U 표기법과 유사하다.

private String testString = "abcd\u5B66\uD800\uDF30";
int charCount = testString.length();
System.out.printf("char count: %d\n", charCount);

length 메소드는 String 오브젝트에서 char 값의 수를 센다. 이 예제 코드는 다음을 출력한다.

char count: 7


문자 유닛 수 세기

유니코드 버전 4.0이 UFFFF 이상 되는 상당히 큰 수의 새로운 문자를 정의하면서부터 16비트 char 타입은 더 이상 모든 문자를 표현할 수 없게 되었다. 따라서 자바 플랫폼 J2SE 5.0버전부터는 새로운 유니코드 문자를 16비트 char 값의 쌍(pair)으로 표현하며, 이를 surrogate pair 라고 부른다. 2개의 char 유닛이 U10000~U10FFFF 범위 내의 유니코드 문자를 대리 표현(surrogate representation)하는데, 이 새로운 범위의 문자를 supplementary characters 라고 한다.

여전히 단일 char 값으로 UFFFF까지의 유니코드 값을 표현할 수는 있지만, char 대리 쌍(surrogate pair)만이 추가 문자(supplementary characters)를 표현할 수 있다. 쌍의 최초값 또는 high 값은 UD800~UDBFF 범위에, 그리고 마지막값 또는 low 값은 UDC00~UDFFF 범위에 속한다. 유니코드 표준은 이 두 범위를 대리 쌍을 위한 특수 용도로 배정하고 있으며, 표준은 또한 UFFFF를 벗어나는 대리 쌍과 문자 값 사이의 매핑을 위한 알고리즘을 정의한다. 프로그래머는 대리 쌍을 이용하여 유니코드 표준 내의 어떠한 문자라도 표현할 수 있는데, 이런 특수 용도의 16비트 유닛을 UTF-16 이라고 부르며, 자바 플랫폼은 UTF-16을 사용하여 유니코드 문자를 표현한다. char 타입은 이제 UTF-16 코드 유닛으로, 반드시 완전한 유니코드 문자(코드 포인트)가 아니어도 된다.

length 메소드는 단지 char 유닛만을 세기 때문에 추가 문자는 셀 수 없다. 다행히도 J2SE 5.0 API에는 codePointCount(int beginIndex, int endIndex) 라는 새로운 String 메소드가 추가되었다. 이 메소드는 두 인덱스 사이에 얼마나 많은 유니코드 포인트(문자)가 있는지 알려주는데, 인덱스 값은 코드 유닛 또는 char 위치를 참조한다. 식 endIndex - beginIndex 의 값은 length 메소드가 제공하는 값과 동일하며, 이 둘의 차이는 codePointCount 메소드가 반환하는 값과 항상 동일하지는 않다. 텍스트에 대리 쌍이 포함되어 있는 경우에는 길이 수가 확실히 달라진다. 대리 쌍은 하나 또는 두 개의 char 유닛으로 된 단일 문자 코드 포인트를 정의한다.

하나의 문자열에 얼마나 많은 유니코드 문자 코드 포인트가 있는지 알아보려면 codePointCount 메소드를 사용하도록 한다.

private String testString = "abcd\u5B66\uD800\uDF30";
int charCount = testString.length();
int characterCount = testString.codePointCount(0, charCount);
System.out.printf("character count: %d\n", characterCount);

이 예제는 다음 내용을 출력한다.

character count: 6

testString 변수에는 두 개의 흥미로운 문자가 포함되어 있는데, 그것은 '배움(learning)'이라는 뜻의 일본어 문자와 GOTHIC LETTER AHSA 라는 이름의 문자이다. 일본어 문자는 유니코드 코드 포인트 U5B66을 사용하며, 이는 동일한 16진수 char 값 \u5B66을 가진다. 고딕체 글자의 코드 포인트는 U10330이고, UTF-16에서 고딕체 글자는 대리 쌍 \uD800\uDF30이다. 이 쌍은 단일 유니코드 코드 포인트를 표현하므로 전체 문자열의 문자 코드 포인트 수는 7이 아니라 6이 된다.


바이트 수 세기

String 안에는 얼마나 많은 바이트가 있을까? 그 대답은 사용되는 바이트 기반 문자 세트에 의해 결정된다. 바이트 수를 알아보려는 이유 중 하나는 데이터베이스의 문자열 길이 제한 조건을 충족하기 위해서이다. getBytes 메소드는 유니코드 문자를 바이트 기반 인코딩으로 변환하며, 이는 byte[] 를 반환한다. UTF-8 은 바이트 기반 인코딩의 일종이지만, 모든 유니코드 코드 포인트를 정확하게 표현할 수 있다는 점에서 대부분의 다른 바이트 기반 인코딩과는 구별된다.

다음 코드는 텍스트를 byte 값의 배열로 변환한다.

byte[] utf8 = null;
int byteCount = 0;
try {
  utf8 = str.getBytes("UTF-8");
  byteCount = utf8.length;
} catch (UnsupportedEncodingException ex) {
  ex.printStackTrace();

System.out.printf("UTF-8 Byte Count: %d\n", byteCount);

타깃 문자 세트가 생성될 바이트의 수를 결정한다. UTF-8 인코딩은 단일 유니코드 코드 포인트를 1~4개의 8비트 코드 유닛(=1 바이트)으로 변환한다. 문자 a , b , c , d 는 모두 4 바이트만을 요구하는 한편, 일본어 문자(한국어도 같음)는 3 바이트로 변환된다. 또한 고딕체 글자는 4 바이트를 차지한다. 결국 다음의 결과가 나오는 것을 알 수 있다.

UTF-8 Byte Count: 11









그림 1. 무엇을 세느냐에 따라 문자열의 길이가 달라진다.


요약

추가 문자를 사용하지 않으면 length codePointCount 의 반환 값 간에는 차이점이 없다고 할 수 있다. 하지만 UFFFF 범위를 넘어서는 문자를 사용할 경우에는 길이를 결정하는 다양한 방법에 관해 알아야 할 필요가 있다. 예컨대 미국에서 한국으로 제품을 발송할 경우에는 십중팔구 length codePointCount 가 서로 다른 값을 반환하는 상황이 발생한다. 데이터베이스의 문자 세트 인코딩과 일부 직렬화 포맷은 UTF-8을 최선의 방법으로 권장되고 있는데, 이 경우에도 역시 텍스트 길이는 달라진다. 결국 길이를 어떻게 활용할 것인지에 따라 다양한 측정 옵션을 사용할 수 있다.


추가 정보

본 테크 팁 주제에 관한 자세한 내용을 보려면 다음 리소스를 참조할 것(영문)


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

싱글턴 패턴 :: Singleton Pattern

설계 패턴은 소프트웨어 설계에서 상에서의 공통 문제에 대한 일반적인 솔루션이라 할 수 있는데, 그 기본 개념은 솔루션을 코드로 변환하면 그 코드를 다양한 문제 상황에 적용할 수 있다는 것이다. 설계 패턴에 관한 논의는 Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides 등이 공동 집필한 Design Patterns: Elements of Reusable Object-Oriented Software에서 시작되었다. 이 책에서는 패턴을 생성, 구조, 동작의 세 가지 주요 영역이 포함하여 다양한 주제 영역으로 분류하고 있다. 생성 패턴(Creational patterns)은 객체가 생성되는(객체 지향 용어로는 ‘예시되는(instantiated)’) 방식을 기술한다. 구조 패턴(Structural patterns)은 객체들을 연결하고 결합하는 방식을 도와주고, 동작 패턴(Behavioral patterns)은 알고리즘 또는 통신 메커니즘을 기술한다. 몇 가지 공통 패턴의 이름으로는 생성 영역의 Singleton, 동작 영역의 Observer, 구조 영역 Facade를 들 수 있다. 본 테크 팁에서는 자바 프로그래밍 언어의 맥락에서 Singleton 패턴을 면밀히 살펴보도록 한다.

Singleton 패턴은 흔히 사용되는 생성 패턴의 하나이다. 이는 하나의 클래스에서 오직 하나의 인스턴스만 생성되도록 보장하는 기법을 기술한다. 이는 본질적으로, 클래스 외부의 누구도 객체의 인스턴스를 생성하지 못하게 하는 접근법을 사용한다. 일반적으로 Singleton은 느리게 생성되어 필요 시점까지 메모리의 요구를 줄여준다. 이 접근법은 다양한 방식으로 구현이 가능하다.

생성되고 있는 하나의 인스턴스가 서브클래스가 되리라는 것을 알고 있다면, 상위(parant) 클래스를 추상화하고 메소드를 제공하여 현재의 인스턴스를 얻도록 한다. 그 일례로 AWT 패키지의 Toolkit 클래스를 들 수 있으며, Toolkit을 위한 생성자는 public(이 특정 경우에는 디폴트 생성자)이다.
   public Toolkit()
그리고 클래스는 특정 서브클래스(이 경우에는 플랫폼을 따름)를 얻기 위한 getDefaultToolkit() 메소드를 가진다.
   public static Toolkit getDefaultToolkit()
썬 자바 런타임을 탑재한 Linux 플랫폼 상에서 특정 서브클래스는 sun.awt.X11.XToolkit 타입이다. 하지만 사용자는 공통 추상 상위 클래스인 Toolkit을 통해 클래스에 액세스할 뿐이므로 이 부분까지 알아야 할 필요는 없다.

Collator 클래스는 이 패턴의 또 다른 예로, 약간의 차이가 있다. 이 클래스는 2개의 getInstance() 메소드를 제공하고, 무인자(no-argument) 버전은 디폴트 locale을 위한 Collator를 얻는다. 이 때, 사용자는 자체 locale을 전달하여 해당 locale을 위한 Collator 인스턴스를 얻을 수 있다. 동일 locale에 대한 Collator를 여러 차례 요청해도 동일한 Collator 인스턴스를 돌려받게 되며, 생성자 자체는 보호된다. 한편, J2SE 표준 라이브러리 전반에 걸쳐 클래서 생성을 제한하는 유사한 방식을 발견할 수 있다.

이 시점에서 클래스 생성자에 대한 액세스를 제한하면 자동적으로 Singleton이 된다고 생각할 수도 있겠으나, 그렇지 않다. 문제가 되는 케이스는 Calendar 클래스인데, Calendar 클래스 생성자는 보호되며, 이 클래스는 클래스 인스턴스를 얻기 위한 getInstance() 메소드를 제공한다. 그러나 getInstance() 메소드를 호출할 때마다 새로운 클래스 인스턴스가 얻어짐으로 결국 이는 Singleton이 아닌 것이다.

사용자의 자체 Singleton 클래스 생성 시에는 오직 하나의 인스턴스만 생성되도록 유의해야 한다.
   public class MySingleton {
     private static final MySingleton INSTANCE = 
       new MySingleton();

     private MySingleton() {
     }

     public static final MySingleton getInstance() {
       return INSTANCE;
     }
   }
정적 메소드 getInstance()는 단일 클래스 인스턴스를 리턴한다. 단일 인스턴스가 서브클래스일 필요가 있더라도 API를 변경할 필요는 없다는 점에 주목할 것.

이론적으로는 INSTANCE 변수가 public일 수 있으므로 getInstance() 메소드는 필요치 않으나 getInstance() 메소드는 향후에 시스템을 변경할 경우 유연성을 제공한다. 바람직한 가상 머신 구현이라면 정적 getInstance() 메소드에 대한 호출을 즉시 처리(inline)해야 한다.

Singleton 생성 작업은 이것으로 그치지 않는다. 즉, Singleton 클래스를 Serializable로 만들 필요가 있다면 반드시 readResolve() 메소드를 제공해야 한다.
  /**
   * Ensure Singleton class
   */
  private Object readResolve() throws ObjectStreamException {
    return INSTANCE;
  }
readResolve() 메소드가 갖추어진 상태에서 deserialization은 단일(오직 하나의) 객체(getInstance() 메소드에 대한 호출에 의해 생성되는 것과 동일한 객체이다)로 귀결되는데, 사용자가 readResolve() 메소드를 제공하지 않을 경우에는 객체를 deserialize할 때마다 객체 인스턴스가 생성된다.

Singleton 패턴은 오직 단일한 리소스만 가지고 있고 그 단일 리소스의 상태 정보에 대한 액세스를 공유할 필요가 있음을 알고 있을 경우에 유용하다. 설계 시에 Singleton 패턴의 필요성을 파악하면 개발을 간소화할 수 있다. 하지만, 때로는 성능 문제로 코드를 refactor하고 나중에 패턴을 사용하게 되기까지 필요성을 인식하지 못하는 수가 있다. 예를 들어, 프로그램이 동일한 클래스의 인스턴스를 반복 생성하여 상태 정보와 함께 전달하기 때문에 시스템 성능이 저하되는 경우가 발생할 수 있다. Singleton 패턴으로 변경하면 동일한 객체가 반복되는 것을 방지할 수 있는데, 이는 시스템이 객체를 재생성하는데 드는 시간을 제거해줄 뿐 아니라 garbage collector가 인스턴스들을 삭제하는 데 소요되는 시간을 줄여준다.

간단히 말해서, 단일의, 그리고 오직 하나의 클래스 인스턴스만 생성되도록 하고자 할 때 Singleton 설계 패턴을 이용하면 된다. 생성자가 연산을 요구하지 않을 경우에는 빈 private 생성자(또는 서브클래스가 필요할 경우에는 보호된 생성자)를 제공한다. 그렇지 않으면 디폴트값으로 시스템이 public 생성자를 제공하게 되는데, 이는 Singleton으로 작업 시 바람직하지 않은 결과라 할 수 있다.

Singleton은 주어진 클래스 로더 내에서만 고유성이 보장된다는 점에 유의할 것. 복수의 서로 다른 엔터프라이즈 컨테이너에 걸쳐 동일한 클래스를 사용할 경우에는 각 컨테이너에 대해 하나의 인스턴스를 얻게 된다.

Singleton 패턴은 종종 Factory 패턴이라 불리는 다른 패턴과 함께 사용되는데, Factory 패턴도 Singleton 패턴과 마찬가지로 생성 패턴의 일종이다. 이 패턴은 특정 객체의 서브클래스, 또는 보다 일반적으로 특정한 인터페이스의 구현이 어떻게 실제로 객체를 생성하는지 기술한다. Factory 패턴의 좋은 보기로 Swing BorderFactory 클래스를 들 수 있다. 이 클래스는 다양한 종류의 Border 객체를 리턴하는 일련의 정적 메소드를 가지는데, 서브클래스의 구현 세부사항을 숨겨서 factory가 인터페이스 구현을 위한 생성자를 직접 호출할 수 있게 해준다. 다음은 BorderFactory의 사용 예제이다.
   Border line = BorderFactory.createLineBorder(Color.RED);
   JLabel label = new JLabel("Red Line");
   label.setBorder(line);
여기서 BorderFactoryLineBorder를 생성한다는 사실이나 그 생성 방법은 숨겨져 있다. 이번 특정 예제에서는 LineBorder 생성자를 직접 호출할 수 있지만 Factory 패턴을 이용하는 대부분의 경우에는 직접 호출이 불가능하다.

Singleton 패턴을 구현하는 클래스는 다른 클래스의 인스턴스를 생성하기 위해 Factory로 사용할 객체를 리턴하는 경우가 흔히 있는데, 이는 PopupFactory 클래스에 의한 Popup 객체 생성 방식에 의해 예증된다.

Singleton factory를 얻으려면 PopupFactorygetSharedInstance() 메소드를 호출한다.
   PopupFactory factory = PopupFactory.getSharedInstance();
그런 다음 factory의 getPopup() 메소드를 호출하여 factory에서 Popup 객체를 생성하고, 상위 컴포넌트, 그 콘텐츠, 포지션을 전달한다.
   Popup popup = factory.getPopup(owner, contents, x, y);
보안 컨텍스트에서 Factory 패턴이 자주 사용되는 것을 볼 수 있을 것이다. 다음 예제에서는 특정 알고리즘에 대한 certificate factory를 획득한 다음 stream certificate를 생성한다.

   FileInputStream fis = new FileInputStream(filename);
   CertificateFactory cf = 
      CertificateFactory.getInstance("X.509");
   Collection c = cf.generateCertificates(fis);
BorderFactory에서 본 것처럼, Factory 패턴이 반드시 Singleton 패턴과 함께 사용되어야 하는 것은 아니지만 실제로는 두 패턴이 함께 사용되는 경우도 종종 볼 수 있다.

But. Singleton 클래스가 단일 클래스 로더를 통해 공유되지 않으면 Singleton이 아님에 주의!!

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

옵저버 패턴 :: Observer Pattern

Observer 패턴은 자바 프로그램에서 많이 사용되는 설계 패턴이다. 이 패턴은 동작 설계 패턴으로, 클래스가 느슨하게 연결되는 방식과 타 클래스 업데이트 시 하나의(또는 다수의) 클래스가 통지되는 방식을 정의한다. 기본적으로, 이는 특정 장소에서 무슨 일이 발생할 경우, 이를 보고있었거나 관심을 가지고 있던 사람들에게 상황을 통지하는 것을 의미한다.

Observer 패턴을 취급하는 방식에는 두 가지가 있는데, 첫 번째 방식은 java.util package에서 볼 수 있는 ObserverObservable 클래스를 수반하는 것이고, 두 번째 방식은 컴포넌트에 이벤트 리스너를 등록하는 JavaBeans 컴포넌트 모델을 따르는 것이다.

JavaBeans 이벤트 모델 생성에 앞서 ObserverObservable 클래스는 Observable 패턴의 구현을 기술한다. 달리 말해서 이 클래스들은 Java 플랫폼 1.0 버전 때부터 사용되어 왔고, 기술적으로 하자가 없었으며 여전히 라이브러리에 존재하고 있는 것이다. 또한, 이 클래스들은 여전히 Observable 패턴 구현에 사용할 수는 있지만, 두 번째 모델인 JavaBeans 컴포넌트 모델이 일반적으로 사용된다. Observable 패턴 구현을 위해 이 클래스들을 사용하는 데 따른 한 가지 중요한 문제는 Observable 확장이 필요하다는 점인데, 이 경우 자바 플랫폼의 단일 상속 세계에서는 불가능할 수도 있는 클래스 하이어라키 구조를 강요 받게 된다.

이벤트 리스너를 등록하는 JavaBeans 컴포넌트 모델은 일련의 add 및 remove 메소드를 수반하는데, 여기서 리스너 타입은 메소드 이름에 내장되어 있다. 예를 들어, 버튼의 선택을 관찰하기 위해서는 컴포넌트에 ActionListener를 등록한다.
   ActionListener listener = new ActionListener() {
      public void actionPerformed(ActionEvent actionEvent) {
          ...
      }
   }; 
   JButton button = new JButton("Pick Me");
   button.addActionListener(listener);
시스템 정의 클래스를 위한 Observer 패턴은 이것이 전부라고 할 수 있는데, 리스너 인터페이스를 구현하고, 그것을 관찰 Subject에 첨부한 다음 기다린다. Subject는 관찰되는 대상으로, 누가 관찰하고 있는지를 기억하는 일을 책임진다. JavaBeans 컴포넌트 모델의 경우, Observer 객체를 첨부, 분리하기 위한 인터페이스로 add/remove 리스너 네이밍 패턴이 사용된다. Subject의 상태가 변경되면 이를 Observer 객체에 통지한다.

패턴의 주된 목표 중 하나는 Subject와 Observer의 느슨한 연결을 가능케 하는 것이다. JButton이 선택되면, ButtonNotification이라 불리는 가상 서브클래스의 특정 메소드를 호출하는 대신 누구나 구현할 수 있는 인터페이스로 통지가 추상화된다. JButton은 첨부된 Observer(리스너)가 어떤 클래스인지 전혀 개의치 않는데, 실제로 버튼은 구현 클래스가 수정되더라도 오로지 Observer가 리스너를 구현한다는 사실에만 관심을 가진다.

Observer 패턴 사용 시 주의할 필요가 있는 여러 가지 복잡한 문제들이 있다. 첫째는 메모리 누출의 가능성이다. Observer에 대한 레퍼런스는 Subject에 의해 관리되는데, Subject가 레퍼런스를 해제할 때까지는 garbage collector로 Observer를 제거할 수 없다. 이런 가능성에 유의하여 적절한 상황에서 Observer를 제거해야 한다. 아울러 (적어도 이벤트 리스너를 등록할 때는) 일련의 Observer 객체가 unordered collection에서 관리된다는 점에 주목할 것. 먼저 등록된 리스너가 먼저 통지되는지 마지막에 통지되는지를 반드시 알아야 할 필요는 없으나 반드시 객체 A가 먼저 통지되고 이어서 객체 B가 통지되는 cascading 방식의 통지가 필요한 경우에는 중간 객체를 도입하여 순서가 지켜지도록 해야한다. 단지 Observer를 특정 순서로 등록한다고 해서 반드시 그 순서에 따라 통지가 이루어지는 것은 아니다.

Observer 패턴을 모델링하는 자바 플랫폼의 또 다른 영역으로는, guaranteed delivery, non-local distribution, persistence 등의 이점을 갖춘 JMS(Java Message Service)를 들 수 있다. JMS publish-subscribe 메시징 모델은 무한한 수의 가입자가 관심 있는 주제를 청취할 수 있게 해주는데, publish된 주제에 대한 메시지가 생성되면 모든 가입자들에게 내용이 통지된다.

그 밖에도 자바 플랫폼에는 Observer 패턴을 모델링하는 여러 다른 분야가 있으며, 이 패턴은 자바 플랫폼 전반에 걸쳐 자주 사용된다.
2007/07/03 18:46 2007/07/03 18:46
Trackback Address:이 글에는 트랙백을 보낼 수 없습니다

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:이 글에는 트랙백을 보낼 수 없습니다

RFID 실무서 / 논문 / 참고문헌 정리

웹상에서 찾아낸 RFID관련 문서를 게시하는 공간입니다.
각 문서의 권한은 각 저작자에게 있습니다.

sample_chapter07.pdf

실무자를 위한 RFID이해와 활용

paper.pdf

RFID태그 객체를 위한 효율적인 구간 색인 구조

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