2009年8月30日日曜日

JavaScript와 Ajax를 이용한 비동기식

지난 글(참고자료)에서는 Ajax 애플리케이션에 관한 서론 및 이 애플리케이션에 필요한 몇 가지 기본개념에 대해 알아봤다. 본 글에서는 JavaScript, HTML 및 XHTML, 동적 HTML, 심지어는 몇 가지 DOM(동적 객체 모델) 등 이미 알고 있는 수많은 기술에 대해 중점적으로 다뤘다.

본 글에서는 모든 Ajax관련 객체 및 프로그래밍 방식의 기초인 XMLHttpRequest 객체에 대해 먼저 다룰 것이다. 이 객체는 모든 Ajax 애플리케이션 전반에 걸쳐 유일한 공통 줄기가 된다. 예상하다시피, XMLHttpRequest 객체를 완전히 이해해서 프로그래밍의 한계에 다다르고자 할 것이다. 사실, XMLHttpRequest 객체를 적절히 이용해도 분명 그 객체를 사용하지 못하는 경우가 있다. 도대체 XMLHttpRequest 객체는 무엇일까?

Web 2.0

먼저 코드에 관해 자세히 알아보기 전에 Web 2.0에 관한 개요를 살펴 보면서 확실한 개념을 얻도록 하자. Web 2.0이라는 용어를 들을 때 다음과 같이 " Web 1.0은 무엇입니까?" 라고 물어봐야 한다. Web 1.0에 대해선 거의 들어보지 못했지만 명료한 요청 및 응답 모델이 포함된 전통 웹을 가리켜 Web 1.0이라 한다. 예를 들어 Amazon.com으로 들어가서 버튼을 클릭하거나 탐색 용어를 입력하면 서버에 요청을 생성하고 이에 대한 응답이 브라우저로 다시 보낸다. 그 요청은 책, 타이틀 목록 이상으로 중요하며 실지로 또 다른 완전 HTML 페이지를 만들어낸다. 그 결과 새로운 HTML 페이지가 웹 브라우저 스크린에 다시 나타날 때 플래시/플리커링 현상이 나타나기도 한다. 사실, 각각의 새로운 페이지에서 나오는 요청, 응답을 분명히 알게 된다.

Web 2.0은 이와 같은 왕복이동 움직임이 상당부분 필요 없다. 예를 들어, Google Maps 또는 Flickr(참고자료)를 방문하면 Google Maps 상에서는 맵을 끌어다가 재 드로잉을 약간만 해도 맵이 축소, 확대된다. 물론, 요청 및 응답은 상상을 초월할 정도로 계속 이루어진다. 이로 인해 사용자로서의 경험은 훨씬 짜릿하며 데스크톱 애플리케이션 상에 있는 것과 같이 느껴진다. 이런 새로운 느낌, 패러다임은 누군가가 Web 2.0에 대해 언급할 때 나오는 현상들이다.

이와 같이 새로운 상호작용이 가능하도록 하는 방법에 대해 주의를 기울여야 한다. 분명 요청 및 필드 응답을 생성하지만 이로 인해 매 순간 요청/응답 상호작용에 관한 HTML 재 드로잉이 발생돼 느리고 볼품없는 웹 인터페이스를 생성하게 된다. 따라서 사용자가 요청을 생성하고 전반적인 HTML 페이지보다는 필요한 데이터만 포함하는 응답을 수신하는 방식이 필요하다. 사용자가 새로운 페이지를 보고자 할 때가 완전히 새로운 HTML을 얻는 유일한 경우다.

하지만 대부분의 상호작용으로 인해 상세사항 추가/본문 텍스트 변환/기존 페이지에 데이터 겹쳐쓰기 등이 발생한다. 모든 경우, Ajax 및 Web 2.0방식으로 전체 HTML 페이지를 업데이트하지 않고 데이터를 전송, 수신한다. 이런 기능으로 임의의 수많은 웹 서퍼들은 애플리케이션의 속도가 빨라지고 응답성이 증가하는 것으로 느끼게 되며 상호작용이 반복적으로 이루어지게 된다.




위로


XMLHttpRequest

이렇게 새롭고 놀라운 현상이 실지로 발생하기 위해선 XMLHttpRequest라 하는 JavaScript 객체에 관해 완전 익숙해져야 한다. 오랜 시간 동안 몇몇 브라우저에서 사용된 이 객체는 Web 2.0, Ajax 및 앞으로 이 글에서 배우게 될 기타 사항을 이해하는 데 있어 중요한 역할을 하게 된다. 실지로 빠른 이해를 위해 이 객체에서 사용되는 방법 및 속성에 대해 알아보자.

  • open(): 새로운 요청을 서버에 설정함.
  • send(): 요청을 서버에 전송함.
  • abort(): 현 요청에서 벗어남.
  • readyState: 현 HTML 준비상태를 제공함.
  • responseText: 요청에 응답하기 위해 서버에서 재전송하는 텍스트.

위의 모든 명령을 다 이해하지 못하더라도(중요한 것을 이해하지 못한다 하더라도) 걱정하지 마라. 다음 글에서 각 명령에 대한 방법 및 속성에 관해 배우게 된다. 여기서는 XMLHttpRequest와 관련된 좋은 아이디어를 얻는 게 필요하다.여기서, 각 방법 및 속성은 요청 전송 및 응답 처리와 연관된다는 것을 명심하라. 사실, XMLHttpRequest 객체에 관한 방법, 속성을 다는 알지 못하기 때문에 이들이 매우 간단한 요청/응답 모델과 연관 있다는 것도 모르게 된다. 그래서 놀랍고도 새로운 GUI 객체, 또는 사용자 상호작용을 생성하는 흥미로운 몇 가지 방식 등에 관해서도 배우지 않는다. 별로 재미가 없는 것 같지만 XMLHttpRequest 객체 하나만 잘 사용해도 완전 애플리케이션을 변경할 수 있다.

단순함

우선, 새로운 변수를 생성한 다음 이를 XMLHttpRequest객체 인스턴스에 할당한다. JavaScript 상에서는 상당히 간단한 작업이다. 여기서 Listing 1에 보다시피, 객체 이름과 같이 new 키워드를 사용하면 된다.


Listing 1. 새로운 XMLHttpRequest 객체 형성
				



새로운 객체 형성과정이 그다지 어려운 일은 아니지 않는가? JavaScript에서는 변수 상에 입력하는 과정이 필요 없어 Listing 2(자바에서 XMLHttpRequest 객체를 생성하는 과정)와 같이 값을 전혀 입력할 필요가 없다.


Listing 2. XMLHttpRequest 객체를 생성하기 위한 자바 유사-코드
				

XMLHttpRequest request = new XMLHttpRequest();

따라서 JavaScript에서 var로 변수를 생성해 변수에 명칭("request" 등)을 부여한 다음 이를 XMLHttpRequest 객체의 새로운 인스턴스에 할당한다. 이 시점에서 XMLHttpRequest 객체를 사용할 준비가 된 것이다.

에러 처리과정

실제 세계에서, 에러가 발생할 수 있어 에러 발생코드는 에러 처리기능을 제공하지 않는다. 따라서 XMLHttpRequest를 생성한 다음 에러가 발생한 경우 이 객체의 기능을 점차 저하시킨다. 일례로, XMLHttpRequest객체를 지원하지 않는 구 브라우저들(믿건 말건, 사람들은 Netscape Navigator의 구 버전을 여전히 이용한다.)이 많아 사용자는 어디서 에러가 났는지 알 필요가 있다. Listing 3은 에러가 난 경우 XMLHttpRequest 객체를 생성하는 방식에 대해 나와 있다. 여기서 XMLHttpRequest 객체로 JavaScript 경고가 발생한다.


Listing 3. 에러 처리기능으로 XMLHttpRequest 객체 생성
				



여기서 다음의 각 단계를 반드시 이해한다.

  1. request라는 새 변수를 생성한 다음 이를 거짓으로 설정한다. XMLHttpRequest 객체가 아직 생성되지 않은 상태에서 거짓 값으로 설정한다.
  2. try/catch 블록에서 추가로 다음과 같은 작업을 한다.
    1. XMLHttpRequest 객체를 시험한 다음 생성한다.
    2. 1번 과정이 실패한 경우(catch (failed)), request가 여전히 거짓으로 설정되어 있는지 확인한다.
  3. request가 여전히 거짓으로 설정되어 있는지 확인한다. (에러가 없는 경우, 거짓으로 설정되지 않는다.)
  4. 에러가 발생한 경우(request가 거짓인 경우), JavaScript 경고를 사용해 문제가 발생했다는 사실을 사용자에게 알린다.

이런 작업은 상당히 간단하다. 실제로 대부분의 JavaScript 및 웹 개발자들은 객체를 읽고 작성하는 것보다는 이해하는 게 더 빠르다. 이제, XMLHttpRequest 객체를 생성하는 일부 에러-증명 코드가 생성되어 에러 여부를 알게 된다.

Microsoft로 처리하기

적어도 인터넷 상에서 이 코드를 적용하기 전까지는 이와 같은 작업이 무난하다. 이 코드를 적용하면 그림 1과 같이 에러가 나오게 된다.


그림 1. 에러 보고 인터넷
Internet Explorer reporting an error
Microsoft가 잘 작동되는가?
Ajax에 관해 쓴 글이 많고 Microsoft는 이 영역에 있어 점점 더 관심을 기울이고 있다. 사실 2006년 말에 출시될 것으로 예정된 Microsoft사의 Internet Explorer 최신버전인 버전 7.0은 XMLHttpRequest 객체를 직접 지원해 모든 Msxml2.XMLHTTP 생성코드 대신 new 키워드를 사용한다. 하지만 너무 빠져 들지 마라. 아직도 구 브라우저를 지원해야 하므로 크로스-브라우저 코드는 곧장 사라지지는 않을 전망이다.

분명, 에러가 발생하고 있다. Internet Explorer는 구식 브라우저가 아니며, 전 세계의 70% 정도가 사용하는 툴이다. 즉, Microsoft 및 Internet Explorer를 지원하지 않는 한 웹 상에서 잘 운영하지 못하게 된다. 따라서 Microsoft 브라우저를 다룰 다른 방식이 필요하다.

Microsoft는 Ajax를 지원하지만 이전과 다른 XMLHttpRequest 버전을 호출하며 사실은 여러 다른 버전을 호출한다. Internet Explorer의 새로운 버전을 사용하는 경우, Msxml2.XMLHTTP 라 하는 객체를 사용해야 한다. Internet Explorer의 구 버전에서는 Microsoft.XMLHTTP 객체를 사용한다. 그러므로 이와 같은 두 가지 객체 형태를 지원해야 한다. (비-Microsoft 브라우저에 대한 지원기능을 손실하지 않은 상태에서.) 이미 언급한 코드에 Microsoft 지원기능을 추가한 Listing 4를 참조하라.


Listing 4. Microsoft 브라우저에 지원기능 추가
				



사실 브라우저에 대한 지원기능이 손실되기 쉽다. 따라서 다음과 같이 단계별로 하길 권한다.

  1. request 명의 새 변수를 생성한 다음 이를 거짓으로 설정한다. XMLHttpRequest 객체가 아직 생성되지 않았다는 전제 하에 거짓으로 설정한다.
  2. try/catch 블록에서 추가로 다음과 같은 작업을 한다.
    1. XMLHttpRequest 객체를 시험한 다음 생성한다.
    2. 1번 과정이 실패한 경우 (catch (trymicrosoft)):
      1. 새로운 Microsoft 버전 (Msxml2.XMLHTTP)을 이용해 Microsoft 호환성 객체를 시험한 다음 생성한다.
      2. 전 과정이 실패한 경우(catch (othermicrosoft)), 이전 Microsoft 버전(Microsoft.XMLHTTP)을 이용해 Microsoft 호환성 객체를 시험한 다음 생성한다.
    3. 그래도 실패한 경우(catch (failed))에는, request가 여전히 거짓으로 설정되어 있는지 확인한다.
  3. request가 여전히 거짓으로 설정되어 있는지 다시 확인한다. (에러가 나지 않는 경우 거짓으로 설정되지 않는다.)
  4. 그래도 문제가 발생한 경우(request가 거짓인 경우), JavaScript 경고를 사용해 사용자에게 문제가 발생했다는 것을 알린다.

코드를 변화시키고 Internet Explorer 상에서 다시 한 번 시도해 보면 에러 메시지 없이 생성한 형태를 보게 된다. 본인의 경우엔 그와 같은 시도가 그림 2와 같은 것으로 나타난다.


그림 2. 정상적으로 작동하는 Internet Explorer
Internet Explorer working normally

동적/정적

Listing 1, 3, 4를 다시 보면 모든 코드는 script 태그 내에 직접 포함되어 있다는 것을 알게 된다. JavaScript가 그와 같이 코드화되고 메소드/기능 본체 내에 들어가지 않은 경우, 이를 정적 JavaScript라 한다. 이는 페이지가 스크린 상에 나타나기 전 코드를 실행했다는 것을 의미한다.(코드 및 브라우저가 따로 실행될 경우, 사양과는 100% 일치하지 않지만 사용자와 페이지가 상호작용하기 전, 코드를 실행했다는 건 확실하다.) 일반적으로 그렇게 해서 대부분의 Ajax 프로그래머가 XMLHttpRequest 객체를 생성한다.

즉, Listing 5처럼, 메소드에 이와 같은 코드를 첨가한다.


Listing 5. XMLHttpRequest 생성코드를 메소드로 이동하기
				



이와 같이 코드를 설정하면, Ajax 작업을 하기 전 이와 같은 메소드를 호출해야 한다. 그럴 경우 Listing 6과 같은 것을 얻을 수도 있다.


Listing 6. XMLHttpRequest 생성 메소드 사용
				



Listing 6을 활용하면 에러 통지기능을 지연시키므로 대부분의 Ajax 프로그래머들은 위의 방법을 활용하지 않는다. 10/15 필드가 있는 복잡한 형태에 선택상자 등등이 있다고 상상해 보면 사용자가 필드 14에 있는 텍스트를 형식에 나온 대로 기입할 때 몇 가지 Ajax 코드를 전송한다. 이 시점에서 getCustomerInfo()를 실행해 XMLHttpRequest 객체를 생성하려 했지만 실패한다. (이 예에서) 그러면 사용자에게 이 애플리케이션을 사용할 수 없다는 것을 경고로 알리게 된다 (많은 경우). 하지만 사용자는 이미 형식 상에서 데이터를 기입하느라 시간을 보냈다. 상당히 짜증을 내게 되면서 사용자는 결국엔 사이트로 관심을 기울이지 않게 된다.

정적 JavaScript를 사용하는 경우, 사용자는 페이지에 에러가 나자마자 에러를 포착하게 된다. 또 짜증나는가? 아마도 사용자로선 웹 애플리케이션이 브라우저 상에서 작동되지 않을 때 상당히 죽을 맛일 게다. 하지만, 10분 동안 정보를 기입한 뒤 동일한 에러가 나오는 것보다는 확실히 낫다. 따라서 정적으로 코드를 설정하고 난 다음 발생할 수 있는 문제에 대해 사용자가 조기에 알도록 하는 게 중요하다고 본다.




위로


XMLHttpRequest로 요청 전송하기

요청 객체가 있으면 요청/응답 사이클을 시작한다. 여기서 요청을 생성한 다음 응답을 수신하는 게 XMLHttpRequest 객체에서 이루고자 하는 유일한 것임을 명심하라. 사용자 인터페이스 변환, 이미지 교환 및 서버에서 재전송하는 데이터 해석 등의 작업은 페이지에 있는 JavaScript, CSS 또는 기타 코드에서 일어나는 현상이다. XMLHttpRequest 객체가 사용 대기 중일 때 서버에 요청을 생성하게 된다.

샌드박스

Ajax는 샌드박스 보안 모델이 포함되어 있다. 그 결과 Ajax 코드(특히 XMLHttpRequest 객체)는 실행 중인 동일한 도메인에만 요청을 생성한다. 다음 글에서 보안 및 Ajax에 관해 더 많은 것을 배우게 되겠지만 지금으로선 로컬 머신 상에서 작동하는 코드만으로도 로컬 머신 상의 서버측 스크립트에 요청을 생성한다는 것을 알게 된다. www.breakneckpizza.com상에서 Ajax 코드를 실행하는 경우, www.breakneckpizza.com상에서 실행하는 스크립트에 관한 요청을 생성한다.

서버 URL 설정

여기서 우선 결정할 것은 연결할 서버의 URL이다. URL은 Ajax에서만 있는 것은 아니다. 분명URL을 구성하는 방법에 대해 알아야 한다. 하지만 URL은 연결 설정 시 여전히 필수적인 것이다. 대부분의 애플리케이션에서 사용자가 다루는 형식에서 나온 데이터와 정적 데이터 세트를 결합해 URL을 구성한다. 예를 들어 Listing 7에서는 전화번호 필드의 값을 알아내고 그 데이터를 이용해 URL을 구성하는 JavaScript에 관해 나와 있다.


Listing 7. 요청 URL 구축
				



여기서 딴지를 걸만한 게 없다. 먼저 여기 나온 코드는 phone 이라는 이름의 새로운 변수를 생성, 이를 "phone"의 ID로 형식 필드 값을 지정한다. Listing 8에는 phone 필드 및 id 속성에서 알 수 있는 특수 형태에 관한 XHTML에 관해 나와 있다.


Listing 8. Break Neck Pizza 형식
				


Break Neck Pizza



Enter your phone number:
onChange="getCustomerInfo();" />


Your order will be delivered to:



Type your order in here:







사용자가 전화번호를 입력/변경하는 경우, Listing 8에서 보는 대로 getCustomerInfo() 메소드가 나온다는 사실을 알아둔다. 이 방법으로 전화번호를 알아낸 다음 url 변수에 저장된 URL 문자열을 구성하는 데 활용한다. Ajax 코드는 묶여져 있고 동일한 도메인에만 연결되기 때문에 URL에서 도메인 명칭이 필요 없다는 사실을 명심해야 한다. 이 예에서 스크립트 명칭은 /cgi-local/lookupCustomer.php다. 결국 전화 번호는 상기 스트립트에 Get 매개변수로서 추가된다. ("phone=" + escape(phone))

이전에 escape() 메소드를 알지 못한 경우, 이 메소드는 정확히 명백한 텍스트로 전송될 수 없는 문자로부터 벗어나는 데 사용된다. 예를 들어, 전화번호에서의 임의의 공간은 %20 문자로 바뀌며 이로 인해 URL과 같이 문자를 전송할 수 있게 된다.

그런 다음 필요한 많은 매개변수를 추가한다. 예를 들어 또 다른 매개변수를 추가하고자 한다면 URL 상에 추가해 여러 매개변수를 ampersand(&) 문자로 분리시킨다. (첫 번째 매개변수는 의문부호(?)로 스크립트 명칭으로부터 분리되어 있다.)

요청 열기

open() 메소드가 열릴까?
인터넷 개발자들은 open() 메소드의 정확한 기능에 대해 서로 의견이 다르다. 실지로 이 메소드에 없는 기능은 요청 열기 기능이다. 네트워크 및 XHYML/Ajax 페이지와 연결 스크립트 간 데이터 이동을 감시했더라면 open() 메소드 호출 시 트래픽 현상이 발생하지도 않았을 것이다. Open() 명칭이 선택된 이유는 여전히 불분명하지만 분명 훌륭한 명칭선택이라 볼 수는 없다.

URL이 연결된 상태에서 XMLHttpRequest 객체 상의 open() 메소드를 사용해 요청을 구성한다. 이 메소드는 5가지 매개변수가 있다.

  • request-type: 전송 요청 형태. GET/POST가 일반적인 값이고 HEAD 요청도 전송함.
  • url: 연결된 URL.
  • asynch: 비동기 요청을 설정할 경우 참값, 동기식 요청인 경우에는 거짓임. 이 매개변수는 옵션이고 기본값이 참값임.
  • username: 사용인증을 요구할 경우 사용자이름을 지정한다. 옵션 매개변수고 기본값이 없다.
  • password: 사용인증을 요구할 경우 암호를 지정한다, 옵션 매개변수고 기본값이 없다.

일반적으로 5개의 매개변수 중 3개의 첫 매개변수만 사용한다. 사실, 비동기식 요청을 원할 경우, 제3의 매개변수로 "true"을 설정한다. 그게 기본값 설정이다. 하지만 훌륭한 자체 문서화 작업으로 이 작업을 통해 항상 요청이 비동기식인지 아닌지 여부를 알 수 있다.

기본값 설정을 완료한 다음 일반적으로 Listing 9와 비슷한 라인으로 작업을 완료한다.


Listing 9. 요청 열기
				

function getCustomerInfo() {
var phone = document.getElementById("phone").value;
var url = "/cgi-local/lookupCustomer.php?phone=" + escape(phone);
request.open("GET", url, true);
}

일단 URL을 이해했으면 그 다음에는 상당히 단순하다. 대부분의 요청의 경우, GET을 사용하는 것만으로도 충분하다. (다음 글에서 POST를 사용하고자 하는 경우를 보게 된다.) URL과 같이 open() 메소드를 사용하기만 하면 된다.

비동시성에 대한 문제

이 시리즈의 후반부에서는 비동기식 코드 작성 및 사용에 관한 설명에 할애할 것이다. 하지만 open() 메소드에서 마지막 매개변수가 중요한 이유에 대해 알아야 한다. 정상 요청/응답 모델에서 Web 1.0,을 생각해 보면 클라이언트(로컬 머신 상에서 실행하는 브라우저/코드)는 서버에 요청을 생성한다. 그 요청은 동기식이다. 다시 말하면 클라이언트는 서버로부터의 응답이 올 때까지 대기한다. 클라이언트가 대기 중일 때 일반적으로 적어도 대기 중인 여러 통지 형태 중 하나만 얻으면 된다.

  • Hourglass (Windows 경우에만).
  • 회전 비치볼(일반적으로 Mac 머신에서의 경우임).
  • 애플리케이션은 기본적으로 정지되고 때로 커서가 변환되기도 한다.

이런 특성으로 웹 애플리케이션은 볼품없거나 느린 것으로 보여진다. 즉 실제 대화성이 부족한 것이다. 버튼을 누르면 트리거된 요청이 응답되기 전까지는 애플리케이션을 사용할 수 없다. 광범위한 서버 처리작업을 요구하는 요청을 생성할 경우 대기시간은 어마어마할 것이다. (적어도 오늘날 멀티 프로세서, DSL, 비대기 세계의 경우처럼.)

하지만 비동기식 요청은 서버가 응답할 때까지 대기하지 않는다. 요청을 전송한 다음에는 애플리케이션을 계속 실행한다. 사용자는 웹 형식에서 데이터를 기입한 다음 기타 버튼을 클릭하고 형식 기입을 종료한다. 회전하는 비치볼, 소용돌이치는 hourglass 및 대형 애플리케이션 정지 등의 현상이 생기지 않는다. 서버는 재빨리 요청에 응답하고 서버가 종료된 경우, 요청으로 인해 원 요청자는 서버가 종료되었음을 알게 된다. 결국 볼품없고 느린 대신 민감하고 대화성 있고 빠른 애플리케이션을 얻게 된다. 정확한 GUI 구성요소 및 웹 디자인 패러다임만 가지고는 느리고 동기적인 요청/응답 모델의 한계를 극복할 수 없다.

요청 전송

일단 open() 메소드로 요청을 구성하고 나면 요청 전송 준비를 한다. 다행히도, 요청을 전송하는 메소드를 open()의 경우에 비해 더 적절하게 명명한다. 명칭은 단순히 send()이다.

send() 메소드는 단 하나의 매개변수인 전송 컨텐트만 있으면 된다. 그 메소드에 대해 너무 깊게 생각하기 이전에 이미 URL 자체를 통해 데이터를 전송했음을 기억하라.

var url = "/cgi-local/lookupCustomer.php?phone=" + escape(phone);


send() 메소드를 사용해 데이터를 전송하지만 URL자체를 통해서도 된다. 사실, GET 요청(일반 Ajax 이용률의 80%를 이루고 있음.)에서는 URL에서 데이터를 전송하는 게 더 용이하다. 안전한 정보/XML을 전송하기 시작한 경우, send() 메소드를 통한 전송 컨텐트에 대해 알아보려고 할 것이다. (이 시리즈 후반부에 안전한 데이터 및 XML 메시징에 대해 논의한다.) send()를 통해 데이터를 전송하지 않아도 되는 경우, 이 메소드에 대한 인수로 null을 전송하면 된다. 따라서 이 글을 통해 알게 된 예에서 보듯 요청 전송작업을 하는 게 필요하다.(Listing 10)


Listing 10. 요청 전송
				

function getCustomerInfo() {
var phone = document.getElementById("phone").value;
var url = "/cgi-local/lookupCustomer.php?phone=" + escape(phone);
request.open("GET", url, true);
request.send(null);
}

콜백 메소드 지정

이 시점에서, 새롭고 혁신적이거나 비동기적이라고 생각될 만한 작업을 거의 하지 못했다. open() 메소드의 키워드 "true"는 비동기식 요청을 설정한다고 하는 게 정확하다. 하지만 그거 말고도 open() 메소드 코드는 Java servlets및 JSP, PHP/Perl이 함께 어우러진 프로그래밍과 유사하다. 그렇다면 Ajax 및 Web 2.0에 담긴 커다란 비밀은 무엇일까? 그 비밀은 onreadystatechange라는 명칭의 XMLHttpRequest의 단순한 속성에서 나오게 된다.

먼저, open() 코드에서 생성된 과정에 대해 확실히 이해한다.(Listing 10) 요청을 설정하고 생성한다. 게다가 이 XMLHttpRequest는 동기식 요청이라 자바 메소드(예에 나온 getCustomerInfo())는 서버 상에서 대기하지 않는다. open() 코드는 계속 진행되고 이 경우 자바 메소드는 정지되며 제어기능은 형태로 나오게 된다. 사용자들은 계속 정보를 입력하고 애플리케이션은 서버 상에서 대기하지 않는다.

이렇게 되면 재미있는 질문이 나오게 된다. 서버가 요청 처리 과정을 완료할 시 발생하는 현상은 어떤 것인가? 적어도 코드가 지금 당장 유지되는 한은 아무 현상도 없다 라는 말이 정답이다. 분명 좋은 현상은 아니다. 따라서 서버에 XXMLHttpRequest객체에 의해 전송된 요청에 관한 처리과정을 완료할 경우 서버는 몇 가지 형태의 명령어를 포함해야 한다.

JavaScript에서의 기능 참조
JavaScript는 약결합 언어며 이 언어에서 모두 다 변수로 참조 가능하다. updatePage()라는 이름의 함수를 선언한 경우, JavaScript는 그 함수 이름을 변수로 취급한다. 즉 updatePage()라는 이름의 변수로 코드에 있는 함수를 참조한다.

이런 상황에서 바로 onreadystatechange 속성이 작용한다. 이 속성으로 콜백 메소드를 지정한다. 콜백 메소드로 서버는 웹 페이지 코드로 다시 호출한다. 그러면서 서버에 어느 정도의 제어 기능이 전달된다. 또한 서버에서 요청을 종료할 때 콜백 메소드는 XMLHttpRequest 객체, 특히 onreadystatechange 속성에서 나타난다. 그 속성에서 지정된 방법이 어떤 메소드든 모두 호출된다. 웹 페이지 자체에서 벌어지는 현상에 관계없이 웹 페이지로 다시 호출할 때 서버에서 개시하기 때문에 콜백이라 부르는 것이다. 예를 들어, 사용자가 의자에 앉아 키보드를 사용하지 않는 동안 콜백 메소드를 호출하기도 한다. 하지만 사용자가 입력하고 마우스를 움직이고, 화면 이동시키고 버튼을 클릭하는 동안에도 콜백 메소드를 호출하기도 한다. 사용자가 하는 업무는 그다지 중요하지 않다.

이런 상황에서 비동시성이 작용한다. 사용자는 다른 레벨에 있는 동안 한 레벨에 있는 형식을 작동하고 서버는 요청에 응답한 다음 onreadystatechange 속성에서 명시된 콜백 메소드를 전송한다. 따라서 Listing 11에 나온 대로 코드에 콜백 메소드를 지정해야 한다.


Listing 11. 콜백 메소드 설정
				

function getCustomerInfo() {
var phone = document.getElementById("phone").value;
var url = "/cgi-local/lookupCustomer.php?phone=" + escape(phone);
request.open("GET", url, true);
request.onreadystatechange = updatePage;
request.send(null);
}

특히 onreadystatechange 속성이 결정된 코드 위치에 주의를 기울인다. 그 위치는 바로 send()가 호출되기 전의 위치다. 요청을 전송하기 전 onreadystatechange 속성을 설정해야 한다. 그래야만, 서버에서 요청 응답을 종료할 때 onreadystatechange 속성을 탐지하게 된다. 인제는 이 글의 마지막 부분에서 중점적으로 다룰 updatePage() 코드에 대해 알아보겠다.




위로


서버 응답 처리

요청을 만들면 사용자는 웹 형식에서 여유롭게 작업하며 (서버에서 요청을 처리하는 동안에는) 서버는 요청 처리과정을 완료한다. 서버는 onreadystatechange 속성에서 나타나며 호출방법을 결정한다. 그런 일이 일어나면 비동기식/동기식 애플리케이션 등의 기타 다른 애플리케이션으로 애플리케이션을 생각할 수도 있다. 즉, 서버에 응답하는 특수 액션 작성 메소드를 취할 필요가 없다. 형식을 변환하고, 사용자를 또 다른 URL에 안내하거나 서버에 응답하는 데 필요한 것들을 하면 된다. 이 단락에서 우리는 서버 응답 및 이에 대한 일반조치 및 사용자가 아는 형식의 일부를 자유롭게 변경하는 것에 대해 중점적으로 다루겠다.

콜백 및 Ajax

이미 서버가 종료될 때의 현상을 서버가 인식하는 방법에 대해 이미 알았다. 일단 XMLHttpRequest 객체의 onreadystatechange 속성을 실행함수 이름에 설정한다. 그 다음 서버에서 요청을 처리하면 서버는 자동적으로 그 함수를 호출한다. 또한 콜백 메소드에 있는 임의의 매개변수에 대해 그리 걱정하지 않아도 된다. Listing 12와 같이 단순한 메소드에서 시작하기 때문이다.


Listing 12. 콜백 메소드 코드화
				



이렇게 하면 간단한 경고가 울리면서 서버가 종료될 때를 알려준다. 자체 페이지에 updatePage() 코드를 시험하고 페이지를 저장한 다음 브라우저에 페이지를 끌어올린다.(이 예에서 XHTML을 원할 경우, Listing 8을 참조한다.) 전화번호를 입력하고 필드를 설정하지 않을 경우 경고는 팝업되어야 한다. 하지만 확인을 클릭한 경우에도 경고는 팝업을 연속한다.


그림 3. 경고를 팝업하는 Ajax 코드
Ajax code popping up an alert

브라우저에 ‘따라 웹 형식에서 경고 팝업을 중지할 때까지 경고가 두 번, 세 번, 심지어는 네 번까지 울린다. 그러면 무슨 일이 벌어지고 있는 것인가? 요청/응답 사이클의 중요 구성요소인 HTTP 준비상태에 대해 고려하지 않았다.

HTTP 준비 상태

초기에 필자는 서버에서 요청이 종료되면 XMLHttpRequestonreadystatechange 속성에서 호출되는 메소드를 탐지한다고 가르쳤다. 사실, HTTP 준비 상태가 변할 때마다 서버에서는 방금 전에 언급한 메소드를 호출한다. 그러면 그 말이 의미하는 것은 무엇인가? 일단 먼저 HTTP 준비상태에 관해 이해해야 한다.

HTTP 준비상태는 요청의 상태를 나타내며 주로 요청을 시작했는지, 요청에 응답했는지, 요청/응답 모델을 완성했는지 여부를 결정하는 데 활용된다. HTTP 준비상태는 서버에서 공급되는 모든 응답 텍스트/데이터를 읽어 들이는 데 안전한지 여부를 결정하는 데 도움이 되기도 한다. 여기서 Ajax 애플리케이션에서의 5가지 준비상태에 관해 알아야 한다.

  • 0: 요청이 개시되지 않음.(open()을 호출하기 전)
  • 1: 요청을 설정했지만 전송되지는 않았음.(send()를 호출하기 전)
  • 2: 요청을 설정한 다음 처리 중(이 시점에서 일반적으로 응답에서 나온 컨텐트 헤더를 얻는다.)
  • 3: 요청 처리 중; 종종 응답에서 부분적인 데이터를 사용할 수 있다. 하지만 서버는 자체 응답이 완료되지 않았다.
  • 4: 응답 완료. 서버 응답을 얻은 다음 이를 활용한다.

거의 모든 크로스-브라우저 이슈에서도 그렇듯 예상치 못한 방식으로 이와 같은 준비 상태를 이용한다. 준비상태는 항상 0~1, 2, 3, 4로 단계적으로 이동한다고 예상할지도 모른다. 하지만 실지로는 그렇지 않다. 0/1상태를 보고하지 않고 곧바로 2로 건너뛰어 3,4까지 가는 브라우저도 있고 모든 상태를 보고하는 브라우저도 있다. 지난 단락에서 보듯, 서버에서는 몇 번이고 updatePage()코드를 호출하고 호출 때마다 경고 상자가 팝업된다. 그건 여러분이 의도하는 바가 아닐 것이다!

Ajax 프로그래밍의 경우, 직접 다뤄야 할 상태는 오로지 상태 4다. 이는 서버 응답이 완료되었고 응답 데이터를 점검, 사용하는 데 안전하다는 것을 의미한다. 이를 설명하기 위해 콜백 메소드에 나온 첫 번째 라인은 Listing 13에서 나온 바여야 한다.


Listing 13. 준비상태 점검
				

function updatePage() {
if (request.readyState == 4)
alert("Server is done!");
}

이런 변환으로 서버가 정말로 그 과정을 종료했는지 확인한다. Ajax 코드의 이 버전을 실행한다. 그러면 한 번에 경고 메시지만을 얻어야 한다.

HTTP 상태 코드

Listing 13에서의 코드의 성공에도 불구하고 여전히 문제는 상존한다. 그러면 서버가 요청에 응답하고 요청 처리과정을 완료했지만 에러를 보고한 경우는 어찌 되는가? Ajax, JSP, 정규 HTML 형식 또는 기타 형태의 코드로 서버측 코드를 호출 중인 경우에 서버측 코드를 관찰해야 한다는 점을 주목한다. 웹 세계에서는 HTTP 코드로 요청에서 발생할지도 모르는 여러 가지 상황을 다룬다.

예를 들어, URL에 관한 요청을 입력했지만, URL을 부정확하게 입력해 404 에러코드가 나와 페이지가 없어졌다고 해보자. 이 코드는 HTTP 요청을 상태로 수신하는 여러 상태 코드 가운데 하나에 지나지 않는다.(참고자료) 403, 401 코드는 둘 다 안전하거나 금지된 데이터 처리를 의미하는 것으로 역시 공통적이다. 각 경우에 있어 이런 코드들은 완전 응답에서 나오는 코드들이다. 즉, 서버는 요청을 수행하지만(HTTP 준비상태는 4임), 클라이언트가 예상한 데이터가 나오지 않을 수도 있다.

여기서 준비 상태에 덧붙여, HTTP 상태를 점검할 필요가 있다. 단순히 확인을 의미하는 상태코드 200을 탐색하는 중에 있다. 준비상태 4와 상태코드 200인 상태에서 서버 데이터를 처리할 준비가 되어 있고 그 데이터는 반드시 요청된 형태여야 한다. (에러 또는 문제가 있는 정보 단편이 아님.) Listing 14에서 보듯이 콜백 메소드에 또 다른 상태 점검기능을 추가한다.


Listing 14. HTTP 상태 코드 점검
				

function updatePage() {
if (request.readyState == 4)
if (request.status == 200)
alert("Server is done!");
}

복잡성을 줄이고 더 강력한 에러 처리기능을 추가하려면 기타 상태코드에 관한 점검기능/두 가지 기능을 추가할지도 모른다. Listing 15에 있는 updatePage()의 수정 버전을 점검한다.


Listing 15. 간단한 에러 점검기능 추가
				

function updatePage() {
if (request.readyState == 4)
if (request.status == 200)
alert("Server is done!");
else if (request.status == 404)
alert("Request URL does not exist");
else
alert("Error: status code is " + request.status);

}

이제 getCustomerInfo()에 있는 URL을 비실제 URL로 변환시킨 다음 일어나는 현상을 보면 요청한 URL은 존재하지 않는다는 의미의 경고가 울린다. 이런 경고 가지고도 모든 에러상태를 거의 처리하지 않는다. 하지만 웹 애플리케이션에서 발생할 수 있는 문제의 80%는 해결하는 단순한 진전이 아닐 수 없다.

응답 텍스트 읽기

인제 요청을 준비상태를 통해 완전히 처리하고, 서버로 정상적인 확인 응답을 상태 코드를 통해 받았으므로 서버에서 재전송되는 데이터를 최종적으로 처리한다. 이 데이터는 XMLHttpRequest객체의 responseText 속성에 저장된다.

포맷/길이에 의한 responseText 속성의 텍스트 모양에 관한 상세사항은 이 장에서는 논하지 않기로 한다. 이렇게 되면 서버는 이 텍스트를 실지로 임의로 설정한다. 예를 들어, 한 스크립트로 콤마-분리 값 및 파이프-분리 값이 나오고 또 다른 파이프-분리 값은 텍스트의 긴 문자열로 나올 수도 있다. 이런 현상은 서버에 따라 다르게 된다.

이 글에서 사용된 예의 경우, 서버는 파이프 기호로 분리된 고객의 마지막 순서 및 주소가 나오게 된다. 형식의 구성요소 값을 설정하는 데 순서 및 고객의 마지막 순서 및 주소를 활용한다. Listing 16은 디스플레이를 업데이트하는 코드에 대해 나와 있다.


Listing 16. 서버 응답 처리
				

function updatePage() {
if (request.readyState == 4) {
if (request.status == 200) {
var response = request.responseText.split("|");
document.getElementById("order").value = response[0];
document.getElementById("address").innerHTML =
response[1].replace(/\n/g, "
");

} else
alert("status is " + request.status);
}
}

우선 JavaScript split() 메소드를 이용해 파이프 기호 상에서 responseText를 얻고 분할한다. 값은 response의 형태로 배열된다. 고객의 마지막 순서에 관한 첫 번째 값은 response[0] 형태의 배열로 처리되고 "순서" ID와 함께 필드 값으로 설정된다. response[1]에서 두 번째 값은 고객 주소로 처리하는 데 좀 더 오랜 시간이 걸린다. 주소 라인은 정상 라인 분리자("\n" 문자) 로 분리되기 때문에 코드는 정상라인 분리자를 XHTML-형 라인 분리자(
)로 바꾸어야 한다. 정규 식 및 replace() 함수의 활용을 통해 분리자를 바꾸는 과정이 이루어진다. 결국 변경 텍스트는 HTML 형태에서 div 의 내부 HTML로 설정된다. 결국 그림 4에도 나오듯이 텍스트 형식은 순식간에 고객정보로 업데이트된다.


그림 4. 고객 데이터 검색 후의 Break Neck 형식
The Break Neck form after retrieving customer data

이 글을 마치기 전에 XMLHttpRequest 객체의 중요한 속성 중 하나인 responseXML 속성에 대해 언급한다. 이 속성은 서버가 XML과의 응답을 선택한 경우, XML 응답을 포함한다. (상상이 되는가?) XML 응답 처리는 평범한 텍스트 처리과정과 상당히 다르며, 문장분석 및 문서 객체 모델(DOM)을 포함한다. 다음 글에서는 XML에 대해 다루게 된다. responseXML 은 공통적으로 responseText과 관련된 논의에서 나오기 때문에 언급할 가치가 있는 것이다. 많은 단순 Ajax 애플리케이션의 경우, responseText만 있으면 된다. 하지만 Ajax 애플리케이션을 통해 XML을 처리하는 방법에 대해서도 곧 배우게 된다.




위로


맺음말

XMLHttpRequest 객체에 대해서는 인제 좀 지루하게 들릴지도 모른다. 필자는 단일 객체, 특히 간단한 객체에 대한 전반적인 글을 거의 읽지 못했다. 하지만 Ajax를 사용하고 작성하는 각 페이지 및 애플리케이션에서 계속 XMLHttpRequest 객체를 사용하게 된다. 아직도 XMLHttpRequest 객체에 대해 언급되지 않은 것들이 많은 건 사실이다. 다음 글에서는 요청에서 GETPOST를 사용하고 서버로부터의 응답 및 요청의 컨텐트 헤더를 설정하고 읽어들이는 방법을 배운다. 그러면 요청을 코드화하고 심지어는 요청/응답 모델에서 XML을 다루는 방법을 배우게 될 것이다.

좀 더 상세하게 나가면 일반적으로 사용하는 Ajax 툴킷에 관해서도 알게 된다. 이 툴킷은 본 글에서 논의된 상세사항의 대부분을 실지로 요약한 것이다. 한편 툴킷을 손쉽게 이용하는 경우, 낮은 레벨의 상세사항을 코드화하는 이유에 대해 궁금해할 수도 있다. 사실은 애플리케이션 상에서 발생하는 현상을 이해하지 못하는 경우, 애플리케이션에서 일어나는 에러를 이해하는 게 어려워진다.

따라서, 이와 같은 사항을 간과하거나 지나치면 안 된다. 가변성 툴킷에서 에러가 발생할 경우, 머리를 끄적이면서 e-메일을 보내지 않아도 된다. 직접 XMLHttpRequest 사용법을 이해하면 가장 이상한 문제를 디버그하고 수정하는 것도 쉬워진다. 툴킷에 집중하면서 모든 문제를 해결하지 않는 한 툴킷은 그런대로 괜찮다.

따라서, XMLHttpRequest 객체에 대해 친숙해져라. 사실, 툴킷을 사용하는 Ajax 코드를 실행할 경우 XMLHttpRequest 객체 및 속성, 메소드를 사용해Ajax 코드를 재작성한다. 상당히 좋은 연습이 될 것이며 현재 이 객체에서 벌어지는 현상에 대해 더 잘 이해하게 될 것이다.

다음 글에서는 XMLHttpRequest에 대해 좀 더 자세하게 들어간다. 이 객체에서 어려운 속성(responseXML등의), POST 요청 사용법 및 몇 가지 다른 포맷에서의 데이터를 전송하는 방법을 조사할 것이다. 한 달 동안 코드화 작업을 시작해 코드를 다시 점검한다.

기사의 원문보기



참고자료

교육

제품 및 기술 얻기

토론


link

http://ebcblue.com/ver2/

http://www.netsko.com/

http://www.movierg.com/

http://www.dcinside.com/

http://www.toshare.kr/

http://www.youtube.com/

2009年8月28日金曜日

タグファイル

http://lab.moyo.biz/recipes/java/jsp/tagfile.jsp

タグファイル は JSP 感覚で拡張タグを作成することの出来る JSP 2.0 の機能です。 いくつかの JSP で共用したい HTML スクラップや JSP 機能を、タグハンドラや TLD を作成するより 簡単に、スクリプトレットより可視性を損なわず部品化することが出来ます。

<%@page%> 宣言された JSP が内部でサーブレット クラスに変換されるのに対して <%@tag%> 宣言 されたタグファイルはタグハンドラへ変換されます。

使用に関してはいくつかの制約があります。

保存場所 タグファイルは /WEB-INF/tags の下に保存するか (サブディレクトリでも可)、あるいは JAR にまとめて /WEB-INF/lib の下に保存する必要があります。JAR にまとめる場合は TLD が必要です。
ファイル名 タグファイルの名前は JSP で使用するときのタグ名として使用されます。 例えば JSP で zip.tag というタグファイルを利用する場合は と記述します。
拡張子 タグファイルの拡張子は *.tag です。
スクリプト タグファイルの拡張タグで囲った Body 部分では <% %> や <%= %> などの JSP スクリプト構文を 記述できません (Body 部分がフラグメントで渡されるため)。これらは EL や JSTL などの拡張タグで実現する必要があります。
jspContext タグファイル内では暗黙変数 pageContext は提供されません。代わりに jspContext を使用する必要があります。
構文が JSP であることからタグファイルはプレゼンテーションベースの拡張タグを作成するのに 向いています。例えば:

サイト内でレイアウトを共通化したい。例えばページのヘッダ、フッタなど。
少々凝った HTML や JavaScript を JSP から分離したい。例えばページがロードされないと SUBMIT ボタンが有効にならないフォームなど。
配置調整のための透過画像 (いわゆるスペーサー) などの部品を簡略記述したい。
逆に複雑なデータベース処理や特定のライブラリに依存する処理などのロジカルな記述がメインになる場合は、 記述の容易さやメンテナンス性の観点から通常の Java コードで拡張タグとして作成する方が良いと言えます。

試行
まずタグファイルを作成してみます。このサンプルは属性 color に指定した色でテキストを 中央表示する HTML です。保存場所は /WEB-INF/tags/sample/foo.tag にしました。

<%@ tag language="java" pageEncoding="UTF-8" %>
<%@ attribute name="color" %>

作成したタグファイルを JSP から呼び出します。<%@taglib%> で指定するのはディレクトリまでです。ファイル名がタグ名に使用されている事に注意してください。

<%@ page language="java" ... %>
<%@ taglib prefix="sample" tagdir="/WEB-INF/tags/sample" %>

Hello, world
この出力結果は以下のようになります。

Hello, world

Hello, world
クラスファイルも TLD も記述しないで拡張タグを実装することが出来ました!

動的属性
JSP 1.x 時代のタグハンドラは属性名ごとに用意した setter で属性値を受け取るという仕組みでした。

これは Java Beans らしいようにも見えますが、現実に汎用的なカスタムタグを設計しようとすると、 タグの処理に必要な属性以外にも、指定可能な全ての HTML 属性に setter を用意する必要がありました。 id, lang, title, style, class などのから onmouseenter, onmousedown, onmouseclick, onclick, onselect, ... などなど 膨大な数の setter を作成し、同時に膨大な数の属性定義を TLD に記述する必要があったわけです (Struts の BaseHandlerTag クラスがまさにその患者です)。

JSP 2.0 では全ての属性についてタグクラスで setter を用意しなくても (タグファイルでは <%@attribute%> を定義しなくても) 任意の属性値を受け取れるようになりました。 <%@tag%> ディレクティブの dynamic-attributes に変数名 を指定した場合、全ての属性の名前-値セットが Map としてその変数に設定されます。

<%@ tag language="java" pageEncoding="UTF-8" dynamic-attributes="attr" %>

${entry.key}=""

>




フラグメント
JSP 2.0 からフラグメントという機能を使用することが出来ます。

カスタムタグで囲んだ内容 (以後 Body と言います) はフラグメントとしてタグファイルに 渡されています。そして Body を表すフラグメントは とすることで 実際の出力が行われます。フラグメントの登場によって Body 部分をいつ処理するかがタグハンドラ側で 自然に記述できるようになったわけです (以前は Body 部の 「前処理」 「後処理」 という記述でした)。

Body 部分を処理する前にタグファイル内で設定した変数を Body 内の EL やカスタムタグで 参照することが出来ます。


製品名: ${product.name}

価格: ${proeuct.price}

個数: ${proeuct.count}



--- writeProduct.tag の内容 ---
<%@ tag language="java" pageEncoding="UTF-8" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>






--- 実行結果 ---
製品名: インスタントラーメン

価格: ¥98

個数: 12個

↓この JSP で実際に実行しています。
製品名: インスタントラーメン
価格: ¥98
個数: 12個
フラグメントを使用することで JSP のスクラップを未評価のままで渡すことが出来ます。 そしてタグファイル (タグハンドラ) 側で任意のタイミングで評価を行うことが出来ます。 出力結果ではなく出力処理そのものを渡しているという意味では、Java の匿名内部クラスや JavaScript のエンクロージャーに似ていると言えるかもしれません。

JSP 2.0 のカスタムタグは Body 部分だけではなく属性値もフラグメントとして渡すことが出来ます。 例として、タグファイル側で用意した時刻を呼び出し元の JSP の指定したフォーマットで 出力してみます。呼び出し側の JSP での記述は以下の通り。


${timestamp}

そしてタグファイル側は以下の通り。現在時刻をフォーマットして変数として設定した後に フラグメントとして渡された date 属性を評価しています。

<%@ tag language="java" pageEncoding="UTF-8" body-content="empty" %>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
<%@ attribute name="date" required="true" fragment="true" %>



この実行結果は以下のようになります。タグファイル側で用意した日時が JSP の指定した書式で 出力されました。

2007/12/08 23:54:55
↓この JSP で実際に実行しています。
2009/08/28 11:11:56


タグファイル は JSP 感覚で拡張タグを作成することの出来る JSP 2.0 の機能です。 いくつかの JSP で共用したい HTML スクラップや JSP 機能を、タグハンドラや TLD を作成するより 簡単に、スクリプトレットより可視性を損なわず部品化することが出来ます。

<%@page%> 宣言された JSP が内部でサーブレット クラスに変換されるのに対して <%@tag%> 宣言 されたタグファイルはタグハンドラへ変換されます。

使用に関してはいくつかの制約があります。

保存場所 タグファイルは /WEB-INF/tags の下に保存するか (サブディレクトリでも可)、あるいは JAR にまとめて /WEB-INF/lib の下に保存する必要があります。JAR にまとめる場合は TLD が必要です。
ファイル名 タグファイルの名前は JSP で使用するときのタグ名として使用されます。 例えば JSP で zip.tag というタグファイルを利用する場合は と記述します。
拡張子 タグファイルの拡張子は *.tag です。
スクリプト タグファイルの拡張タグで囲った Body 部分では <% %> や <%= %> などの JSP スクリプト構文を 記述できません (Body 部分がフラグメントで渡されるため)。これらは EL や JSTL などの拡張タグで実現する必要があります。
jspContext タグファイル内では暗黙変数 pageContext は提供されません。代わりに jspContext を使用する必要があります。
構文が JSP であることからタグファイルはプレゼンテーションベースの拡張タグを作成するのに 向いています。例えば:

サイト内でレイアウトを共通化したい。例えばページのヘッダ、フッタなど。
少々凝った HTML や JavaScript を JSP から分離したい。例えばページがロードされないと SUBMIT ボタンが有効にならないフォームなど。
配置調整のための透過画像 (いわゆるスペーサー) などの部品を簡略記述したい。
逆に複雑なデータベース処理や特定のライブラリに依存する処理などのロジカルな記述がメインになる場合は、 記述の容易さやメンテナンス性の観点から通常の Java コードで拡張タグとして作成する方が良いと言えます。

試行
まずタグファイルを作成してみます。このサンプルは属性 color に指定した色でテキストを 中央表示する HTML です。保存場所は /WEB-INF/tags/sample/foo.tag にしました。

<%@ tag language="java" pageEncoding="UTF-8" %>
<%@ attribute name="color" %>

作成したタグファイルを JSP から呼び出します。<%@taglib%> で指定するのはディレクトリまでです。ファイル名がタグ名に使用されている事に注意してください。

<%@ page language="java" ... %>
<%@ taglib prefix="sample" tagdir="/WEB-INF/tags/sample" %>

Hello, world
この出力結果は以下のようになります。

Hello, world

Hello, world
クラスファイルも TLD も記述しないで拡張タグを実装することが出来ました!

動的属性
JSP 1.x 時代のタグハンドラは属性名ごとに用意した setter で属性値を受け取るという仕組みでした。

これは Java Beans らしいようにも見えますが、現実に汎用的なカスタムタグを設計しようとすると、 タグの処理に必要な属性以外にも、指定可能な全ての HTML 属性に setter を用意する必要がありました。 id, lang, title, style, class などのから onmouseenter, onmousedown, onmouseclick, onclick, onselect, ... などなど 膨大な数の setter を作成し、同時に膨大な数の属性定義を TLD に記述する必要があったわけです (Struts の BaseHandlerTag クラスがまさにその患者です)。

JSP 2.0 では全ての属性についてタグクラスで setter を用意しなくても (タグファイルでは <%@attribute%> を定義しなくても) 任意の属性値を受け取れるようになりました。 <%@tag%> ディレクティブの dynamic-attributes に変数名 を指定した場合、全ての属性の名前-値セットが Map としてその変数に設定されます。

<%@ tag language="java" pageEncoding="UTF-8" dynamic-attributes="attr" %>

${entry.key}=""

>




フラグメント
JSP 2.0 からフラグメントという機能を使用することが出来ます。

カスタムタグで囲んだ内容 (以後 Body と言います) はフラグメントとしてタグファイルに 渡されています。そして Body を表すフラグメントは とすることで 実際の出力が行われます。フラグメントの登場によって Body 部分をいつ処理するかがタグハンドラ側で 自然に記述できるようになったわけです (以前は Body 部の 「前処理」 「後処理」 という記述でした)。

Body 部分を処理する前にタグファイル内で設定した変数を Body 内の EL やカスタムタグで 参照することが出来ます。


製品名: ${product.name}

価格: ${proeuct.price}

個数: ${proeuct.count}



--- writeProduct.tag の内容 ---
<%@ tag language="java" pageEncoding="UTF-8" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>






--- 実行結果 ---
製品名: インスタントラーメン

価格: ¥98

個数: 12個

↓この JSP で実際に実行しています。
製品名: インスタントラーメン
価格: ¥98
個数: 12個
フラグメントを使用することで JSP のスクラップを未評価のままで渡すことが出来ます。 そしてタグファイル (タグハンドラ) 側で任意のタイミングで評価を行うことが出来ます。 出力結果ではなく出力処理そのものを渡しているという意味では、Java の匿名内部クラスや JavaScript のエンクロージャーに似ていると言えるかもしれません。

JSP 2.0 のカスタムタグは Body 部分だけではなく属性値もフラグメントとして渡すことが出来ます。 例として、タグファイル側で用意した時刻を呼び出し元の JSP の指定したフォーマットで 出力してみます。呼び出し側の JSP での記述は以下の通り。


${timestamp}

そしてタグファイル側は以下の通り。現在時刻をフォーマットして変数として設定した後に フラグメントとして渡された date 属性を評価しています。

<%@ tag language="java" pageEncoding="UTF-8" body-content="empty" %>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
<%@ attribute name="date" required="true" fragment="true" %>



この実行結果は以下のようになります。タグファイル側で用意した日時が JSP の指定した書式で 出力されました。

2007/12/08 23:54:55
↓この JSP で実際に実行しています。
2009/08/28 11:11:56

[DB]ナチャラルキー(自然キー)とサロゲートキー(代替キー)

■[DB]ナチャラルキー(自然キー)とサロゲートキー(代替キー) 22:12

http://d.hatena.ne.jp/samehada3/20070320/1174396324
テーブルの主キーを決める場合、ナチャラルキー(自然キー)とサロゲートキー(代替キー)というアプローチがある。

ナチャラルキーとは、システムの外部から入力される社員番号のような業務上意味のあるキーであり、サロゲートキーとは、単にレコードをユニークに扱う為の業務上意味を持たない連番などのキーである。

一概にどちらが優れていると言えないが次のようなメリット、デメリットがあると考える。

  ナチュラルキー サロゲートキー
メリット ・テーブルの関連が理解しやすい ・変更に強い
・ORMする場合、オブジェクトとテーブルの対応がしやすい
デメリット ・変更に弱い
・・ORMする場合、オブジェクトとテーブルの対応がしづらい テーブルの関連がわかりづらい

個人的に・・・

単にレコードのデータを見た場合、サロゲートキーだと何のレコードかわかりづらい気がするし、テーブルの関連をみる場合もナチュラルキーの方が関連の意味が読み取りやすい気がする。

でもORMするなら、複合キーを利用するよりはサロゲートキーを利用した方が扱いやすいし、ナチュラルキーより断然向いている気がする。いちいち複合キークラスを作るのも面倒くさいし。。

あと、社員番号の体系が数値から英数を含むコードに変更されたなんていう場合もサロゲートキーの方が修正のインパクトが小さいだろうなぁ。


というわけで今後、JPAを使う場合は、サロゲートキーを導入して、ナチュラルキーとなりそうな項目は候補キーとしてユニーク制約を貼るといったアプローチをとってみようと思う。

Version Number パターンによる排他制御

Version Number パターンによる排他制御
Java, Seasar, SQL

http://d.hatena.ne.jp/r_ikeda/20090206/version

排他制御の実装パターンに Version Number パターンというものがある。

テーブルに INT 型の version_no というカラムを用意する。
UPDATE 時に version_no をインクリメントする。
UPDATE 時の WHERE 条件に version_no を追加する。
UPDATE dept
SET dept_no = ?, dept_name = ?, loc = ?, version_no = version_no + 1
WHERE id = ? AND version_no = ?
statement = connection.prepareStatement(UPDATE_SQL);
statement.setInt(1, dept.getDeptNo());
statement.setString(2, dept.getDeptName());
statement.setString(3, dept.getLoc());
statement.setInt(4, dept.getId());
statement.setInt(5, dept.getVersionNo());
これで自分以外の誰かが更新していた場合に version_no が合わない状態になるため、更新されない。S2Dao では自動でこれをやってくれる。

[S2JDBC]排他制御について

いづのです。

現在のS2JDBCのVersionカラムの仕様だと以下のような操作に対して、排他制御が無効になってしまいます。

1.新規レコードを作成
2.そのレコードをAとBの画面で表示。
3.A画面でレコードを物理削除
4.A画面で同じキーで新規レコードを作成
5.B画面で更新

S2JDBCとしてはこのような操作に対する排他制御は現在の仕様上対応出来ないと思っていますが、それは正しいでしょうか。


その場合、現在以下のような対応方法を検討しています。
・Versionカラムを日付型に対応するよう拡張する
・S2JDBCをやめる⇒S2Daoに乗り換え

他になにか良い方法ないでしょうか

ーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーー

ひがです。

主キーを代理キー(@GeneratedValue)を使うようにするのが、
一番簡単だと思います。

ビジネスキーを主キーにするのは、レガシーマイグレーション以外では
お勧めしません。

3,4の操作は、普通は、更新でやると思うのですが、
このようなホストっぽい操作をするときは
1の後にダミーで一回更新することで、
versionが更新されるので、5で排他エラーになります。

*サロゲートキー
データベーステーブルの主キーとして導入された、ビジネス上の意味を持たない、連番などのキー。



ーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーー
いづのです。

> 主キーを代理キー(@GeneratedValue)を使うようにするのが、
> 一番簡単だと思います。
>
> ビジネスキーを主キーにするのは、レガシーマイグレーション以外では
> お勧めしません。

今回はそのレガシーマイグレーションなんです。

> 3,4の操作は、普通は、更新でやると思うのですが、
> このようなホストっぽい操作をするときは
> 1の後にダミーで一回更新することで、
> versionが更新されるので、5で排他エラーになります。

排他制御用のカラムに日付をサポートしていないのは、恐らくサロゲートキーがあるからだと思ってました。
やっぱりそうですね。

ではサロゲートキーを使用していない場合、以下のケースでは排他制御は無理ということですよね。

>1.新規レコードを作成
>2.そのレコードをAとBの画面で表示。
>3.A画面でレコードを物理削除
>4.A画面で同じキーで新規レコードを作成
>5.B画面で更新

なにか対処方があればヒントをいただけるとありがたいです。
ーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーー

ひがです。

> いづのです。
>
> ではサロゲートキーを使用していない場合、以下のケースでは排他制御は無理ということですよね。
>
> >1.新規レコードを作成
> >2.そのレコードをAとBの画面で表示。
> >3.A画面でレコードを物理削除
> >4.A画面で同じキーで新規レコードを作成
> >5.B画面で更新
>
> なにか対処方があればヒントをいただけるとありがたいです。

今回、@VersionでTimestampサポートも必要だということがわかりました。
S2JDBC自体でTimestampサポートを検討したいと思います。

ただ、次のバージョンが出るのは、来年の初めだと思うので、
それまで待てない場合は、ご面倒をおかけしますが、
S2JDBCを独自に拡張してください。

よろしくお願いします。

ひがです。

> いづのです。
>
> ではサロゲートキーを使用していない場合、以下のケースでは排他制御は無理ということですよね。
>
> >1.新規レコードを作成
> >2.そのレコードをAとBの画面で表示。
> >3.A画面でレコードを物理削除
> >4.A画面で同じキーで新規レコードを作成
> >5.B画面で更新
>
> なにか対処方があればヒントをいただけるとありがたいです。

今回、@VersionでTimestampサポートも必要だということがわかりました。
S2JDBC自体でTimestampサポートを検討したいと思います。

ただ、次のバージョンが出るのは、来年の初めだと思うので、
それまで待てない場合は、ご面倒をおかけしますが、
S2JDBCを独自に拡張してください。

よろしくお願いします。

ーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーー

小林 (koichik) です.

Date: Wed, 17 Dec 2008 13:02:48 +0900
From: "IZUNO Tadashi" <[E-MAIL ADDRESS DELETED]>
To: [E-MAIL ADDRESS DELETED]
Subject: [Seasar-user:16522] Re: [S2JDBC] 排他制御について

> ではサロゲートキーを使用していない場合、以下のケースでは排他制御は無理ということですよね。
>
> >1.新規レコードを作成
> >2.そのレコードをAとBの画面で表示。
> >3.A画面でレコードを物理削除
> >4.A画面で同じキーで新規レコードを作成
> >5.B画面で更新
>
> なにか対処方があればヒントをいただけるとありがたいです。

insert するエンティティのバージョンカラムに
タイムスタンプ (long 値) を設定すればよいかと.

Entity e = new Entity();
e.version = System.currentTimeMillis();
...
jdbcManager.insert(e).execute();

version フィールドの初期値をタイムスタンプに
してしまってもいいかも.

public class Entity {
...
@Version
public Long version = System.currentTimeMillis();
}

insert 時,version プロパティの値が null または
0 以下の場合は初期値として 1 が設定されますが,
1 以上の場合はそれがそのまま version カラムの
初期値になります.

Date: Wed, 17 Dec 2008 13:54:05 +0900
From: Yasuo Higa <[E-MAIL ADDRESS DELETED]>
To: [E-MAIL ADDRESS DELETED]
Subject: [Seasar-user:16523] Re: [S2JDBC] 排他制御について

> 今回、@VersionでTimestampサポートも必要だということがわかりました。
> S2JDBC自体でTimestampサポートを検討したいと思います。

Timestamp 型をサポートしなくても大丈夫じゃ
ないかなー.

ーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーー

2009年8月19日水曜日

Javaの理論と実践: 並行コレクション・クラス

http://www.ibm.com/developerworks/jp/java/library/j-jtp07233/index.html

Javaの理論と実践: 並行コレクション・クラス

ConcurrentHashMapとCopyOnWriteArrayListにより、スレッド・セーフとスケーラビリティーの改良が得られます


2003年 7月 23日

数多くの他の便利な並行性ビルディング・ブロックに加えて、Doug Lea氏のutil.concurrentパッケージには、コレクション・タイプのListやMapを利用した高機能でスレッド・セーフな処理系が含まれていますが、今回、Brian Goetz氏は、HashtableやsynchronizedMapをConcurrentHashMapに変えるだけで、並行プログラムはどれだけ恩恵を得ることができるかについて説明しています。

Javaクラス・ライブラリーの最初の結合的コレクション・クラスは、JDK 1.0の機能であるHashtableでした。Hashtableは使いやすくてスレッド・セーフな結合的マップ機能を保持しており、確かに便利なものでした。しかし、Hashtableのスレッド・セーフ機能はすべてのメソッドが同期化されるという点で、かなり不便なものでした。よってJDK1.0での競合のない同期化には、かなりのパフォーマンス・コストがかかっていました。Hashtableの後継であるHashMapはJDK 1.2のコレクション・フレームワークの機能として登場し、非同期の基底クラスおよび同期化ラッパーであるCollections.synchronizedMapで、スレッド・セーフに対処しました。つまり、スレッド・セーフなCollections.synchronizedMapから基本機能を分割することで、同期化が必要なユーザーは機能を有し、同期化が必要ではないユーザーは機能を有さないようにすることができるようになりました。

HashtableやsynchronizedMap(Hashtableあるいは同期化Mapラッパーオブジェクトの各メソッドを同期化します)による同期化のアプローチは、2つの重要な問題を抱えています。1つ目は、1度にハッシュ・テーブルにアクセスすることができるスレッドは1つだけであるというスケーラビリティーの障害です。2つ目は、多くのcommon複合オペレーションが追加の同期を要求するいう点で、真のスレッド・セーフを提供しているとは言えないということです。get()やput()のようなシンプルなオペレーションは追加同期なしでも支障はないものの、イテレーションや、put-if-absentというようなオペレーションの共通シーケンスは、データ競合の回避を外部同期に要求します。

条件付きスレッド・セーフ

同期化コレクション・ラッパーのsynchronizedMapとsynchronizedListは、条件付きスレッド・セーフ と呼ばれることがあります。個別の操作はすべてスレッド・セーフであるものの、制御フローが前のオペレーションの結果に依存するようなオペレーション・シーケンスの場合は、データ競合が起こる可能性があるためです。リスト1の前半は、よくある「put-if-absent 技法」であり、エントリーがMapに存在しない場合は追加するというものですが、あいにく、リスト1に書かれているcontainsKey()のメソッドの戻りとput()メソッドが呼ばれるまでの間に別のスレッドが同じkeyに値を設定することが可能です。書き込みを一度だけにしたければ、Map mで同期化する同期化ブロックでステートメントをラップする必要があります。

リスト1では、イテレーションも扱われています。最初の例では、別のスレッドがリストからアイテムを削除することができたので、ループの実行中にList.size()の結果は無効になる可能性がありました。タイミング悪くループの最後のイテレーションを入力した直後に、アイテムが別のスレッドによって削除された場合、List.get()はnullを返し、その結果doSomething()はNullPointerExceptionを投げることになるでしょう。これを回避するためには何ができるでしょう?もしListを使って繰返し処理をしている間に、別のスレッドがListにアクセスする可能性があるとしたら、同期化ブロックでListをラップしList lを同期化して、繰返しの間はList全体をロックしなければなりません。これによりデータ競合を解決することはできますが、繰返しの間List全体をロックすると、他のスレッドは長い間Listにアクセスするすることができなくなり、並行性に深刻な影響を与えることになってしまいます。

コレクション・フレームワークはリストあるいは他のコレクションの検索にイテレータを導入しました。これにより、コレクションの要素を通じて繰返し処理を最大限に利用することができます。しかし、java.utilのCollectionsクラスで実装されたイテレータはfail-fastであるので、あるスレッドがIteratorを通じて検索している間に別のスレッドがコレクションを変更すれば、この後のIterator.hasNext()もしくはIterator.next()呼出しはConcurrentModificationExceptionを投げるということになります。上の例のように、ConcurrentModificationExceptionを回避したければ、List lで同期化する同期化ブロックでList全体をラップし、繰返しの間List全体をロックしなければなりません。(または、同期化を必要としない配列のList.toArray()やiterateを起動することもできますが、リストが大きい場合はパフォーマンス・コストがかかってしまいます。)

リスト1. synchronized mapのよくある競合状態


Map m = Collections.synchronizedMap(new HashMap());
List l = Collections.synchronizedList(new ArrayList());
// put-if-absent idiom -- contains a race condition
// may require external synchronization
if (!map.containsKey(key))
map.put(key, value);
// ad-hoc iteration -- contains race conditions
// may require external synchronization
for (int i=0; i doSomething(list.get(i));
}
// normal iteration -- can throw ConcurrentModificationException
// may require external synchronization
for (Iterator i=list.iterator(); i.hasNext(); ) {
doSomething(i.next());
}


信頼に関する誤った意識

synchronizedListとsynchronizedMapが提供している条件付きスレッド・セーフには、隠された危険性があります。開発者はこれらのコレクションは同期化しており、完全にスレッド・セーフであると考え、複合的なオペレーションを適切に同期化させることを怠たるようになるのです。その結果、これらのプログラムは簡単に機能しているように見えて、実際は高い負荷により、NullPointerExceptionやConcurrentModificationExceptionを投げることになるかもしれないのです。



上に戻る


スケーラビリティーの問題

スケーラビリティーとは、アプリケーションのワークロードや利用できるコンピュータ・リソースの増加に応じて、スループットがどのようになるかということです。スケーラブルなプログラムは、プロセッサー、メモリー、I/O帯域幅に比例して、より大きなワークロードを扱うことができます。排他アクセス用の共有リソースをロックすることは、スケーラビリティーのボトルネックです。というのも、たとえアイドル・プロセッサーをスレッド使用に組み入れることができたとしても、その他のスレッドはリソースにアクセスすることができないからです。スケーラビリティーを実現するためには、排他的なリソース・ロックへの依存を排除するか縮小しなければなりません。

同期化コレクション・ラッパーに関する重要な問題は、HashtableやVectorクラスが一回のロックで同期化するということです。つまり、1度に1つのスレッドだけがコレクションにアクセスでき、1つのスレッドがMapから読みこむ途中である場合、読み込みや書き込みを行いたい他のすべてのスレッドは待機しなければなりません。最も一般的なMapオペレーションのget()とput()は、明白となっている処理よりも多くの処理をしている可能性があります。それは特定のキーを見つけるためにハッシュ・バケットを検索する際、get()は多くの候補でObject.equals()を呼び出さなければならないかもしれないためです。keyクラスに使用されるhashCode()関数が値をハッシュ範囲に均一に広げなかったり、ハッシュ衝突がしばしば起こる場合、あるバケットの連鎖は他のバケットより長くなるかもしれません。その結果、長いハッシュ連鎖の検索や、何%かの要素はequals()呼び出しが遅くなる可能性があります。これらの条件下でのget()とput()に起こりうる問題は、アクセスが単に遅くなるというだけではなく、ハッシュ連鎖が検索されている間は他のすべてのスレッドはMapをアクセスできなくなるということです。

get()の実行にかなりの時間が必要となるケースがあるという事実は、上で説明した条件付きスレッド・セーフ問題において顕著です。リスト1で説明した競合状況では、1回のオペレーションの実行より長い間1つのコレクションをロックしていなければなりません。もし全てのイテレーションの間コレクションをロックするならば、他のスレッドはコレクションのロックを長い間待っていなければならない可能性があります。

シンプルなキャッシュの例

サーバー・アプリケーションにおけるMapを使用する最も一般的なアプリケーションの1つは、キャッシュの実装です。サーバー・アプリケーションはファイルコンテンツ、生成されたページ、データベース・クエリーの結果、解析されたXMLファイルに関連したDOMツリー、および他の多くのデータ型をキャッシュしています。キャッシュの主な目的は、サービス時間を縮小し、以前の計算結果を再使用することにより、スループットを増加させることです。キャッシュ・ワークロードの典型的な特徴は、検索が更新よりもはるかに一般的であるということです、したがって、キャッシュは素晴らしいget()パフォーマンスを提供しています。アプリケーションのパフォーマンスを低下させるキャッシュは最悪です。

キャッシュの実装にsynchronizedMapを使用することは、アプリケーションへ潜在的なスケーラビリティーのボトルネックを導入することになります。それは、Mapへ新しいkeyやvalueを設定したいスレッドだけでなく、Mapから値を検索しているスレッドも含めて、1度に1つのスレッドしかMapにアクセスすることができないためです。

ロックの粒度を小さくする

スレッド・セーフを提供しつつHashMapの並行性を改善するためのアプローチは、テーブル全体用の1つのロックはやめて、各ハッシュ・バケット用のロック(一般的には、それぞれのロックがいくつかのバケットを保護するロックのpool)を使用することです。これにより、複数のスレッドは、コレクション全体に渡る1つのロックを使用するのではなく、同時に異なるMapにアクセスすることができるようになります。このアプローチを使えば、簡単に設定、検索、削除オペレーションのスケーラビリティーを改善することができます。ただし、この並行性がうまくいかない機能しない場合もあります。たとえば、size()やisEmpty()のようなコレクション全体で機能するメソッドに関しては、1度に多くのロックが必要になったり、不正確な結果を返すリスクがあるために、実装しづらくなるのです。しかし、キャッシュを実装するような状況にはこのアプローチは非常に適しています。なぜなら、キャッシュでは検索と設定のオペレーションは頻繁に行われますが、size()やisEmpty()はそれほど頻繁に行わないためです。



上に戻る


ConcurrentHashMap

util.concurrentのConcurrentHashMapクラス(JDK 1.5のjava.util.concurrentパッケージで登場します)は、synchronizedMapよりはるかに素晴らしい並行性を提供しているMapのスレッド・セーフな処理系です。複数読み取りが常にほぼ同時に実行することができ、同時読み書きも通常ほぼ同時に実行することができ、複数書き込みも多くの場合同時に実行することができるのです(関連するConcurrentReaderHashMapクラスもまた、同じような複数読み取りという並行性を提供していますが、アクティブな書き込みに関しては並行ではありません)。ConcurrentHashMapは、検索オペレーションを最適化するように設計されています。実際、get()オペレーションは、通常ロックなしでうまくいきます。ただし、ロックのないスレッド・セーフティーには注意が必要で、Java Memory Modelの詳細についてよく理解していなければなりません。残りのutil.concurrentと同様に、ConcurrentHashMapの実装は正確さおよびスレッド・セーフティーに関して並行性のエキスパートに広く評価されてきました。ConcurrentHashMapの実装の詳細については、次回の記事で見ていくことにします。

ConcurrentHashMapは、呼び出し側との決め事を少し緩めることで高い並行性を得ることができます。検索オペレーションは、直近の設定オペレーションによって設定された値を返したり、同時進行中である設定オペレーションに追加された値を返したりする可能性があります(決して無意味な結果を返すわけではありません)。ConcurrentHashMap.iterator()によって返されたイテレータは各要素を一度に返して、ConcurrentModificationExceptionを投げることはありませんが、イテレータが構築されたことで生じた設定あるいは削除に関しては反映するかもしれないし、反映しないかもしれません。コレクションの繰返しに際して、スレッド・セーフティーを提供するためにテーブル全体をロックする必要はありません(不可能でさえあります)。ConcurrentHashMapは、更新を防ぐためにテーブル全体をロックする必要がないすべてのアプリケーションでsynchronizedMapまたHashtableの代わりに使用することができます。

ConcurrentHashMapは、共有キャッシュのような様々な一般的な利用の有効性を失うことなく、上記の歩み寄りによってHashtableよりもはるかに優れたスケーラビリティーを提供することができます。

どれ程よいのか?

表1は、HashtableとConcurrentHashMapのスケーラビリティーの違いについての大まかな見解です。それぞれの実行において、nスレッドはHashtableまたはConcurrentHashMapでランダムなキー値を検索するようなタイトなループを同時に実行しました。検索結果はput()オペレーションの実行では80%失敗し、remove()オペレーションの実行は1%成功しました。テストは、Linuxが起動しているデュアルプロセッサーXeonシステムで行なわれました。データはConcurrentHashMapの1スレッドで正規化されて、10,000,000回繰返した実行時間をミリ秒で示しています。ConcurrentHashMapのパフォーマンスはスレッドが多くなってもスケーラブルなままであるのに対して、Hashtableのパフォーマンスはロック競合が発生して、すぐに値が悪化していることがお分かりになるでしょう。

このテストでのスレッド数は代表的なサーバー・アプリケーションと比較して、少なく見えるかもしれません。しかし、それぞれのスレッドはテーブルを繰り返し hitしているので、実際に若干のコンテキストがテーブルを使用するスレッドよりもはるかに多くの競合をシミュレートしています。

表1. Hashtable対ConcurrentHashMapのスケーラビリティー
Threads ConcurrentHashMap Hashtable
1 1.00 1.03
2 2.59 32.40
4 5.58 78.23
8 13.21 163.48
16 27.58 341.21
32 57.27 778.41



上に戻る


CopyOnWriteArrayList

CopyOnWriteArrayListクラスは、挿入や削除よりも圧倒的に検索が多い並行アプリケーションでArrayListに代わるものとしてみなされます。ArrayListが、AWTやSwingアプリケーションあるいは一般的なJavaBeanクラスのように、リスナーのリストを格納するために使用される場合は、CopyOnWriteArrayListと全く同じです。(関連するCopyOnWriteArraySetは、Setインタフェースを実装するためにCopyOnWriteArrayListを使用しています。)

リストが可変の状態で、複数のスレッドにアクセスされる可能性があるにも関わらず、リスナーのリストの格納に通常のArrayListを使用するならば、繰返しの間全リストをロックするか、繰返しの前にリストのクローンを作らなければなりません。それらはいずれもかなり手間のかかることです。CopyOnWriteArrayListはその代りに、変更のオペレーションを行なう場合は常にリストの新規コピーを作成します。また、CopyOnWriteArrayListのイテレータは、イテレータが構築された時点で必ずリストの状態を返して、ConcurrentModificationExceptionを投げません。イテレータが見るリストのコピーは変わらないので、繰返しの前にリストのクローンを作ったり、繰返しの間ロックする必要はありません。言いかえれば、CopyOnWriteArrayListは、不変配列への可変の参照を含んでいます。したがって、その参照が固定されている限り、ロックの必要なく不変のスレッド・セーフの利点を得ることができます。



上に戻る


要約

synchronized collectionsクラス、Hashtable、Vector、および同期化ラッパー・クラスのCollections.synchronizedMapとCollections.synchronizedListは、MapとListの基本的な条件付きスレッド・セーフの実装を提供しています。しかし、これらの使用は次のような要因により、高度な並行アプリケーションには適していません。1つはコレクション全体に渡る1つのロックはスケーラビリティーの障害であること、2つ目は繰返しの間ConcurrentModificationExceptionsを回避するためには、相当な時間コレクションをロックする必要がある、とい理由です。しかし、ConcurrentHashMapおよびCopyOnWriteArrayListの実装では、呼び出し側が若干の歩み寄りをすることでスレッド・セーフを維持しつつ高い並行性を提供することができます。ConcurrentHashMapおよびCopyOnWriteArrayListは、HashMapまたはArrayListを使用してきた全てにおいて必ずしも役立つとは限りませんが、特定のcommon状況を最適化するように設計されています。多くの並行アプリケーションはConcurrentHashMapおよびCopyOnWriteArrayListを使用することで、利益を得られるようになるのです。


参考文献

* Brian Goetz氏による Javaの理論と実践 のすべての記事をお読みください。特に今回の記事と関係があるのは、「可変性か?不変性か?」(developerworks 、2003年2月)で、不変性のスレッド・セーフティーの利点に関して説明されています。

* Doug Lea氏の『Concurrent Programming in Java, Second Edition 』は、Javaアプリケーションでのマルチスレッド化プログラミングに関する微妙な問題について書かれたすばらしい本です。

* util.concurrent パッケージをダウンロードしてください。

* javadocのConcurrentHashMapに関するページでは、 ConcurrentHashMapとHashtableの違いについて詳しく説明されています。

* JSR 166 は、JDK 1.5用のutil.concurrent ライブラリーを標準化しています。

* 2002年11月のJavaの理論と実践 のコラムでは、util.concurrentの実行機能を取り扱っています。

* 競合コストの削減については、「システム負荷を軽減したスレッド化: 競合を低減させる」(developerworks 、2001年9月)で解説されています。

* その他のJava参考文献に関してはdeveloperWorksJava technology ゾーン を参照してください。

2009年8月18日火曜日

Tigerでのアノテーション 第1回: Javaコードにメタデータを追加する

http://www.ibm.com/developerworks/jp/java/library/j-annotate1/

2004年 9月 02日
アノテーション(Annotations)はJ2SE 5.0 (Tiger)での新しい機能ですが、コアとなるJava言語に長年待ち望まれていたメタデータ機能をもたらします。2回シリーズの第1回として、今回はBrett McLaughlinが、なぜメタデータがそれほど便利なのかを説明します。またJava言語でのアノテーションについて紹介し、次にTigerに組み込まれているアノテーションについて見て行きます。第2回 (US)ではカスタムのアノテーションについて説明します。
プログラミング、特にJavaプログラミングにおける最新の傾向として、メタデータの使用が挙げられます。メタデータを単純に言えば、データに関するデータです。メタデータはドキュメンテーションの作成やコードの依存性追跡に、あるいはごく基本的なコンパイル時のチェックにも使われます。XDoclet(参考文献)のようなメタデータ用のツールが次々と現れたため、コアとしてのJava言語にこうした機能が追加され、ここしばらくはJavaプログラミング風景の一部となってきています。
J2SE 5.0(別名はTigerで、現在は2番目のベータ・リリースです)が入手できるようになるまでは、コアとしてのJava言語でメタデータ機能に最も近いものはJavadocによる手法でした。この手法では、特別なタグ・セットを使ってコードをマークアップしてからjavadocコマンドを実行し、タグが付加されているクラスを文書化してフォーマットしたHTMLページにタグを変換します。ただしJavadocはドキュメンテーションを生成するという目的以外には、データに到達するための確実、現実的で標準化された方法がないため、不十分なメタデータ・ツールです。HTMLコードがしばしばJavadoc出力に混在されてしまうという事実も、Javadocをドキュメンテーション生成以外の他の用途には使いにくくしています。
Tigerには、はるかに用途の広いメタデータ機能が、アノテーション(annotations)と呼ばれる新しい機能を通してコアのJava言語の中に用意されています。アノテーションはコードに追加する修飾子であり、パッケージ宣言やタイプ宣言、コンストラクター、メソッド、フィールド、パラメーター、変数などに適用することができます。Tigerには組み込みのアノテーションがあり、またカスタム・アノテーションもサポートしているので、自分でアノテーションを書くこともできます。この記事ではメタデータの利点の概要を説明し、Tigerに組み込まれているアノテーションについて紹介します。第2回の記事では、カスタム・アノテーションについて説明して行きます。ここで私はO'Reilly Media, Inc. に対して、Tigerに関する私の著書(参考文献)からコード例を引用することを承諾してくださったことに感謝致します。
メタデータの重要性
一般的に言ってメタデータの利点は、ドキュメンテーション、コンパイラー・チェック、コード解析という、3つの視点から見ることができます。コード・レベルのドキュメンテーションが最もよく引用される使い方です。メタデータによって、メソッドが他のメソッドに依存しているかどうか、またメソッドが不完全かどうか、あるクラスが別のクラスを参照する必要があるかどうか、などが分かるようになります。これは確かに便利なのですが、Java言語にメタデータを追加する理由として、ドキュメンテーションは最も優先度の低いものです。それに、もう既にツールが存在していてほぼ問題なく動作しているのに、誰がドキュメンテーションのツールを書こうと思うでしょうか。

第2回もお忘れなく!
カスタム・アノテーションについて説明した、このシリーズ「第2回」の記事も忘れずに読んでください。
コンパイラー・チェック
メタデータの利点としてもっと重要なのは、コンパイラーがメタデータを使って基本的なコンパイル時のチェックが行えるようになる、という点です。例えば後ほどOverrideアノテーションで説明しますが、Tigerで用意されているアノテーションを使うことによって、あるメソッドが、スーパークラスからの別メソッドをオーバーライドするように規定できるのです。Javaコンパイラーは、メタデータで示した振る舞いが、実際にコード・レベルで起きることを保証できるのです。そう聞くと、この種類のバグを追跡した経験がない人はバカバカしいと思うかも知れません。しかしベテランのJavaプログラマーの大部分は、なぜコードが動かないのかを見つけるために、夜遅くまで働き続けたことが二度や三度はあるのです。あるメソッドが間違ったパラメーターを持っていることにようやく気がつき、実際そのメソッドがスーパークラスからのメソッドをオーバーライドしていないとすると、痛い目に会うのです。メタデータを受け取るツールを使うことによって、このタイプのエラーを簡単に見つけられるようになり、Haloトーナメントを長々と実行する夜を過ごす必要もなくなるのです。

JSR 175
JSR 175,A Metadata Facility for the Java Programming Languageには、コアのJava言語にメタデータを採り入れるにあたっての正式な理由や仕様が出ています(参考文献)。このJSRによると、アノテーションは「プログラムの意味体系には直接の影響は与えない。ただし開発ツールやデプロイメント・ツールはこうしたアノテーションを読み取り、何らかの処理を行う。処理としては、アノテーションを含むプログラムに関連して使用するような追加的Javaソース・ファイルを生成する、またはXML文書やその他の成果物を生成する、などがある。」
コード解析
良質なアノテーションやメタデータ・ツールの機能の中で一番素晴らしいのは、コード解析のために別のデータが使えるという点でしょう。単純な場合では、コード・カタログを構築したり、必要な入力タイプを用意したり、戻りタイプを示したりすることができます。しかし、おそらく皆さんも考えていると思いますが、Javaのリフレクション(reflection)も同じことができます。結局、こうした情報の全てに関してコードを検証できるのです。これは表面的にはそう見えるかも知れませんが、現実的には必ずしもそうではありません。多くの場合メソッドは、実はそのメソッドが必要としていないタイプを入力として受け付けてしまったり、出力として戻してしまったりするのです。例えばパラメーターのタイプがObjectとし、そのメソッドがIntegerでのみ動作する、とします。これは、メソッドがオーバーライドされてスーパークラスが汎用パラメーターでそのメソッドを宣言する場合や、多くのシリアル化が行われているようなシステムでは容易に起こります。どちらの場合でもメタデータはコード解析ツールに対して、パラメーター・タイプはObjectですが、実際に必要なのはIntegerである、と指示できるのです。こうした解析は信じられないほど便利なもので、その重要性は言い尽くせないほどです。
より複雑な場合では、コード解析ツールはコード解析以外にも、あらゆる種類の余分な課題をこなすことができます。今日的な例としてはEJB(Enterprise JavaBean)コンポーネントです。ごく単純なEJBシステムであっても、依存性や複雑さはめまいがするほどです。ホーム・インターフェースやリモート・インターフェース、それにローカル・インターフェースとローカル・ホーム・インターフェースも可能性があり、さらに実装クラスもあります。こうしたクラスを全て同期させるのは王者の苦痛とも言えます。ところがメタデータによってこの問題が解決できるのです。良いツールであれば(ここでもXDocletを挙げるべきでしょう)こうした全ての依存性を管理でき、「コード・レベル」での関連はなくても「論理レベル」では関連があるようなクラスの同期を保証できるのです。これこそメタデータが本領を発揮するところです。




上に戻る


アノテーションの基礎
さて、メタデータがどういう場合に有効かは理解できたと思いますので、Tigerでのアノテーションを紹介することにしましょう。アノテーションは「at」記号(@)で始まり、アノテーション名が後に続きます。次に、(データが必要な時には)name=valueの対としてアノテーションにデータを与えます。こうした表記を使うと、その度にアノテーションを入れていることになります。一つのコードでアノテーションが10や50、あるいはそれ以上の場合もあり得ます。ただし、アノテーションの幾つかは、どれも同じアノテーション・タイプを使うことに気がつくでしょう。このアノテーション・タイプが実際に使われる構造体です。アノテーション自体は、ある特定なコンテキストにおける、そのタイプの特定な使い方になります(囲み記事アノテーションか、アノテーション・タイプかを見てください)

アノテーションか、アノテーション・タイプか
何がアノテーションで何がアノテーション・タイプかに混乱していますか? これを正しく理解するためには、皆さんが既に慣れているJava言語の概念で考えてみれば良いのです。一つのクラス(例えばPerson)を定義することができ、JVMでのそのクラスには(たちの悪いクラスパスの類をしていなければ)常に一つのバージョンしかありません。ところが、ある瞬間においては、そのクラスのインスタンスを10とか20は使っている、ということはあり得ます。Personクラスは相変わらず一つのままですが、様々な方法で何度も使われているのです。アノテーションとアノテーション・タイプについても同じことが言えます。アノテーション・タイプはクラスと似ており、アノテーションは、そのクラスのインスタンスと似ています。
アノテーションは3つの種類に分けることができます。
マーカー・アノテーション は変数を持ちません。アノテーションは名前で指定されて単純に現れ、追加的なデータは何もつきません。例えば@MarkerAnnotationはマーカーアノテーションです。データは含まず、アノテーション名のみです。
単一値アノテーション はマーカーと似ていますが、一つのデータを持っています。持っているのが一つのデータだけなので、(この構文を受け付けるようにアノテーション・タイプが定義されているとすれば)次のようなショートカット構文を使うことができます。@SingleValueAnnotation("my data") @記号を除けば、これは通常のJavaメソッド・コールとよく似ています。
フル・アノテーション は複数のデータ・メンバーを持ちます。そのため次のような、より完全な構文を使用する必要があります(そしてアノテーションはもはや通常のJavaメソッドとは似ていません)。@FullAnnotation(var1="data value 1", var2="data value 2", var3="data value 3")
一つ以上の値を渡す必要がある時には、デフォルト構文を通してアノテーションに値を与える他に、名前と値の対を使うこともできます。また、中括弧を使って、アノテーション変数に対して値の配列を与えることもできます。リスト1はアノテーションでの値の配列の例です。

リスト1. 配列化した値をアノテーションに使う

@TODOItems({ // Curly braces indicate an array of values is being supplied
@TODO(
severity=TODO.CRITICAL,
item="Add functionality to calculate the mean of the student's grades",
assignedTo="Brett McLaughlin"
),
@TODO(
severity=TODO.IMPOTANT,
item="Print usage message to screen if no command-line flags specified",
assignedTo="Brett McLaughlin"
),
@TODO(
severity=TODO.LOW,
item="Roll a new website page with this class's new features",
assignedTo="Jason Hunter"
)
})

リスト1の例は見た目よりは簡単です。TODOItemsアノテーション・タイプには、値をとる変数が一つあります。ここで与えられている値はかなり複雑ですが、実は単一値が配列であることを除けば、TODOItemsの使い方は単一値アノテーションのスタイルに一致します。この配列には3つのTODOアノテーションがあり、それぞれに複数の値があります。各アノテーション内で値を区切るにはカンマを使い、一つの配列内で値を区切る場合にもカンマを使います。簡単ですよね?
ここでちょっと先回りしましょう。TODOItemsとTODOはカスタム・アノテーションで、第2回の話題です。ただ私としては、たとえ複雑なアノテーションであっても(そしてリスト1は充分複雑ですが)、それほど気の遠くなるようなものではないことを皆さんに分かって欲しいのです。Java言語での標準アノテーション・タイプに関する限り、あまり複雑に入り組んだものを見るのは稀でしょう。次の3つのセクションで見る通り、Tigerでの基本的なアノテーション・タイプは非常に簡単に使えるのです。




上に戻る


Overrideアノテーション
Tigerに組み込まれている最初のアノテーション・タイプはOverrideです。Overrideは(クラスやパッケージ宣言、あるいは他の構造体などではなく)メソッドに対してのみ使用し、このアノテーションで注釈を付けられたメソッドが、スーパークラスにあるメソッドをオーバーライドすることを表します。リスト2は簡単な例です。

リスト2. Overrideアノテーションの実際

package com.oreilly.tiger.ch06;
public class OverrideTester {
public OverrideTester() { }
@Override
public String toString() {
return super.toString() + " [Override Tester Implementation]";
}
@Override
public int hashCode() {
return toString().hashCode();
}
}

リスト2は簡単に分かると思います。@OverrideアノテーションがtoString()とhashCode()という2つのメソッドに注釈を付け、OverrideTesterクラスのスーパークラス(java.lang.Object)にある方のメソッドをオーバーライドすることを示しています。これは大したことではないと思えるかも知れませんが、実は便利な機能なのです。こうしたメソッドをオーバーライドしない限り、このクラスをコンパイルすることは本当にできないのです。またこのアノテーションによって、toString()をもてあそんでいる時でも、hashCode()の方は一致していることを確認するように知らせるものが、何かしらあることにもなります。
このアノテーション・タイプが本当に役に立つのは、コーディングが遅れたのに慌てて、何かを打ち間違えたような時です(リスト3)。

リスト3. Overrideアノテーションに打ち間違いを捉えさせる

package com.oreilly.tiger.ch06;
public class OverrideTester {
public OverrideTester() { }
@Override
public String toString() {
return super.toString() + " [Override Tester Implementation]";
}
@Override
public int hasCode() {
return toString().hashCode();
}
}

リスト3で、hashCode()はhasCode()と打ち間違いされています。アノテーションによればhasCode()はメソッドをオーバーライドすべきだと言っています。しかしjavacはコンパイルする時に、スーパークラス(ここでもjava.lang.Objectです)にはオーバーライドすべきメソッドとしてhasCode()という名前のメソッドが無いことに気がつきます。その結果、コンパイラーは図1に示すようなエラーを出します。

図1. Overrideアノテーションによるコンパイラー警告



欠けている機能
Deprecatedが、エラーのタイプを示すメッセージを単一値アノテーションの形式で含められるようになっていれば良かったのにと思います。そうすれば、ユーザーが使用すべきでないメソッド(deprecated method)を使った時にはコンパイラーがメッセージを出力することができます。このメッセージによって、そのメソッドを使うとどの程度深刻な結果を引き起こすか、そのメソッドを使えなくなるのはいつか、さらにはどんな代替手段があり得るか、などを示すことができます。残念なことに現在言えることは、次のJ2SEバージョン(「Mustang」と呼ばれています)でこれが実現するかも知れない、という程度です。
この便利な機能を使えば、打ち間違いをすぐに見つけることができます。
Deprecatedアノテーション
次の標準アノテーション・タイプはDeprecatedです。Overrideと同じようにDeprecatedはマーカー・アノテーションです。その名前から想像できる通り、Deprecatedは、もはや使用すべきでないメソッドに注釈付けするために使います。Overrideとは異なり、Deprecatedは使用すべきでないメソッドと同じ行に置く必要があります(なぜかって? 私にも分かりません)。リスト4がこの例です。


リスト4. Deprecatedアノテーションを使う

package com.oreilly.tiger.ch06;
public class DeprecatedClass {
@Deprecated public void doSomething() {
// some code
}
public void doSomethingElse() {
// This method presumably does what doSomething() does, but better
}
}

このクラス自体はコンパイルしても何も特別なことは起こりません。ただしコンパイルした後で、使用すべきでないメソッドをオーバーライドしたり、呼び出したりして使おうとすると、コンパイラーは(アノテーションを処理することによって)そのメソッドを使うべきではないことに気がつき、図2のようなエラー・メッセージを出します。

図2. Deprecatedアノテーションによるコンパイラー警告

Javaコンパイラーに対して通常のdeprecation警告を受けたいと指示するためにも、コンパイラー警告をオンにする必要があることには注意してください。それにはjavacコマンドにある2つのフラグ、-deprecatedと新しい-Xlint:deprecatedフラグのどちらかを使うことができます。




上に戻る


SuppressWarningsアノテーション
Tigerで「無料で」手に入る最後のアノテーション・タイプはSuppressWarningsです。これが何をするのかは難なく分かると思いますが、なぜこのアノテーション・タイプがそれほど重要なのかはそれほど明白ではありません。実はこれはTigerで一新された機能の副産物なのです。例えばgenericsを考えてみてください。genericsによって、特にJavaのコレクションに関して言うと、あらゆる種類のタイプセーフ操作ができるようになりました。ところが今度はgenericsのために、コレクションがタイプセーフ(type-safe)無しに使われると、コンパイラーは警告を投げるようになったのです。これはTiger用に書かれたコードには便利なのですが、Java 1.4.xやそれ以前のバージョン用に書かれたコードに対しては実にやっかいです。全く気にもかけないものに対して、頻繁に警告を受けるようになってしまうのです。コンパイラーを静かにさせるにはどうしたら良いでしょう?
そういう時にSupressWarningsが役に立つのです。SupressWarningsはOverrideやDeprecatedとは異なり、変数を持っています。ですからDeprecatedには単一アノテーション・スタイルを使い、それぞれの値が抑制すべき特定な警告タイプを示すような、値の配列を変数として与えるのです。リスト5に示す例を見てください。このコードは、通常であればTigerで警告を引き起こします。

リスト5. タイプセーフでないTigerコード

public void nonGenericsMethod() {
List wordList = new ArrayList(); // no typing information on the List
wordList.add("foo"); // causes error on list addition
}

図3はリスト5のコードをコンパイルした結果を示します。

図3. non-typed コードによるコンパイラー警告

リスト6はSuppressWarningsアノテーションによって、このうるさい警告を除去します。

リスト6. 警告を抑える

@SuppressWarings(value={"unchecked"})
public void nonGenericsMethod() {
List wordList = new ArrayList(); // no typing information on the List
wordList.add("foo"); // causes error on list addition
}

簡単ですよね? ただ単に警告のタイプ(図3で「unchecked」として表示されているものです)を探してきてSuppressWarningsに渡すだけです。
SuppressWarningsでの変数の値には配列が使えるため、同じアノテーションで複数の警告を抑えることができます。例えば、@SuppressWarnings(value={"unchecked", "fallthrough"})は2つの値を持った配列を受け付けます。この機能によって、過度に冗長にならずに、柔軟にエラーを処理できるようになります。




上に戻る


まとめ
ここでご紹介した構文は新しく見えるかも知れませんが、アノテーションは簡単に理解でき、また使うのも簡単なことが分かるでしょう。とは言ってもTigerについてくる標準アノテーションはやや素っ気なさすぎ、色々注文をつけたくなります。メタデータは大分便利になっているので、皆さんはきっと自分のアプリケーションに最適なアノテーション・タイプを思いつくでしょう。このシリーズ2回目の次回では、Tigerでサポートされている、自分独自のアノテーション・タイプが書ける機能の詳細を説明する予定です。説明の内容は、どのようにJavaクラスを作ってそれをアノテーション・タイプとして定義するか、どのようにしてコンパイラーにそのアノテーション・タイプを認識させるか、どのように使ってコードに注釈をつけるか、などです。さらに、ちょっと変に聞こえるかも知れませんが、でも便利な、アノテーションのアノテーションについても触れる予定です。皆さんもこの、Tigerでの新しい構造体をすぐにマスターできるでしょう。


参考文献
このシリーズの2回目、「 Annotations in Tiger, Part 2」 (US)も忘れずに読んでください。この記事ではカスタムのアノテーションを説明しています。

オープンソースのコード生成エンジンXDocletを使うと、Java言語で属性指向のプログラミングができるようになります。

JSR 175はJava言語にメタデータ機能を採り入れるための仕様です。現在はJava Community Processで最終ドラフト提案の段階にあります。

Sunのホーム・ベースであるall things J2SE 5.0を見てください。

Tigerをダウンロードして、自分で試してみてください。

John ZukowskiによるTigerを使いこなす  シリーズは、Java 5.0の新しい機能を実際的なヒントを中心に説明しています。

Brett McLaughlinとDavid Flanagan共著によるJava 1.5 Tiger: A Developer's Notebook(2004年O'Reilly & Associates刊)はアノテーションを含めたTigerの最新機能のほぼ全てを、コード中心で開発者に分かりやすく網羅しています。

developerWorksのJava technologyゾーンにはJava技術に関する資料が豊富に取り揃えられています。

Developer BookstoreにはJava関連の書籍をはじめ、広範囲な話題を網羅した技術書が豊富に取り揃えられています。

実用的なXML: Java NIOへの取り組み バッファーおよびチャネルに寄り道する方法

2002年 6月 01日

http://www.ibm.com/developerworks/jp/xml/library/x-wxxm10/#resources

このコラムは、XIプロジェクトを次のステップへと進めるものです。このコラムでBenoit氏は、新規JavaテクノロジーのAPIに関する (特に、正規表現エンジンとNew I/O (NIOとも呼ばれます) における) 氏の新しい発見について報告しています。XIはまだ作動可能にはなっていませんが、近い将来にどのような姿で登場するのかをかいま見ることができます。
前回の記事では、このコラムのための新規ツール・プロジェクトであるXIを紹介しました。このプロジェクトの課題は、次のとおりです。私の会社では、XMLと、XMLに基づく公開ソリューションであるXMに関する作業グループのWebサイトを運営しています。(XMは「実用的なXML」コラムの最初のプロジェクトです。参考文献を参照してください。)
そのサイトに関する文書の1つに、プロジェクト参加者のリストがあります。このリストは、Eメール・プログラムの住所録として保持されています。その理由は、このコラムでは取り扱いません。ファイル・フォーマットは私が担当しましたが、XMLではありません。それでは、どのようにしてXML公開ソリューションにリストを送り込むのでしょうか?このリストをXMLに変換する必要があります。
もちろん、そのための特別な変換ルーチンを書くのも難しいことではないでしょうが、それは時間のむだのような気がします。それに、XML以外の文書をXMLソリューションに供給しなければならない場合は、ほかにもありそうです。住所録のほかにも、日程表、スプレッドシート、カタログ化情報、およびその他のレガシー・データを処理する必要がありそうです。
XIはこの問題のためのかなり一般的な解決策です。XIは正規表現 (現在はJDK 1.4に組み込まれています) を使用して入力ファイルを構文解析し、対応するXMLファイルを作成します。こうした変換の詳細、およびXIに関する簡単な分析は、前回のコラムで紹介しました (参考文献を参照)。
JDK 1.4の新規機能
私がここでXIを開発することに決めた理由の1つは、JDK 1.4に組み込まれている新しい正規表現 (regex) エンジンを体験してみたかったからです。これから説明しますが、New I/Oパッケージ (一般にNIOと呼ばれ、パッケージjava.nio に入っています) の探求に取り掛かってみると、予想以上の収穫が得られました。
java.regexパッケージ
この正規表現エンジンは、単純明快なインターフェースを備えています。これを使用するためには、基本的に、2つの新規クラスと1つの新規インターフェースを学習する必要があります。新規クラスはPattern とMatcher で、新規インターフェースはCharSequence です。(後者は、いくつかの問題を抱えています。これについては、後で採り上げます。)リスト1 は、Pattern とMatcher の使用方法を示しています。

リスト1. 正規表現エンジンの使用法

import java.util.regex.*;
public class SampleRegex
{
public static void main(String[] params)
{
Pattern pattern =Pattern.compile("(.*):(.*)");
Matcher matcher =pattern.matcher(params[0]);
if(matcher.matches())
{
System.out.print("Key:");
System.out.println(matcher.group(1));
System.out.print("Value:");
System.out.println(matcher.group(2));
}
else
System.out.print("No match");
}
}

Pattern は正規表現コンパイラーです。これは、正規表現を受け入れて、それをMatcher にコンパイルします。Matcher は、正規表現を文字列に (より正確にはCharSequence に) 適用するために使用されます。
Pattern にはパブリック・コンストラクターがありません。Pattern を作成するためには、そのcompile() を呼び出し、引数として正規表現を渡す必要があります。
正規表現初級講座
正規表現は文字列のフォーマットを記述します。最も単純な形式の正規表現では、突き合わせしたいテキストを入力することになります。例えば、ABC という正規表現はABC に一致しますが、DEF には一致しません。
もちろん、正規表現では文字列が厳密に比較されます。例えば、ジョーカー と呼ばれるワイルドカード文字、つまりドット (.) を使用すると、行末の1文字以外のどのような文字とも一致するようになります。したがって、A.C という正規表現は、ABC、AAC、AKC、およびその他多くの文字列に一致しますが、それでもDEF には一致しません。
文字またはジョーカーの後の星印 (*) は、その文字またはジョーカーがいくつ繰り返されていても一致するということを意味します。したがって、A*B という正規表現は、AB、AAB、AAAB、またはAで始まって1つのBで終わる任意の文字列に一致します。
ドットはジョーカーですので、.* は、ABC、IBM developerWorks、およびDEF などの、任意の文字列と一致します。
グループ化演算子として括弧が使用されます。読者もお気付きでしょうが、これを使用すると、文字列からグループの内容を抜き出すことができて、非常に便利です。例えば、リスト1 で使用されている正規表現(*):(.*) は、1つのコロンで区切られた2つの文字列に一致します。
正規表現については、まだ語り残したことがありますので、「Mastering Regular Expressions」などの解説書を参照することをお勧めします (参考文献を参照)。
PatternとMatcher
リスト1 では、正規表現がPattern にコンパイルされると、それがmatcher() メソッドを使用するMatcher を作成します。Matcher はCharSequence (これについては、次のセクション『NIO』で説明します) を受け入れ、正規表現が一致したかどうかを報告します。Matcher は、正規表現をテストするためのメソッドとして、matches()、lookingAt()、find() を備えています。これらはそれぞれ、異なる方法で正規表現を適用します。
Matcher にはまた、指定されたグループに一致する文字列を取り出すためのメソッドgroup() も備わっています。グループには1からn までの番号が付けられます。group(0) は完全な正規表現です。
リスト1 は、コマンド・ライン・パラメーターに正規表現を適用します。そして、グループが検出された場合にはそれを出力します。例えば、次のように指定してこのアプリケーションを呼び出すと、
java SampleRegex
"domain:ananas.org"

次のような出力が得られます。
Key: domainValue:
ananas.org

これは、入力 (domain:ananas.org) が正規表現に一致しているためです。しかし、次のようにして呼び出すと、
java SampleRegex "ananas.org"

次のような出力が得られます。
No match

この入力が正規表現に一致していないためです。
NIO
前のセクション『PatternとMatcher』でCharSequence について触れました。これは、java.langパッケージで文字の配列に関して定義されている新規インターフェースです。String が更新されて、CharSequenceを実装するようになりました。
さらに重要なこととして、NIOパッケージを使用することにより、ファイルを CharSequence としてアクセスすることができます。したがって、Matcher が CharSequence を受け入れますので、ファイル全体に対して正規表現を適用することが可能になりました。これは、java.nioパッケージを調べて分かったことです (参考文献を参照)。
結局、このプロジェクトではjava.nioを使用しないことにしましたが、ソリューションを探すために多くの時間を費やしましたので、これについて説明します。(最後まで突き詰めて追及することは、ソフトウェア開発におけるまたとない気晴らしであり、また、そうしたことをお伝えしなければ、このコラムの本来の意味は失われてしまうでしょう。それに、私の経験を紹介しておけば、読者が同じことを調べなくて済むようになるはずです。)
リスト2 は、ファイルを CharSequence に作り替える方法を示しています。読者は実際には、テキスト・ファイルを基に CharSequence を実装する、CharBuffer という新規クラスを使用することになります。

リスト2. CharBufferの使用法

FileInputStream input = new FileInputStream(params[0]);
FileChannel channel = input.getChannel();
int fileLength = (int)channel.size();
MappedByteBuffer buffer = channel.map(FileChannel.MapMode.READ_ONLY,0,fileLength);
Charset charset = Charset.forName("ISO-8859-1");
CharsetDecoder decoder = charset.newDecoder();
CharBuffer charBuffer = decoder.decode(buffer);
Matcher matcher = pattern.matcher(charBuffer);
// ...

先へ進む前に、私は、NIOの背後にある論理をまだ完全には把握していないことを白状しなければなりません。私がこのAPIを使い始めたのは、最近になってからですが、これまでに分かったことは以下のとおりです。
ご存じのように、ファイルを CharBuffer に変換するためには、多くのオブジェクトが必要となります。これは、新規APIを使用する場合のパターンと言えます。私が理解した範囲では、NIOの目標は、通常の入出力よりも制御しやすく、柔軟性の高い入出力を提供することです。
NIOが提供するAPIは、通常の入出力のAPIほど抽象的ではありません。例えば、Java IOの場合、バッファー管理のことを考慮する必要がありませんが、その代わり、バッファー管理を制御することもできません。NIOでは、バッファー管理を制御することができます。バッファー管理は、ユーザーが行うようになっているのです!効果的ではありますが、より複雑になると言って差し支えないでしょう。
ここまで書いてきて抱いた印象では、NIOは特に、データベース・エンジン、サーバー、および高性能クライアントなどの、高性能アプリケーションの開発者に向いているのではないかと思います。通常のプログラミングでこれを使う理由 (そしてそれに伴う追加作業を行う理由) は見当たりません。
さらに、XIにおける私の最終目標は、SAXで定義された XMLReader との互換性を維持することです。XMLReader は現在 InputStream または Reader では使用できますが、NIOでは使用できません。私は、InputStream を CharBuffer に変換するための完全に汎用的なソリューションを見付けることができませんでした。(部分的なソリューションは見付かりました。)よいアイデアをお持ちの方は、ぜひEメールでお知らせください。次回の記事で紹介させていただきます。
最終的に私は、通常の入出力を使用して、ファイルを1行ずつ文字列に読み込み、それらの文字列に正規表現を適用することにしました。




上に戻る


機能の仕組み
何ができなかったかということについての話は、もう十分でしょう。私がやり遂げることのできた ことをご披露しましょう。分析を行う過程で、私は、ファイルを記述するための単純なデータ構造 (および対応するXMLボキャブラリー) を定義しました。新規ファイルをXIで処理するために、別の記述も作成しました。
まず最初に、次のようなファイルを含むデータ構造を実装しました。
Ruleset (リスト3 を参照) は、正規表現の集合を表しています。
Match (リスト4 を参照) は、単一の正規表現を表しています。このクラスは java.regex をカプセル化しています。
Group (リスト5 を参照) は、正規表現の中のグループ (括弧で囲まれた式) です。

リスト3. Ruleset.java

package org.ananas.xi;
import java.util.*;
public class Ruleset
extends QName
{
private List matches = new ArrayList();
private String error = null;
public Ruleset(String namespaceURI,
String localName,
String qualifiedName)
{
super(namespaceURI,localName,qualifiedName);
}
public void setError(String error)
{
this.error = error;
}
public String getError()
{
return error;
}
public synchronized void addMatch(Match match)
{
matches.add(match);
}
public synchronized Match getMatchAt(int index)
{
return (Match)matches.get(index);
}
public synchronized int getMatchCount()
{
return matches.size();
}
}

Ruleset は、基本的には Match オブジェクトのリストのコンテナーです。

リスト4. Match.java

package org.ananas.xi;
import java.util.*;
import java.util.regex.*;
public class Match
extends QName
{
private Pattern pattern;
private Matcher matcher = null;
private String input = null;
private List groups = new ArrayList();
public Match(String namespaceURI,
String localName,
String qualifiedName,
String pattern)
{
super(namespaceURI,localName,qualifiedName);
this.pattern = Pattern.compile(pattern);
}
public synchronized void addGroup(Group group)
{
groups.add(group);
}
public synchronized Group getGroupNameAt(int index)
{
if(index < 1 || index > groups.size())
throw new IndexOutOfBoundsException("index out of bounds");
return (Group)groups.get(index - 1);
}
public synchronized String getGroupValueAt(int index)
throws IllegalStateException, IllegalArgumentException
{
if(matcher == null)
throw new IllegalStateException("Call matches() first");
return getGroupNameAt(index).isText() ?
matcher.group(0) : matcher.group(index);
}
public synchronized int getGroupCount()
{
return groups.size();
}
public boolean matches(String st)
{
input = st;
if(matcher == null)
matcher = pattern.matcher(st);
else
matcher.reset(st);
return matcher.lookingAt();
}
public String rest()
{
if(matcher == null)
throw new IllegalStateException("Call matches() first");
int end = matcher.end(),
length = input.length();
if(end < length)
return input.substring(end,length);
else
return null;
}
}

Match は、このデータ構造における最も重要なクラスです。これは正規表現を表し、その正規表現を文字列と突き合わせるための論理を提供します。正規表現を適用するために lookingAt() が使用されていることに注意してください。lookingAt() は部分的な文字列と一致することができるため、文字列をサブ文字列に分解することが可能です。

リスト5. Group.java

package org.ananas.xi;
public class Group
extends QName
{
public Group(String namespaceURI,
String localName,
String qualifiedName)
{
super(namespaceURI,localName,qualifiedName);
}
}

私はすべてのクラスを QName (リスト6 を参照) から派生させました。QName は、XMLエレメントの名前を名前空間URIとローカル名の組み合わせとして表現します。

リスト6. QName.java

package org.ananas.xi;
public class Group
extends QName
{
public Group(String namespaceURI,
String localName,
String qualifiedName)
{
super(namespaceURI,localName,qualifiedName);
}
}





上に戻る


使ってみましょう
このコラムを書くまでに XIReader の最初のバージョンを完成させることはできませんでしたが (われわれソフトウェア開発者は、自分たちの能力に対して全面的な期待と確信をもっているのですが、問題に突き当たってしまうことがあります。これは特に、新規ライブラリーを学習する場合にはよくあることです)、正規表現のAPIを体験できるような簡単なテスト・クラス (このクラスは、リスト7 に示してあります) を書くことができます。この XIReader ではXML文書は書かれませんが、正規表現を使用してテキスト・ファイルをその構成要素に分解するための論理は含まれています。
この再帰的アルゴリズムは read() メソッドで使用されています。XML文書の階層構造が本来再帰的なものであるため、再帰的アルゴリズムはXML文書でうまく機能します。このアルゴリズムは次のようになっています。
文字列が与えられた場合、Match を走査して適切な正規表現の検出を試みる。
Match に接続された Group ごとに内容を出力する。
Group 名が別の Ruleset と一致した場合、再帰呼び出しによって文字列をさらに分解することを試みる (前回のコラムの例で示した an:fields エレメントが、これに該当しています)。
正規表現によって文字列の一部だけしか使用されなかった場合、再帰呼び出しが残りの部分を処理する。

リスト7. Test.java

package org.ananas.xi;
import java.io.*;
import java.util.regex.*;
public class Test
{
public static void main(String[] params)
throws IOException
{
Ruleset[] rulesets = getRulesets();
BufferedReader reader = new BufferedReader(new FileReader(params[0]));
String st = reader.readLine();
while(st != null)
{
read(rulesets,st);
st = reader.rine();
}
}
public static Ruleset[] getRulesets()
{
Ruleset[] rulesets = new Ruleset[2];
rulesets[0] = new Ruleset("http://ananas.org/2002/sample",
"address-book",
"an:address-book");
rulesets[1] = new Ruleset("http://ananas.org/2002/sample",
"fields",
"an:fields");
Match match = new Match("http://ananas.org/2002/sample",
"alias",
"an:alias",
"^alias (.*):(.*)$");
Group group = new Group("http://ananas.org/2002/sample",
"id",
"an:id");
match.addGroup(group);
group = new Group("http://ananas.org/2002/sample",
"email",
"an:email");
match.addGroup(group);
rulesets[0].addMatch(match);
match = new Match("http://ananas.org/2002/sample",
"note",
"an:note",
"^note .*:(.*)$");
group = new Group("http://ananas.org/2002/sample",
"fields",
"an:fields");
match.addGroup(group);
rulesets[0].addMatch(match);
match = new Match("http://ananas.org/2002/sample",
"fields",
"an:fields",
"[\\s]*<([^<]*)>");
group = new Group("http://ananas.org/2002/sample",
"field",
"an:field");
match.addGroup(group);
rulesets[1].addMatch(match);
return rulesets;
}
public static void read(Ruleset[] rulesets,String st)
{
read(rulesets,rulesets[0],st,false);
}
public static void read(Ruleset[] rulesets,Ruleset ruleset,String st,boolean next)
{
boolean found = false;
for(int i = 0;i < ruleset.getMatchCount() && !found;i++)
{
if(ruleset.getMatchAt(i).matches(st))
{
found = true;
Match match = ruleset.getMatchAt(i);
if(!next)
{
System.out.print(ruleset.getMatchAt(i).getQualifiedName());
System.out.print(' ');
}
for(int j = 1;j <= match.getGroupCount();j++)
{
String qname = match.getGroupNameAt(j).getQualifiedName();
boolean deep = false;
for(int k = 0;k < rulesets.length && !deep;k++)
if(rulesets[k].getQualifiedName().equals(qname))
{
System.out.print("\n >> \"");
System.out.print(match.getGroupValueAt(j));
System.out.print("\" >> ");
read(rulesets,rulesets[k],match.getGroupValueAt(j),false);
deep = true;
}
if(!deep)
{
System.out.print(match.getGroupNameAt(j).getQualifiedName());
System.out.print(' ');
System.out.print(match.getGroupValueAt(j));
System.out.print(' ');
}
}
String rest = match.rest();
if(rest != null)
read(rulesets,ruleset,rest,true);
}
}
System.out.println();
}
}

この getRulesets() メソッドを見て、がっかりしないでください。差し当たり、これによってメモリー内にファイル記述を作成するようにしておきます。次回の記事では、XMLファイルからファイル記述を読み取るようにします。




上に戻る


XIReaderに向けて
まもなく、XIReader の稼働バージョンが出来上がる予定です。残っている作業は、リスト7 の System.out.println() を ContentHandler への適切な呼び出しに置き換えることだけです。また、XMLReader インターフェースの完全な実装にも取り掛かる必要がありますが、これはさほど難しいことではありません。
このコラムがよい例ですが、新規ライブラリーの学習には、多くの時間を要することがあります。


参考文献
Regex for Java を入手してください。これは、IBMによるもう1つの正規表現エンジンです。

MEC-Eagle を参照してください。これも、レガシー・ファイルをXMLにインポートすることのできる、e-commerceアプリケーション用のツールです。

Wordまたはその他のワード・プロセッサー文書をお持ちの読者は、upCast を参照してください。

正規表現に関する有益な参考書として、『Mastering Regular Expressions』(Jeffrey E. F. Friedl著、O'Reilly発行、1997年) をお勧めします。

java.nioの詳細については、http://java.sun.com/j2se/1.4/docs/api/java/nio/package-summary.html を参照してください。

XIで提供される変換の詳細については、Benoit Marchalの前回のコラム実用的なXML: XIでテキストをXMLとしてインポート (developerWorks、2002年4月) を参照してください。

著者の最初のプロジェクトであるXMについては、「実用的なXML」コラムのこれまでの記事を読み返してください。
実用的なXML: コンテンツ・マネージメントにXSLTを使用する (developerWorks、2001年7月)
実用的なXML: リンク・マネージメントと将来への準備 (developerWorks、2001年8月)
実用的なXML: 処理命令およびパラメーター (developerWorks、2001年9月)
実用的なXML: XMバージョン1のまとめ (developerWorks、2001年10月)

XMLおよび関連テクノロジーにおけるIBM認定開発者になる方法を探してください。

developerWorks XMLゾーン には、ほかにも多くのXML関連の参考文献が掲載されています。


著者について



Benoit Marchal氏は、ベルギーのナミュールを拠点にしたコンサルタントおよび著述家です。彼の著作には、 XML by Example(Que社、邦訳: インプレス社「実例で学ぶXML」。間もなく第2版が出版される予定です)、 Applied XML Solutions および XML and the Enterprise があります。また、Gamelanのコラムや、developerWorks XML zoneのコラムWorking XML の著者でもあります。最新プロジェクトの詳細については、www.marchal.com をご覧ください。

マイブログ リスト


Jang ki hote

自己紹介