'분류 전체보기'에 해당되는 글 2491건

  1. 2008.11.12 언인스톨 완전삭제 1
  2. 2008.11.12 원도우탐색기 4개 쓰기 Q-Dir 외...
  3. 2008.11.12 솔라리스에서 호스트 이름 혹은 IP 주소 변경하기
  4. 2008.11.12 개발을 솔라리스에서 해야 하는 이유
  5. 2008.11.12 윈도우 혹은 리눅스에서 솔라리스 파티션에 접근하기
  6. 2008.11.12 솔라리스 볼륨 메니저를 이용하여 솔라리스9, 10 에서 루트를 미러링 하는 방법
  7. 2008.11.12 Solaris 안전한 FTP 서버 만들기
  8. 2008.11.12 SAML을 이용한 SSO Service의 구현
  9. 2008.11.12 학습 곡선 일지 4편: 웹 서비스 액세스
  10. 2008.11.12 학습 곡선 일지 3편: JavaFX 스크립트 함수
  11. 2008.11.12 학습 곡선 일지 2편: 선언적 사용자 인터페이스
  12. 2008.11.12 학습 곡선 일지 1편: JavaFX 스크립트 탐구
  13. 2008.11.12 TableNode: JavaFX에서 스크롤 가능한 사용자 지정 테이블 만들기
  14. 2008.11.12 자신만의 JavaFX "사용자 지정 노드" 만들기: 그래픽 메뉴의 예
  15. 2008.11.12 INETADDRESS를 이용한 호스트 이름 검색(룩업) 및 호스트 도달 능력
  16. 2008.11.12 CookieHandler를 이용한 쿠키 관리
  17. 2008.11.12 java swing table 정렬 및 필터링
  18. 2008.11.12 캐치되지 않은 예외 캐치하기
  19. 2008.11.12 Java SE의 정규 표현식
  20. 2008.11.12 java 에서 쿠키 처리
  21. 2008.11.12 2008년 1월 코어 자바 테크팁 퀴즈
  22. 2008.11.12 문자열 정렬
  23. 2008.11.12 경량화 DB의 꿈 JavaDB
  24. 2008.11.12 JNDI의 소개
  25. 2008.11.12 커스텀 태그 파일
  26. 2008.11.12 JDBC 연습하기
  27. 2008.11.12 기존 웹 리소스 이용하기
  28. 2008.11.12 J2EE 커넥터 아키텍처 1.5
  29. 2008.11.12 JAVA 트랜잭션 API 소개
  30. 2008.11.12 자바 기술을 이용한 AJAX의 활용
91..개발관련프로그램2008. 11. 12. 17:57
반응형

V Free and Full Download

autoupdate Revo Uninstaller is completely free - no cost, no adware, no spyware;
Download Revo Uninstaller Freeware
Total size: 1.56 MB
Current Version: 1.75

Revo Uninstaller includes: Junk Files Cleaner, Windows Tools, Auto Run Manager, Browsers Cleaner, MS Office Cleaner, Windows Cleaner, Evidence Remover and Unrecoverable Delete tools! You can reach all these tools from "Tools" tool bar button of Revo Uninstaller.


설치 기록도 않남는다.
정품유틸 사용후 삭제 할때...

Posted by 1010
91..개발관련프로그램2008. 11. 12. 17:54
반응형
Posted by 1010
60.Unix2008. 11. 12. 17:45
반응형

순서


개요

이 테크 팁은 운용중인 솔라리스 시스템의 호스트 이름 혹은 IP 주소를 바꾸는 방법에 대해 설명합니다.

이 정보는 썬 InfoDoc 21252, "How to Change the hostname and/or IP Address Without Running the sys-unconfig Command" 를 기반으로 만들어 졌습니다. (좀 더 자세한 정보는 SunSolve 를 참고하시기 바랍니다; 컨텐츠는 썬 서비스 플랜을 계약 하신 분께 공개되어 있습니다.)


옵션 1: sys-unconfig 이용하기

sys-unconfig 를 사용하면 여러개의 파일들을 기본값으로 되돌릴 수 있고, 그 다음에 호스트를 셧다운 시킵니다. 재부팅에 관해서 멘 페이지에서 설명되지 않은 것은 이러한 파일들을 재생성하도록 설정에 관한 질문을 받는 다는 것입니다. 이러한 질문들은 서버가 처음 배달 됐을때 혹은 운영체제를 제일 처음 로딩 시켰을때 받는 질문과 동일 합니다.

이제 부팅 과정에서 다음과 같은 화면을 보게 될 것입니다:

 Select the language you want to use: <select your language>
 
 Select a Locale,
 Please make a choice (0 - 59), or press h or ? for help: 
<select your location>
 
 What type of terminal are you using?
 Type the number of your choice and press Return: 
<select your terminal type, I use a DEC VT100>
 
 F2 continue, F2 continue
 
 Network Connectivity
       Networked [X] Yes
 
 DHCP
       Use DHCP [X] No
 
 Primary Network Interface
       Primary network interface [X] eri0
 
 Host Name <hostname>
 
 IP Address <ipaddress>
 
 Subnets
       System part of a subnet [X] Yes
 
 Netmask
 <netmask>
 
 IPv6
       Enable IPv6 [X] No
 
 Set a default Route
       [X] Specify one
 Default Route IP Address
       Router IP Address: <defaultrouter>
 
 Confirm Information
       F2 continue
 
 Configure Security Policy:
       Configure Kerberos Security [X] No
 
 Confirm Information
       F2 continue
 
 Name Service
       Name service [X] DNS
 
 Domain Name
       <dnsdomain>
 
 DNS Server Address
       Servers IP Address: <nameserver1>
       Servers IP Address: <nameserver2>
       Servers IP Address:
 
 DNS Search List
       Search Domain: <dnsdomain>
       Search Domain:
       Search Domain:
       Search Domain:
       Search Domain:
 
 Confirm Information
       F2 continue
 
 Time Zone
       Regions [X] <select your region>
 
 Time Zone
       Time zones [X] <select your time zone>
 
 Date and Time
   > Accept the default date and time or enter new values.

옵션 2: 파일 수정하기

호스트 이름 변경

솔라리스7 이전 버전은 다음의 파일들을 수정합니다:

/etc/inet/hosts
/etc/nodename
/etc/hostname.<interface>

여기서 <interface> 는 프라이머리 인터페이스의 이름입니다, 예를 들어 bge0.

/etc/net/ticlts/hosts
/etc/net/ticots/hosts
/etc/net/ticotsord/hosts

이제 시스템을 재부팅 합니다.

솔라리스7 에서 9 버전은 다음의 파일들을 수정합니다:

/etc/inet/hosts
/etc/nodename
/etc/hostname.<interface>

여기서 <interface> 는 프라이머리 인터페이스의 이름입니다, 예를 들어 bge0.

/etc/net/ticlts/hosts
/etc/net/ticots/hosts
/etc/net/ticotsord/hosts
/etc/inet/ipnodes

이제 시스템을 재부팅 합니다.

솔라리스10 버전은 다음의 파일들을 수정합니다:

/etc/inet/hosts
/etc/nodename
/etc/hostname.<interface>

여기서 <interface> 는 프라이머리 인터페이스의 이름입니다, 예를 들어 bge0.

/etc/inet/ipnodes

이제 시스템을 재부팅 합니다.

IP 주소 변경

솔라리스7 이전 버전은 다음의 파일들을 수정합니다:

/etc/inet/hosts
/etc/hostname.<interfaces>

여기서 <interfaces> 는 인터페이스의 이름들 입니다, 예를 들어 bge0, bge1, eri0.

/etc/net/ticlts/hosts
/etc/net/ticots/hosts
/etc/net/ticotsord/hosts

만약 새로운 서브넷으로 이동하려 한다면 다음의 파일들을 수정합니다:

/etc/inet/netmasks
/etc/defaultrouter
/etc/resolv.conf

이제 시스템을 재부팅 합니다.

솔라리스7 에서 9 버전은 다음의 파일들을 수정합니다:

/etc/inet/hosts
/etc/hostname.<interfaces>

여기서 <interfaces> 는 인터페이스의 이름들 입니다, 예를 들어 bge0, bge1, eri0.

/etc/net/ticlts/hosts
/etc/net/ticots/hosts
/etc/net/ticotsord/hosts
/etc/inet/ipnodes

만약 새로운 서브넷으로 이동하려 한다면 다음의 파일들을 수정합니다:

/etc/inet/netmasks
/etc/defaultrouter
/etc/resolv.conf

이제 시스템을 재부팅 합니다.

솔라리스10 버전은 다음의 파일들을 수정합니다:

/etc/inet/hosts
/etc/hostname.<interfaces>

여기서 <interfaces> 는 인터페이스의 이름들 입니다, 예를 들어 bge0, bge1, eri0.

/etc/inet/ipnodes

만약 새로운 서브넷으로 이동하려 한다면 다음의 파일들을 수정합니다:

/etc/inet/netmasks
/etc/defaultrouter
/etc/resolv.conf

이제 시스템을 재부팅 합니다.


저자에 관하여

Ross Moffatt 은 10년 이상 UNIX 시스템 관리자로 실한 경험이 있습니다. ross.stuff [at] telstra.com 로 연락 가능합니다.


이 아티클의 영문 원본은
http://www.sun.com/bigadmin/content/submitted/change_hostname.jsp

에서 볼수 있습니다.

"관리자코너" 카테고리의 다른 글

Posted by 1010
60.Unix2008. 11. 12. 17:43
반응형

솔라리스 익스프레스 개발자 에디션 (SXDE), 은 분기별로 배포되는 썬의 차세대 솔라리스입니다. 이 버전은 개발자들에게 특수한 기능, 하위 호환성 보장, 양질의 개발 툴, 손쉬운 다운 로드 방법, 및 오픈소스 커뮤니티에 참여할 수 있는 기회를 제공합니다.


순서

 

솔라리스 운영체제 (솔라리스 OS) 는 강력하고 안정적인 환경과 더불어 리눅스에서 볼 수 있는 현대화된 인스톨러를 통해서 갈끔하고 직관적인 설치 경험을 안겨 줍니다. 솔라리스는 무료이고 오픈된 UNIX 운영체제로 x86 과 SPARC 용 두가지 버전을 다운로드 받을 수 있습니다. 천백만명 이상의 개발자들이 지금까지 가입했고 200만이상의 CPU 에서 상용 라이센스하에 운영되고 있습니다.

썬 솔라리스10 은 최근에 InfoWorld 의 2008년 의 기술상을 가장 혁신적인 서버 OS 라는 평가를 들으며 수상했습니다. InfoWorld 는 "어떠한 서버 운영체제도 솔라리스 보다 더 다양한 요구조건을 만족하거나 환상적인 기술들을 뽐낼 수 없었다" 라고 평했습니다.

솔라리스는 단 하나의 소스 코드 베이스가 존재 합니다. 그러므로 동일한 기능을 모든 플랫폼상에서 제공합니다. 개발자는 솔라리스 운영체제에서 어플리케이션을 개발하고 최적화 함으로써 썬, HP, IBM, 그리도 Dell 등의 선도 벤더들의 시스템에서 사용할 수 있습니다.

개발자들에게 특히 솔라리스 익스프레스 개발자 에디션 이 중요한 이유는 일단 무료이고 분기별로 배포되는 썬의 차세대 솔라리스로써 오픈솔라리스의 홈이며 개발자의 협력의 장소인 OpenSolaris.org 의 코드 저장소의 소스를 이용해 빌드 되었다는 것입니다. SXDE 는 솔라리스10 에는 포함되지 않는 많은 신기술들을 포함하고 있습니다.

x86 플랫폼을 위한 SXDE 배포판은 솔라리스 어플리케이션, 자바 어플리케이션 플랫폼, 웹2.0을 개발하기 위한 최신 툴, 기술 및 플랫폼을 제공합니다. 개발자들은 다운로드, DVD 주문, 혹은 SXDE 가 미리 설정된 VMWare 가상 머신 등을 통해서 무료로 얻으실 수 있습니다.

랩탑에서의 개발환경을 지원하기위해 각 배포판마다 무선랜 지원을 위한 새로운 드라이버들을 추가 하였습니다. 9/07 배포판은 중요한 전원 관리 기능인 Enhanced Intel SppedStep Technology (EIST) 을 지원합니다. 1/08 배포판은 EIST 를 향상시켰으며 인텔4965 802.11agn 을 위한 드라이버를 포함하고 있습니다. 개발자들에게 더 편리한 설치 과정을 제공하기 위해 썬은 SXDE 를 위한 설치 및 설정 지원을 솔라리스10 서비스 지원 추가비용 없이 추가하였습니다.

SPARC 시스템을 이용하는 개발자는 최신 솔라리스 익스프레스 커뮤니티 에디션 빌드 (DVD) 혹은 (CD) 를 통해서 동일한 기능을 얻으실 수 있고 SPARC 개발자를 위한 툴은 2월 중순까지 번들 되도록 계획하고 있습니다. 이후의 솔라리스 익스프레스 개발자 에디션은 x86 과 SPARC 모두의 지원을 포함할 것입니다. 솔라리스 익스프레스 개발자 에디션의 VMWare 또한 제공 됩니다.

 
InfoWorld 수상

 썬 솔라리스10 과 ZFS 파일 시스템이 최근에 InfoWorld 의 2008 올해의 기술 상을 수상하였습니다.

 썬 솔라리스10 은 플랫폼들 중에서 가장 혁신적인 서버 OS 라는 영예를 안았습니다.

 InfoWorld 는 말하기를 "어떠한 서버 운영체제도 솔라리스 보다 더 다양한 요구조건을 만족하거나 환상적인 기술들을 뽐낼 수 없었다. 2008 년 올해의 기술상을 수상한 ZFS 파일 시스템부터 시작하여 DTrace 라는 최고의 OS 분석 툴이 지원되고 레드햇 리눅스 바이너리를 실행할 수 있는 솔라리스 컨테이너를 통한 네이티브, 빌트인 가상화(현재 어떠한 OS 도 번들하고 있지 못한) 등. 현시대의 서버 운영체제중 가장 혁신적인 버전이라고 할 수 있다"

 여기에 덧붙여 ZFS 파일 시스템은 최고의 파일 시스템 상을 수상했습니다. InfoWorld 는 ZFS 의 혁신을 칭찬했습니다: "썬의 ZFS 수준의 혁신이 매일 매일 컴퓨터 산업에서 일어 나는 것은 아니다. ZFS 의 유연함과 확장성은 현시대의 어떠한 파일 시스템또 훌쩍 뛰어 넘는다. 지속적인 IT 과학의 발전이 발전이 현 상태를 간단히 제곱으로 뛰어 넘어 버렸다. ZFS 는 현재까지의 모든 룰을 깨버리고 놀랍게도 뛰어나고 잘 구현된 솔루션이다."

솔라리스 OS 는 오픈솔라리스 소스 코드, 개발자 커뮤니티 그리고 웹사이트로 구성되어 있는 오픈솔라리스 프로젝트와 혼동되서는 안됩니다. 두가지 모두 썬에 의해 지원되고 있지만 오픈솔라리스는 개발자 커뮤니티에 의해 운영되는 개발 프로젝트 입니다.

솔라리스의 향후 버전은 오픈솔라리스 프로젝트에 의해 개발된 기술들에 기반을 둘 것입니다. 둘다 무료 바이너리 다운로드 형태로 제공되고 썬은 서비스 패키지와 정기 업데이트를 둘 모두를 위해 제공 합니다.

솔라리스는 개발자들에게 여러가지 장점을 제공합니다:

2005년 초기에 발표된 솔라리스10은 600개 이상의 신기술을 포함하고 있었고 이후 업데이트 버전과 솔라리스 익스프레스 프로그램을 통해서 더 많은 것들이 추가 되었습니다. 이들 중 상당 수는 다른 운영체제에서는 볼 수 없는 것들인데 대표적으로 솔라리스 동적 트레이스(DTrace), 솔라리스 컨테이너, 자가 예측 치료, 솔라리스 ZFS, 솔라리스 트러스티드 익스텐션 그리고 로지컬 도메인(LDoms) 가 바로 그것입니다.

DTrace 는 솔라리스10에서 소개된 디버깅 툴로 기존의 디버깅 툴로는 잡아내기 어려운 시스템의 문제를 디버그 하는데 도움을 줍니다. 이 툴은 개발자가 웹2.0에서는 매우 흔한 멀티 레이어 스포트웨어 스택(서로 다른 수많은 레이어가 존재 하는데 이것은 자바 스크립트, 자바, C/C++ 같은 서로 다른 언어로 구현되어 있음) 을 넘나다는 디버깅을 가능하도록 해줍니다.

이러한 멀티레이어에 대한 가시성은 기존의 디버깅툴로는 불가능 합니다. DTrace 의 경량성은 개발자가 프로덕션 서버에서 어플리케이션의 디버그 버전 없이도 어플리케이션을 조사할 수 있도록 도와 줍니다. 커스텀 probe 가 Firefox 같은 데스크탑의 중요 어플리케이션들에 추가 되어서 Firefox 내에서 자바 스크립트 코드에 DTrace 의 힘을 이용할 수 있고 Firefox 자체도 튜닝 할 수 있습니다.

DTrace 는 커맨드 라이브러리 libdtrace 에 의해 동작하고 이 라이브러리는 커널 내의 다양한 "DTrace provider" 에 대한 진입 포인트를 가지고 있고 이러한 "DTrace provider" 는 몇몇 커널 시스템의 논리적인 뷰를 제공 합니다. 솔라리스10은 거의 4만개 이상의 probe 를 솔라리스 커널 내에서 제공 합니다. DTrace 도구는 필요에 따라 끄거나 킬 수 있고 끈 상태로는 어떠한 오버헤드도 일으키지 않습니다. 커스텀 probe 를 만들기 위해 쿼리들이 조합될 수 있습니다.

ZFS 는 128비트이고, 예술의 경지의 파일 시스템으로, 엔드-투-엔드의 오류 검사 및 보정, 간단한 커맨드 라인 인터페이스 그리고 가상적으로 스토리지 용량에 제한이 없습니다. ZFS 는 InfoWorld 에 의해 2008년 최고의 파일 시스템 으로 선정되었습니다.

솔라리스 존 은 개발자가 머신을 서로에게 독립적인 여러개의 가상 머신으로 나눌 수 있습니다. 솔라리스 존은 솔라리스 내에서 파티션된 가상 OS 환경 입니다. 각 존은 독립된 가상 서버로 싱글 머신처럼 독잡하게 됩니다. 존은 기본적인 단위로써 운영체제의 리소스 관리 설비 와 같이 사용되면 솔라리스 컨테이너로 불립니다. 많은 개발자들은 "존" 과 "컨테이너" 를 혼용해서 사용하고 있습니다.

어플리케이션은 존을 독립적이고 보안이 강화된 운영체제 환경으로 다루고 있으며 개발자가 서로 다른 존에 어플리케이션을 설치 함으로써 각각의 어플리케이션을 독립시킬 수 있도록 합니다. 그러면서도 동시에 운영체제 리소스의 중앙 집중적인 관리가 이루어 집니다.

자가 예측 치료 기능은 솔라리스10을 위해 만들어 졌으며 하드웨어 및 어플리케이션 오류 상황을 자동으로 진단하고 고립화 시키고 복구 시킵니다. 결과적으로 비지니스-크리티컬 한 어플리케이션과 기본적인 시스템 서비스들이 소프트웨어 오류, 주요 하드웨어 컴포넌트의 오류 또한 소프트웨어의 잘못된 설정으로 인해 발생하는 문제등으로 부터 어떠한 영향도 받지 않고 계속적으로 실행될 수 있도록 합니다.

2007년 10월 자바 기술의 아버지인 제임스 고슬링은 그의 솔라리스에 관한 감격에 대해 블로깅 한 적이 있습니다: "솔라리스에는 멋진 기능들이 너무 많습니다. 저는 존과 ZFS 에 완전히 중독되었습니다. 예를 들어 저는 렙탑에서 ZFS 를 이용해 디스크를 관리합니다. 이후에 랩탑의 디스크를 USB 로 곧바로 미러링 합니다."

솔라리스는 개발자들이 어플리케이션을 배포할 모든 환경과 호환되는 통합되고 바로사용이 가능한 툴을 제공합니다. SXDE 를 통해서 최신 개발자 툴이 자동적으로 운영체제와 함께 설치 됩니다.

개발자 툴은 다음을 포함합니다:

  • 썬 스튜디오 12 12/07 소프트웨어 는 고성능 컴파일러와 툴을 제공하고, SPARC 플랫폼, x86/x64 플랫폼의 솔라리스와 리눅스를 위한 최적화된 C, C++,포트란 컴파일러를 제공하고, 또한 최신의 멀티-코어 시스템과 소스, 메모리 그리고 쓰레드 디버깅등을 지원합니다.
  • Project D-Light 는 시스템의 동작을 분석할 수 있는 몇가지 도구들을 제공합니다. (프로젝트 D-Light 는 현재 솔라리스10에서만 실행되며 DTrace 활성화된 최소 6이상의 JRE 가 필요 합니다) 프로젝트 D-Light 는 복잡한 어플리케이션과 시스템의 프로파일링을 퍼포먼스 병목현상을 가시적으로 보여주고 어플리케이션 시스템 스택상의 리소스 병목현상에 대해 개발자들이 접근 할 수 있도록 도와 줌으로써 어플리케이션과 시스템 환경을 최적화 할 수 있도록 도와 줍니다. 아주 간단한 드래그앤드롭 인터페이스로 되어 있고 DTrace 를 포함한 최신의 프로파일링 기술들의 확장 라이브러리를 제공 합니다.
  • 넷빈즈 IDE 6 는 개발자가 크로스 플랫폼 자바 데스크탑, 엔터프라이즈, Ruby on Rails, JRuby, PHP 등의 지원을 포함하는 웹 어플리케이션을 개발 할 수 있는 모든 툴들을 제공합니다.
  • 자바 플랫폼 Standard Edition 6 (Java SE) 은 개발자가 임베디드 및 실시간 환경 등을 포함한 데스크탑과 서버에서의 자바 어플리케이션 생성과 배치를 가능하도록 합니다. 자바 SE6 는 웹서비스 개발등에 필요한 클래스들을 기본적으로 제공하고 자바 플랫폼 Enterprise Edition (Java EE) 의 기반입니다.

SXDE 의 일부로 제공되는 웹 어플리케이션 스택은 널리 사용되는 오픈 소스 어플리케이션들이 솔라리스에 최적화된 형태로 제공됩니다. 이 것은 가장 대중적인 어플리케이션(아파치, PHP, MySQL, Ruby 등) 이 미리 설정되어 사용이 가능하며 컨트롤 패널을 통해서 빠르고 쉽게 서비스를 관리할 수 있습니다.

솔라리스10은 랩탑에서 데스크탑 그리고 SPARC 과 x64/x86 등의 모든 시스템에서 실행 됩니다. 랩탑과 데스크탑에서 완벽히 통합된 썬 자바 데스크탑 시스템을 통해서 솔라리스10은 파워 유저와 개발자들이 고급 기능들과 작업 생성에 가치를 줄 수 있는 개발자 툴들의 장점들을 누릴 수 있도록 합니다.


하위 호환성 보장

하위 호환성 보장은 상용 소프트웨어 개발자들에게 특히 중요합니다. 왜냐하면 유지보스는 일반적으로 소프트웨어와 연관된 가장 큰 비용이기 때문입니다. 썬은 하위 호환성 보장은 벤더들의 소프트웨어가 한 버전의 솔라리스에서 빌드된 것이 그대로 다른 버전에서도 올바르게 동작하도록 보장합니다.

이것이 다른 운영체제와 확연하게 비교 되는 점입니다. 예를 들어 시스템 컴포넌트의 비호환적인 변경 -- 예를 들어 라이브러리들 -- 이 어플리케이션에 미칠 수 있는 영향을 고려하지 않고 만들어지는 것들과 비교 됩니다. 결과적으로 어플리케이션은 고장나고 유지 보수 비용이 증가 함으로써 어플리케이션 벤더와 사용자들에게 좌절을 안겨다 줄 것입니다.

2006년 11월 과 2007년 11월 사이에 OpenSolaris.org 의 가입자는 2만명에서 8만명으로 늘었습니다. 오픈 솔라리스 프로젝트 이상으로 오픈솔라리스는 커뮤니티 이며 협동을 위한 웹 사이트 입니다. 솔라리스 소스 코드, 다운로드, 개발자 툴, 메일링 리스트, 유저 그룹, 이벤트 정보, 그리고 그 이상의 것들이 opensolaris.org 에서 이용 가능합니다. 오픈솔라리스 기술은 SPARC 과 x86 플랫폼을 위한 단일 소스 코드 베이스로써 모든 플랫폼에서 동일한 기능을 제공합니다.

GNOME 은 개발자들이 필요에 맞는 데스크탑 어플리케이션을 다양하게 사용할 수 있는 현대적인 데스크탑을 제공합니다.

GNOME 커뮤니티는 좀 더 많은 개발자들을 끌어 들이기 위해 데스크탑 어플리케이션을 C 로 개발하는 것에 대한 대안이 필요함을 인정하고 있습니다. GNOME 데스크탑의 95 퍼센트 이상이 C 로 쓰여졌으므로 많은 사람들이 여전히 썬 스튜디오 소프트웨어, 넷빈즈와 C/C++ 모듈, 그리고 이클립스를 통해서 그들의 데스크탑 어플리케이션을 C 로 개발하고 있습니다.

자바 GNOME 바인딩 은 GNOME 과 GTK+ 어플리케이션을 개발할떄 필요한 핵심 라이브러리에 대한 접근을 제공하고 있습니다. 바인딩은 하부 플랫폼의 C API 를 자바SE 에서 제공하는 JNI 를 통해 자바 API 에서 접근이 가능하도록 지원합니다. 바인딩은 솔라리스 패키지 셋의 형태로 제공되며 다음을 포함하고 있습니다:

  • API jars: 자바 API 를 제공하고 자바 GNOME API 를 import 하도록 한다
  • API libraries: JNI 네이티브 C 라이브러리를 지원하여 자바 API 가 호출할 수 있도록 한다
  • 도움말: API 소스 jar 파일들에 대한 설명을 제공함으로써 code 자동 완성 및 소스 브라우징을 IDE 에서 가능하도록 한다
  • API Javadoc: API 에 대한 이해하기 쉬운 설명을 제공한다

자바 GNOME 바인딩은 Glade-기반의 GNOME/GTK+ 어플리케이션 개발에 친숙한 개발자들에게 이러한 타입의 데스크탑 어플리케이션을 자바 언어로 대치할 수 있는 길을 제공합니다. 자바 GNOME 커뮤니티는 바인딩의 자동 생성이 가능하도록하는 재디자인 작업중에 있습니다. 이 작업을 통해서 하부 GNMOE 플랫폼과 Cairo 네이티브 라이브러리에 대한 거의 100% 의 지원을 제공하게 될 것이고 GNOME 플랫폼의 차후버전과도 계속해서 동기되도록 해 줄 것입니다.

이와 덧붙여서 새로운 GNOME DevHelp 는 gtk-doc 과 네이티브한 작업을 통해서 개발자들이 GNOME API 문서를 브라우징 하고 검색 할 수 있는 어플리케이션입니다.

솔라리스는 배치의 유연성을 제공합니다. 솔라리스는 SPARC 기반의 시스템에서 운용되든 x64/x86 시스템에서 운용되든 동일한 기능을 제공합니다. 솔라리스의 모듈화된 아키텍처는 커널을 재빌드 할 필요 없이 드라이버들이 동적으로 로딩 될 수 있도록 합니다. 커널 자체는 단일 프로세서와 멀티프로세서 환경을 지원하고 대부분 자체적으로 튜닝이 되어 있습니다. 이러한 기능들은 대용량의 배치 환경에서 단일화되고 최적화되고 강력하게 보안이 강화된 환경을 제공 합니다.

이러한 효율적인 작업은 임베디드 시스템 혹은 컴퓨팅 팜에서도 마찬 가지 입니다. 솔라리스는 그러므로 텔레커뮤니케이션, 스토리지, 네트워크 보안, 병원 그리고 정부 공공 같은 분야에 매우 적합합니다. 또한 가능한 모든 PC 의 설정 (랩탑, 데스크탑, 워크스테이션, 블레이드, 랙마운트 서버, x64/x86 기반의 8-way 서버를 포함한) 에서도 매우 적당합니다.

SXDE 를 이용해 개발된 어플리케이션은 Solaris Ready Test Suite 을 이용해 솔라리스10 API 를 사용하는지 검사함으로써 솔라리스에서 테스트되고 사용될 수 있습니다. 덧붙여서 배치 전에 솔라리스10 서버에서 파이널 빌드를 만들어야 합니다.


솔라리스에 포함된
가상화 기능은 배치를 좀 더 간편하고 유연하도록 해줍니다. 썬은 가상화 엔터프라이즈를 위한 현존하는 모든 기술들을 제공하고 미래의 가상화를 위한 혁신적인 최신 기술을 제공합니다.

솔라리스10은 메모리 배치 최적화(Memory Placement Optimization:MPO) 를 통해서 서버의 물리적 메모리의 배치를 향상시킴으로써 퍼포먼스 향상을 이룩했습니다. MPO 를 통해서 솔라리스10은 메모리를 그것에 접근하는 프로세서와 최대한 가깝게 위치시키는 동시에 시스템에서 충분히 밸런스를 이룰 수 있도록 하였습니다. 결과적으로 TPC-H 런타임이 획기적으로 감소하였고 TPC-C 퍼포먼스가 향상되었고 다수의 고성능 컴퓨팅(HPC) 어플리케이션의 실행시간이 반으로 단축되었습니다.

최근의 몇몇 솔라리스 배포판에서 쓰레드 라이브러리는 멀티쓰레드 어플리케이션을 위해 향상되었습니다. 솔라리스9 부터 썬은 기존의 'MxN' 구현을 대체하는 최적으로 튜닝되고 테스트된 '1:1' 모델을 채용하였습니다. 하부의 쓰레드 구현을 단순화 함으로써 어플리케이션은 재컴파일 없이 엄청난 퍼포먼스 향상과 안정성의 향상을 얻을 수 있습니다. 솔라리스10에서는 쓰레드 로컬 스토리지(TLS) 가 추가되었고 스토리지 퍼포먼스의 단순화 및 퍼포먼스 향상을 꾀했습니다. 새로운 쓰레드 모델과 최신 자바 가상 머신(JVM) 기술의 조합은 SPECjbb2000 퍼포먼스의 비약적인 향상을 가져 왔습니다.

많은 개발자들이 고유의 소프트웨어 환경을 관리하고 있습니다. 솔라리스10은 레드햇 리눅스에 비해 20% 이상의 소유비용 이득이 있음이 Crimson 컨설팅 그룹의 새로운 연구결과 에 의해 밝혀 졌습니다. 이 연구는 썬에 의해 스폰서 되었고 솔라리스와 레드햇 엔터프라이즈 리눅스 배치 경험이 없는 연구 참여자를 선별하여 독립적이고 분석적인 평가를 통해 디자인되고 실행 되었습니다.

솔라리스 와 SXDE 는 개발자들에게 특별한 기능, 다양한 어플리케이션, 손쉬운 다운로드 방법, 복수플랫폼 지원, 그리고 오픈 소스 커뮤니티 경험, 보장되는 하위 호환성 보장, 우월한 배치방법 및 관리 비용의 절감을 가져다 줍니다. 직접 사용해 보면 모든 것이 명확해 질 것입니다.

썬과 번영하고 있는 오픈 소스 커뮤니티의 강력한 지원을 통해서 미래는 매우 밝을 것입니다.


저자에 관하여

Janice J. Heiss 는 썬 마이크로시스템즈의 필자진중 한명으로 자바 기술을 담당하고 있습니다.
 

이 글의 영문 원본은
http://developers.sun.com/solaris/artic ··· nsolaris
에서 보실 수 있습니다.

"개발자코너" 카테고리의 다른 글

Posted by 1010
60.Unix2008. 11. 12. 17:36
반응형

파트 1 - 윈도우에서 솔라리스 파티션 접근하기

운나쁘게도 윈도우는 솔라리스 파티션을 읽을 수 없습니다. 그러므로 여러분은 듀얼 부트 (솔라리스, 윈도우즈) 설정 환경에서의 윈도우에서 솔라리스 파티션에 데이타를 읽을 수 없습니다.

이 것을 극복할 하기 위해서 여러분은 가상 PC 에뮬레이터 Qemu 와 Mialx 라이브 시디를 이용할 수 있습니다.

윈도우용 Qemu 바이너리는 아래 링크에서 구하실 수 있습니다.

http://www1.interq.or.jp/~t-takeda/qemu/

(Qemu 홈페이지는 http://fabrice.bellard.free.fr/qemu/index.html 입니다.)

그리고 Milax 라이브 시디는 아래 링크에서 구하실 수 있습니다.

http://www.milax.org/?page_id=9

Qemu 어카이브의 압축을 푼 다음 (Qemu 는 설치가 필요하지 않음) Milax ISO 이미지 milax03.iso 를 Qemu 디렉토리에 복사하고 Mialx 가상 머신을 시작하기 위해 아래의 명령어를 입력 합니다.

cd <directory_with_the_qemu_binary>

qemu.exe  -L ".\bios" -m 256 -localtime  -boot d -cdrom ./milax03.iso  -hda \\.\physicaldrive0 -net user -net nic,model=rtl8139 -redir tcp:1135::22 -name "Milax_(ssh_port:_1135)" -snapshot


주의:

파라미터"-snapshot" 은 "디스크 이미지 파일 대신 임시 파일에 쓸것" 을 의미 합니다. 이것은 Qemu 가 하드디스크에 쓰지 않음을 의미 합니다. 만약 변경사항을 하드디스크에 쓰길 원한다면 Qemu CLI 로 ctrl-alt-2 키를 이용해서 전환 하실 수 있습니다. 그리고 Qemu 커맨드 "commit ide0-hd0" 를 입력하시면 됩니다(자세한 사항은 Qemu 메뉴얼을 확인하시기 바랍니다). 가상 머신으로 돌아가기 위해서는 ctrl-alt-1 키를 이용합니다. 여러분은 절대로 Qemu 를 이용하여 -snapshot 파리미터 없이 실제 하드디스크를 접근해서는 안됩니다!!!

여러분은 Qemu 0.9.1 에서 백슬래시를 사용하려면 반드시 백슬래시 두개를 입력해야 합니다.

파라미터 "\\\\.physicaldrive0" 에서의 숫자는 사용할 디스크를 가르킵니다 - 만약 솔라리스가 첫번째 하드디스크에 있지 않다면 적절한 숫자로 바꿔주시기 바랍니다.

이 구문은 윈도우 XP (테스트됨) 에서 잘 동작하고 윈도우 2000 에서도 문제 없을 것입니다. Qemu 포럼에는 이 구문이 Vista 에서는 잘 동작하지 않는 다는 메세지들이 올라오고 있습니다 - 필자는 Vista 가 없어서 이부분을 확인해 보지 못했습니다.

이제 여러분은 가상 머신에서 솔라리스 파티션에 접근하실 수 있습니다.

여러분의 윈도우 호스트에서 가상머신에 접근하기 위해서는 다음의 명령을 이용합니다.

ssh -l alex -p 1135 localhost

혹은 scp 를 이용 합니다.

scp -p 1135  alex@localhost:/etc/release .


주의:

필자는 오직 Qemu 0.9.1 로 테스트 해 보았습니다 - 구버전의 Qemu 에서는 동작하지 않을 수도 있습니다.


파트 2 - 리눅스에서 솔라리스 파티션 접근하기

리눅스에서 위에서 언급한 workaround 는 필요 하지 않습니다. 왜냐하면 리눅스는 적어도 솔라리스 파티션과 UFS 빌트인에 대한 읽기 전용 접근을 제공하고 있기 때문입니다:

예제:

dmesg 를 이용해서 솔라리스 슬라이스를 위한 디바이스 이름을 확인해 보겠습니다:

root@tp61p:~# dmesg | grep sda

[   23.652384] sd 2:0:0:0: [sda] 312581808 512-byte hardware sectors (160042 MB)

[   23.652393] sd 2:0:0:0: [sda] Write Protect is off

[   23.652394] sd 2:0:0:0: [sda] Mode Sense: 00 3a 00 00

[   23.652403] sd 2:0:0:0: [sda] Write cache: enabled, read cache: enabled, doesn't support DPO or FUA

[   23.652434] sd 2:0:0:0: [sda] 312581808 512-byte hardware sectors (160042 MB)

[   23.652439] sd 2:0:0:0: [sda] Write Protect is off

[   23.652441] sd 2:0:0:0: [sda] Mode Sense: 00 3a 00 00

[   23.652449] sd 2:0:0:0: [sda] Write cache: enabled, read cache: enabled, doesn't support DPO or FUA

[   23.652451]  sda:sr0: scsi3-mmc drive: 24x/24x writer dvd-ram cd/rw xa/form2 cdda tray

[   23.677465]  sda1 sda2 sda3 sda4 < sda5 sda6 sda7 >

[   23.705547]  sda1: <solaris: [s0] sda8 [s1] sda9 [s2] sda10 [s3] sda11 [s4] sda12 [s6] sda13 [s7] sda14 >

[   23.706865] sd 2:0:0:0: [sda] Attached SCSI disk

[   28.739904] Adding 4128664k swap on /dev/sda6.  Priority:-1 extents:1 across:4128664k

[   28.945181] EXT3 FS on sda3, internal journal

[   30.894480] EXT3 FS on sda5, internal journal

root@tp61p:~#

이 예제에서 솔라리스 파티션의 슬라이스들은 리눅스 디바이스의 이름들로 접근할 수 있습니다.

Slice 	Linux device name

--------------------------------------------

s0 		sda8

s1 		sda9

s2 		sda10

s3 		sda11

s4 		sda12

s6 		sda13

s7 		sda14

여기에서 슬라이스 5번은 어떠한 리눅스 디바이스도 존재하지 않습니다. 왜냐하면 이 슬라이스는 솔라리스에서 사용되지 않기 때문입니다(슬라이스의 사이즈가 0 임)

이 슬라이스를 마운트 하려면:

root@tp61p:~# mount -t ufs -o ro /dev/sda8 /mnt
root@tp61p:~# df -k /mnt

Filesystem           1K-blocks      Used Available Use% Mounted on
/dev/sda8             11098457   9442316   1545157  86% /mnt
root@tp61p:~# ls /mnt

alternate_root  Desktop    home        milax03.usb         mnt21                   pool           test_root
bin             dev        kernel      milax.compressed    modinfo.out             proc           tmp
boot            devices    lib         milax.uncompressed  nautilus-debug-log.txt  root           usbcopy
BSITscite.pkg   Documents  lost+found  mnt                 net                     sbin           usr
cdrom           etc        media       mnt1                opt                     sol10hvm.save  var
data            export     milax       mnt2                platform                system         xen


만약 솔라리스 슬라이스에 대한 쓰기 권한이 필요 하다면 (그리고 여러분의 리눅스 배포판이 UFS 를 위한 쓰기 접근을 지원하지 않는 다면) 혹은 여러분이 솔라리스에서 ZFS 를 사용한다면 여러분은 Qemu 를 리눅스에서 실행해서 솔라리스 파티션의 데이타에 접근하실 수 있습니다:

qemu -net user -net nic -usb -usbdevice tablet -L "/usr/local/share/qemu"  -boot d \

                        -m 512 -net nic,model=rtl8139 -redir tcp:1135::22 \

			-name "Milax_(user_network)_(ssh_port:_1135)" \

                        -hda /dev/sda -cdrom ./milax03.iso -snapshot

주의:

/dev/sda 는 솔라리스 파티션이 있는 디스크 (파티션이 아님!) 입니다.

파라미터"-snapshot" 은 "디스크 이미지 파일 대신 임시 파일에 쓸것" 을 의미 합니다. 이것은 Qemu 가 하드디스크에 쓰지 않음을 의미 합니다. 만약 변경사항을 하드디스크에 쓰길 원한다면 Qemu CLI 로 ctrl-alt-2 키를 이용해서 전환 하실 수 있습니다. 그리고 Qemu 커맨드 "commit ide0-hd0" 를 입력하시면 됩니다(자세한 사항은 Qemu 메뉴얼을 확인하시기 바랍니다). 가상 머신으로 돌아가기 위해서는 ctrl-alt-1 키를 이용합니다. 여러분은 절대로 Qemu 를 이용하여 -snapshot 파리미터 없이 실제 하드디스크를 접근해서는 안됩니다!!!


이 글의 영문 원본은
How to access a Solaris partition from Windows or Linux
에서 보실 수 있습니다.

"관리자코너" 카테고리의 다른 글

Posted by 1010
60.Unix2008. 11. 12. 17:35
반응형

이 글에서는 솔라리스 볼륨 메니저를 이용해서 루트를 미러링하는 절차에 대해 소개 합니다. 솔라리스10에서 필자는 SVM 버전 3.0, REV=2005.01.09.21.19, 를 사용했고 솔라리스9에서는 SVM 버전 1.0, REV=2002.04.14.23.49 을 사용했습니다. (주의: 결과는 동일하지 않을 수 있음.)
필요조건

첫번째로 여러분은 미러링 하고자 하는 디스크들을 구분해야 합니다. 문제의 디스크들은 format 커맨드를 이용하여 찾아낼 수 있습니다.

format 커맨드를 실행합니다; 출력 결과는 다음과 같습니다:

AVAILABLE DISK SELECTIONS:
       0. c3t2d0 <DEFAULT cyl 17845 alt 2 hd 255 sec 63>

          /pci@7b,0/pci1022,7458@11/pci1000,3060@2/sd@2,0
       1. c3t3d0 <DEFAULT cyl 17845 alt 2 hd 255 sec 63>
          /pci@7b,0/pci1022,7458@11/pci1000,3060@2/sd@3,0

예제에서 필자는 디스크 드라이브의 다른 파티션들과 루트 파티션을 미러링 할 것입니다.

드라이브들은 c3t2d0 과 c3t3d0 입니다.

루트 미러링을 위한 과정

첫째로 프라이머리 드라이브를 파티셔닝 합니다. 일반적으로 프라이머리 는 솔라리스가 현재 실행되고 있는 드라이브 입니다. (필자의 경우는 c3t2d0.) 일반적으로 이러한 작업은 데이타 손실을 방지하기 위해 솔라리스 설치 시에 수행 합니다.

여러분은 메타 데이타베이스를 위해 약 10Mbyte 정도의 용량을 가진 파티션이 필요할 것입니다.

여러분이 생성한 파티션에 만족했다면 디스크에 레이블을 지정한다음에 아래의 명령을 통해서 동일한 파티션 테이블을 전송하도록 합니다.

파티션 테이블을 하나의 드라이브로 다른 드라이브로 전송 합니다.

prtvtoc /dev/rdsk/c3t2d0s2 | fmthard -s - /dev/rdsk/c3t3d0s2

주의: s2 는 보통 오버랩(overlap) 파티션임을 유의하시기 바랍니다; 만약 디스크에 변경을 가했다면 적절한 슬라이스로 변경하시기 바랍니다.

이제 여러분은 동일한 구조를 가진 두개의 디스크를 가지게 되었습니다. 다음의 명령을 실행 합니다:

metadb -a -c 3 -f  c3t2d0s7 c3t3d0s7

-c 3 옵션은 metastat 데이타베이스의 복제본을 3벌로 생성하고 이것은 하나의 복사본이 잘못될 경우를 대비해서 입니다.

아래의 커맨드를 이용해서 루트 파티션을 생성함으로써 디스크를 초기화 할 것입니다. 필자는s0 을 사용했는데 왜냐하면 이것이 저의 루트 파티션이기 때문입니다; 여러분 상황에 맞게 적절하게 변경하시기 바랍니다.

metainit -f d11 1 1 c3t2d0s0
metainit -f d12 1 1 c3t3d0s0

이제 실제 미러를 생성 합니다:

metainit d10 -m d11

이전의 모든 단계들을 수행한 후에는 아래의 커맨드를 수행합니다. 아래의 커맨드는 자동으로 /etc/system/etc/vfstab 을 업데이트함으로써 여러분이 메타디바이스를 여러분의 루트 디스크를 사용하고 있음을 인식하도록 합니다.

metaroot d10

위의 커맨드를 수행한다음에는 다른 반쪽 미러를 루트 디바이스에 연결시키기 전에 재부팅을 해야 합니다. 여러분은 현재 마운트 되어 있는 디바이스를 연결시킬 수 없습니다. 만약 연결을 할 경우에 머신은 미쳐 버릴 것입니다. 디바이스를 연결하기 위해서 여러분은 아래의 커맨드를 수행해야 합니다:

metattach d10 d12

미러의 상태를 확인하기 위해서 여러분은 아래의 커맨드를 실행 할 수 있습니다:

metastat d10

부트 디바이스를 위한 우선순위 alias 를 이용해서 Openboot 를 수정하길 원할 수도 있는데 이럴 경우에는 다음의 명령을 이용 합니다:

ls -l /dev/dsk/c0t0d0s0

출력은 다음과 같을 것입니다.

lrwxrwxrwx 1 root root 42 Jul 12 2007 /dev/dsk/c0t0d0s0 -> ../../devices/bold*pci@1e,600000/ide@d/sd*bold@0,0:a

여러분은 위의 굵은 부분을 여러분의 결과물로 대체해야 합니다. OS 에서 다음의 명령을 수행 합니다.

eeprom "nvramrc=devalias mirror pci@1e,600000/ide@d/sd@0,0:a"
eeprom "use-nvramrc?=true"
*bold* Commands for doing this from the OK prompt, don't do this elsewise.*bold*
nvalias mirror pci@1e,600000/ide@d/sd@0,0:a

만약 단지 두개의 내장 드라이브를 미러링 하려 한다면 아래의 라인을 /etc/system 에 추가 함으로써 싱글 드라이브에서 부팅이 될수 있도록 해야 합니다. 이 것은 SVM Quorum rule 을 무시할 것입니다.

set md:mirrored_root_flag = 1



이 글의 영문 원본은
How to Mirror root With Solaris Volume Manager in the Solaris 9 and 10 OS
에서 보실 수 있습니다.


"관리자코너" 카테고리의 다른 글

Posted by 1010
60.Unix2008. 11. 12. 17:34
반응형

이 테크팁은 안전한 ftp 서버를 생성하는 방법에 대해서 설명 합니다. ftp 데몬은 비-root 유저에 의해 실행되고, 유저가 정의한 포트를 리스닝하며, chroot 를 이용한 고립된 환경에서 동작합니다. 이 구현은 표준 ftp 포트를 이용해서 root 유저가 ftpd 를 실행하도록 변경하실 수도 있습니다. 이 환경은 설정이 가능한 접근제어 권한을 통해서 미리 지정된 사용자만이 로그인 할 수 있도록 합니다. 물론 여러분이 이러한 고립된 환경을 생성하려면 root 유저 권한이 필요 합니다.

참고: 이 방법은 솔라리스9 에서만 테스트 되었지만 기타 다른 솔라리스 버전에서도 잘 동작할 것입니다.

고립된 ftp 환경을 위치할 장소를 지정합니다, 필자는 이 글에서 /ftpjail 을 사용할 것입니다.
이 디렉토리는 설정파일과 ftp 서버 바이너리를 담기에 충분한 공간,즉 약 8메가 정도 와 ftp 를 통해 전송될 파일들의 용량을 더한 만큼의 공간을 가진 마운트된 파일시스템이어야 합니다.

이 테크팁에서 설명하고 있는 고립된 FTP 환경을 위한 전체 파일 리스트를 보여주고 있는 보조자료는 다음 링크에서 찾으실 수 있습니다: http://wikis.sun.com/display/BigAdmin/Building+a+Secure+FTP+Server+-+File+List

<ftp jail root> (/ftpjail) 환경 만들기

<ftp jail root> (/ftpjail) 디렉토리 아래에 심볼릭 링크와 디렉토리들을 생성합니다.

mkdir -p /ftpjail
cd
/ftpjail
mkdir
-p dev etc etc/ftpd etc/default usr/bin usr/sbin usr/lib/security usr/lib/locale usr/lib/security/sparcv9 usr/lib usr/share/lib/zoneinfo upload
chmod
100 usr/sbin
chmod
444 dev etc/default usr/share usr/share/lib usr/share/lib/zoneinfo
chmod
555 etc etc/ftpd usr usr/bin usr/lib usr/lib/locale usr/lib/security
chmod
777 upload
Create ln -s usr/bin bin


고립 환경에 필요한 특수 디바이스 파일들을 생성하기

디바이스 심볼링 링크의 목록을 통해서 현재 사용하고 있는 특수 디바이스 파일들과 메이저, 마이너 넘버를 찾습니다.

cd /dev
ls
-l conslog null tcp ticlts ticotsord udp zero
lrwxrwxrwx  
1 root     other         31 Aug  8 12:36 conslog -> ../devices/pseudo/log@0:conslog
lrwxrwxrwx  
1 root     other         27 Aug  8 12:36 null -> ../devices/pseudo/mm@0:null
lrwxrwxrwx  
1 root     other         27 Aug  8 12:36 tcp -> ../devices/pseudo/tcp@0:tcp
lrwxrwxrwx  
1 root     other         29 Aug  8 12:36 ticlts -> ../devices/pseudo/tl@0:ticlts
lrwxrwxrwx  
1 root     other         32 Aug  8 12:36 ticotsord -> ../devices/pseudo/tl@0:ticotsord
lrwxrwxrwx  
1 root     other         27 Aug  8 12:36 udp -> ../devices/pseudo/udp@0:udp
lrwxrwxrwx  
1 root     other         27 Aug  8 12:36 zero -> ../devices/pseudo/mm@0:zero


이제 실제 디바이스 파일을 찾습니다.

cd ../devices/pseudo
ls
-l log@0:conslog mm@0:null tcp@0:tcp tl@0:ticlts tl@0:ticotsord udp@0:udp mm@0:zero
crw
-rw-rw-   1 root     sys       21,  0 Aug  8 12:36 log@0:conslog
crw
-rw-rw-   1 root     sys       13,  2 Sep 25 11:47 mm@0:null
crw
-rw-rw-   1 root     sys       13, 12 Aug  8 12:36 mm@0:zero
crw
-rw-rw-   1 root     sys       42,  0 Aug  8 12:36 tcp@0:tcp
crw
-rw-rw-   1 root     sys      105,  2 Aug  8 12:36 tl@0:ticlts
crw
-rw-rw-   1 root     sys      105,  1 Aug  8 12:36 tl@0:ticotsord
crw
-rw-rw-   1 root     sys       41,  0 Aug  8 12:36 udp@0:udp


위에서 찾은 메이저, 마이너 넘버를 이용하여 새로운 디바이스 파일들을 고립된 환경에 생성합니다.

cd /dev
ls
-l conslog null tcp ticlts ticotsord udp zero
lrwxrwxrwx  
1 root     other         31 Aug  8 12:36 conslog -> ../devices/pseudo/log@0:conslog
lrwxrwxrwx  
1 root     other         27 Aug  8 12:36 null -> ../devices/pseudo/mm@0:null
lrwxrwxrwx  
1 root     other         27 Aug  8 12:36 tcp -> ../devices/pseudo/tcp@0:tcp
lrwxrwxrwx  
1 root     other         29 Aug  8 12:36 ticlts -> ../devices/pseudo/tl@0:ticlts
lrwxrwxrwx  
1 root     other         32 Aug  8 12:36 ticotsord -> ../devices/pseudo/tl@0:ticotsord
lrwxrwxrwx  
1 root     other         27 Aug  8 12:36 udp -> ../devices/pseudo/udp@0:udp
lrwxrwxrwx  
1 root     other         27 Aug  8 12:36 zero -> ../devices/pseudo/mm@0:zero


그 다음으로 실제 디바이스 파일들의 목록을 살펴 봅니다.

cd ../devices/pseudo
ls
-l log@0:conslog mm@0:null tcp@0:tcp tl@0:ticlts tl@0:ticotsord udp@0:udp mm@0:zero
crw
-rw-rw-   1 root     sys       21,  0 Aug  8 12:36 log@0:conslog
crw
-rw-rw-   1 root     sys       13,  2 Sep 25 11:47 mm@0:null
crw
-rw-rw-   1 root     sys       13, 12 Aug  8 12:36 mm@0:zero
crw
-rw-rw-   1 root     sys       42,  0 Aug  8 12:36 tcp@0:tcp
crw
-rw-rw-   1 root     sys      105,  2 Aug  8 12:36 tl@0:ticlts
crw
-rw-rw-   1 root     sys      105,  1 Aug  8 12:36 tl@0:ticotsord
crw
-rw-rw-   1 root     sys       41,  0 Aug  8 12:36 udp@0:udp


위에서 찾은 메이저, 마이너 넘버를 이용하여 새로운 디바이스 파일들을 생성합니다.

cd  /ftpjail/dev
mknod conslog c
21 0
mknod
null c 13 2
mknod zero c
13 12
mknod tcp c
42 0
mknod ticlts c
105 2
mknod ticotsord c
105 1
mknod udp c
41 0
chmod
666 conslog null tcp ticlts ticotsord udp zero


작업한 결과를 확인 합니다.

ls -l
crw
-rw-rw-   1 root     other     21,  0 Sep 25 11:57 conslog
crw
-rw-rw-   1 root     other     13,  2 Sep 25 11:57 null
crw
-rw-rw-   1 root     other     42,  0 Sep 25 11:57 tcp
crw
-rw-rw-   1 root     other    105,  2 Sep 25 11:57 ticlts
crw
-rw-rw-   1 root     other    105,  1 Sep 25 11:57 ticotsord
crw
-rw-rw-   1 root     other     41,  0 Sep 25 11:57 udp
crw
-rw-rw-   1 root     other     13, 12 Sep 25 11:57 zero


고립된 ftp 환경에서 설정 파일들을 생성합니다
vi 같은 에디터를 사용해서 다음의 파일들을 생성합니다.

/ftpjail/etc/group

other::1:root
ftp
::30000:

/ftpjail/etc/pam.conf

ftp auth required /usr/lib/security/pam_unix.so.1
ftp account required
/usr/lib/security/pam_unix.so.1
ftp session required
/usr/lib/security/pam_unix.so.1


/ftpjail/etc/passwd
참고: 아래의 gftp 를 ftp 서버를 이용해 로그인할 유저 이름으로 변경합니다. 여러분은 ftp 서버를 로그인할때 사용할 다른 유저들을 이 파일에 추가하실 수 있습니다.

root:x:0:1:::
ftp
:x:30000:30000::/upload:/bin/false
gftp
:x:30000:30000::/upload:/bin/sh





/ftpjail/etc/shadow
참고: 아래의 gftp 를 ftp 서버를 이용해 로그인할 유저 이름으로 변경하고, $$ 를 유저가 사용할 패스워드로 변경 합니다.
암호화된 패스워드 생성을 위해서, 패스워드 설비를 이용해서 여러분이 패스워드를 알고 있는 호스트 유저의 패스워드를 변경 합니다. 그다음에 암호화된 패스워드를 /etc/shadow 에서 복사해 옵니다. passwd 유틸리티를 이용해서 패스워드를 다시 변경해야 함을 기억하시기 바랍니다. 이 파일에 있는 유저들은 반드시 var/ftpjail/etc/passwd 에 있는 유저들과 매치 되어야 합니다.
root:*LK*:6445::::::
ftp
:*LK*:13651::::::
gftp
:$$:13651::::::


/ftpjail/etc/shells

/bin/sh


/ftpjail/etc/ftpd/ftpaccess
hostname ftpserver
defaultserver
private
class   all   real,guest,anonymous  *
# all the following default to "yes" for everybody
delete          no      real,guest,anonymous
overwrite       no      real
,guest,anonymous
rename          no      real
,guest,anonymous
chmod           no      real
,guest,anonymous
umask           no      real
,guest,anonymous
# specify the upload directory information
upload  
/       *       no
upload  
/       /upload yes
greeting terse
noretrieve
*
#allow-retrieve /upload/*
defumask
777


/ftpjail/usr/bin/runme
/usr/sbin/in.ftpd -P 2020 -p 2021 -S -u 022 -W -a -Q


새롭게 생성된 파일의 퍼미션 정정하기

cd /ftpjail/etc
chmod
444 group pam.conf passwd shadow shells /ftpjail/etc/ftpd/ftpaccess
chmod
100 /ftpjail/usr/bin/runme


고립된 ftp 환경으로 현재 파일들을 복사하고, 퍼미션 정정하기

cp -p /etc/default/init /ftpjail/etc/default/init
cp
/usr/bin/sh /ftpjail/usr/bin/sh; chmod 111 /ftpjail/usr/bin/sh
cp
/usr/sbin/in.ftpd /ftpjail/usr/sbin/in.ftpd; chmod 6100 /ftpjail/usr/sbin/in.ftpd; chown 30000:30000 /ftpjail/usr/sbin/in.ftpd
cp
-rp /usr/lib/locale/* /ftpjail/usr/lib/locale
cp -rp /usr/share/lib/zoneinfo/*  /ftpjail/usr/share/lib/zoneinfo
cd /ftpjail/usr/lib
cp -p /usr/lib/libbsm.so.1 .
cp -p /usr/lib/libc.so.1 .
cp -p /usr/lib/libcmd.so.1 .
cp -p /usr/lib/libdl.so.1 .
cp -p /usr/lib/libgen.so.1 .
cp -p /usr/lib/libmd5.so.1 .
cp -p /usr/lib/libmp.so.2 .
cp -p /usr/lib/libnsl.so.1 .
cp -p /usr/lib/libpam.so.1 .
cp -p /usr/lib/libresolv.so.2 .
cp -p /usr/lib/libsecdb.so.1 .
cp -p /usr/lib/libsocket.so.1 .
cp -p /usr/lib/ld.so.1 .
cp -p /usr/lib/nss_user.so.1 .
cp -p /usr/lib/nss_files.so.1 .
chmod 555 *
cd /ftpjail/usr/lib/security
cp -p /usr/lib/security/crypt_bsdbf.so.1 .
cp -p /usr/lib/security/crypt_bsdmd5.so.1 .
cp -p /usr/lib/security/crypt_sunmd5.so.1 .
cp -p /usr/lib/security/pam* .
cd /ftpjail/usr/lib/security/sparcv9
cp -p /usr/lib/security/sparcv9/* .
 
새로운 환경을 살펴 보기 위한 초기 테스트

한가지 빼먹은것이 있는데 만약 유저가 ls 커맨드를 사용할 수 있도록 하길 원한다면 해당 파일을 환경으로 복사해야 합니다. 필자는 파일을 테스트 후에 지울 것을 추천합니다.

cp /usr/bin/ls /ftpjail/usr/bin/ls; chmod 111 /ftpjail/usr/bin/ls


고립 환경에서 sh 쉘 스크립트를 실행합니다.
참고: 환경안에서는 오직 제한된 커맨드들만이 존재 합니다. 그러나 ls 와 cd 같은 명령은 사용이 가능합니다.

chroot /ftpjail /usr/bin/sh

ftp 서버 시작하기

참고: ftpd 프로세스가 고립된 환경에서 생성되고 실행될 것이고 런타임 커맨드에서 지정된 포트를 리스닝하게 될 것입니다.

chroot /ftpjail /usr/bin/sh -c runme


서버가 시작되었는지 확인합니다.
참고: 필자의 /etc/password 파일에는 UID 30000 에 대한 항목이 없으므로 아래의 결과는 오직 숫자로 출력 됩니다.

ps -ef|grep ftpd
0030000 26704     1  0 09:04:30 ?        0:00 /usr/sbin/in.ftpd -P 2020 -p 2021 -S -u 022 -W -a -Q

ftp 서버 테스트하기

참고:/ftpjail/etc/ftpd/ftpaccess 에 설정된대로 여러분은 오직 파일의 업로드만 가능합니다.

ftp 127.0.0.1 2021
login gftp
/<as set in /ftpjail/etc/shadow>


저자에 관하여
Ross Moffatt 은 유닉스 시스템 관리자로 10년 이상 일해 왔고
ross.stuff@telstra.com 로 연락하실 수 있습니다.

이 글의 영문 원본은
Building a Secure FTP Server
에서 보실 수 있습니다.

"관리자코너" 카테고리의 다른 글

Posted by 1010
90.개발관련문서2008. 11. 12. 17:32
반응형

기고해주신 분

페이게이트 이동산 이사 (자바기반 지불 솔루션 업체)
mountie@paygate.net


1. 개요

SAML은 웹 브라우저에서의 SSO문제를 해결하기 위해서 OASIS의 연구결과로 탄생하였다.
인트라넷 레벨에서의 SSO는 다양한 방식들이 이용되어왔고 구현에 있어서도 크게 문제될요소는 적다.
예를 들어 Cookie 기반의 SSO, LDAP기반의 SSO, 인증서 기반의 SSO등이 있다.

그러나 도메인간의 SSO 구현을 위한 방식은 통제할 수 없는 외부 환경을 포함하므로 통일된 하나의 표준방식이 필요하게 되었고
SAML은 이러한 도메인간의 SSO구현을 가능하게하는 XML 표준이다.

사용되는 용어에 대해서 알아보자.
SAML : Security Asserting Markup Language, http://en.wikipedia.org/wiki/SAML 참조
SSO : Single Sign On 하나의 일관된 인증방식으로 여러 서비스에 로그온할 수 있는 방법
ACS : Assertion Consumer Service, Identity Provider에 의하여 인증된 사용자에 대한 정보가 담긴 SAML response정보를 verify 하고 서비스를 제공할 수 있도록 포워딩 한다

SAML에는 3가지 중요한 구성원이 존재한다.
  • Service Provider : 서비스를 제공하는 주체
  • 유저 : 서비스를 이용하는 사용자
  • Identify Provider : 유저에 대한 인증을 담당하는 주체

SAML의 특징은 Cross domain상황에서 표준화된 방식으로 SSO를 구현할 수 있으면서
Platform에 관계없이 다양한 환경에서 표준적인 방법으로 SSO 구현이 가능하다는 것이다.
실제 다양한 iPhone등 다양한 플랫폼에서 테스트해본 결과 잘 동작한다.


2. SAML Basic Steps

아래 그림은 Wikipedia에서 제시한 흐름도이다.



Google에서도 아래 그림을 제시하고 있다.



각 단계별 과정을 설명하면 아래와 같다.
  • 1단계 : 유저는 서비스 제공자(Service Provider)에게 접근한다. (서비스 이용을 위하여)
  • 2단계 : 서비스 제공자는 SAMLRequest를 생성한다. 생성된 SAMLRequest은 XML format의 텍스트로 구성된다.
  • 3단계 : 유저의 브라우저를 이용하여 인증 제공자(Identity Provider)로 Redirect 한다.
  • 4단계 : 인증 제공자(Identity Provider)는 SAMLRequest를 파싱하고 유저인증을 진행한다.
  • 5단계 : 인증제공자(Identity Provider)는 SAML Response를 생성한다.
  • 6단계 : 인증제공자(Identity Provider)는 유저 브라우저를 이용하여 SAMLResponse data를 ACS로 전달한다.
  • 7단계 : ACS는 Service Provider가 운영하게 되는데 SAMLResponse를 확인하고 실제 서비스 사이트로 유저를 Forwarding한다.
  • 8단계 : 유저는 로그인에 성공하고 서비스를 제공받는다.

위 단계중 SAMLResponse가 중요한 역할을 하는데 Identity Provider로서 SAMLResponse에 대하여 전자서명하고 ACS가 전자서명을 검증하여 유효한 Response인지를 확신할 수 있게 된다.

Identity Provider는 자체적인 다양한 방식으로 유저인증을 진행할 수 있으며 서비스 제공자는 Identity Provider를 신뢰하여 인증의 전권을 Identity Provider에 의존하게 되어 Identity Provider의 신뢰 및 책임부분이 중요한 요소이다.


3. SAML SSO 실제 구현

SAML SSO를 실제 구현해가면서 어떻게 동작하는지 살펴보자.


3.1 사전 준비

구현은 Service Provider로서의 단계와 Identity Provider로서의 단계를 모두 포함하여 Full Cycle을 순환할 수 있도록 하지만
실제로는 대부분 Service Provider나 Identity Provider의 역할중 어느 한 역할을 주로 하게 될것이다.


필요한 핵심 Library는 아래와 같다.


OpenSAML 2.0 Library

기타 apache commons의 유용한 Library들을 필요에 맞게 사용한다.


RSA Keypair를 준비한다.

openssl을 이용한 keypair 생성 command
$ openssl genrsa -out sso_private.pem 1024
$ openssl rsa -in sso_private.pem -pubout -ourform DER -out sso_public.der
$ openssl pkcs8 -topk8 -inform PEM -outform DER -in sso_private.pem -out sso_private.der -nocrypt

생성된 2개의 파일은 sso_private.der, sso_public.der이다.

한가지 주의할점은 sso_public.der은 우리가 통상적으로 알고 있는 인증서(certificate)가 아니다.
certifificate는 인증기관으로부터 public key의 유효성을 인정받은 경우에 인증기관의 전자서명이 첨부되어 발행되는데
여기서는 original rsa public key만으로 SSO를 구현한다.

대신 신뢰성을 확보하기 위하여 identity provider의 public key는 신뢰성 있는 방식으로 service provider에게 전달되어야하며
이 전달과정이나 identity provider의 private key 관리에는 주의를 기울여야한다.

SAML에서 이용하는 xmldsig spec이나 구현 사례를 봐도 인증서를 탑재하여 SAML SSO를 분명히 진행할 수 있다.
이 경우에는 identity provider의 public key를 특별한 방법으로 전달할 필요 없이
service provider는 신뢰할 수 있는 CA(Certificate Authority)가 보장하는 전자인증서를 확보하고 인증서 CRL 또는 OCSP checking등을 추가할 수 있을것이다.


3.2 SAMLRequest 생성 및 redirect

이 단계에서는 유저가 서비스 제공자의 사이트에 접속하고 SAML SSO 로그인을 진행하기 위해 SAMLRequest 생성 및 redirect의 과정을 설명한다.


3.2.1 ServiceProviderForm : 실제 유저가 최초로 접근하는 페이지이다. 여기서부터 시작

<form name="ServiceProviderForm" action="https://api.paygate.net/t/sso/saml/CreateRequestServlet.jsp" method="post">

  <input type="hidden" name="loginForm" value="https://api.paygate.net/t/sso/saml/login_form.jsp" />

  <input type="hideen" name="providerName" value="paygate.net" />

  <input type="hidden" name="RelayState" value="https://api.paygate.net/t/sso/saml/result_view.jsp" />

  <input type="hidden" name="acsURI" value="https://api.paygate.net/t/sso/saml/ACS.jsp" />

  <input type="submit" value="Sign On">

</form>

   * loginForm : Identity Provider에 위치하는 인증을 위한 로그인 폼
   * providerName : 서비스를 제공하는 Service Provider Name
   * RelayState : ACS에서 인증을 마친후 최종적으로 Redirecting하게 되는 서비스 제공 페이지 URL
   * acsURI : Identity Provider가 보낸 SAMLResponse를 검증(Verify)하고 실제 서비스 제공사이트로 Forwarding하게 되는 URL


3.2.2 CreateRequestServlet : SAMLRequst 데이터 생성 단계

  • 먼저 Parameter를 받고
    String ssoURL = request.getParameter("loginForm");
    String providerName = request.getParameter("providerName");
    String RelayState = request.getParameter("RelayState");
    String acsURI = request.getParameter("acsURI");

   public void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        String ssoURL = request.getParameter("loginForm");
        String providerName = request.getParameter("providerName");
        String RelayState = request.getParameter("RelayState");
        String acsURI = request.getParameter("acsURI");
        .....
    }

  • SAMLRequest xml data를 생성
    String SAMLRequest = createAuthnRequest(acsURI, providerName);
    private String createAuthnRequest(String acsURL, String providerName)
            throws SamlException {
        String filepath = getServletContext().getRealPath("t/sso/saml/templates/" + SAML_REQUEST_TEMPLATE);
        String authnRequest = Util.readFileContents(filepath);
        authnRequest = StringUtils.replace(authnRequest, "##PROVIDER_NAME##", providerName);
        authnRequest = StringUtils.replace(authnRequest, "##ACS_URL##", acsURL);
        authnRequest = StringUtils.replace(authnRequest, "##AUTHN_ID##", Util.createID());
        authnRequest = StringUtils.replace(authnRequest, "##ISSUE_INSTANT##", Util.getDateAndTime());
        return authnRequest;
    }

  • Identity Provider로 redirect할 URL생성
    String redirectURL = computeURL(ssoURL, SAMLRequest, RelayState);
    private String computeURL(String ssoURL, String authnRequest,
            String RelayState) throws SamlException {
        StringBuffer buf = new StringBuffer();
        try {
            buf.append(ssoURL);

            buf.append("?SAMLRequest=");
            buf.append(RequestUtil.encodeMessage(authnRequest));

            buf.append("&RelayState=");
            buf.append(URLEncoder.encode(RelayState));
            return buf.toString();
        } catch (UnsupportedEncodingException e) {
            throw new SamlException(
                    "Error encoding SAML Request into URL: Check encoding scheme - "
                            + e.getMessage());
        } catch (IOException e) {
            throw new SamlException(
                    "Error encoding SAML Request into URL: Check encoding scheme - "
                            + e.getMessage());
        }
    }

  • CreateRequestServlet.java 구현 종합
/*
 * CreateRequestServlet
 */
public class CreateRequestServlet extends HttpServlet {

    private static final String SAML_REQUEST_TEMPLATE = "AuthnRequestTemplate.xml";

    public void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {

        String returnPage = "service_proc.jsp"; // 실제 forwarding수행할 JSP
        response.setHeader("Content-Type", "text/html; charset=UTF-8");
        response.setContentType("text/html; charset=UTF-8");
       
        // get PARAMETERS
        String ssoURL = request.getParameter("loginForm");
        String providerName = request.getParameter("providerName");
        String RelayState = request.getParameter("RelayState");
        String acsURI = request.getParameter("acsURI");

        String SAMLRequest;
        String redirectURL;

        try {
            // create SAMLRequest
            SAMLRequest = createAuthnRequest(acsURI, providerName);
            request.setAttribute("authnRequest", SAMLRequest);

            // compute URL to forward AuthnRequest to the Identity Provider
            redirectURL = computeURL(ssoURL, SAMLRequest, RelayState);
            request.setAttribute("redirectURL", redirectURL);

        } catch (SamlException e) {
            request.setAttribute("error", e.getMessage());
        }

        request.getRequestDispatcher(returnPage).include(request, response);
    }
}

  • AuthnRequestTemplate.xml 참조
<?xml version="1.0" encoding="UTF-8"?>
<samlp:AuthnRequest
    xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
    ID="##AUTHN_ID##"
    Version="2.0"
    IssueInstant="##ISSUE_INSTANT##"
    ProtocolBinding="urn:oasis:names.tc:SAML:2.0:bindings:HTTP-Redirect"
    ProviderName="##PROVIDER_NAME##"
    AssertionConsumerServiceURL="##ACS_URL##"/>


3.2.3 service_proc.jsp : Identity Provider로 SAMLRequest를 forward

<%@page import="net.paygate.saml.util.RequestUtil"%>
<%@page import="java.net.*"%>
<%
      String error = (String) request.getAttribute("error");
      String authnRequest = (String) request.getAttribute("authnRequest");
      String redirectURL = (String) request.getAttribute("redirectURL");
%>
<html><head> <meta http-equiv="content-type" content="text/html; charset=UTF-8"><title>SAML-based Single Sign-On Service </title></head>
<%
    if (error != null) {
%>
        <body><center><font color="red"><b><%= error %></b></font></center><p>
<%
    } else {
        if (authnRequest != null && redirectURL != null) {       
%>
        <body onload="document.location = '<%=redirectURL%>';return true;">
          <h1 style="margin-bottom:6px">Submitting login request to Identity provider</h1>
     <%
       } else {
       %>
       <body>
          <center><font color="red"><b>no SAMLRequest or redirectURL</b></font></center><p>
          <%
       }
     }
     %>
</body></html>


3.3 SAMLRequest를 받고 유저인증 진행 단계

이 단계는 Identity Provider에서 진행하게 된다.
유저인증은 각 Identity Provider별로 다양한 방법을 취할 수 있고 여기서는 LDAP 인증을 사용하고 있다.


3.3.1 login_form.jsp : 기본적인 유저 인증 정보를 입력받는다.

  • Parameter를 받고
    String SAMLRequest = request.getParameter("SAMLRequest");
    String RelayState = request.getParameter("RelayState");

  • SAMLRequest를 Parsing한다.
    String requestXmlString = ProcessResponseServlet.decodeAuthnRequestXML(SAMLRequest);
    String[] samlRequestAttributes = ProcessResponseServlet.getRequestAttributes(requestXmlString);
       
  • 사용자 이름과 비밀번호를 입력받고
    <input type="text" name="username" id="username" size="18">
    <input type="password" name="password" id="password" size="18">

  • IdentityProviderForm을 생성한다.
    <form name="IdentityProviderForm" action="....." method="post">
    ....
    </form>

  • login_form.jsp 구현 종합
<%
    String SAMLRequest = request.getParameter("SAMLRequest");
    String RelayState = request.getParameter("RelayState");
    String ServiceProvider = "";
    if (SAMLRequest == null || SAMLRequest.equals("null")) {
        ServiceProvider = "";
    } else {
        String requestXmlString = ProcessResponseServlet.decodeAuthnRequestXML(SAMLRequest);
        String[] samlRequestAttributes = ProcessResponseServlet.getRequestAttributes(requestXmlString);
        String issueInstant = samlRequestAttributes[0];
        ServiceProvider = samlRequestAttributes[1];
        String acsURL = samlRequestAttributes[2];
    }
%>

<html><head><title>SSO Login Page</title></head>
<body>
<h1><%=ServiceProvider%> Service Login</h1>
<form name="IdentityProviderForm" action="https://api.paygate.net/t/sso/saml/ProcessResponseServlet.jsp" method="post">
      <input type="hidden" name="SAMLRequest" value="<%=SAMLRequest%>"/>
      <input type="hidden" name="RelayState" value="<%=RelayState%>"/>
      <input type="hidden" name="returnPage" value="./login_proc.jsp">
username : <input type="text" name="username" id="username" size="18">
<br>
password :
<input type="password" name="password" id="password" size="18"><br>
<input type="submit" value="로그인">
</form>
</body></html>


3.4 유저 확인후 SAMLResponse 생성


3.4.1 ProcessResponseServlet

  • login_form.jsp에서 parameter를 받는다.
    String SAMLRequest = request.getParameter("SAMLRequest");
    String returnPage = request.getParameter("returnPage");
    String username = request.getParameter("username");
    String password = request.getParameter("password");
    String RelayState = request.getParameter("RelayState");

    public void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        String SAMLRequest = request.getParameter("SAMLRequest");
        String returnPage = request.getParameter("returnPage");
        String username = request.getParameter("username");
        String password = request.getParameter("password");
        String RelayState = request.getParameter("RelayState");
        ...
    }

  • SAMLRequest를 parsing한다.
    String requestXmlString = decodeAuthnRequestXML(SAMLRequest);

    public static String decodeAuthnRequestXML(String encodedRequestXmlString) throws SamlException {
        try {
            Base64 base64Decoder = new Base64();
            byte[] xmlBytes = encodedRequestXmlString.getBytes("UTF-8");
            byte[] base64DecodedByteArray = base64Decoder.decode(xmlBytes);
            try {

                Inflater inflater = new Inflater(true);
                inflater.setInput(base64DecodedByteArray);
                byte[] xmlMessageBytes = new byte[5000];
                int resultLength = inflater.inflate(xmlMessageBytes);

                if (!inflater.finished()) {
                    throw new RuntimeException("didn't allocate enough space to hold decompressed data");
                }

                inflater.end();
                return new String(xmlMessageBytes, 0, resultLength, "UTF-8");

            } catch (DataFormatException e) {

                ByteArrayInputStream bais = new ByteArrayInputStream(base64DecodedByteArray);
                ByteArrayOutputStream baos = new ByteArrayOutputStream();
                InflaterInputStream iis = new InflaterInputStream(bais);
                byte[] buf = new byte[1024];
                int count = iis.read(buf);
                while (count != -1) {
                    baos.write(buf, 0, count);
                    count = iis.read(buf);
                }
                iis.close();

                return new String(baos.toByteArray());
            }

        } catch (UnsupportedEncodingException e) {
            throw new SamlException("Error decoding AuthnRequest: Check decoding scheme - " + e.getMessage());
        } catch (IOException e) {
            throw new SamlException("Error decoding AuthnRequest: Check decoding scheme - " + e.getMessage());
        }
    }


  • SAMLRequest에서 Attribute을 발췌한다.
    String[] samlRequestAttributes = getRequestAttributes(requestXmlString);
    String issueInstant = samlRequestAttributes[0];
    String providerName = samlRequestAttributes[1];
    String acsURL = samlRequestAttributes[2];

    public static String[] getRequestAttributes(String xmlString) throws SamlException {
        Document doc = Util.createJdomDoc(xmlString);
        if (doc != null) {
            String[] samlRequestAttributes = new String[3];
            samlRequestAttributes[0] = doc.getRootElement().getAttributeValue("IssueInstant");
            samlRequestAttributes[1] = doc.getRootElement().getAttributeValue("ProviderName");
            samlRequestAttributes[2] = doc.getRootElement().getAttributeValue("AssertionConsumerServiceURL");
            return samlRequestAttributes;
        } else {
            throw new SamlException("Error parsing AuthnRequest XML: Null document");
        }
    }

  • User 인증을 진행한다.
    boolean isValiduser = login(username, password);

    private boolean login(String username, String password) {
        LdapLoginHandler ldaplh = new LdapLoginHandler();

        if (password.length() < 1) return false;
        if (ldaplh.isValidOfficeUser(username, password)) {
            return true;
        } else {
            return false;
        }
    }
    * 실제 인증은 Identity Provider의 사정에 맞게 다양한 방식으로 진행한다.

  • RSA Keypair loading
    String publicKeyFilePath = keysDIR + "paygate_public.der";
    String privateKeyFilePath = keysDIR + "paygate_private.der";
    RSAPrivateKey privateKey = (RSAPrivateKey) Util.getPrivateKey(privateKeyFilePath, "RSA");
    RSAPublicKey publicKey = (RSAPublicKey) Util.getPublicKey(publicKeyFilePath, "RSA");

  • SAMLResponse의 유효기간정보 설정, 현시점부터 24시간 유효하게 설정함
    long now = System.currentTimeMillis();
    long nowafter = now + 1000*60*60*24;
    long before = now - 1000*60*60*24;

    SimpleDateFormat dateFormat1 = new SimpleDateFormat("yyyy-MM-dd'T'hh:mm:ss'Z'");
    java.util.Date pTime = new java.util.Date(now);
    String notBefore = dateFormat1.format(pTime);
    java.util.Date aTime = new java.util.Date(nowafter);
    String notOnOrAfter = dateFormat1.format(aTime);

    request.setAttribute("notBefore", notBefore);
    request.setAttribute("notOnOrAfter", notOnOrAfter);

  • 인증을 거친 로그인 유저네임과 유효기간을 포함한 전자서명전의 SAMLResponse XML data생성
    String responseXmlString = createSamlResponse(username, notBefore, notOnOrAfter);

    private String createSamlResponse(
            String authenticatedUser,
            String notBefore,
            String notOnOrAfter) throws SamlException {
        String filepath = getServletContext().getRealPath("t/sso/saml/templates/" + samlResponseTemplateFile);
        String samlResponse = Util.readFileContents(filepath);
        samlResponse = StringUtils.replace(samlResponse, "##USERNAME_STRING##", authenticatedUser);
        samlResponse = StringUtils.replace(samlResponse, "##RESPONSE_ID##", Util.createID());
        samlResponse = StringUtils.replace(samlResponse, "##ISSUE_INSTANT##", Util.getDateAndTime());
        samlResponse = StringUtils.replace(samlResponse, "##AUTHN_INSTANT##", Util.getDateAndTime());
        samlResponse = StringUtils.replace(samlResponse, "##NOT_BEFORE##", notBefore);
        samlResponse = StringUtils.replace(samlResponse, "##NOT_ON_OR_AFTER##", notOnOrAfter);
        samlResponse = StringUtils.replace(samlResponse, "##ASSERTION_ID##", Util.createID());
        return samlResponse;
    }
    * createID()는 UniqueID를 생성하는 함수임.
    * getDateAndTime()은 SAML date format에 맞게 날짜시간정보를 생성하는 함수임.
    * SAML date format : yyyy-MM-ddThh:mm:ssZ (예: 2008-01-30T23:05:23Z)
    * 시간정보는 Localtime이 아닌 UTC에 맞춰줘야함

  • SAMLResponse에 대하여 전자서명
     String signedSamlResponse = SAMLSigner.signXML(responseXmlString, privateKey, publicKey);
    * 이때 publicKey는 SAMLResponse message에 포함되지만 데이터 암호화에는 사용되지 않는다.
    * xmldsig.jar, xmlsec.jar가 필요함

  • 서명된 SAMLResponse를 포함하여 ACS로 forwarding한다.
    request.setAttribute("samlResponse", signedSamlResponse);
    request.getRequestDispatcher(returnPage).include(request, response);

  • ProcessResponseServlet.java 구현 종합
/*
 * ProcessResponseServlet
 */

public class ProcessResponseServlet extends HttpServlet {

    private final String keysDIR = System.getProperty("PGV3_HOME")
            + SystemUtils.FILE_SEPARATOR + "SSO"
            + SystemUtils.FILE_SEPARATOR + "keys" + SystemUtils.FILE_SEPARATOR;

    private final String samlResponseTemplateFile = "SamlResponseTemplate.xml";
    private static final String domainName = "paygate.net";

    public void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        String SAMLRequest = request.getParameter("SAMLRequest");
        String returnPage = request.getParameter("returnPage");
        String username = request.getParameter("username");
        String password = request.getParameter("password");
        String RelayState = request.getParameter("RelayState");
       
        boolean continueLogin = true;

        if (SAMLRequest == null || SAMLRequest.equals("null")) {
            continueLogin = false;
            request.setAttribute("error","ERROR: Unspecified SAML parameters.");
            request.setAttribute("authstatus","FAIL");
           
        } else if (returnPage != null) {
            try {
               
                String requestXmlString = decodeAuthnRequestXML(SAMLRequest);
                String[] samlRequestAttributes = getRequestAttributes(requestXmlString);
                String issueInstant = samlRequestAttributes[0];
                String providerName = samlRequestAttributes[1];
                String acsURL = samlRequestAttributes[2];

                boolean isValiduser = login(username, password); // 유저인증
               
                if (!isValiduser) {
                    request.setAttribute("error", "Login Failed: Invalid user.");
                    request.setAttribute("authstatus","FAIL");
                } else {
                    request.setAttribute("issueInstant", issueInstant);
                    request.setAttribute("providerName", providerName);
                    request.setAttribute("acsURL", acsURL);
                    request.setAttribute("domainName", domainName);
                    request.setAttribute("username", username);
                    request.setAttribute("RelayState", RelayState);

                    String publicKeyFilePath = keysDIR + "paygate_public.der";
                    String privateKeyFilePath = keysDIR + "paygate_private.der";
                    RSAPrivateKey privateKey = (RSAPrivateKey) Util.getPrivateKey(privateKeyFilePath, "RSA");
                    RSAPublicKey publicKey = (RSAPublicKey) Util.getPublicKey(publicKeyFilePath, "RSA");

                    long now = System.currentTimeMillis();
                    long nowafter = now + 1000*60*60*24;
                    long before = now - 1000*60*60*24;
                   
                    SimpleDateFormat dateFormat1 = new SimpleDateFormat("yyyy-MM-dd'T'hh:mm:ss'Z'");
                    java.util.Date pTime = new java.util.Date(now);
                    String notBefore = dateFormat1.format(pTime);
                   
                    java.util.Date aTime = new java.util.Date(nowafter);
                    String notOnOrAfter = dateFormat1.format(aTime);
                   
                    request.setAttribute("notBefore", notBefore);
                    request.setAttribute("notOnOrAfter", notOnOrAfter);

                    if (!validSamlDateFormat(issueInstant)) {
                        continueLogin = false;
                        request.setAttribute("error", "ERROR: Invalid NotBefore date specified - " + notBefore);
                        request.setAttribute("authstatus","FAIL");
                    } else if (!validSamlDateFormat(notOnOrAfter)) {
                        continueLogin = false;
                        request.setAttribute("error", "ERROR: Invalid NotOnOrAfter date specified - " + notOnOrAfter);
                        request.setAttribute("authstatus","FAIL");
                    }

                    if (continueLogin) {
                        // 서명전의 SAMLResponse Message 생성
                       
String responseXmlString = createSamlResponse(username, notBefore, notOnOrAfter);
                        // SAMLResponse에 대한 전자서명
                        String signedSamlResponse = SAMLSigner.signXML(responseXmlString, privateKey, publicKey);
                        request.setAttribute("samlResponse", signedSamlResponse);
                        request.setAttribute("authstatus","SUCCESS");
                    } else {
                        request.setAttribute("authstatus","FAIL");
                    }
                }
            } catch (SamlException e) {
                request.setAttribute("error", e.getMessage());
                request.setAttribute("authstatus","FAIL");
            }
        }
        // Forward SAML response to ACS
        response.setContentType("text/html; charset=UTF-8");
        request.getRequestDispatcher(returnPage).include(request, response);
    }
}

  • SamlResponseTemplate.xml 참조
<samlp:Response ID="##RESPONSE_ID##" IssueInstant="##ISSUE_INSTANT##" Version="2.0"
    xmlns="urn:oasis:names:tc:SAML:2.0:assertion"
    xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
    xmlns:xenc="http://www.w3.org/2001/04/xmlenc#">
    <samlp:Status>
        <samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
    </samlp:Status>
    <Assertion ID="##ASSERTION_ID##"
        IssueInstant="2003-04-17T00:46:02Z" Version="2.0"
        xmlns="urn:oasis:names:tc:SAML:2.0:assertion">
        <Issuer>https://www.opensaml.org/IDP
        </Issuer>
        <Subject>
            <NameID
                Format="urn:oasis:names:tc:SAML:2.0:nameid-format:emailAddress">
                ##USERNAME_STRING##
            </NameID>
            <SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer"/>
        </Subject>
        <Conditions NotBefore="##NOT_BEFORE##"
            NotOnOrAfter="##NOT_ON_OR_AFTER##">
        </Conditions>
        <AuthnStatement AuthnInstant="##AUTHN_INSTANT##">
            <AuthnContext>
                <AuthnContextClassRef>
                    urn:oasis:names:tc:SAML:2.0:ac:classes:Password
                </AuthnContextClassRef>
            </AuthnContext>
        </AuthnStatement>
    </Assertion>
</samlp:Response>


3.5 SAMLResponse Message를 Service Provider의 ACS로 forward


3.5.1 login_proc.jsp : forwarding을 담당


<%@ page contentType="text/html; charset=UTF-8"%>
<%
    String acsURL = (String) request.getAttribute("acsURL");
    String samlResponse = (String) request.getAttribute("samlResponse");
    String RelayState = (String) request.getAttribute("RelayState");
    String authstatus = (String) request.getAttribute("authstatus");
    if (authstatus == null) authstatus = "FAIL";
    if (RelayState == null) RelayState = "";
%>
<html>
<head>
<title>forward to ACS</title>
</head>
<%
    if (samlResponse != null && authstatus.equals("SUCCESS")) {
%>
<body onload="javascript:document.acsForm.submit();">
    <form name="acsForm" action="<%=acsURL%>" method="post">
        <div style="display: none">
          <textarea rows=10 cols=80 name="SAMLResponse"><%=samlResponse%></textarea>
          <textarea rows=10 cols=80 name="RelayState"><%=RelayState%></textarea>
        </div>
    </form>
<%
    } else {
        %><script>alert('Login error'); history.back(-2); </script><%
    }
%>
</body>
</html>




3.6 Service Provider이 ACS에서 SAMLResponse를 verify

3.6.1 PublicACSServlet : Service Provider측의 ACS


  • Parameter를 받고
    String SAMLResponse = request.getParameter("SAMLResponse");
    String RelayState = request.getParameter("RelayState");

  • Identity Provider의 public Key를 load
    RSAPublicKey publicKey;
    publicKey = (RSAPublicKey) Util.getPublicKey(publicKeyFilePath,"RSA");

  • Identity Provider가 보내온 SAMLResponse를 서명검증(verify)
    boolean isVerified = SAMLVerifier.verifyXML(SAMLResponse, publicKey);

  • 검증을 거친이후 실제 서비스 사이트로 forward 요청
    request.getRequestDispatcher("./acs_proc.jsp").include(request, response);

  • PublicACSServlet.java 구현 종합
public class PublicACSServlet extends HttpServlet {
    private final String keysDIR = System.getProperty("PGV3_HOME")
            + SystemUtils.FILE_SEPARATOR + "CryptoServer"
            + SystemUtils.FILE_SEPARATOR + "keys" + SystemUtils.FILE_SEPARATOR;
   
    public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        doPost(request, response);
    }
   
    public void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        String SAMLResponse = request.getParameter("SAMLResponse");
        String RelayState = request.getParameter("RelayState");

        // acs knows public key only.
        String publicKeyFilePath = keysDIR + "paygate_public.der";

        RSAPublicKey publicKey;
        try {
            publicKey = (RSAPublicKey) Util.getPublicKey(publicKeyFilePath,"RSA");

            boolean isVerified = SAMLVerifier.verifyXML(SAMLResponse, publicKey);

            if (isVerified) {

                String loginid = null;
                Document doc = Util.createJdomDoc(SAMLResponse);

                Iterator itr = doc.getDescendants();

                itr = doc.getDescendants(new ElementFilter());
                while (itr.hasNext()) {
                    Content c = (Content) itr.next();
                    if (c instanceof Element) {
                        Element e = (Element) c;
                        System.out.println("Element:" + e.getName());
                       
                        if ("NameID".equals(e.getName())) {
                            loginid = e.getText().trim();
                            break;
                        }
                    }
                }
                request.setAttribute("mid", loginid);
                request.setAttribute("RelayState", RelayState);
               
                response.setContentType("text/html; charset=UTF-8");
                request.getRequestDispatcher("./acs_proc.jsp").include(request, response);
            } else {
                System.out.println("SAMLResponse is modified!!");
                return;
            }

        } catch (SamlException e) {
            e.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

3.7 완전하게 동작하는 WAR file

  • 화면상의 제약으로 인해 완전하게 동작하는 WAR File을 별도 준비하였다.
  • 파일이 필요한 분은 dev@paygate.net으로 메일요청하십시오.
  • 또한 아래 링크를 통해서 file download 위치를 확인할 수 있다.
    http://docs.google.com/Doc?id=dcxqxct2_4dsh8w4dt

4. SAML SSO 적용 실제 사이트


4.1 PayGate Admin Login

PayGate가 Service Provider 역할을 함.


4.1.1 최초 Service Login Page


  • 이 단계는 SAMLRequest를 생성하기전 서비스 제공자가 제시하는 화면이다.

4.1.2 Identity Provider로서 Login Form 제시


  • 이 화면이 보이기까지 서비스 제공자는 SAMLRequest를 생성하고
  • Identity Provider로 SAMLRequest를 redirect하면
  • 유저인증을 위해서 보여지는 화면이다.
  • 이 화면 이후 로그인 버튼을 누르게 되면 SAMLResponse 메세지를 생성하여 Service Provider의 ACS Site로 forwarding하게 된다.

4.1.3 Service Provider ACS verify를 거친후 마지막 페이지



  • Service Provider에 위치하는 ACS가 SAMLResponse를 verify한 이후 실제 서비스 제공 사이트로 forwarding한 결과이다.

4.2 Google Apps Service

  • google은 service provider역할을 하며
  • paygate는 identity provider역할을 한다.

4.2.1 gmail 서비스 로그인을 위한 인증 화면


  • google에서 SAMLRequest를 생성하여 identity provider인 페이게이트로 redirect하면
  • 페이게이트에서 유저 인증을 위하여 생성한 화면이다.


4.2.2 identity provider의 인증을 거친후 로그인된 화면



  • Identity Provider가 SAMLResponse를 생성하여
  • google acs로 forwarding하면 verify한 이후 실제 서비스 제공 사이트로 연결한다.


5. SAML의 확장


5.1 공인인증기관과 Identity Provider

SAML SSO는 국내에서는 공인인증기관이 Identity Provider역할을 한다면 아주 적당한 business model이 될것 같다.

공인인증기관의 신뢰성을 바탕으로 인증서 발행한 유저에 대한 확인을 직접 수행하고
Service Provider는 공인인증기관의 확인만을 검증하여 그 결과를 신뢰할 수 있다.

더 나아가서는 꼭 공인인증기관이 Identity Provider역할을 하지 않더라도
공인인증서 기반의 Public Identity Provider는 쉽게 예상할 수 있다.


5.2 Payment Gateway와 Identity Provider

결제대행사(Payment Gateway)사가 Identity Provider역할을 하고
쇼핑몰이 Service Provider가 되는 구조를 생각해볼 수 있다.

SAMLRequest는 충분히 확장가능한 구조이므로 Payment 요청에 필요한 필수정보 (상품명, 가격 등)을 포함하여
PG사에 대하여 Identity Provider로서 요청하고
PG사 사이트에서 안전하게 Payment를 처리하고 그 결과를 SAMLResponse format으로 돌려주는 구조이다.

이는 Cross Domain간에 Payment Protocol에 대한 표준이 부재한 현 상황에서
의미있게 시도해봄직한 목표이다.


6. 참고자료

SAML Single Sign On Service for Google Apps


 

OpenSAML


 

SAML at Wikipedia

"특별기고" 카테고리의 다른 글

Posted by 1010
98..Etc/JavaFX2008. 11. 12. 17:29
반응형

2007년 8월과 9월에 썬 개발자 네트워크의 John O'Conner는 JavaFX 스크립트 프로그래밍 언어(이 기사에서는 JavaFX 스크립트라고 줄여서 부름)를 시작하는 사용자에게 도움을 주고자 "학습 곡선 일지(Learning Curve Journal)"라는 제목의 시리즈를 기고했습니다.

 그 이후로 이 언어의 많은 중요한 부분이 개선되었습니다. 아마도 가장 중요한 변화는 JavaFX 스크립트의 초기 인터프리터 기반 버전을 대신하여 컴파일러 기반 버전을 사용할 수 있게 되었다는 점입니다. 학습 곡선 일지 시리즈의 1편, 2편 및 3편은 컴파일러 기반 버전의 언어 사용 방법을 보여주기 위해 업데이트 되었습니다. 최신 내용을 반영하여 다른 변경 사항도 적용되었습니다. 4편은 시리즈의 2편에서 시작된 JavaFX 이미지 검색 애플리케이션을 완결하는 새로운 부분입니다.

학습 곡선 일지의 앞 부분에서는 JavaFX 스크립트를 사용하여 Flickr에서 이미지를 검색하는 기존 이미지 검색 애플리케이션의 사용자 인터페이스(UI)를 재현했습니다. 결과 JavaFX 스크립트는 원래 UI와 완전히 똑같진 않지만 상당히 근접했습니다. 그림 1은 JavaFX 스크립트 구현으로 생성된 기본 UI를 보여줍니다.


그림 1. JavaFX 이미지 검색 애플리케이션 UI

 UI 구축에는 계층 구조적인 Swing 기반 접근 방법을 따랐습니다. 이 시리즈의 1편에서 언급한 것처럼 JavaFX 개발자는 앞으로 노드 기반 접근 방법을 사용할 것입니다.

이제 애플리케이션의 JavaFX 스크립트 버전을 완료하고 사용자가 Flickr 웹 사이트에서 이미지를 검색, 나열 및 표시할 수 있도록 하겠습니다. 완성된 JavaFX 스크립트 애플리케이션을 위해 NetBeans 프로젝트 다운로드를 받을 수 있습니다.


이미지 검색

구 현할 첫 번째 동작은 이미지 검색입니다. 사용자가 UI의 검색 필드에 검색어를 입력하면 애플리케이션은 Flickr 웹 사이트에서 이미지 검색을 시작합니다. 검색어와 매칭되는 이미지가 있으면 애플리케이션은 최대 100개의 매칭되는 축소판 이미지의 목록을 로드하여 UI의 Matched Images 영역에 선택 가능한 목록으로 표시합니다. 또한 진행 표시줄이 이미지 검색을 추적합니다.

기 존 이미지 검색 애플리케이션은 Matched Images 영역에 반환된 축소판 이미지의 목록을 제목과 함께 표시합니다. 우리는 JavaFX 스크립트 애플리케이션 목록에서 매칭되는 이미지의 반환된 제목만 보여주도록 단순화하겠습니다. JavaFX 스크립트의 ListItem 구성요소는 현재 icon 속성을 갖지 않습니다. 따라서 현재 ListListItem 구성요소가 생성하는 목록은 이미지를 포함할 수 없습니다.

이미지 검색을 위해 ImageSearcherPhoto의 두 가지 JavaFX 스크립트 클래스를 생성했습니다. 또한 진행 표시줄을 위한 별도의 클래스도 생성했습니다. JavaFX 스크립트 패키지에 ProgressBar 구성요소는 아직 없습니다. 임시 해결책으로 학습 곡선 일지 시리즈에서 구축한 이미지 검색 UI에 Swing JProgressBar 구성요소로부터 JavaFX 스크립트 진행 표시줄을 생성하는 createProgressBar 함수를 포함했습니다. 우리는 이 함수를 자체 파일의 자체 클래스로 이동하기로 결정했습니다. 이는 애플리케이션의 주요 부분을 깔끔하게 만드는 이점이 있습니다. 따라서 애플리케이션에는 이제 4개의 파일이 있습니다.

  • Main.fx: 애플리케이션의 주요 부분을 제공합니다. UI를 표시하고 이미지 검색 및 가져오기를 호출합니다.
  • ImageSearcher.fx: 이미지 검색을 수행합니다.
  • Photo.fx: Flickr 사진을 나타냅니다.
  • TempProgressBar.fx: 진행 표시줄을 생성합니다.

다음은 이미지 검색을 호출하는 Main.fx의 코드입니다.


  var searcher = ImageSearcher {
      callback: function(photos:Photo[]):Void {
          thumbnailList.items = for(photo in photos) {
              ListItem {
                  text: photo.title
                  value: photo
              }
          };

          matchedImagePB.indeterminate= false;
      }
  };

  var search = function():Void {
      System.out.println("searching... {searchTextField.text}");
      matchedImagePB.indeterminate = true;
      searcher.search(searchTextField.text);
  };

  searchTextField.action=search;

사용자가 UI의 검색 필드에 검색어를 입력하면 ImageSearcher 클래스 내의 search 함수를 호출하는 작업을 트리거하고 검색어를 search 함수로 전달합니다.

변수에 함수가 지정된 것에 유의하십시오.


  var search = function():Void {...}
 

 JavaFX 스크립트에서 함수는 변수에 지정되거나 매개 변수로써 다른 함수에 전달될 수 있는 1급 개체(first-class object)입니다.

또한 함수는 matchedImagePB의 indeterminate 등록 정보를 true로 설정합니다.


  var matchedImagePB = TempProgressBar { };

        matchedImagePB.indeterminate = true;
 


TempProgressBar 클래스는 matchedImagePB 변수에 지정됩니다. 따라서 matchedImagePB의 indeterminate 등록 정보를 설정하는 것은 실질적으로 TempProgressBar 개체의 indeterminate 등록 정보를 설정하는 것입니다. 그 영향을 이해하기 위해 TempProgressBar 클래스를 살펴보겠습니다.


TempProgressBar 클래스

다음은 TempProgressBar 클래스입니다.


 
package javafxscriptimgsearch2;

  import javafx.ext.swing.*;

  public class TempProgressBar extends Component {
      protected function createJComponent():javax.swing.JComponent {
          return new javax.swing.JProgressBar();
      }
      public attribute indeterminate:Boolean = false on replace {
          var prog = this.getJComponent() as javax.swing.JProgressBar;
          prog.setIndeterminate(indeterminate);
      }
}
 

클래스에는 createJComponent 함수와 indeterminate 속성이 있습니다.
replace
트리거가 indeterminate 속성에 연결되어 있습니다.


      public attribute indeterminate:Boolean = false on replace {
          var prog = this.getJComponent() as javax.swing.JProgressBar;
          prog.setIndeterminate(indeterminate);
      }
 

애플리케이션의 주요 부분이 속성을 true로 설정하는 것과 같이 속성 값이 변경되면 트리거는 진행 표시줄을 생성합니다. 특히 트리거는 getJComponent() 함수를 사용하여 JavaFX 스크립트 구성요소로 캡슐화된 Swing JProgressBar 구성요소를 생성합니다. 트리거는 또한 JProgressBar 구성요소의 indeterminate 등록 정보를 true로 설정하여 검색 진행 중에 진행 표시줄이 계속 움직이도록 합니다.

트 리거는 JavaFX 스크립트의 강력한 기능 중 하나입니다. 트리거는 특정 조건 충족 시 코드 블록을 실행하도록 합니다. 또한 기존 Swing 구성요소의 재사용이 얼마나 쉬운지에도 유의하십시오. Swing 구성요소를 사용하려면 JavaFX 스크립트로 간단한 래퍼(wrapper)를 작성하기만 하면 됩니다.


ImageSearcher 클래스

다음은 ImageSearcher 클래스입니다.


    package javafxscriptimgsearch2;

import javax.xml.parsers.*;
import org.xml.sax.helpers.DefaultHandler;
import java.lang.System;
import java.lang.Thread;
import java.lang.Runnable;
import javax.swing.SwingUtilities;

public class ImageSearcher {
public attribute callback: function(photos:Photo[]):Void;

public function search(search:String) {
var thread = new Thread(Runnable {
public function run():Void {
var photos:Photo[];

var handler = DefaultHandler {
public function startDocument() { }
public function startElement(uri:String, localName:String, qName:String , attributes:org.xml.sax.Attributes ) {
if(qName == "photo") {
var photo = Photo {
id: attributes.getValue("id")
server: attributes.getValue("server")
farm: attributes.getValue("farm")
title: attributes.getValue("title")
secret: attributes.getValue("secret")
};
insert photo into photos;
}
}
public function endElement(uri:String , localName:String , qName:String ) { }
public function endDocument() { }

};

var SEARCH_URL = "http://api.flickr.com/services/rest/?" +
"method=flickr.photos.search";
var key = "339db1433e5f6f11f3ad54135e6c07a9";
var MAX_IMAGES = 100;
var searchUrl = "{SEARCH_URL}&api_key={key}&per_page={MAX_IMAGES}&text={search}";
var url = new java.net.URL(searchUrl);
var is = url.openStream();
var factory = SAXParserFactory.newInstance();
var saxParser = factory.newSAXParser();
saxParser.parse(is, handler);

SwingUtilities.invokeLater(Runnable {
public function run():Void {

if(callback != null) {
callback(photos);
}
}
});
}
});
thread.start();
}
}
 

ImageSearcher 클래스는 애플리케이션에서 이미지 검색을 수행하는 search 함수의 래퍼(wrapper)입니다.

새 스레드를 시작하여 search 함수가 시작됩니다. 스레드 내에서 함수는 검색어와 매칭되는 이미지(Flickr에서는 사진)를 가져오기 위해 Flickr 사진 검색 웹 서비스를 호출합니다. 다음은 Flickr 서비스를 호출하는 코드입니다.


   var SEARCH_URL = "http://api.flickr.com/services/rest/?" +
          "method=flickr.photos.search";
  var key = "339db1433e5f6f11f3ad54135e6c07a9";
  var MAX_IMAGES = 100;
  var searchUrl = "{SEARCH_URL}&api_key={key}&per_page={MAX_IMAGES}&text={search}";
  var url = new java.net.URL(searchUrl);
  var is = url.openStream();
 


호출은 Flickr API의 REST(Representational State Transfer) 버전을 사용합니다(Flickr는 XML-RPC 및 SOAP 버전도 제공합니다). API에 전달되는 인수에는 API 키, 반환되는 이미지의 최대 갯수, 사용자가 지정한 검색어가 포함됩니다. 애플리케이션에서 최대 100개의 매칭되는 이미지 목록을 다운로드하고 싶으므로 이미지 최대 갯수를 100으로 설정했습니다.

사진 검색 서비스는 사진을 XML 문서로 반환하므로 문서를 분석하는 기법이 필요합니다. 이 애플리케이션에서는 분석 수행을 위해 SAX DefaultHandler를 사용했습니다.

   var handler = DefaultHandler {
public function startDocument() { }
public function startElement(uri:String, localName:String, qName:String , attributes:org.xml.sax.Attributes ) {
if(qName == "photo") {
var photo = Photo {
id: attributes.getValue("id")
server: attributes.getValue("server")
farm: attributes.getValue("farm")
title: attributes.getValue("title")
secret: attributes.getValue("secret")
};
insert photo into photos;
}
}
public function endElement(uri:String , localName:String , qName:String ) { }
public function endDocument() { }

};

 

각 Flickr 사진에 대해 DefaultHandler사진 개체를 인스턴스화하고 속성 값을 Flicker 사진의 해당 속성 값으로 설정합니다. DefaultHandler는 그 다음 각 Photo 개체를 photos라는 시퀀스에 추가합니다. 학습 곡선 일지 3편: JavaFX 스크립트 함수에서 시퀀스는 동일한 유형을 갖는 개체의 순서별 목록을 나타냈습니다.

search 함수는 그 다음 callback을 호출하는 다른 스레드를 시작합니다.


 
 SwingUtilities.invokeLater(Runnable {
          public function run():Void {

              if(callback != null) {
                  callback(photos);
              }
          }
  }
 

SwingUtilities.invokeLater 메소드는 스레드의 Runnable 작업을 이벤트 디스패치 스레드에 놓습니다.


Callback 함수

ImageSearcher 클래스는 속성 유형이 함수인 callback이라는 속성을 갖습니다. 함수는 photos 시퀀스를 매개 변수로 받고 아무 것도 반환하지 않습니다.


   
public attribute callback: function(photos:Photo[]):Void;
 
ImageSearcher 클래스가 인스턴스화되면(애플리케이션의 주요 부분에 발생) callback 함수는 UI의 Matched Images 영역에 표시하기 위해 제목의 목록을 구축합니다. 목록의 각 항목은 속성 값이 반환된 사진의 제목인 text 속성과 속성 값이 Photo 개체인 value 속성을 갖습니다. 다음 코드는 애플리케이션의 주요 부분 내에서 ImageSearcher 클래스를 인스턴스화하고 callback 함수를 제공합니다.


  var searcher = ImageSearcher {

       callback: function(photos:Photo[]):Void {
          thumbnailList.items = for(photo in photos) {
              ListItem {
                  text: photo.title
                  value: photo
              }
          };

          matchedImagePB.Indeterminate= false;
      }
  };
 


매칭되는 사진 목록을 구축한 후 callback 함수는 Matched Images 진행 표시줄의 indeterminate 등록 정보를 기본값인 false로 다시 설정합니다.


이미지 검색 수행

이미지 검색을 다루는 코드를 검토했으니 이제 작업을 살펴봅시다. 그림 2는 사용자가 검색어를 입력한 후 Search 필드와 Matched Images 진행 표시줄의 상태를 보여줍니다.



그림 2.
검색어 입력

애플리케이션이 매칭되는 이미지를 검색하는 동안 검색어를 기반으로 검색 중임을 나타내는 메시지가 나타납니다. 이 예제에서는 "searching... polar bear" 메시지가 나타납니다.

그림 3은 매칭되는 이미지의 반환된 제목 목록 일부를 보여줍니다.


그림 3. 이미지 검색 결과

검색 함수가 올바르게 작동합니다!


이미지 표시

구현할 다음 동작은 이미지 표시입니다. 사용자가 반환된 이미지 제목 목록에서 제목을 선택하면 애플리케이션은 해당 이미지를 Flickr 사이트에서 가져와 UI의 Selected Image 영역에 표시해야 합니다.

표시를 위해 이미지를 가져오도록 onChange 위임을 추가하는 List 클래스의 사용자 정의 하위 클래스인 PhotoList라는 클래스를 추가했습니다. 다음은 Main.fx 파일에서 PhotoList 가 인스턴스화되는 방법을 보여줍니다.


   class PhotoList extends List {
       
public attribute onChange:function(photo:Photo);
       
public attribute selectedPhoto:ListItem = bind selectedItem on replace {
           
var photo = selectedPhoto.value as Photo;
           
if(onChange != null) {
               onChange
(photo);
           
}
       
}
   
}
 
   
var thumbnailList = PhotoList {
       preferredSize
:[300, 230]
       hmax
: Layout.UNLIMITED_SIZE
       vmax
: Layout.UNLIMITED_SIZE
   
};


사용자가 목록에서 항목을 선택하면 애플리케이션은 PhotoList 개체의 selectedPhoto 속성을 선택된 항목으로 설정합니다. 이는 PhotoList 개체의 selectedPhoto 속성이 다음 표현식의 결과에 바인딩되었기 때문입니다.

   
      selectedItem on replace {
      var photo = selectedPhoto.value as Photo;
  }
 

바인딩은 표현식의 결과를 변수와 연관시키는 것을 의미합니다. 표현식이 변경되면(이 경우 selectedItem가 변경됨) 변수 값은 변경됩니다(이 경우 PhotoListselectedPhoto 속성이 자동으로 업데이트됨). 바인딩은 JavaFX 스크립트의 또 다른 강력한 기능입니다. 바인딩을 사용하여 애플리케이션의 부분을 직접적이고 우아하게 동기화할 수 있습니다.

또한 사용자가 목록에서 항목을 선택하면 애플리케이션은 PhotoList 개체의 onChange 속성과 연관된 이미지 로더 함수를 호출합니다. 이 함수는 선택된 진행 표시줄의 indeterminate 등록 정보를 true로 설정하여 이미지를 가져오는 동안 계속 진행되도록 합니다. 그 다음 선택된 Photo 개체에서 해당 이미지를 가져오기 위해 loadFullImage 함수를 호출합니다.


 class PhotoList extends List {
      public attribute onChange:function(photo:Photo);
      ...
          if(onChange != null) {
              onChange(photo);
       }

  // configure the image loader
  var imageLoader = function(photo:Photo):Void {
      if(photo != null) {
          selectedImagePB.indeterminate = true;
          photo.loadFullImage(function():Void{
              selectedImageDisplay.icon = Icon { image: photo.fullImage };
              selectedImagePB.indeterminate = false;
          });
      }
  };

thumbnailList.onChange = imageLoader;
 

이미지 로더는 Selected Image 영역에 표시하기 위해 이미지를 설정하고 이미지를 가져온 후 선택된 진행 표시줄의 indeterminate 등록 정보를 다시 false로 설정합니다.


  selectedImageDisplay.icon = Icon { image: photo.fullImage };
  selectedImagePB.indeterminate = false;
 


Photo 클래스

Photo 클래스를 살펴봅시다.

    package javafxscriptimgsearch2;

import javafx.scene.image.*;
import java.lang.*;
import javax.swing.SwingUtilities;
import javax.imageio.ImageIO;

public class Photo {
public attribute id:String;
public attribute server:String;
public attribute farm:String;
public attribute title:String;
public attribute secret:String;

private attribute image:Image = null;

public attribute fullImage:Image = null;

public attribute fullImageURL = bind "http://static.flickr.com/{server}/{id}_{secret}.jpg";

public function loadFullImage(
callback:function():Void
):Void {

if(image == null) {
var thread = new Thread(Runnable {
public function run():Void {
var strImageUrl = "http://static.flickr.com/{server}/{id}_{secret}.jpg";
System.out.println("loading: {strImageUrl}");
var buffImg = ImageIO.read(new java.net.URL(strImageUrl));

SwingUtilities.invokeLater(Runnable {
public function run():Void {
image = Image.fromBufferedImage(buffImg);
fullImage = image;
if(callback != null) {
callback();
}
}
});

}});
thread.start();
} else {
callback();
}

}
}
 

Photo 클래스는 Flickr에 저장된 사진과 연관시키는 XML 속성에 해당하는 몇 가지 속성을 갖습니다. 예를 들어 Photo 클래스의 id 속성은 Flickr에 저장된 사진의 id 속성에 해당합니다. 또한 클래스는 Flickr에서 사진을 가져오는 loadFullImage 함수도 제공합니다.

새 스레드를 시작하여 loadFullImage 함수가 시작됩니다. 스레드 내에서 함수는 특정 사진에 대해 사진 소스 URL을 구성한 다음 사진을 가져옵니다. 다음은 사진 소스 URL을 구성하고 사진을 가져오는 코드입니다


   var strImageUrl = "http://static.flickr.com/{server}/{id}_{secret}.jpg"

   var buffImg = ImageIO.read(new java.net.URL(strImageUrl));
 


loadFullImage 함수가 URL 구성을 위해 Photo 개체의 server, idsecret 속성을 사용하는 것에 유의합니다. 또한 이미지 로드를 위해 자바 imagio 패키지의 ImageIO.read 메소드를 사용했습니다.

함수는 JavaFX 스크립트 Image 클래스의 fromBufferedImage 메소드를 사용하여 가져온 사진을 Photo 개체의 image 속성에 지정합니다. 그런 다음 호출자(이 경우 애플리케이션의 주요 부분에 있는 이미지 로더)에게의 callback을 호출하는 다른 스레드를 시작합니다.


   SwingUtilities.invokeLater(Runnable {
       public function run():Void {
           image = Image.fromBufferedImage(buffImg);
           fullImage = image;
           if(callback != null) {
               callback();
           }
       }
   });
 

SwingUtilities.invokeLater 메소드는 스레드의 Runnable 작업을 이벤트 디스패치 스레드에 놓습니다.


목록에 이미지 표시

애플리케이션의 이미지 표시 부분을 테스트해봅시다. 그림 4는 Matched Images 영역에서 이미지를 선택한 후 Selected Image 진행 표시줄의 상태를 보여줍니다.


그림 4. 표시할 이미지 선택

애플리케이션이 이미지를 가져오면 이미지의 사진 소스 URL을 식별하는 메시지를 표시합니다. 이 예제에서는 "loading: http://static.flickr.com/3234/2595583764_a2e6661d3a.jpg" 메시지가 나타납니다. 이미지 가져오기가 끝나면 애플리케이션은 "loaded" 메시지를 표시합니다.

그림 5는 UI의 Selected Image에 선택한 이미지가 표시된 완전한 UI를 보여줍니다.


그림 5. 표시된 이미지

애플리케이션의 이미지 가져오기 부분이 작동합니다. 애플리케이션도 의도대로 작동합니다.

완성된 JavaFX 스크립트 애플리케이션을 위해 NetBeans 프로젝트 다운로드를 받을 수 있습니다.

요약

이 학습 곡선 일지 시리즈에서는 JavaFX 스크립트 프로그래밍 언어(이 시리즈에서는 JavaFX 스크립트라고 줄여서 부름)의 사용을 시작하는데 도움이 되는 몇 가지 기본 개념 및 기법을 소개했습니다.

시리즈의 1편에서는 간단한 JavaFX 애플리케이션을 만드는 방법을 소개했습니다.

2편에서는 풍부한 UI 생성을 위해 사용 가능한 JavaFX 라이브러리 내의 일부 클래스와 언어의 선택적 구문을 설명했습니다. UI 구축에는 계층 구조적인 Swing 기반 접근 방법을 따랐습니다. 이 시리즈의 1편에서 언급한 것처럼 앞으로 JavaFX 개발자는 노드 기반 접근 방법을 사용할 것입니다. 노드 기반 접근 방법에서는 UI 구축을 위해 다른 JavaFX 라이브러리를 사용할 것입니다.

3편에서는 뷰와 데이터 모델 등의 애플리케이션 부분을 동기화하기 위해 사용 가능한 JavaFX 스크립트의 바인딩 기능과 JavaFX 스크립트 함수를 다뤘습니다.

4편(본 기사)에서는 웹 서비스 액세스를 위한 JavaFX 스크립트 사용 방법을 보여주었습니다. 그 과정 중에 FX 스크립트에서 Swing 클래스와 같은 자바 기술 슬래스의 액세스가 얼마나 쉬운지도 보여주었습니다.

또한 이 시리즈를 통해 코드 완성과 같은 기능이 어떻게 NetBeans IDE 6.1에서 JavaFX 애플리케이션의 빌드 및 실행을 단순화하는지도 볼 수 있었습니다. JavaFX Preview SDK를 사용하여 명령줄에서 JavaFX 애플리케이션을 빌드 및 실행할 수도 있습니다.

JavaFX 포함 NetBeans IDE 6.1을 다운로드하여 설치하거나 JavaFX Preview SDK를 다운로드하여 JavaFX 스크립트를 시작하십시오.


자세한 정보
이 글의 영문 원본은
Learning Curve Journal, Part 4: Accessing a Web Service
에서 보실 수 있습니다.

"Java FX" 카테고리의 다른 글

Posted by 1010
98..Etc/JavaFX2008. 11. 12. 17:29
반응형

2007년 8월과 9월에 썬 개발자 네트워크의 John O'Conner는 JavaFX 스크립트 프로그래밍 언어(이 기사에서는 JavaFX 스크립트라고 줄여서 부름)를 시작하는 사용자에게 도움을 주고자 "학습 곡선 일지(Learning Curve Journal)"라는 제목의 시리즈를 기고했습니다.

그 이후로 이 언어의 많은 중요한 부분이 개선되었습니다. 아마도 가장 중요한 변화는 JavaFX 스크립트의 초기 인터프리터 기반 버전을 대신하여 컴파일러 기반 버전을 사용할 수 있게 되었다는 점입니다. 이전의 학습 곡선 일지에서는 인터프리터 기반 버전 사용에 대해 설명했습니다.

업데이트된 학습 곡선 일지에서는 컴파일러 기반 버전의 언어 사용법을 보여줍니다. 최신 내용을 반영하여 다른 변경 사항도 적용되었습니다.

지난 학습 곡선 기사에서는 간단한 사용자 인터페이스(UI)를 구현하고 UI 시험이 성공적이었음을 확인했습니다. 결과 JavaFX 스크립트는 Flickr에서 이미지를 검색하는 이미지 검색 애플리케이션 구축을 위한 원래 자바 프로그래밍 언어 UI와 비슷하게 보였습니다. 그림 1은 결과적인 기본 프레임을 보여줍니다.


그림 1. 원래 UI를 복제한 JavaFX 이미지 검색 애플리케이션

선언적 JavaFX 스크립트 구문을 사용한 결과 코드는 자바 언어 UI의 이식을 위한 괜찮은 시작이었습니다. 그러나 유휴 상태의 프레임, 응답하지 않는 검색 필드, 비활성 진행 표시줄, 빈 목록 상자 및 빈 이미지 레이블에 대한 추가 작업이 필요합니다. 여기에서는 아무 일도 발생하지 않습니다. 현재까지는 활성 데이터모델에 아무 UI 요소도 연결되지 않았으며 사용자 상호작용에도 응답하지 않습니다. 예를 들어 Search 텍스트 필드는 입력한 문자를 받아서 보여주지만 아직 아무 것도 하지 않습니다.

이 골격 뿐인 UI에는 추가 작업이 필요합니다. 이를 위해 비활성 애플리케이션에서 필요한 작업을 수행하도록 하는 함수가 필요합니다.


함수

JavaFX 스크립트 함수는 자바 프로그래밍 언어 메소드와 비슷합니다. 이들 메소드와 마찬가지로 함수는 매개 변수와 반환 값을 갖습니다. 또한 속성 및 변수와 if-then, while 루프, for 루프 및 기타 조건문도 가질 수 있습니다. 다음은 몇 가지 유효한 함수의 예입니다.


 
function z(a,b) {
      var x = a + b;
      var y = a - b;
      return sq(x) / sq (y);
  }

  function sq(n) {return n * n; }

  function main() {
      return z(5, 10);
  }

  function min(x1 : Number, x2 : Number ): Number {
      if (x1 < x2) {
        return x1;
      }
      else {
        return x2;
  }
 

반환 값 유형이나 매개 변수 유형을 선언할 필요가 없습니다. 그러나 자바 언어 프로그래머인 저에게는 min 함수에서와 같이 유형을 사용하는 것이 익숙하므로 가능한 경우에는 항상 사용하곤 합니다. 매개 변수 유형을 선언하는 것은 명확성에 도움이 됩니다. 따라서 z 함수를 다음과 같이 다시 작성하겠습니다.


function z(a: Number, b: Number): Number {
   var x: Number = a + b;
   var y: Number = a - b;
   return sq(x) / sq (y);
}
 


return 키워드는 선택 사항입니다.

   function sq(n) {n*n;}

위 함수는 다음과 같습니다.

   function sq(n) {return n*n;}

함수는 function 키워드로 시작합니다. 그 뒤에는 함수 이름과 매개 변수 목록이 나옵니다. JavaFX 스크립트에서는 유형이 변수나 함수 이름 다음에 나옵니다. 예를 들면 b: Number 매개변수는 b 라는 인수의 유형이 Number라는 의미입니다. 마지막으로 함수는 Number를 반환하므로 이를 매개 변수 목록 뒤에 선언할 수 있습니다. 함수의 본문 앞뒤에는 자바 언어에서 메소드 본문을 둘러싸는 것과 같이 괄호가 있습니다.

함수는 매개 변수나 기타 참조 변수가 변경될 때마다 반환 값을 재평가합니다. 이 기능은 개체를 자주 변경될 수 있는 특정 값에 바인드하려는 경우 유용합니다. 바인딩에 대해서는 나중에 자세히 설명하겠습니다.

클래스에 대해 함수가 정의될 수도 있습니다. 다음은 Friends 클래스에 대해 정의된 함수의 예입니다.


   import java.lang.System;
  import javafx.lang.Sequences;

  class Friends {
      attribute knownNames: String[];
      function sayHello(name: String): String {
          var index = Sequences.indexOf(knownNames,name);
          if (index >= 0) {
              return "Hello, {name}!";
              } else {
              return "Sorry, I can't talk to strangers.";
              }
      }
  }

  var buddies = Friends {
      knownNames: ["John", "Robyn", "Jack", "Nick", "Matthew",
      "Tressa", "Ruby"]
  };

  var greeting = buddies.sayHello("John");
  System.out.println(greeting);
 


이 작은 프로그램은 sayHello 메소드에 아는 이름을 입력하면 "Hello"라고 응답합니다. 그렇지 않은 경우엔 "can't talk to strangers"라고 합니다. Friends 클래스에는 한 가지 속성과 속성을 사용하여 메시지를 반환하는 함수가 포함되어 있습니다. Sequences 클래스를 사용하는 것에 유의합니다. 이 클래스는 시퀀스 조작을 위한 다양한 함수를 포함합니다. 시퀀스는 이 예제에서 아는 이름의 목록과 같이 개체의 순서별 목록을 나타냅니다. SequencesindexOf 함수는 지정된 시퀀스에서 같은 값을 갖는 개체를 검색합니다. 여기에서 indexOf는 아는 이름 시퀀스에서 지정된 이름(이 예제에서는 "John")과 매칭되는 개체를 검색합니다.


반응적 UI 요소

UI 요소(JavaFX 스크립트 라이브러리에서는 노드)는 키 입력이나 마우스 클릭과 같은 사용자 상호작용에 응답할 수 있습니다. 위젯은 action, onMouseClicked, onKeyTyped 및 기타 이벤트 기반 속성을 갖습니다. 이들 속성과 함수를 연관시킬 수 있습니다. 예를 들어 함수를 TextFieldaction 속성과 연관시키면 해당 함수는 필드 내에서 Enter를 누를 때 실행됩니다. Button 위젯의 동일한 action 속성은 사용자가 클릭할 때마다 활성화됩니다.

JavaFX 이미지 검색 애플리케이션과 관련된 함수가 필요하므로 UI 요소에 대한 이벤트 핸들러를 작성하기로 했습니다. 다음 애플리케이션은 두 개의 버튼과 하나의 레이블을 생성합니다. Bigger 버튼을 누르면 레이블의 글꼴 크기가 증가하고 텍스트가 변경됩니다. Smaller 버튼을 누르면 레이블의 글꼴 크기가 감소하고 텍스트가 변경됩니다.

이 애플리케이션에서는 계층 구조적인 Swing 기반 접근 방법을 따릅니다. 이 시리즈의 1편에서 언급한 것처럼 앞으로는 JavaFX 스크립트 개발자가 노드 기반 접근 방법을 사용하도록 할 것입니다.


   import javafx.ext.swing.SwingFrame;
  import javafx.ext.swing.BorderPanel;
  import javafx.ext.swing.FlowPanel;
  import javafx.ext.swing.Button;
  import javafx.scene.Font;
  import javafx.scene.HorizontalAlignment;
  import javafx.ext.swing.Label;

  var font = Font { size: 18 };

  class FontDataModel {
      attribute text: String;

      function increaseFontSize() {
         font = Font { size: font.size + 1 };
         text= "Font Test ({font.size})";
         }
      function decreaseFontSize() {
         font = Font { size: font.size - 1 };
         text= "Font Test ({font.size})";
      }
  }

  SwingFrame {
       var myFont = FontDataModel {
           text: "Font Test (18)"

       }

      content:
      BorderPanel {
          top: FlowPanel {
              alignment: HorizontalAlignment.LEADING
              content: [
                  Button { text:"Bigger"
                      action :
                      function() {
                           myFont.increaseFontSize();
                      }
              },

              Button { text:"Smaller"
                  action : function() {
                      myFont.decreaseFontSize();
                  }
              }

              ]
          }
          center:
              Label {
                  width: 200
                  font: bind font
                  text: bind myFont.text

          }

       }
       visible: true
  }
 

이 코드를 잘라내어 JavaFX 애플리케이션에 바로 붙여넣을 수 있습니다. Preview 버튼을 사용하면 그림 2의 결과가 보입니다.


그림 2. 미리보기 기능을 사용하여 JavaFX 스크립트 언어를 대화형으로 시험

이 Bigger-Smaller 글꼴 애플리케이션은 텍스트 문자열 속성을 갖는 FontDataModel을 생성합니다. 애플리케이션은 FontDataModel 인스턴스를 생성하고 text 속성을 초기화합니다. FontDataModel에도 increaseFontSizedecreaseFontSize라는 두 가지 함수가 있습니다. 이들 함수는 텍스트 속성을 변경하고 Font 클래스의 인스턴스를 생성하며 인스턴스의 글꼴 크기를 업데이트합니다.

왜 글꼴을 FontDataModel의 속성으로 지정하고 텍스트 속성과 같은 방법으로 함수에서 업데이트하지 않는지 궁금할 지도 모릅니다. 이러한 접근 방법은 Font가 변경할 수 없는 개체, 즉 원래 개체의 속성을 변경할 수 없기 때문에 사용할 수 없습니다. 개체의 인스턴스에서만 속성을 변경할 수 있습니다.

각 버튼은 연관된 함수를 포함하는 action 속성을 갖습니다. 예를 들어 Bigger 레이블이 있는 버튼은 myFont 변수의 increaseFontSize 함수를 호출합니다.


           
Button { text:"Bigger"

       action :
      function() {
           myFont.increaseFontSize();
      }
   
}
 

버튼을 누를 때마다 글꼴 크기와 텍스트가 변경되는 것을 볼 수 있습니다. 그림 3은 Bigger 버튼을 두 번 눌렀을 때의 결과를 보여줍니다.


그림 3. 버튼을 클릭하면 텍스트와 글꼴 크기 변경

분명히 increaseFontSize 메소드는 글꼴과 텍스트를 변경합니다. 이를 위해 바인딩이라는 JavaFX 스크립트 기능을 사용했습니다.


뷰와 모델 바인딩

JavaFX 스크립트에는 하나의 속성이 다른 속성의 변경을 추적하도록 하는 bind 연산자가 있습니다. 하나의 속성을 다른 속성에 바인딩한다는 것은 바인딩된 속성이 대상 속성의 변경 사항을 항상 인지한다는 것을 의미합니다. 이 Bigger-Smaller 글꼴 애플리케이션에서는 Label이 글꼴과 텍스트의 변경 사항을 추적하도록 하려고 합니다. 이를 위해 Labelfont 속성을 Font 변수에 바인드하도록 bind 연산자를 사용했습니다. 또한 Labeltext 속성을 FontDataModeltext 속성에 바인드하도록 bind 연산자를 사용했습니다.

다음은 Label 선언입니다.


 
Label {
      width: 200
      font: bind font
      text: bind myFont.text

  }
 

bind 연산자는 함수에도 사용할 수 있습니다. 함수는 인수나 참조 변수가 변경될 때마다 결과를 업데이트하므로 함수에의 바인딩은 단일 속성에의 바인딩과 마찬가지로 작용합니다. 사실 함수는 실제로 바인딩과 함께 사용하도록 설계되었습니다. 본문 내의 매개 변수 및 참조 변수를 모두 포함하는 모든 종속성을 자동으로 추적하는 재사용 가능한 하위 루틴으로의 바인딩을 리팩터링하기 위해 이들을 사용할 수 있습니다.

표 1에서 코드의 일부를 참조하십시오.

표1. 함수와 함께/함수 없이 바인드 사용
함수 없이 바인드
함수와 함께 바인드
   import java.lang.System;

class Data {
attribute foo: Number;
attribute baz: Number;
}

var data = Data {
foo: 4
baz: 7
};

var zoo = bind data.foo +
data.baz + 10;
System.out.println(zoo);
data.baz = 12;
System.out.println(zoo);
   import java.lang.System;

class Data {
attribute foo: Number;
attribute baz: Number;
function add(x, y, z): Number {
return x+y+z;
}
}

var data = Data {
foo: 4
baz: 7
};

var zoo = bind data.add(data.foo, data.baz, 10);
System.out.println(zoo);
data.baz = 12;
System.out.println(zoo);
출력:
21.0
26.0
출력:
21.0
26.0

요약

JavaFX 애플리케이션에 동작을 추가하기 위해 함수를 사용합니다. UI 구성요소의 속성은 함수에 매핑됩니다. action, onMouseClickedonKeyTyped와 같은 UI 이벤트 처리를 위해 이들 함수를 정의할 수 있습니다. 함수에는 함수 본문 내에서 매개 변수 및 참조 변수를 재평가하는 추가적인 등록 정보가 있습니다.

bind 연산자를 사용하여 하나의 속성을 다른 속성에 연결할 수 있습니다. 이는 UI 위젯이 모델 속성을 추적하도록 할 때 특히 유용합니다. 뷰 속성을 모델 속성에 바인딩한다는 것은 모델과 뷰가 동일한 데이터로 항상 동기화됨을 의미합니다.

JavaFX 이미지 검색 애플리케이션의 UI에 아직 기능을 추가하지 않았지만 함수가 필요하다는 것을 알게 되었습니다. 검색 텍스트를 가져오기 위해서는 거의 확실히 함수를 사용해야 합니다. 같은 함수가 이미지를 가져오기 위해 Flickr 사이트에도 액세스할 것입니다. 또한 뷰를 기본 모델에 연결하기 위해 bind 연산자도 사용해야 할 것입니다.

이제 UI를 일부 기본 작업 및 함수에 연결하는 방법을 알게 되었습니다. 또한 UI를 기본 모델에 연결하기 위해 bind 연산자도 사용할 수 있습니다.

자세한 정보
이 글의 영문 원본은
Learning Curve Journal, Part 3: JavaFX Script Functions
에서 보실 수 있습니다.

"Java FX" 카테고리의 다른 글

Posted by 1010
98..Etc/JavaFX2008. 11. 12. 17:28
반응형

2007 년 8월과 9월에 썬 개발자 네트워크의 John O'Conner는 JavaFX 스크립트 프로그래밍 언어(이 기사에서는 JavaFX 스크립트라고 줄여서 부름)를 시작하는 사용자에게 도움을 주고자 "학습 곡선 일지(Learning Curve Journal)"라는 제목의 시리즈를 기고했습니다.

그 이후로 이 언어의 많은 중요한 부분이 개선되었습니다. 아마도 가장 중요한 변화는 JavaFX 스크립트의 초기 인터프리터 기반 버전을 대신하여 컴파일러 기반 버전을 사용할 수 있게 되었다는 점입니다. 이전의 학습 곡선 일지에서는 인터프리터 기반 버전 사용에 대해 설명했습니다.

업데이트된 학습 곡선 일지에서는 컴파일러 기반 버전의 언어 사용법을 보여줍니다. 최신 내용을 반영하여 다른 변경 사항도 적용되었습니다.

사용자 인터페이스를 정의하기 위해 10년 가까이 자바 프로그래밍 언어를 사용해온 저는 JavaFX 스크립트를 처음 사용해보고 두 가지 환경 사이의 큰 차이점을 금방 느낄 수 있었습니다. 프로그래머는 자바 언어에서 사용자 인터페이스(UI) 정의를 위해 절차적 언어를 사용하지만 JavaFX Script에서는 UI 정의에 선언적 문을 사용할 수 있습니다. 이는 커다란 차이이며 여기에 적응하는 데는 약간의 시간과 노력이 필요할 수 있습니다.

UI 생성을 위한 새로운 선언적 스타일에 대해 알아보고자 기존 애플리케이션 UI를 자바 언어 구현에서 JavaFX 스크립트로 이식하기로 했습니다. 썬 개발자 네트워크자바 언어 허브에 있는 Swingworker 기사에서 만든 이미지 뷰어 애플리케이션을 선택했습니다. 원래 애플리케이션은 Java SE 6에서 SwingWorker 클래스의 사용 방법을 보여주기 위한 것이었지만 UI 자체는 JavaFX 스크립트로의 간편한 이전을 제공할 만큼 충분히 단순해 보입니다.


기존 사용자 인터페이스

기 존 애플리케이션에서는 사용자가 유명한 Flickr 웹 사이트에서 이미지를 검색, 나열 및 표시할 수 있도록 했습니다. 사용자는 검색어를 입력할 수 있으며 애플리케이션은 REST API를 사용하여 매칭되는 축소판 이미지 목록을 Flickr에 쿼리합니다. 사용자는 축소판 이미지를 하나 선택하여 크고 상세한 이미지를 가져올 수 있습니다. 그림 1은 검색 결과가 나온 기존 애플리케이션을 보여줍니다.

그림 1. 검색 결과가 나온 애플리케이션 UI

이 UI는 위에서부터 아래로 다음 구성요소로 이루어져 있습니다.

  • 기본 프레임 창 컨테이너
     
  • 검색 레이블 및 검색 텍스트 필드
     
  • 검색 매칭 레이블 및 진행 표시줄
     
  • 매칭되는 축소판 이미지 목록과 짧은 설명
     
  • 선택 레이블 및 진행 표시줄
     
  • 선택한 이미지를 보여주는 레이블
     

UI는 JFrame, JLabel, JProgressBar, JScrollPaneJList.와 같은 일반 Swing 구성요소로 이루어집니다. JList 구성요소는 축소판 이미지와 제공되는 짧은 설명을 표시하기 위한 사용자 정의 렌더러를 갖지만 여전히 상대적으로 간단한 UI로서 JavaFX 스크립트의 선언적 UI 측면을 조사하는 데 도움이 될 것입니다. 전체 애플리케이션의 구현을 시도해 보겠지만 현재로는 기존 UI와 적당히 비슷한 정도로도 충분합니다. 작동하는 데모처럼 극적이진 않겠지만 그림 2에 나온 비활성 UI는 JavaFX 스크립트의 선언적 UI에 대한 초기 목표를 보여줍니다.

그림 2. 애플리케이션 UI

원래 UI는 NetBeans IDE 6.1과 Matisse GUI 편집기를 사용하여 구현했습니다. Swingworker 기사에서 모든 원래 코드 및 생성된 UI 주변의 코드를 다운로드 받을 수 있습니다. 코드에서 이 UI를 생성하기 위해 NetBeans IDE가 GroupLayout를 어떻게 사용했는지 볼 수 있습니다.

    private void initComponents() {
lblSearch = new javax.swing.JLabel();
txtSearch = new javax.swing.JTextField();
lblImageList = new javax.swing.JLabel();
scrollImageList = new javax.swing.JScrollPane();
listImages = new JList(listModel);
lblSelectedImage = new javax.swing.JLabel();
lblImage = new javax.swing.JLabel();
progressMatchedImages = new javax.swing.JProgressBar();
progressSelectedImage = new javax.swing.JProgressBar();

setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE);
setTitle("Image Search");
lblSearch.setText("Search");
lblImageList.setText("Matched Images");

// ...
// event listeners, models, and cell renderers removed for this example
//

lblSelectedImage.setText("Selected Image");

lblImage.setBorder(javax.swing.BorderFactory.createLineBorder(
new java.awt.Color(204, 204, 204)));
lblImage.setFocusable(false);
lblImage.setMaximumSize(new java.awt.Dimension(500, 500));
lblImage.setMinimumSize(new java.awt.Dimension(250, 250));
lblImage.setOpaque(true);
lblImage.setPreferredSize(new java.awt.Dimension(500, 250));

javax.swing.GroupLayout layout = new javax.swing.GroupLayout(getContentPane());
getContentPane().setLayout(layout);
layout.setHorizontalGroup(
layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup()
.addContainerGap()
.addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING)
.addComponent(lblImage, javax.swing.GroupLayout.Alignment.LEADING,
javax.swing.GroupLayout.DEFAULT_SIZE, 462, Short.MAX_VALUE)
.addComponent(scrollImageList, javax.swing.GroupLayout.DEFAULT_SIZE,
462, Short.MAX_VALUE)
.addGroup(javax.swing.GroupLayout.Alignment.LEADING, layout.createSequentialGroup()
.addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addComponent(lblImageList)
.addComponent(lblSelectedImage))
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
.addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addComponent(progressMatchedImages, javax.swing.GroupLayout.DEFAULT_SIZE,
350, Short.MAX_VALUE)
.addComponent(progressSelectedImage, javax.swing.GroupLayout.DEFAULT_SIZE,
350, Short.MAX_VALUE)))
.addGroup(javax.swing.GroupLayout.Alignment.LEADING, layout.createSequentialGroup()
.addComponent(lblSearch)
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
.addComponent(txtSearch, javax.swing.GroupLayout.DEFAULT_SIZE,
411, Short.MAX_VALUE)))
.addContainerGap())
);
layout.setVerticalGroup(
layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGroup(layout.createSequentialGroup()
.addContainerGap()
.addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
.addComponent(lblSearch)
.addComponent(txtSearch, javax.swing.GroupLayout.PREFERRED_SIZE,
javax.swing.GroupLayout.DEFAULT_SIZE,
javax.swing.GroupLayout.PREFERRED_SIZE))
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
.addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addComponent(lblImageList)
.addComponent(progressMatchedImages, javax.swing.GroupLayout.PREFERRED_SIZE,
javax.swing.GroupLayout.DEFAULT_SIZE,
javax.swing.GroupLayout.PREFERRED_SIZE))
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
.addComponent(scrollImageList, javax.swing.GroupLayout.PREFERRED_SIZE, 235,
javax.swing.GroupLayout.PREFERRED_SIZE)
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
.addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addComponent(lblSelectedImage)
.addComponent(progressSelectedImage, javax.swing.GroupLayout.PREFERRED_SIZE,
javax.swing.GroupLayout.DEFAULT_SIZE,
javax.swing.GroupLayout.PREFERRED_SIZE))
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
.addComponent(lblImage, javax.swing.GroupLayout.DEFAULT_SIZE, 305, Short.MAX_VALUE)
.addContainerGap())
);
pack();
}
 
NetBeans IDE를 사용한 UI 레이아웃은 쉽습니다. 드래그 앤 드롭이면 충분합니다. 이 예제에서 NetBeans IDE 6.1은 여러 가지 호스트 플랫폼에서 정확한 크기, 위치 및 구성요소의 간격을 제공하는 javax.swing.GroupLayout을 사용하여 UI 코드를 생성했습니다. 결과 코드는 그다지 읽기 쉽진 않지만 도구 지원이 뛰어나 레이아웃 코드를 직접 수동으로 작업할 필요가 없습니다.


선언적 JavaFX 스크립트 인터페이스

JavaFX 스크립트용의 설계 도구가 개발 중이지만 아직은 사용할 수 없습니다. 그러나 JavaFX 스크립트 플러그인이 포함된 NetBeans IDE 6.1의 미리보기 기능을 사용하면 수동으로 UI 코드를 입력하고 결과를 즉시 볼 수 있습니다.

이 인터페이스에 필요한 UI 구성요소는 javafx.ext.swing 패키지 및 javafx.scene으로 시작하는 다양한 패키지에 들어 있습니다. UI 구축은 계층 구조적인 Swing 기반 접근 방법을 따릅니다. 이 시리즈의 1편에서 언급한 것처럼 앞으로 JavaFX 개발자는 노드 기반 접근 방법을 사용할 것입니다. 노드 기반 접근 방법으로 UI를 구축하면 javafx.application 클래스의 일부 클래스가 필요할 것입니다.

JavaFX 스크립트는 패키지 구조 및 가져오기 문을 지원한다는 점에서 자바 언어와 유사합니다. 구성요소에 대해 공부하는 동안은 전체 패키지를 가져오는 대신 한 번에 하나씩 특정 구성요소를 가져오겠습니다. 이는 매우 지루하지만 하나씩 사용하게 함으로써 패키지에 어떤 구성요소가 있는지 보여줍니다.

JavaFX 스크립트 구성요소는 height, width, textcontent와 같은 속성을 갖습니다. content 속성은 자녀 구성요소를 갖습니다. 구성요소에 따라 content 속성은 array로 선언되는 여러 개의 구성요소를 포함할 수 있습니다. 이미지 검색 애플리케이션을 위해 사용자 인터페이스를 선언할 때는 JavaFX 스크립트 구성요소의 선택 및 사용 후 속성을 설정합니다.

예를 들어 다음의 짧은 스크립트는 빈 프레임을 정의합니다. 이 코드는 컨텐츠가 없는 프레임 컨테이너를 생성합니다.


  import javafx.ext.swing.SwingFrame;

  SwingFrame {
      title: "JFX Image Search"
      height: 500
      width: 500
      visible: true
  }
 

Swing의 JFrame에 해당하는 JavaFX 스크립트는 javafx.ext.swing.SwingFrame입니다. title 속성은 프레임에 나타나는 텍스트인 창 프레임 제목을 선언합니다. heightwidth 속성은 픽셀 단위로 크기 규격을 정의합니다. 마지막으로 visible 속성은 Swing의 JFrame 클래스 setVisible 메소드와 유사하게 프레임의 가시성 여부를 선언합니다.


Border Panel 및 Flow Panel

javafx.ext.swing 패키지에는 SwingFrame, Label, BorderPanel, FlowPanelList와 같은 구성요소가 포함됩니다. 이러한 이름은 일반적인 Swing 구성요소 같으므로 대상 UI 구현에 이들을 먼저 사용해보겠습니다. javafx.scene 패키지의 HorizontalAlignment 구성요소와 javafx.scene.paint 패키지의 Color 구성요소도 사용했습니다. 그러나 JavaFX 스크립트 패키지에 ProgressBar 구성요소는 아직 없습니다. 따라서 진행 표시줄을 생성하는 함수를 코딩했습니다. 이 시리즈의 3편에서는 JavaFX 스크립트 함수를 검토합니다. BorderPanelFlowPanel 등의 구성요소와 진행 표시줄의 함수를 사용하기로 결정하고 다음 JavaFXImageSearchUI1.fx 코드를 생성했습니다.


 
package com.sun.demo.jfx;

  import javafx.ext.swing.SwingFrame;
  import javafx.ext.swing.BorderPanel;
  import javafx.ext.swing.FlowPanel;
  import javafx.ext.swing.Label;
  import javafx.ext.swing.TextField;
  import javafx.ext.swing.List;
  import javafx.ext.swing.Component;
  import javafx.scene.paint.Color;
  import javafx.scene.HorizontalAlignment;
  import java.awt.Dimension;

  function createProgressBar(preferredSize:Integer[]) :Component {
      var jprogressbar = new javax.swing.JProgressBar();
      var comp = Component.fromJComponent(jprogressbar);
      comp.preferredSize = preferredSize;
      comp;
  }

  SwingFrame {
      title: "JFX Image Search"
      content: BorderPanel  {

          top: FlowPanel {
              alignment: HorizontalAlignment.LEFT
              content: [
              Label {
                  text: "Search"
                  },
              TextField {
                  columns: 50
              }
              ]
          }
          center: BorderPanel {
              top: FlowPanel {
                  alignment: HorizontalAlignment.LEFT
                  content: [
                  Label {
                      text: "Matched Images"

                      },
                  createProgressBar([360, 20])
                  ]
              }
              center: List {
                  preferredSize: [100, 200]
              }
              bottom: FlowPanel {
                  alignment: HorizontalAlignment.LEFT
                  content: [
                  Label {
                      text: "Selected Image"
                      },
                  createProgressBar([365, 20])
                  ]
              }
          }
          bottom: Label {
                  preferredSize: [400, 300]

          }

      }
      visible: true
  }
 

NetBeans IDE 6.1에 프로젝트를 생성한 후에 JFXImageSearchUI1.fx 파일에 위의 코드를 입력했습니다. 미리보기 버튼을 클릭하면 그림 3과 같은 결과가 나타납니다.

그림 3. JavaFX 이미지 검색 UI -- 첫 번째 시도


프레임의 컨텐츠는 BorderPanel 구성요소로, top 속성은 FlowPanel 구성요소에 지정되고 center 속성은 다른 BorderPanel 구성요소에 지정되며 bottom 속성은 Label 구성요소에 지정되었습니다. FlowPanel 구성요소는 content 등록 정보를 갖습니다. 이 등록 정보에 하나 이상의 구성요소를 넣을 수 있습니다. 여러 개의 구성요소를 삽입할 때는 array를 정의하는 괄호 속에 구성요소를 넣어야 합니다. 다음과 같이 쉼표를 사용하여 컨텐츠 array에서 개별 구성요소를 구분합니다.


  content: [
  Label {
     text: "Search"
     },
  TextField {
     columns: 50
  }
  ]
 

상단 FlowPanel에는 Label 구성요소와 TextField 구성요소가 중첩되어 있습니다. FlowPanel 의 방향 속성은 HorizontalAlignment.LEFT입니다. 이는 FlowPanel 내의 구성요소를 가로와 왼쪽으로 정렬합니다. 가운데의 BorderPanel은 이 영역의 상단에 가로로 정렬된 레이블과 진행 표시줄을, 중앙에 목록을, 하단에 다른 레이블과 진행 표시줄을 레이아웃합니다. 프레임의 하단에는 레이블이 들어갑니다.

FlowPanel 구성요소는 레이블-구성요소 쌍을 만들기에 좋습니다. 여기에서는 여러 개의 FlowPanel을 사용하여 레이블과 TextField 등의 구성요소를 묶었습니다.

createProgressBar 함수는 JavaFX 스크립트를 사용하여 Swing 구성요소로부터 javafx.gui 구성요소를 생성하는 방법을 보여줍니다. 여기에서는 Swing JProgressBar 구성요소로부터 JavaFX 스트립트 진행 표시줄을 생성했습니다.


 
 function createProgressBar(preferredSize:Integer[]) :Component {
          var jprogressbar = new javax.swing.JProgressBar();
          var comp = Component.fromJComponent(jprogressbar);
          comp.preferredSize = preferredSize;
          comp;
  }
 

첫 번째 시도에서 UI는 상당히 잘 작동하지만 원래 UI를 제대로 재현하지는 못합니다. 그래서 다른 접근 방법을 시도해 보겠습니다.


클러스터

원래 UI를 복제하려는 두 번째 시도에서는 JavaFX 스크립트 ClusterPanel 구성요소를 활용하겠습니다. 이 구성요소를 사용하여 프레임 내에서 구성요소를 클러스터할 수 있습니다. JavaFX 스크립트에는 ClusterPanel 내에서 구성요소를 순서대로나 병렬로 정리하기 위해 사용할 수 있는 SequentialClusterParallelCluster도 포함됩니다.

진행 표시줄, 검색 텍스트 필드, 축소판 이미지 목록 및 선택된 이미지 표시 영역의 최대 너비 및 높이를 설정하기 위해 JavaFX 스크립트 Layout 구성요소도 활용하기로 결정했습니다. Layout 구성요소는 UNLIMITED_SIZE 등의 레이아웃 관련 상수를 제공합니다.

UI의 두 번째 버전은 다음 JFXImageSearchUI.fx 파일에 코딩되었습니다.


   package com.sun.demo.jfx;

import javafx.ext.swing.Component;
import javafx.ext.swing.SwingFrame;
import javafx.ext.swing.Label;
import javafx.ext.swing.TextField;
import javafx.ext.swing.List;
import javafx.ext.swing.Cluster;
import javafx.ext.swing.Layout;
import javafx.ext.swing.ClusterPanel;
import javafx.ext.swing.SequentialCluster;
import javafx.ext.swing.ParallelCluster;
import javafx.scene.paint.Color;
import java.awt.Dimension;
import java.lang.System;

import javax.swing.border.LineBorder;

function createProgressBar() :Component {
var jprogressbar = new javax.swing.JProgressBar();
var comp = Component.fromJComponent(jprogressbar);
comp.hmax = Layout.UNLIMITED_SIZE;
comp;
}

var searchLabel = Label {
text: "Search:"

}
var searchTextField = TextField {
hmax: Layout.UNLIMITED_SIZE
columns: 50
}

var matchedImageLabel = Label {
text: "Matched Images"
}
var matchedImagePB = createProgressBar();


var thumbnailList = List {
preferredSize:[300, 230]
hmax: Layout.UNLIMITED_SIZE
vmax: Layout.UNLIMITED_SIZE
}

var selectedImageLabel = Label {
text: "Selected Image"
}
var selectedImagePB = createProgressBar();

var selectedImageDisplay = Label {
preferredSize: [400,300]
hmax: Layout.UNLIMITED_SIZE
vmax: Layout.UNLIMITED_SIZE
}

selectedImageDisplay.getJComponent().setOpaque(true);
selectedImageDisplay.getJComponent().setBackground(Color.WHITE.getAWTColor());
selectedImageDisplay.getJComponent().setBorder(new LineBorder(Color.BLACK.getAWTColor(), 1, true));

SwingFrame {
title: "JavaFX Image Search"
content:
// main panel within the frame
ClusterPanel {

hcluster: SequentialCluster {
content: [
ParallelCluster{ // mainCol
resizable: true
content: [
SequentialCluster{
content: [
ParallelCluster { //searchLabelCol
content: searchLabel
},
ParallelCluster { //searchTextFieldCol
resizable: true
content: searchTextField
}
]},
SequentialCluster {
content: [
ParallelCluster { //lblCol
content: matchedImageLabel
},
ParallelCluster { //progressBarCol
resizable: true
content: matchedImagePB
}
]},
thumbnailList,
SequentialCluster {
content: [
ParallelCluster { //lblCol
content: selectedImageLabel
},
ParallelCluster { //progressBarCol
resizable: true
content: selectedImagePB
}
]},
selectedImageDisplay
]
}
]

}

vcluster: SequentialCluster {
content: [
ParallelCluster{ //searchRow
content: SequentialCluster {
content: [
ParallelCluster { // row
content: [searchLabel, searchTextField]
}
]
}
},
ParallelCluster{ // matchedProgressRow
content: SequentialCluster {
content: [
ParallelCluster { // row
content: [matchedImageLabel, matchedImagePB]
}
]
}
},
ParallelCluster{ // thumbNailRow
resizable:true
content: thumbnailList
},
ParallelCluster{ // selectedProgressRow
content: SequentialCluster {
content: [
ParallelCluster { // row
content: [selectedImageLabel, selectedImagePB]
}
]
}
},
ParallelCluster{ // imageRow
content: selectedImageDisplay
}

]
}

}
visible: true
}


 
이 코드의 결과는 훨씬 보기 좋습니다.
그림 4에 나온 것처럼 구성요소는 이제 프레임의 왼쪽과 오른쪽으로 완벽하게 정렬되었습니다.

그림 4. JavaFX 이미지 검색 UI -- 두 번째 시도

프레임의 주 컨텐츠는 ClusterPanel 구성요소입니다. ClusterPanel 내에서 SequentialCluster 구성요소 그룹의 구성요소는 모두 순서대로 있습니다. ParallelCluster 구성요소 그룹의 구성요소는 병렬로 있습니다. 예를 들어 다음은 구성요소를 프레임의 상단 영역에 정리합니다. 검색 레이블과 검색 텍스트 필드를 병렬 열로 클러스터합니다.


 
 hcluster: SequentialCluster {
      content: [
          ParallelCluster{ // mainCol
              resizable: true
              content: [
                  SequentialCluster{
                      content: [
                          ParallelCluster {  //searchLabelCol
                              content: searchLabel
                          },
                          ParallelCluster { //searchTextFieldCol
                              resizable: true
                              content: searchTextField
                          }
              ]},
 

SequentialClusterSequentialClusterParallelCluster와 같은 ClusterElement 구성요소를 지정할 수 있는 content 속성을 갖습니다. ParallelCluster 구성요소는 크기 조정이 가능하므로 필요에 따라 프레임 너비를 채웁니다.

인터페이스의 나머지도 같은 패턴을 따릅니다. 인터페이스의 각 5개 영역 내에서 구성요소는 SequentialCluster 구성요소 내에서 ParallelCluster 구성요소를 사용하여 그룹화 및 정리됩니다. 예를 들어 다음 코드는 매칭되는 이미지 레이블과 매칭되는 이미지 진행 표시줄을 병렬 열로 클러스터합니다. 클러스터 다음에는 검색과 매칭되는 이미지의 축소판 목록이 나옵니다.


 
SequentialCluster {
      content: [
          ParallelCluster {  //lblCol
              content: matchedImageLabel
          },
          ParallelCluster { //progressBarCol
              resizable: true
              content: matchedImagePB
          }
      ]},
  thumbnailList,
 

한 가지 주목할 점은 javafx.ext.swing 패키지의 Label 구성요소가 테두리 특성, 불투명도 또는 배경 색상의 설정을 위한 속성을 현재 지원하지 않는다는 것입니다. 따라서 이들 설정을 위해 getJComponent 함수에 대한 Label 구성요소의 지원을 활용했습니다. 함수는 JavaFX 스크립트 구성요소로 캡슐화된 Swing jComponent를 반환합니다. 이 경우에는 Swing Label 구성요소를 반환합니다. 다음 코드를 사용하여 필요한 레이블 속성을 정의할 수 있습니다.


var selectedImageDisplay = Label {
         ...
  }

  selectedImageDisplay.getJComponent().setOpaque(true);
  selectedImageDisplay.getJComponent().setBackground(Color.WHITE.getAWTColor());
  selectedImageDisplay.getJComponent().setBorder(new LineBorder(Color.BLACK.getAWTColor(), 1, true));
 

설계 도구 없이 이미지 검색 사용자 인터페이스를 복제하는 것에 대해 처음에 걱정했던 것과 달리 가장 유용하고 필요한 기능은 빠르고 쉽게 액세스가 가능합니다. 또한 NetBeans IDE용 JavaFX 플러그인은 구성요소를 사용하여 작업할 때 사용 가능한 속성을 띄우는 문맥 인식 코드 완성을 제공합니다. 코드 완성을 사용하면 UI의 구현이 처음 생각처럼 어렵지만은 않습니다. 그림 5는 IDE에서 Ctrl+spacebar를 입력할 때 활성화되는 일부 팝업 옵션을 보여줍니다.


그림 5. 옵션


요약

UI 생성을 위한 선언적 구문을 살펴보기 위해 기존 애플리케이션의 UI를 이식했습니다. 원래 애플리케이션에서는 구성요소의 배치 및 정렬에 Swing의 GroupLayout을 사용했습니다. NetBeans IDE 6.1은 아직 JavaFX 스크립트를 위한 그래픽 설계 도구를 지원하지 않지만 수동으로 인터페이스를 레이아웃하는 것은 처음 걱정했던것 만큼 어렵지 않았습니다. ClusterPanel, SequentialClusterParallelCluster 구성요소의 조합을 통해 JavaFX 스크립트 구현은 원래 UI와 사실상 똑같아 보입니다. 이들 조합에 NetBeans IDE용 JavaFX 플러그인 및 문맥 인식 코드 완성이 더해져 작업은 더욱 쉬워졌습니다.


자세한 정보


이 글의 영문 원본은
# Learning Curve Journal, Part 2: Declarative User Interfaces
에서 보실 수 있습니다.

"Java FX" 카테고리의 다른 글

Posted by 1010
98..Etc/JavaFX2008. 11. 12. 17:28
반응형

2007년 8월과 9월에 썬 개발자 네트워크의 John O'Conner는 JavaFX 스크립트 프로그래밍 언어(이 기사에서는 JavaFX 스크립트라고 줄여서 부름)를 시작하는 사용자에게 도움을 주고자 "학습 곡선 일지(Learning Curve Journal)"라는 제목의 시리즈를 기고했습니다.

그 이후로 이 언어의 많은 중요한 부분이 개선되었습니다. 아마도 가장 중요한 변화는 JavaFX 스크립트의 초기 인터프리터 기반 버전을 대신하여 컴파일러 기반 버전을 사용할 수 있게 되었다는 점입니다. 이전의 학습 곡선 일지에서는 인터프리터 기반 버전 사용에 대해 설명했습니다. 업데이트된 학습 곡선 일지에서는 컴파일러 기반 버전의 언어 사용법을 보여줍니다. 최신 내용을 반영하여 다른 변경 사항도 적용되었습니다.

이 전과 마찬가지로 시리즈의 1편은 JavaFX 프로그램, 즉 JavaFX 스크립트 언어로 쓰여진 간단한 프로그램으로 시작합니다. JavaFX 스크립트에서의 프로그래밍을 위한 환경 설정 방법과 JavaFX 프로그램 빌드 및 실행 방법을 배우게 됩니다. 2편은 JavaFX 스크립트에서 사용 가능한 선언적 코딩 스타일에 중점을 둡니다. 이러한 스타일이 어떻게 그래픽 애플리케이션을 더욱 단순하고 직관적으로 만들 수 있는지 볼 수 있을 것입니다. 3편에서는 JavaFX 프로그램에서 작업 구현을 위한 JavaFX 스크립트 함수의 사용 방법을 보여줍니다. 4편에서는 웹 서비스 액세스를 위한 JavaFX 스크립트 사용 방법을 보여줍니다. 그 과정 중에 FX 스크립트에서 Swing 클래스와 같은 자바 기술 슬래스의 액세스가 얼마나 쉬운지도 보여줍니다.

JavaFX 스크립트는 개발자가 동적인 그래픽 컨텐츠 생성에 사용할 수 있는 새로운 스트립팅 언어입니다. 데스크탑에서 언어는 Swing 사용자 인터페이스(UI) 툴킷과 자바 2D API를 편리하게 사용할 수 있는 라이브러리를 제공합니다. Swing 또는 자바 2D를 대체하는 것이 아니고 풍부한 컨텐츠 개발자가 이들 API에 더욱 쉽게 액세스할 수 있도록 하는 것이 목적입니다. 모바일 시스템과 같은 다른 환경에서 JavaFX 스크립트는 Swing 이외의 사용자 인터페이스 기술을 사용합니다. JavaFX 스크립트를 사용하여 여러 가지 플랫폼과 운영 환경에서 실행되는 시각적으로 풍부한 애플리케이션을 만들 수 있습니다.

언어는 선언적 및 절차적 구문을 모두 제공합니다. 선언적으로 풍부한 사용자 인터페이스를 만든 다음 이벤트 처리 루틴과 작업을 추가할 수 있습니다.

그러나 대부분의 사용자는 좀 더 소박하게 시작해야 하며 이 기사의 목적도 그러합니다. 목표는 JavaFX 스크립트를 시작하는 방법을 보여주는 것입니다. 먼저 다음이 필요합니다.



자바 플랫폼 설정

개발자라면 물론 시스템에 JDK가 설치되어 있을 것입니다. 그러나 시스템을 한동안 업데이트하지 않은 경우에는 자바 SE 6이 설치되어 있는지 확인하십시오. 학습 곡선 일지는 JavaFX 스크립트의 컴파일러 기반 버전과 NetBeans IDE 6.1에서의 지원에 중점을 두고 있습니다. JavaFX 기술이 적용된 NetBeans IDE 6.1을 설치 및 사용하려면 시스템에 자바 SE 6의 최신 수준(현재는 자바 SE 6 업데이트 10 베타)을 설치하는 것이 좋습니다. 썬 개발자 네트워크의 자바 SE 다운로드 페이지에서 최신 JDK를 다운로드합니다. Mac OS X를 사용하는 경우에는 Apple Developer Connection의 자바 섹션에서 직접 Apple의 최신 자바 플랫폼 개발 키트(현재는 Mac OS X 10.5, 업데이트 1용 자바)를 다운로드 받을 수 있습니다.


자료 참조

새로운 환경이나 언어를 경험할 때는 교착 상태에 빠지거나 난관에 봉착하게 됩니다. 이는 최첨단 기술을 사용할 때 모두가 겪게 되는 과정입니다. 학습 곡선을 원만하게 하기 위해서는 훌륭한 문서와 예제가 매우 중요합니다. 썬 개발자 네트워크의 JavaFX 기술 허브와 함께 javafx.comProject OpenJFX 웹 사이트는 정확한 정보를 얻을 수 있는 최신 문서와 데모 자료를 제공합니다.

일 부 사용자들은 언어 참조 자료를 읽지도 않고 프로그래밍을 바로 시작하고자 할 수 있습니다. 또다른 사용자들은 JavaFX 스크립트를 실제로 사용하기 전에 모든 자료를 읽을 것입니다. 바로 시작하는 유형의 사용자더라도 일종의 언어 사양이나 자습서부터 시작해야 합니다. 전형적인 "Hello, world" 예제를 쓰기 전에 기본 언어 구문을 알아야 합니다. JavaFX 참조 페이지의 문서부터 시작하는 것이 좋습니다. 여기에서 JavaFX 스크립트 프로그래밍 언어 참조 자료 등의 참조 문서와 JavaFX 기술 시작하기NetBeans IDE를 사용하여 간단한 JavaFX 애플리케이션 생성 등의 많은 기사와 자습서로의 링크를 찾을 수 있습니다.


JavaFX 애플리케이션 생성

일부 언어 참조 문서를 읽었다면 이제는 간단한 JavaFX 애플리케이션을 만들어 볼 차례입니다. 명령줄에서 수동으로 JavaFX 애플리케이션 빌드 및 실행이 가능하긴 하지만 애플리케이션 개발을 단순화하는 많은 기능을 가진 NetBeans IDE 6.1를 사용해 봅시다. NetBeans용 JavaFX 플러그인을 설치해야 합니다.

NetBeans IDE 6.1을 설치하지 않은 경우에는 NetBeans IDE 6.1과 NetBeans용 JavaFX을 포함하는 하나의 패키지인 JavaFX 포함 NetBeans IDE 6.1 다운로드가 가능합니다. NetBeans IDE 6.1을 이미 설치한 경우에는 NetBeans 업데이트 센터에서 JavaFX 플러그인을 설치하여 JavaFX 기술 지원을 추가할 수 있습니다. NetBeans용 JavaFX는 현재 Windows 및 Mac OS/X 환경에서 사용 가능합니다. JavaFX 플러그인을 설치하면 NetBeans IDE 6.1을 사용하여 JavaFX 스크립트의 컴파일러 기반 버전으로 작성된 애플리케이션을 생성, 테스트, 디버그 및 배포할 수 있습니다. 플러그인은 JavaFX 스크립트 파일 포함을 위한 프로젝트 및 편집기 지원을 향상시킵니다. 또한 스크립트 엔진 및 라이브러리를 위한 코어 라이브러리도 제공합니다.

JavaFX 포함 NetBeans IDE 6.1이나 NetBeans용 JavaFX 플러그인을 설치했으면 첫 번째 JavaFX 애플리케이션을 빌드할 준비가 된 것입니다. 물론 "Hello, world!"부터 시작해야겠지요.

다음과 같이 프로젝트 생성을 시작합니다.

  1. 주 메뉴에서 File -> New Project를 선택합니다.
  2. New Project 마법사에서 JavaFX 범주와 JavaFX Script Application 프로젝트 유형을 선택합니다.
  3. Next 버튼을 클릭합니다.
  4. HelloWorldJFX와 같이 프로젝트의 이름을 지정합니다.
  5. 기본 프로젝트 위치를 수락하거나 다른 위치를 선택하여 이동합니다.
  6. Create Main Class 확인란을 선택된 상태로 두고 다른 기본 설정도 변경하지 않습니다.
  7. Finish 버튼을 클릭합니다.

그림 1과 같이 IDE는 지정된 프로젝트 폴더에 프로젝트 디렉토리를 생성하고 프로젝트 이름과 같은 HelloWorldJFX라는 이름을 부여합니다. HelloWorldJFX 프로젝트를 확장합니다. Source Packages 노드의 helloworldjfx 패키지 아래에 Main.fx 클래스 파일이 있습니다. IDE는 프로젝트 생성 시 Create Main Class 확인란이 선택되어 있었으므로 Main.fx 파일을 생성합니다. 이 파일에 애플리케이션의 소스 코드가 들어갑니다.


그림 1.
HelloWorldJFX 프로젝트 파일


  /*
   * Main.fx
   *
   * Created on ...
   */

  package helloworldjfx;

  /**
   * @author ...
   */

  // place your code here
 

// place your code here 라인을 다음 코드로 바꿉니다.

  import javafx.ext.swing.Label;

  Label {
      text: "Hello, world!"
  }
 


JavaFXScript 편집기는 기본 서식 설정과 코드 완성을 제공합니다. 우리와 같이 JavaFX 스크립트에 익숙하지 않은 프로그래머는 언어 구문에 확신이 들지 않을 때도 있으므로 코드 완성이 도움이 됩니다. Ctrl + Space 키를 누르면 편집기에서 코드 완성이 활성화됩니다.

또 한 JavaFX 스크립트 플러그인은 컴파일과 실행을 해 볼 필요 없이 애플리케이션의 결과를 볼 수 있는 미리보기 기능을 제공합니다. 소스 파일에 변경한 내용은 즉시 미리보기 창에 반영됩니다. 미리보기 기능은 현재 Mac OS X 플랫폼에서는 사용할 수 없습니다.

Enable Preview 버튼 을 클릭하여 미리보기 기능을 작동시킵니다. 그림 2와 같이 편집기 바로 위에서 출력을 볼 수 있습니다.



그림 2. 기본적인 "Hello, world!"

별로 놀라지 않으셨나요? 좋습니다. 설정 방법을 보여주려는 것 뿐이었지만 좀 더 재미있는 것을 해보도록 하겠습니다. JavaFX 환경은 모든 Swing UI 구성요소를 구현하므로 레이블에만 한정될 필요는 없습니다. 버튼이나 대화 상자 같은 다른 위젯을 사용할 수도 있습니다.

다음은 버튼의 이벤트 핸들러를 소개하는 예제입니다. 언어 참조 자료에서 actionfunction 구문을 읽었으면 버튼을 누를 때 메시지 상자가 표시되도록 해봅시다.


  import javafx.ext.swing.SwingFrame;
   
import javafx.ext.swing.Button;
   
import javafx.ext.swing.SwingDialog;
   
import javafx.ext.swing.Label;
 
   
SwingFrame {
       content
: Button {
           text
: "Press me!"
           action
: function() {
               
SwingDialog {
                   title
: "You pressed me"
                   content
: Label{ text: "Hey, don't do that!"}
                   visible
: true
               
}
       
}
   
}
 
   visible
: true
   
}



Main.fx 파일에 이를 입력한 후에 Press me! 버튼을 누르면 그림 3과 같은 결과가 나타납니다.

그림 3. "Press me!" 버튼 메시지


앞에서 말한 것처럼 미리보기 기능을 사용하여 코드를 컴파일 및 실행하지 않고도 애플리케이션의 결과를 볼 수 있습니다. 코드를 컴파일하려는 경우에는 Project 창에서 프로젝트를 마우스 오른쪽 버튼으로 클릭하고 Build Project를 선택합니다. 애플리케이션을 실행하려면 Project 창에서 프로젝트를 마우스 오른쪽 버튼으로 클릭하고 Run Project를 선택합니다.


구성요소 기반에서 노드 기반 UI로의 이동

이전 예제에서 Frame과 Dialog의 구성요소가 간단하게 FrameDialog가 아니라 SwingFrameSwingDialog라 는 클래스 이름을 갖는지 궁금하셨을지도 모릅니다. 그 답은 JavaFX 스크립트 개발자가 Swing 기반 구성요소의 계층 구조를 사용하는 기존 접근 방법 대신 노드 기반 접근 방법을 사용하여 UI를 구축하는 향후의 접근 방법에 관련되어 있습니다. 사실 이러한 향후의 접근 방법에 대한 초기 지원은 이미 JavaFX 라이브러리에 포함되어 있습니다. 예를 들면 javafx.application 패키지에는 노드 기반 접근 방법을 지원하는 Frame, DialogWindow 등의 클래스가 포함됩니다. 다른 접근 방법을 지원하는 클래스와의 혼동을 피하기 위해 javafx.application 패키지에 해당 항목이 있는 javafx.ext.swing 패키지는 클래스 이름에 접두어 "Swing"을 추가했습니다.

학습 곡선 시리즈에서는 계층 구조적인 Swing 기반 접근 방법을 사용했지만 javafx.ext.swing 패키지의 SwingFrame, SwingDialogSwingWindow와 같은 클래스는 임시적입니다. JavaFX 팀은 Swing 구성요소와 유사하지만 더욱 유연하고 강력한 새로운 노드 기반 구성요소 집합을 설계 중입니다.

노드 기반 접근 방법을 사용한 UI 구축에 대한 자세한 내용은 장면 그래프를 사용하여 JavaFX 스크립트에서 비주얼 개체 표시 기사를 참조하십시오. NetBeans IDE를 사용하여 간단한 JavaFX 애플리케이션 생성 자습서도 노드 기반 접근 방법을 사용하는 JavaFX 애플리케이션을 설명합니다.


프로파일

JavaFX는 특정 플랫폼이나 장치에서만 사용 가능한 클래스 그룹을 의미하는 프로파일을 지원합니다. javafx.ext.swing 패키지의 클래스는 데스크탑 프로파일에 들어 있으며 데스크탑 환경에서만 가용성이 보장됩니다. 예를 들어 많은 휴대 전화는 Swing 클래스를 갖지 않습니다. 휴대 전화와 TV를 포함한 모든 플랫폼에서 보장되는 클래스를 정의한 일반 프로파일도 있습니다. 새로운 노드 기반 구성요소는 일반 프로파일에 있으므로 모든 화면 및 장치에서 작동합니다.

JavaFX API 문서는 정확한 프로파일 사용을 보장하기 위해 하나의 프로파일에서 다른 프로파일로 전환할 수 있도록 하는 버튼을 제공합니다. 이 기사 시리즈에서는 데스크탑 환경을 위한 애플리케이션을 구축하므로 데스크탑 프로파일의 클래스와 일반 프로파일의 일부 클래스를 사용할 것입니다.


명령줄에서 JavaFX 애플리케이션 빌드 및 실행

다음과 같이 명령줄에서 JavaFX 애플리케이션을 빌드 및 실행할 수 있습니다.

  1. JavaFX Preview SDK 다운로드를 받습니다. SDK에는 JavaFX 스크립트 컴파일러, 문서, 런타임, 라이브러리 및 코드 샘플이 포함됩니다.
  2. 다운로드한 패키지를 확장합니다. 여러 디렉토리 중에 javafxcjavafx 명령을 위한 실행 가능 파일을 포함하는 bin 디렉토리가 보여야 합니다.
  3. fx 확장자를 갖는 파일(예: MyApp.fx)에 애플리케이션의 소스 코드를 저장합니다
  4. 다음과 같이 javafxc 명령을 수행하고 소스 파일을 지정하여 애플리케이션을 컴파일합니다.
       javafxc MyApp.fx

    애플리케이션을 위한 클래스 파일이 생성됩니다.

  5. 다음과 같이 javafx 명령을 수행하고 클래스 파일을 지정하여 애플리케이션을 위한 클래스 파일을 실행합니다.
       javafx MyApp

요약

새로운 기술을 조사할 때는 올바르게 시작하는 것이 중요합니다. 정확한 정보와 도구를 사용하여 시작하도록 하십시오. JavaFX 스크립트에 대한 최선의 방법은 4단계 절차를 따르는 것입니다.

  1. 최신의 Java SE Development Kit를 받습니다.
  2. 정보의 출처로 JavaFX 기술 허브, javafx.com 사이트Project OpenJFX 사이트를 사용합니다.
  3. IDE용 개발 플러그인을 받습니다. JavaFX 포함 NetBeans IDE 6.1은 새로운 JavaFX 스크립트의 컴파일러 기반 버전을 지원합니다. NetBeans IDE 6.1을 이미 설치한 경우에는 NetBeans 업데이트 센터에서 JavaFX 플러그인을 설치하여 JavaFX 기술 지원을 추가할 수 있습니다.
  4. 첫 번째 스크립트 시험에 미리보기 기능을 사용합니다.


자세한 정보

이 시리즈의 2편인 선언적 사용자 인터페이스를 참조하십시오.



이 글의 영문 원본은
Learning Curve Journal, Part 1: Exploring JavaFX Script
에서 보실 수 있습니다.


"Java FX" 카테고리의 다른 글

2008/08/27 10:05 2008/08/27 10:05
Posted by 1010
98..Etc/JavaFX2008. 11. 12. 17:27
반응형

2008년 7월 저는 그래픽 디자이너(Mark Dingman of Malden Labs)와 공동 작업으로 상상 속의 Sound Beans 애플리케이션을 만드는 연속 게시물이 포함된 JFX 사용자 지정 노드 카테고리를 시작했습니다.  이 애플리케이션을 작성하는 목표는 JavaFX 사용자 지정 노드를 만드는 방법을 보여주고, 그래픽 디자이너와 애플리케이션 개발자가 공동으로 JavaFX 애플리케이션을 효과적으로 개발할 수 있는 방법에 관한 사례 연구를 제공하기 위한 것입니다. 

이 연속 게시물의 첫 게시물인 자신만의 JavaFX "사용자 지정 노드" 만들기: 그래픽 메뉴의 예는 JavaFX에서 자신만의 UI를 만드는 방법을 보여줍니다. 해당 게시물에서는 마우스를 갖다댈 경우 밝아지고 확장되는 버튼으로 구성된 메뉴를 쉽게 만들 수 있도록 MenuNodeButtonNode 사용자 지정 노드를 정의했습니다.  이어지는 다음 게시물에서는 다음 내용을 다룹니다.

오늘의 게시물에서는 테이블의 행을 보고 선택할 수 있는 확장 가능한 테이블을 제공할 수 있도록 TableNode라는 이름의 사용자 지정 노드를 작성해보겠습니다. 테이블의 각 셀에는 Node의 하위 클래스를 포함할 수 있으므로 JavaFX SDK 1.0에서 사용할 노드 중심 방식으로 줄에 놓여질 수 있습니다. 또한, JavaFX SDK 1.0에 일종의 테이블 UI 컨트롤을 포함하려고 생각하고 있습니다. 다음은 상상 속의 Sound Beans 프로그램에서 사용하는 TableNode의 스크린샷입니다.

테이블


이것은 Mark Dingman이 제공한 재생 목록 comp(웹 사이트에 대한 종합 이미지, 모형)를 기초로 만든 것입니다(Getting Decked: Another JavaFX Custom Node post 참조). 그 다음에는 Mark에게 모양을 그려(이미지를 사용하는 것과는 다름) 구현할 수 있는 스크롤바 comp를 부탁했습니다. Mark의 comp에는 위에서 보듯이 수평 스크롤바 트랙과 함께 둥근 사각형 모양의 진행률 스크롤바 썸이 있습니다.

상상 속의 Sound Beans 프로그램에 대한 이번 반복에서는 테이블에서 다른 행을 클릭하면 UI의 왼쪽 위 모서리의 숫자가 변경되어 TableNodeselectedIndex 속성을 바인딩할 수 있음을 나타냅니다. 다음의 반복에서는 앨범 그래픽, 제목 등이 바뀌게 하고 왼쪽 위 모서리에 해당 번호가 나타나도록 할 것입니다. 이 Java Web Start 링크를 클릭하여 이 코드를 사용해보십시오. JRE 6 이상이 필요합니다. 또한 자바 SE 6 업데이트 10을 설치하면 배포 시간이 단축됩니다.

Web

다음은 TableNode.fx 파일에 있는 TableNode 사용자 지정 노드의 코드입니다.

/*
*  TableNode.fx -
*  A custom node that contains rows and columns, each cell
*  containing a node.
*
*  Developed 2008 by James L. Weaver (jim.weaver at lat-inc.com)
*  to demonstrate how to create custom nodes in JavaFX
*/


package com.javafxpert.custom_node;

import javafx.input.*;
import javafx.scene.*;
import javafx.scene.geometry.*;
import javafx.scene.paint.*;
import javafx.scene.transform.*;
import java.lang.System;

/*
* A custom node that contains rows and columns, each cell
* containing a node.  Column widths may be set individually,
* and the height of the rows can be set.  In addition, several
* other attributes such as width and color of the scrollbar
* may be set.  The scrollbar will show only when necessary,
* and overlays the right side of each row, so the rightmost
* column should be given plenty of room to display data and
* a scrollbar.
*/

public class TableNode extends CustomNode {

 
/*
   * Contains the height of the table in pixels.
   */

 
public attribute height:Integer = 200;
   
 
/*
   * Contains the height of each row in pixels.
   */

 
public attribute rowHeight:Integer;
   
 
/*
   * A sequence containing the column widths in pixels.  The
   * number of elements in the sequence determines the number of
   * columns in the table.
   */

 
public attribute columnWidths:Integer[];
   
 
/*
   * A sequence containing the nodes in the cells.  The nodes are
   * placed from left to right, continuing to the next row when
   * the current row is filled.
   */

 
public attribute content:Node[];
   
 
/*
   * The selected row number (zero-based)
   */

 
public attribute selectedIndex:Integer;
   
 
/*
   * The height (in pixels) of the space between rows of the table.
   * This space will be filled with the tableFill color.
   */

 
public attribute rowSpacing:Integer = 1;
   
 
/*
   * The background color of the table
   */

 
public attribute tableFill:Paint;
   
 
/*
   * The background color of an unselected row
   */

 
public attribute rowFill:Paint;
   
 
/*
   * The background color of a selected row
   */

 
public attribute selectedRowFill:Paint;
   
 
/*
   * The color or gradient of the vertical scrollbar.
   */

 
public attribute vertScrollbarFill:Paint = Color.BLACK;
   
 
/*
   * The color or gradient of the vertical scrollbar thumb.
   */

 
public attribute vertScrollbarThumbFill:Paint = Color.WHITE;
   
 
/*
   * The width (in pixels) of the vertical scrollbar.
   */

 
public attribute vertScrollbarWidth:Integer = 20;
   
 
/*
   * The number of pixels from the left of a cell to place the node
   */

 
private attribute cellHorizMargin:Integer = 10;
   
 
/*
   * Contains the width of the table in pixels.  This is currently a
   * calculated value based upon the specified column widths
   */

 
private attribute width:Integer = bind
    computePosition
(columnWidths, sizeof columnWidths);
   
 
private function computePosition(sizes:Integer[], element:Integer) {
   
var position = 0;
   
if (sizeof sizes > 1) {
     
for (i in [0..element - 1]) {
        position
+= sizes[i];
     
}
   
}
   
return position;
 
}
 
 
/**
   * The onSelectionChange function attribute that is executed when the
   * a row is selected
   */

 
public attribute onSelectionChange:function(row:Integer):Void;
   
 
/**
   * Create the Node
   */

 
public function create():Node {
   
var numRows = sizeof content / sizeof columnWidths;
   
var tableContentsNode:Group;
   
var needScrollbar:Boolean = bind (rowHeight + rowSpacing) * numRows  > height;
   
Group {
     
var thumbStartY = 0.0;
     
var thumbEndY = 0.0;
     
var thumb:Rectangle;
     
var track:Rectangle;
     
var rowRef:Group;
      content
: [
       
for (row in [0..numRows - 1], colWidth in columnWidths) {
         
Group {
            transform
: bind
             
Translate.translate(computePosition(columnWidths, indexof colWidth) +
                                  cellHorizMargin
,
                                 
((rowHeight + rowSpacing) * row) + (-1.0 * thumbEndY *
                                 
((rowHeight + rowSpacing) * numRows) / height))
            content
: bind [
             
Rectangle {
                width
: colWidth
                height
: rowHeight
                fill
: if (indexof row == selectedIndex)
                        selectedRowFill
                     
else
                        rowFill
             
},
             
Line {
                startX
: 0
                startY
: 0
                endX
: colWidth
                endY
: 0
                strokeWidth
: rowSpacing
                stroke
: tableFill
             
},
              rowRef
= Group {
               
var node =
                  content
[indexof row * (sizeof columnWidths) + indexof colWidth];
                transform
: bind Translate.translate(0, rowHeight / 2 -
                                                       node
.getHeight() / 2)
                content
: node
             
}
           
]
            onMouseClicked
:
             
function (me:MouseEvent) {
                selectedIndex
= row;
                onSelectionChange
(row);
             
}
         
}
       
},
       
// Scrollbar
       
if (needScrollbar)
         
Group {
            transform
: bind Translate.translate(width - vertScrollbarWidth, 0)
            content
: [
              track
= Rectangle {
                x
: 0
                y
: 0
                width
: vertScrollbarWidth
                height
: bind height
                fill
: vertScrollbarFill
             
},
             
//Scrollbar thumb
              thumb
= Rectangle {
                x
: 0
                y
: bind thumbEndY
                width
: vertScrollbarWidth
                height
: bind 1.0 * height / ((rowHeight + rowSpacing) * numRows) * height
                fill
: vertScrollbarThumbFill
                arcHeight
: 10
                arcWidth
: 10
                onMousePressed
: function(e:MouseEvent):Void {  
                  thumbStartY
= e.getDragY() - thumbEndY;  
               
}  
                onMouseDragged
: function(e:MouseEvent):Void {
                 
var tempY = e.getDragY() - thumbStartY;
                 
// Keep the scroll thumb within the bounds of the scrollbar
                 
if (tempY >=0 and tempY + thumb.getHeight() <= track.getHeight()) {
                    thumbEndY
= tempY;  
                 
}
                 
else if (tempY < 0) {
                    thumbEndY
= 0;
                 
}
                 
else {
                    thumbEndY
= track.getHeight() - thumb.getHeight();
                 
}
               
}
                onMouseDragged
: function(e:MouseEvent):Void {
                 
var tempY = e.getDragY() - thumbStartY;
                 
// Keep the scroll thumb within the bounds of the scrollbar
                 
if (tempY >=0 and tempY + thumb.getHeight() <= track.getHeight()) {
                    thumbEndY
= tempY;  
                 
}
                 
else if (tempY < 0) {
                    thumbEndY
= 0;
                 
}
                 
else {
                    thumbEndY
= track.getHeight() - thumb.getHeight();
                 
}
               
}
             
}
           
]
         
}  
       
else
         
null
     
]
      clip
:
       
Rectangle {
          width
: bind width
          height
: bind height
       
}
      onMouseWheelMoved
: function(e:MouseEvent):Void {
       
var tempY = thumbEndY + e.getWheelRotation() * 4;
       
// Keep the scroll thumb within the bounds of the scrollbar
       
if (tempY >=0 and tempY + thumb.getHeight() <= track.getHeight()) {
          thumbEndY
= tempY;  
       
}
       
else if (tempY < 0) {
          thumbEndY
= 0;
       
}
       
else {
          thumbEndY
= track.getHeight() - thumb.getHeight();
       
}
     
}
   
}    
 
}
}
 

public 속성에서 볼 수 있듯이, 테이블 높이, 행 높이, 각 열의 폭, 다양한 UI 요소의 색 또는 그라데이션 등 개발자가 구성할 수 있는 여러 가지 TableNode 속성이 있습니다. 목록 끝의 코드는 마우스 휠 지원을 제공합니다. 이제 The "Play" page로 주석 처리한 섹션을 중심으로 기본 프로그램을 살펴보겠습니다. 이 섹션이 TableNodeExampleMain.fx 파일에 TableNode 인스턴스가 만들어지는 부분입니다.

/*
 *  TableNodeExampleMain.fx -
 *  An example of using the TableNode custom node.  It also demonstrates
 *  the ProgressNode, DeckNode, MenuNode and ButtonNode custom nodes
 *
 *  Developed 2008 by James L. Weaver (jim.weaver at lat-inc.com)
 *  to demonstrate how to create custom nodes in JavaFX
 */

package com.javafxpert.table_node_example.ui;

import javafx.application.*;
import javafx.ext.swing.*;
import javafx.scene.*;
import javafx.scene.geometry.*;
import javafx.scene.image.*;
import javafx.scene.layout.*;
import javafx.scene.paint.*;
import javafx.scene.text.*;
import javafx.scene.transform.*;
import java.lang.Object;
import java.lang.System;
import com.javafxpert.custom_node.*;
import com.javafxpert.table_node_example.model.*;

var deckRef:DeckNode;

Frame {
 
var model = TableNodeExampleModel.getInstance();
 
var stageRef:Stage;
 
var menuRef:MenuNode;
  title
: "TableNode Example"
  width
: 500
  height
: 400
  visible
: true
  stage
:
    stageRef
= Stage {
      fill
: Color.BLACK
      content
: [
        deckRef
= DeckNode {
          fadeInDur
: 700ms
          content
: [
           
// The "Splash" page
           
Group {
             
var vboxRef:VBox;
             
var splashFont =
               
Font {
                  name
: "Sans serif"
                  style
: FontStyle.BOLD
                  size
: 12
               
};
              id
: "Splash"
              content
: [
               
ImageView {
                  image
:
                   
Image {
                      url
: "{__DIR__}images/splashpage.png"
                   
}
               
},
                vboxRef
= VBox {
                  translateX
: bind stageRef.width - vboxRef.getWidth() - 10
                  translateY
: 215
                  spacing
: 1
                  content
: [
                   
Text {
                      content
: "A Fictitious Audio Application that Demonstrates"
                      fill
: Color.WHITE
                      font
: splashFont
                   
},
                   
Text {
                      content
: "Creating JavaFX Custom Nodes"
                      fill
: Color.WHITE
                      font
: splashFont
                   
},
                   
Text {
                      content
: "Application Developer: Jim Weaver"
                      fill
: Color.WHITE
                      font
: splashFont
                   
},
                   
Text {
                      content
: "Graphics Designer: Mark Dingman"
                      fill
: Color.WHITE
                      font
: splashFont
                   
},
                 
]
               
}
             
]
           
},
           
// The "Play" page
           
VBox {
             
var tableNode:TableNode
              id
: "Play"
              spacing
: 4
              content
: [
               
Group {
                  content
: [
                   
ImageView {
                      image
:
                       
Image {
                          url
: "{__DIR__}images/playing_currently.png"
                       
}
                   
},
                   
Text {
                      textOrigin
: TextOrigin.TOP
                      content
: bind "{tableNode.selectedIndex}"
                      font
: Font {
                        size
: 24
                     
}
                   
}
                 
]
               
},
                tableNode
= TableNode {
                  height
: 135
                  rowHeight
: 25
                  rowSpacing
: 2
                  columnWidths
: [150, 247, 25, 70]
                  tableFill
: Color.BLACK
                  rowFill
: Color.rgb(28, 28, 28)
                  selectedRowFill
: Color.rgb(45, 45, 45)
                  selectedIndex
: -1
                  vertScrollbarWidth
: 20
                  vertScrollbarFill
: LinearGradient {
                    startX
: 0.0
                    startY
: 0.0
                    endX
: 1.0
                    endY
: 0.0
                    stops
: [
                     
Stop {
                        offset
: 0.0
                        color
: Color.rgb(11, 11, 11)
                     
},
                     
Stop {
                        offset
: 1.0
                        color
: Color.rgb(52, 52, 52)
                     
}
                   
]
                 
}
                  vertScrollbarThumbFill
: Color.rgb(239, 239, 239)
                  content
: bind
                   
for (obj in model.playlistObjects) {
                     
if (obj instanceof String)
                       
Text {
                          textOrigin
: TextOrigin.TOP
                          fill
: Color.rgb(183, 183, 183)
                          content
: obj as String
                          font
:
                           
Font {
                              size
: 11
                           
}
                       
}
                     
else if (obj instanceof Image)
                       
ImageView {
                          image
: obj as Image
                       
}
                     
else
                       
null
                   
}
                  onSelectionChange
:
                   
function(row:Integer):Void {
                     
System.out.println("Table row #{row} selected");
                   
}
               
}
             
]
           
},
           
// The "Burn" page
           
Group {
             
var vboxRef:VBox;
              id
: "Burn"
              content
: [
                vboxRef
= VBox {
                  translateX
: bind stageRef.width / 2 - vboxRef.getWidth() / 2
                  translateY
: bind stageRef.height / 2 - vboxRef.getHeight() / 2
                  spacing
: 15
                  content
: [
                   
Text {
                      textOrigin
: TextOrigin.TOP
                      content
: "Burning custom playlist to CD..."
                      font
:
                       
Font {
                          name
: "Sans serif"
                          style
: FontStyle.PLAIN
                          size
: 22
                       
}
                      fill
: Color.rgb(211, 211, 211)
                   
},
                   
ProgressNode {
                      width
: 430
                      height
: 15
                      progressPercentColor
: Color.rgb(191, 223, 239)
                      progressTextColor
: Color.rgb(12, 21, 21)
                      progressText
: bind "{model.remainingBurnTime} Remaining"
                      progressFill
:
                       
LinearGradient {
                          startX
: 0.0
                          startY
: 0.0
                          endX
: 0.0
                          endY
: 1.0
                          stops
: [
                           
Stop {
                              offset
: 0.0
                              color
: Color.rgb(0, 192, 255)
                           
},
                           
Stop {
                              offset
: 0.20
                              color
: Color.rgb(0, 172, 234)
                           
},
                           
Stop {
                              offset
: 1.0
                              color
: Color.rgb(0, 112, 174)
                           
},
                         
]
                       
}
                      barFill
:
                       
LinearGradient {
                          startX
: 0.0
                          startY
: 0.0
                          endX
: 0.0
                          endY
: 1.0
                          stops
: [
                           
Stop {
                              offset
: 0.0
                              color
: Color.rgb(112, 112, 112)
                           
},
                           
Stop {
                              offset
: 1.0
                              color
: Color.rgb(88, 88, 88)
                           
},
                         
]
                       
}
                      progress
: bind model.burnProgressPercent / 100.0
                   
},
                   
ComponentView {
                      component
:
                       
FlowPanel {
                          background
: Color.BLACK
                          content
: [
                           
Label {
                              text
: "Slide to simulate burn progress:"
                              foreground
: Color.rgb(211, 211, 211)
                           
},
                           
Slider {
                              orientation
: Orientation.HORIZONTAL
                              minimum
: 0
                              maximum
: 100
                              value
: bind model.burnProgressPercent with inverse
                              preferredSize
: [200, 20]
                           
}
                         
]
                       
}
                   
}
                 
]
               
}
             
]
           
},
           
// The "Config" page
           
Group {
              id
: "Config"
              content
: [
               
ImageView {
                  image
:
                   
Image {
                      url
: "{__DIR__}images/config.png"
                   
}
               
}
             
]
           
},
           
// The "Help" page
           
Group {
              id
: "Help"
              content
: [
               
ImageView {
                  image
:
                   
Image {
                      url
: "{__DIR__}images/help.png"
                   
}
               
}
             
]
           
}
         
]
       
},
        menuRef
= MenuNode {
          translateX
: bind stageRef.width / 2 - menuRef.getWidth() / 2
          translateY
: bind stageRef.height - menuRef.getHeight()
          buttons
: [
           
ButtonNode {
              title
: "Play"
              imageURL
: "{__DIR__}icons/play.png"
              action
:
               
function():Void {
                  deckRef
.visibleNodeId = "Play";
               
}
           
},
           
ButtonNode {
              title
: "Burn"
              imageURL
: "{__DIR__}icons/burn.png"
              action
:
               
function():Void {
                  deckRef
.visibleNodeId = "Burn";
               
}
           
},
           
ButtonNode {
              title
: "Config"
              imageURL
: "{__DIR__}icons/config.png"
              action
:
               
function():Void {
                  deckRef
.visibleNodeId = "Config";
               
}
           
},
           
ButtonNode {
              title
: "Help"
              imageURL
: "{__DIR__}icons/help.png"
              action
:
               
function():Void {
                  deckRef
.visibleNodeId = "Help";
               
}
           
},
         
]
       
}
     
]
   
}
}

deckRef
.visibleNodeId = "Splash";


The Model Behind the UI

"JavaFX 방식"은 UI 속성을 모델에 바인딩하는 것이므로 위에서 보는 바와 같이 TableNode의 콘텐츠 속성이 모델에 바인딩됩니다. 아래는 현재까지 TableNodeExampleModel.fx 파일에 만들어진 Sound Beans 프로그램의 모델입니다. playlistObjects 시퀀스에는 어떠한 종류의 개체도 포함할 수 있으며, Node 인스턴스를 모델에 포함하지만 않으면 됩니다(이러한 인스턴스는 UI에 속하므로). 그러므로 TableNode,를 채우려면 모델에 앨범 제목 및 이미지 URL 같은 문자열을 포함하는 방식을 사용합니다. 위에 표시된 TableModel의 콘텐츠 속성에 바인딩하는 동안 Node 하위 클래스(예: TextImageView)가 만들어집니다.

/*
 *  TableNodeExampleModel.fx -
 *  The model behind the TableNode example
 *
 *  Developed 2008 by James L. Weaver (jim.weaver at lat-inc.com)
 */

package com.javafxpert.table_node_example.model;

import java.lang.Object;
import javafx.scene.*;
import javafx.scene.image.*;
import javafx.scene.text.*;

/**
 * The model behind the TableNode example
 */

public class TableNodeExampleModel {
 
 
/**
   * The total estimated number of seconds for the burn.
   * For this example program, we'll set it to 10 minutes
   */

 
public attribute estimatedBurnTime:Integer = 600;

 
/**
   * The percent progress of the CD burn, represented by a number
   * between 0 and 100 inclusive.
   */

 
public attribute burnProgressPercent:Integer on replace {
     
var remainingSeconds = estimatedBurnTime * (burnProgressPercent / 100.0) as Integer;
      remainingBurnTime
= "{remainingSeconds / 60}:{%02d (remainingSeconds mod 60)}";
 
};

 
/**
   * The time remaining on the CD burn, expressed as a String in mm:ss
   */

 
public attribute remainingBurnTime:String;

 
/**
   * An image of a play button to be displayed in each row of the table
   */

 
private attribute playBtnImage = Image {url: "{__DIR__}images/play-btn.png"};
   
 
/**
   * The song information in the playlist
   */

 
public attribute playlistObjects:Object[] =
   
["Who'll Stop the Rain", "Three Sides Now", playBtnImage, "2:43",
     
"Jackie Blue", "Ozark Mountain Devils", playBtnImage, "2:15",
     
"Come and Get Your Love", "Redbone", playBtnImage, "3:22",
     
"Love Machine", "Miracles", playBtnImage, "2:56",
     
"25 or 6 to 4", "Chicago", playBtnImage, "3:02",
     
"Free Bird", "Lynard Skynard", playBtnImage, "5:00",
     
"Riding the Storm Out", "REO Speedwagon", playBtnImage, "3:00",
     
"Lay it on the Line", "Triumph", playBtnImage, "2:00",
     
"Secret World", "Peter Gabriel", playBtnImage, "4:00"];
 
 
 
 
//-----------------Use Singleton pattern to get model instance -----------------------
 
private static attribute instance:TableNodeExampleModel;

 
public static function getInstance():TableNodeExampleModel {
   
if (instance == null) {
      instance
= TableNodeExampleModel {};
   
}
   
else {
      instance
;
   
}
 
}
}


언제나처럼 질문이나 의견이 있으면 남겨 주십시오. 그리고 이 기사의 이미지를 다운로드한 후 이 그래픽으로 이 예에서 소개한 대로 작성하고 실행할 수 있습니다. 이 이미지는 프로젝트의 클래스 경로에서 확장할 수 있는 zip 파일입니다. 이 JFX 사용자 지정 노드 연속 게시물 중 이전 게시물에 소개했던 ButtonNode, MenuNode, DeckNode, ProgressNode 코드가 필요합니다.

감사합니다.
Jim Weaver
JavaFXpert.com

이 글의 영문 원본은
Creating a Custom Scrollable Table with JavaFX
에서 보실 수 있습니다.

"Java FX" 카테고리의 다른 글

Posted by 1010
98..Etc/JavaFX2008. 11. 12. 17:26
반응형

이제 JavaFX SDK 기술 Preview가 릴리스되었으므로 자신만의 "사용자 지정 노드"를 빠르게 만드는 방법을 설명하겠습니다. JavaFX에서 사용자 지정 노드는 위젯, 가젯, UI 구성 요소 등 어느 것이나 모두 의미할 수 있으나 목적은 동일합니다. 다시 사용 가능한 JavaFX 프로그램용 UI를 만들 수 있도록 하는 것입니다. 오늘의 예는 사용자 지정 노드(2개)를 만드는 방법을 보여줍니다. 다음 스크린샷을 참조하십시오.

 

그런데 현재 이 예에 구현된 코드가 단순해진 것은 Edgar Merino 덕분입니다. 이 코드를 사용해보려면 Java Web Start 링크를 클릭하십시오. JRE 6 이상이 필요합니다. 또한 자바 SE 6 업데이트 10을 설치하면 배포 시간이 단축됩니다.



JavaFX SDK Packages are Taking Shape 게시물에서 언급한 것처럼 JavaFX는 UI를 개발할 때 그래픽 "노드 중심" 방식을 채택하고 있으므로 JavaFX 사용자 인터페이스의 거의 대부분은 노드(Node)입니다.  사용자 지정 노드를 만들려는 경우 CustomNode 클래스를 확장하여 원하는 특성과 동작을 지정합니다.  아래 코드는 이 예에서 이미지를 표시하고 마우스 이벤트에 응답(예: 마우스를 위에 갖다대면 좀 더 투명해지고 텍스트가 표시되는 이벤트)하는 사용자 지정 노드를 만드는 코드입니다. 

주: javafx.ext.swing 패키지에 있는 Button 클래스를 사용하지 않는 이유가 궁금하실 것입니다. 이유는 Button 클래스는 Node가 아니라 Component이기 때문이며, 위에 언급한 대로 노드 중심 방식으로 변화되는 방향을 따르는 것이 가장 좋다고 생각합니다. 어떤 부분에서는 노드를 하위 클래스로 만드는 버튼이 나타납니다. 이 경우에는 ButtonNode 클래스가 더 이상 필요하지 않을 수 있습니다.

ButtonNode.fx


/*
*  ButtonNode.fx -
*  A node that functions as an image button
*
*  Developed 2008 by James L. Weaver (jim.weaver at lat-inc.com)
*  and Edgar Merino (http://devpower.blogsite.org/) to demonstrate how
*  to create custom nodes in JavaFX
*/


package com.javafxpert.custom_node;

import javafx.animation.*;
import javafx.input.*;
import javafx.scene.*;
import javafx.scene.effect.*;
import javafx.scene.geometry.*;
import javafx.scene.image.*;
import javafx.scene.paint.*;
import javafx.scene.text.*;
import javafx.scene.transform.*;

public class ButtonNode extends CustomNode {
 
/**
   * The title for this button
   */

 
public attribute title:String;

 
/**
   * The Image for this button
   */

 
private attribute btnImage:Image;

 
/**
   * The URL of the image on the button
   */

 
public attribute imageURL:String on replace {
    btnImage
=
     
Image {
        url
: imageURL
     
};
 
}
   
 
/**
   * The percent of the original image size to show when mouse isn't
   * rolling over it.  
   * Note: The image will be its original size when it's being
   * rolled over.
   */

 
public attribute scale:Number = 0.9;

 
/**
   * The opacity of the button when not in a rollover state
   */

 
public attribute opacityValue:Number = 0.8;

 
/**
   * The opacity of the text when not in a rollover state
   */

 
public attribute textOpacityValue:Number = 0.0;

 
/**
   * A Timeline to control fading behavior when mouse enters or exits a button
   */

 
private attribute fadeTimeline =
   
Timeline {
      toggle
: true
      keyFrames
: [
       
KeyFrame {
          time
: 600ms
          values
: [
            scale
=> 1.0 tween Interpolator.LINEAR,
            opacityValue
=> 1.0 tween Interpolator.LINEAR,
            textOpacityValue
=> 1.0 tween Interpolator.LINEAR
         
]
       
}
     
]
   
};

 
/**
   * This attribute is interpolated by a Timeline, and various
   * attributes are bound to it for fade-in behaviors
   */

 
private attribute fade:Number = 1.0;
 
 
/**
   * This attribute represents the state of whether the mouse is inside
   * or outside the button, and is used to help compute opacity values
   * for fade-in and fade-out behavior.
   */

 
private attribute mouseInside:Boolean;

 
/**
   * The action function attribute that is executed when the
   * the button is pressed
   */

 
public attribute action:function():Void;
   
 
/**
   * Create the Node
   */

 
public function create():Node {
   
Group {
     
var textRef:Text;
      content
: [
       
Rectangle {
          width
: bind btnImage.width
          height
: bind btnImage.height
          opacity
: 0.0
       
},
       
ImageView {
          image
: btnImage
          opacity
: bind opacityValue;
          scaleX
: bind scale;
          scaleY
: bind scale;
          translateX
: bind btnImage.width / 2 - btnImage.width * scale / 2
          translateY
: bind btnImage.height - btnImage.height * scale
          onMouseEntered
:
           
function(me:MouseEvent):Void {
              mouseInside
= true;
              fadeTimeline
.start();
           
}
          onMouseExited
:
           
function(me:MouseEvent):Void {
              mouseInside
= false;
              fadeTimeline
.start();
              me
.node.effect = null
           
}
          onMousePressed
:
           
function(me:MouseEvent):Void {
              me
.node.effect = Glow {
                level
: 0.9
             
};
           
}
          onMouseReleased
:
           
function(me:MouseEvent):Void {
              me
.node.effect = null;
           
}
          onMouseClicked
:
           
function(me:MouseEvent):Void {
              action
();
           
}
       
},
        textRef
= Text {
          translateX
: bind btnImage.width / 2 - textRef.getWidth() / 2
          translateY
: bind btnImage.height - textRef.getHeight()
          textOrigin
: TextOrigin.TOP
          content
: title
          fill
: Color.WHITE
          opacity
: bind textOpacityValue
          font
:
           
Font {
              name
: "Sans serif"
              size
: 16
              style
: FontStyle.BOLD
           
}
       
},
     
]
   
};
 
}
}  



위의 ButtonNode.fx 코드 목록에서는 다음 내용을 짚고 넘어가겠습니다.

  • ButtonNode 클래스는 CustomNode를 확장합니다.
  • 이 새로운 클래스는 사용자 지정 노드에 표시될 이미지와 텍스트를 저장하는 속성을 채용합니다.
  • create() 함수는 사용자 지정 노드의 UI 모양과 동작에 관한 선언 표현식을 반환합니다.
  • javafx.scene.effect 패키지의 Glow 효과는 이미지를 클릭할 경우 이미지에 빛나는 효과를 줄 때 사용됩니다.
  • 이미지의 투명도, 이미지의 크기, 사용자 지정 노드의 제목은 마우스를 누르거나 버튼을 놓을 때 전환됩니다. Timeline은 이러한 전환이 점진적으로 이루어지도록 하는 데 사용됩니다.
  • 투명도를 조정하고 Glow 효과를 적용한 후에 onMouseClicked 함수가 목록의 앞 부분에 정의된 action() 함수 속성을 호출합니다. 그러면 사용자 지정 노드가 Button과 유사하게 동작합니다.

"메뉴"에 ButtonNode 인스턴스 배열

Setting the "Stage" for the JavaFX SDK 게시물에서 설명한 것처럼 HBox 클래스는 javafx.scene.layout 패키지에 있으며, 이 패키지 안의 다른 노드를 배열하는 노드입니다. 아래와 같은 MenuNode 사용자 지정 노드는 ButtonNode 인스턴스를 수평으로 배열하고, javafx.scene.effects 패키지의 Reflection 클래스는 해당 버튼 아래에 멋진 반사 효과를 추가합니다. 코드는 다음과 같습니다.

MenuNode.fx

/*
 *  MenuNode.fx -
 *  A custom node that functions as a menu
 *
 *  Developed 2008 by James L. Weaver (jim.weaver at lat-inc.com)
 *  to demonstrate how to create custom nodes in JavaFX
 */


package com.javafxpert.custom_node;
 
import javafx.scene.*;
import javafx.scene.effect.*;
import javafx.scene.layout.*;

public class MenuNode extends CustomNode {

 
/*
   * A sequence containing the ButtonNode instances
   */

 
public attribute buttons:ButtonNode[];
   
 
/**
   * Create the Node
   */

 
public function create():Node {
   
HBox {
      spacing
: 10
      content
: buttons
      effect
:
       
Reflection {
          fraction
: 0.50
          topOpacity
: 0.8
       
}
   
}    
 
}
}  


사용자 지정 노드 사용

이제 사용자 지정 노드를 정의했으므로 간단한 프로그램에서 이 노드를 사용하는 방법을 보여드리겠습니다. 이 블로그를 따라해보신 분은 "JavaFX가 UI와 모델을 바인딩하는 방식"을 알게 되었을 것입니다. 이 간단한 예에서는 사용자 지정 노드를 만드는 방법을 알리는 데 중점을 두기 때문에 모델을 만들어서 이 모델에 UI를 바인딩하는 복잡한 작업까지 보여드리지는 않겠습니다. 대신, ButtonNode 인스턴스를 클릭할 때마다 문자열을 콘솔에 인쇄하는 간단한 작업을 보여드리겠습니다. 이번 예의 기본 프로그램의 코드는 다음과 같습니다.

MenuNodeExampleMain.fx

/*
 *  MenuNodeExampleMain.fx -
 *  An example of using the MenuNode custom node
 *
 *  Developed 2008 by James L. Weaver (jim.weaver at lat-inc.com)
 *  to demonstrate how to create custom nodes in JavaFX
 */

package com.javafxpert.menu_node_example.ui;

import javafx.application.*;
import javafx.scene.paint.*;
import javafx.scene.transform.*;
import java.lang.System;
import com.javafxpert.custom_node.*;

Frame {
 
var stageRef:Stage;
 
var menuRef:MenuNode;
  title
: "MenuNode Example"
  width
: 500
  height
: 400
  visible
: true
  stage
:
    stageRef
= Stage {
      fill
: Color.BLACK
      content
: [
        menuRef
= MenuNode {
          translateX
: bind stageRef.width / 2 - menuRef.getWidth() / 2
          translateY
: bind stageRef.height - menuRef.getHeight()
          buttons
: [
           
ButtonNode {
              title
: "Play"
              imageURL
: "{__DIR__}icons/play.png"
              action
:
               
function():Void {
                 
System.out.println("Play button clicked");
               
}
           
},
           
ButtonNode {
              title
: "Burn"
              imageURL
: "{__DIR__}icons/burn.png"
              action
:
               
function():Void {
                 
System.out.println("Burn button clicked");
               
}
           
},
           
ButtonNode {
              title
: "Config"
              imageURL
: "{__DIR__}icons/config.png"
              action
:
               
function():Void {
                 
System.out.println("Config button clicked");
               
}
           
},
           
ButtonNode {
              title
: "Help"
              imageURL
: "{__DIR__}icons/help.png"
              action
:
               
function():Void {
                 
System.out.println("Help button clicked");
               
}
           
},
         
]
       
}
     
]
   
}
}

앞서 언급한 대로 사용자가 해당 ButtonNode를 마우스로 클릭할 때마다 호출된 함수에 action 속성이 할당됩니다. 그리고 __DIR__ 표현식은 CLASS 파일이 있는 디렉토리로 평가됩니다. 이 경우 그래픽 이미지는 com/javafxpert/menu_node_example/ui/icons 디렉토리에 있습니다.

이 기사의 이미지를 다운로드한 후 이 그래픽으로 이 예에서 소개한 대로 작성하고 실행할 수 있습니다. 이 이미지는 프로젝트의 클래스 경로에서 확장할 수 있는 zip 파일입니다.

이 파일은 JavaFX SDK Technology Preview에 유용한 사용자 지정 노드 라이브러리를 작성하여 이 블로그의 JFX Custom Nodes 카테고리에 게시하기 위해 만든 것입니다. 사용자 지정 노드와 관련하여 아이디어가 있거나 자신이 개발한 사용자 지정 노드를 공유하려면 lat-inc.com의 jim.weaver로 연락해주십시오.

이 게시물을 실행한 후에 Weiqi Gao가 Java WebStart Works On Debian GNU/Linux 4.0 AMD64 게시물에 몇 가지 좋은 소식을 올렸습니다. JavaFX 스크립트 설명서의 기술 검토를 훌륭하게 해주셔서 저는 Weiqi(발음: 웨이치) 씨가 매우 좋습니다. ;-)

감사합니다.
Jim Weaver
JavaFX Script: Dynamic Java Scripting for Rich Internet/Client-side Applications

지금 바로 the book's Apress site에서 eBook(PDF)을 다운로드할 수 있습니다.

이 글의 영문 원본은
Rolling Your Own JavaFX "Custom Nodes": A Graphical Menu Example
에서 보실 수 있습니다.

"Java FX" 카테고리의 다른 글

Posted by 1010
01.JAVA/Java2008. 11. 12. 17:24
반응형

InetAddress 클래스는 자바 플랫폼이 처음 출시된 이래로 줄곧 사용되어 왔으며, 이 클래스의 역할은 인터넷 프로토콜(IP) 주소를 통해 호스트의 ID를 알려주는 것이다. 다시 말해서, 가령 yahoo.com 같은 이름을 입력하면 InetAddress 클래스의 도움으로 그 IP 주소를 알아낼 수 있다.

InetAddress 클래스 디자인은 다소 기묘해 보일 수도 있다. 클래스에는 static 선언만 있고 public 생성자가 없으며, 인스턴스는 불변, 즉 일단 클래스의 인스턴스를 가지면 변경할 수 없다. 하지만 그 디자인은 자바 보안의 목적을 위한 것으로서 사용자는 검색 간의 결과를 변경할 수 없다.

다음은 InetAddress 클래스의 사용 예제이다. 아래의 프로그램 Lookup은 명령어 라인에서 전달하는 아규먼트의 이름과 IP 주소를 알려준다.

   import java.net.*;

   public class Lookup {

     public static void main(String args[]) {
       for (String name: args) {
         try {
           InetAddress address = InetAddress.getByName(name);
           System.out.println("Name: " + address.getHostName());
           System.out.println("Addr: " + address.getHostAddress());
         } catch (UnknownHostException e) {
           System.err.println("Unable to lookup " + name);
         }
       }
     }
   }

이 프로그램에서 getByName() 메소드는 Microsoft Windows와 Unix의 nslookup 명령어와 같은 역할을 한다. 명령어 라인 아규먼트에 지정된 이름을 가져온 다음, 시스템에 지정된 네임 서버에서 룩업을 수행한다. 룩업을 수행할 때는 일반적으로 도메인 네임 시스템(DNS)을 이용한다. 룩업 작업은 yahoo.com 같은 이름이나 66.94.234.13 같은 IP 주소를 대상으로 이루어지는데, yahoo.com을 입력하면 66.94.234.13을 얻게 되고, 66.94.234.13을 입력하면 Yahoo의 호스트 팜(farm) 이름 목록을 얻게 된다. 이 경우 호스트의 퍼블릭 이름은 w2.rc.vip.scd.yahoo.com이다.

   > java Lookup sun.com yahoo.com 66.94.234.13

     Name: sun.com
     Addr: 209.249.116.195
     Name: yahoo.com
     Addr: 216.109.112.135
     Name: w2.rc.vip.scd.yahoo.com
     Addr: 66.94.234.13
	 

일부 호스트 이름은 복수의 IP 주소로 변환된다. InetAddressgetByName() 메소드를 이용해서 하나의 이름을 룩업하는 대신 getAllByName() 메소드를 이용하여 일련의 InetAddress 오브젝트를 얻어낼 수 있다.

아울러, InetAddress 클래스는 역순 이름 해석을 지원한다. 이는 특정 호스트에 대해 IP 주소를 룩업한 다음 그 IP 주소를 이용하여 호스트 이름을 룩업할 수 있음을 의미한다. 두 가지가 일치하지 않더라도 이것이 반드시 잘못되었다고 볼 수는 없겠지만, 만약 이 두 가지가 일치해야 하고 여러분이 그 사실을 알고 있는 상태라면 불일치 현상은 스푸핑 공격의 징후로 간주될 수 있다.

아래의 LookupAll 프로그램은 getAllByName() 메소드를 이용하여 호스트에 대한 모든 주소를 룩업한다. 그런 다음 getCanonicalHostName() 메소드를 이용하여 호스트에 대해 완전한 자격을 갖춘 도메인 이름을 룩업한다.

   import java.net.*;

   public class LookupAll {
     public static void main(String args[]) {
       for (String name: args) {
         try {
           InetAddress address[] = 
               InetAddress.getAllByName(name);
           for (InetAddress each: address) {
             System.out.println("Name: " + each.getHostName());
             System.out.println("Addr: " + 
                 each.getHostAddress());
             System.out.println("Canonical: " + 
                 each.getCanonicalHostName());
           }
           System.out.println("----");
         } catch (UnknownHostException e) {
           System.err.println("Unable to lookup " + name);
         }
       }
     }
   }

복수의 주소를 가지는 호스트의 하나로 google.com을 들 수 있는데, 다음 예제에는 google.com과 yahoo.com이 아규먼트로 포함되어 있다. 예제는 어떻게 yahoo.com에 대한 IP 주소의 역 룩업이 다른 호스트 이름으로 해석되는지를 잘 보여주고 있다.

   >> java LookupAll yahoo.com google.com

      Name: yahoo.com
      Addr: 216.109.112.135
      Canonical: w2.rc.vip.dcn.yahoo.com
      Name: yahoo.com
      Addr: 66.94.234.13
      Canonical: w2.rc.vip.scd.yahoo.com
      ----
      Name: google.com
      Addr: 216.239.39.99
      Canonical: 216.239.39.99
      Name: google.com
      Addr: 216.239.57.99
      Canonical: 216.239.57.99
      Name: google.com
      Addr: 216.239.37.99
      Canonical: 216.239.37.99
      ----

J2SE 플랫폼의 1.4 버전에는 getCanonicalHostName() 메소드가 추가되었다.

J2SE 5.0에서는 InetAddress를 이용하여 호스트의 도달 능력(reachability)을 확인할 수도 있다. 다시 말해서 호스트가 ‘살아있는지(즉, 활성화되어있는지)’의 여부를 결정할 수 있는 것이다. 이 테스트는 일반적으로 명령어 라인에서 ‘ping’ 명령어나 간단한 TCP ECHO 요청을 통해 수행된다. 그러나, 가령 2005년 9월 13일자 테크팁 Runtime.exe에서 ProcessBuilder까지 에서 설명했던 ProcessBuilder 클래스를 이용하여 굳이 명령어 라인 프롬프트를 열어야 할 필요가 있을까라는 질문을 던져본다. 그 대신 우리는 InetAddress에서 isReachable() 메소드를 이용하기만 하면 된다.

isReachable() 메소드에는 다음 두 가지 형태가 있다:

  • public boolean isReachable(int timeout) throws IOException
  • public boolean isReachable(NetworkInterface netif, int ttl, int timeout) throws IOException

대부분의 경우에는 첫 번째 버전의 isReachable로도 충분하다. 이 포맷에서는 단순히 검사(check)를 위한 타임아웃을 제공하는데, 타임아웃은 대상 호스트가 살아있는지 알아보기 위해 기다려야 하는 밀리초의 수를 나타낸다. 호스트가 죽어있으면 메소드는 ‘false’를 반환하고 핑 서비스는 사용 불능 상태로 되거나 요청은 방화벽에서 차단된다. 도달 가능한 호스트를 찾을 경우에는 메소드는 ‘true’를 반환하고, 네트워크 오류가 발행할 경우에는 메소드는 IOException을 전달한다

두 번째 버전의 isReachable은 테스트에 어떤 NetworkInterface를 이용할지 확인할 수 있게 해준다. 또한 특정 호스트에 도달하기 위해 시도하는 최대 호프(hop) 수를 지정할 수도 있다. 하나의 네트워크 인터페이스만을 가지고 있지만 호프 수를 지정하고자 한다면 임의의 인터페이스에 대해 null을 전달하면 된다. 첫 번째 버전의 isReachable과 마찬가지로, 타임아웃 값도 지정한다. 이 버전의 isReachable에서 가능한 반환 값은 원(1) 아규먼트 버전의 경우와 동일하다.

다음 프로그램 LookupReach는 이전의 단일 이름 룩업에 도달 가능 검사(check)를 추가한다.

   import java.io.*;
   import java.net.*;

   public class LookupReach {
     public static void main(String args[]) {
       for (String name: args) {
         try {
           InetAddress address = InetAddress.getByName(name);
           System.out.println("Name: " + address.getHostName());
           System.out.println("Addr: " + 
               address.getHostAddress());
           System.out.println("Reach: " + 
               address.isReachable(3000));
         } catch (UnknownHostException e) {
           System.err.println("Unable to lookup " + name);
         } catch (IOException e) {
           System.err.println("Unable to reach " + name);
         }
       }
     }
   }

yahoo.com, sun.com, goole.com 등을 아규먼트로 하여 LookupReach를 실행해 본다. 프로그램이 ‘false’를 반환하는 것을 보게 될 것이다. 거대 웹 기업들은 대부분 자사의 인터넷 사이트에서 이 서비스를 사용 불능으로 하거나 요청을 차단하고 있다. 다시 web.mit.edu 같은 대학교 사이트에 대한 아규먼트로 프로그램을 실행해 본다. 대부분의 대학교 사이트는 요청을 허용한다.

   > java LookupReach yahoo.com sun.com google.com web.mit.edu
   
     Name: yahoo.com
     Addr: 216.109.112.135
     Reach: false
     Name: sun.com
     Addr: 209.249.116.195
     Reach: false
     Name: google.com
     Addr: 216.239.37.99
     Reach: false
     Name: web.mit.edu
     Addr: 18.7.22.69
     Reach: true

InetAddress 클래스를 이용할 때는 일반적으로 사용되는 a.b.c.d. 형태로 된 IPv4 이름을 지정할 수 있다는 것을 염두에 두어야 한다. 또한 더 최근의 x:x:x:x:x:x:x:x형태로 이루어진 IPv6 이름도 지정할 수 있는데, 여기서 ‘x’는 8개의 16비트 주소에 대한 헥스(hex) 값이다. IPv6 아키텍처에 관한 자세한 내용은 RFC 2373을 참조하도록 한다.

이름이 어떤 버전을 나타내는지 확실치 않으면 InetAddressInet4Address 또는 Inet6Address의 인스턴스인지 확인하면 되는데, 이 작업은 주로 사용자가 제공한 주소에 대해서 수행된다.

마지막으로 짚고 넘어가야 한 사항은 이름 룩업과 역 룩업은 비용이 많이 드는 연산이라는 점이다. 시스템은 성능과 보안의 두 가지 이유에서 캐시 메커니즘을 이용한다. 일단 호스트를 룩업하면 동일한 결과를 얻게 되고 룩업 간에 결과를 변경할 수 없다는 점을 여러분은 잘 알고 있을 것이다.

"Java SE" 카테고리의 다른 글

Posted by 1010
01.JAVA/Java2008. 11. 12. 17:24
반응형

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

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

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

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

  • getDefault()
  • setDefault(CookieHandler)

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

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

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

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

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

   import java.io.*;
   import java.net.*;
   import java.util.*;

   public class Fetch {
     public static void main(String args[]) throws Exception {
       if (args.length == 0) {
         System.err.println("URL missing");
         System.exit(-1);
       }
       String urlString = args[0];
       CookieHandler.setDefault(new ListCookieHandler());
       URL url = new URL(urlString);
       URLConnection connection = url.openConnection();
       Object obj = connection.getContent();
       url = new URL(urlString);
       connection = url.openConnection();
       obj = connection.getContent();
     }
   }

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

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

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

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

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

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

   import java.net.*;
   import java.util.*;

   public class ListHeaders {
     public static void main(String args[]) throws Exception {
       if (args.length == 0) {
         System.err.println("URL missing");
       }
       String urlString = args[0];
       URL url = new URL(urlString);
       URLConnection connection = url.openConnection();
       Map<String,List<String>> headerFields = 
         connection.getHeaderFields();
       Set<String> set = headerFields.keySet();
       Iterator<String> itor = set.iterator();
       while (itor.hasNext()) {
         String key = itor.next();
         System.out.println("Key: " + key + " / " + 
           headerFields.get(key));
       }
     }
   }

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

   Key: <key> / [<value>]

따라서 다음을 입력하면,

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

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

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

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

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

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

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

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

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

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

    if (setCookieList != null) {
      for (String item : setCookieList) {
        Cookie cookie = new Cookie(uri, item);
        // Remove cookie if it already exists in cache
        // New one will replace it
        for (Cookie existingCookie : cache) {
          ...
        }
        System.out.println("Adding to cache: " + cookie);
        cache.add(cookie);
      }
    }

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

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

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

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

    // Retrieve all the cookies for matching URI
    // Put in comma-separated list
    StringBuilder cookies = new StringBuilder();
    for (Cookie cookie : cache) {
      // Remove cookies that have expired
      if (cookie.hasExpired()) {
        cache.remove(cookie);
      } else if (cookie.matches(uri)) {
        if (cookies.length() > 0) {
          cookies.append(", ");
        }
        cookies.append(cookie.toString());
      }
    }

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

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

    // Map to return
    Map<String, List<String>> cookieMap =
      new HashMap<String, List<String>>(requestHeaders);

    // Convert StringBuilder to List, store in map
    if (cookies.length() > 0) {
      List<String> list =
        Collections.singletonList(cookies.toString());
      cookieMap.put("Cookie", list);
    }
    return Collections.unmodifiableMap(cookieMap);

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

   import java.io.*;
   import java.net.*;
   import java.util.*;

   public class ListCookieHandler extends CookieHandler {

     // "Long" term storage for cookies, not serialized so only
     // for current JVM instance
     private List<Cookie> cache = new LinkedList<Cookie>();

     /**
      * Saves all applicable cookies present in the response 
      * headers into cache.
      * @param uri URI source of cookies
      * @param responseHeaders Immutable map from field names to 
      * lists of field
      *   values representing the response header fields returned
      */

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

       System.out.println("Cache: " + cache);
       List<String> setCookieList = 
         responseHeaders.get("Set-Cookie");
       if (setCookieList != null) {
         for (String item : setCookieList) {
           Cookie cookie = new Cookie(uri, item);
           // Remove cookie if it already exists
           // New one will replace
           for (Cookie existingCookie : cache) {
             if((cookie.getURI().equals(
               existingCookie.getURI())) &&
                (cookie.getName().equals(
                  existingCookie.getName()))) {
              cache.remove(existingCookie);
              break;
            }
          }
          System.out.println("Adding to cache: " + cookie);
          cache.add(cookie);
        }
      }
    }

    /**
     * Gets all the applicable cookies from a cookie cache for 
     * the specified uri in the request header.
     *
     * @param uri URI to send cookies to in a request
     * @param requestHeaders Map from request header field names 
     * to lists of field values representing the current request 
     * headers
     * @return Immutable map, with field name "Cookie" to a list 
     * of cookies
     */

    public Map<String, List<String>> get(
        URI uri,
        Map<String, List<String>> requestHeaders)
          throws IOException {

      // Retrieve all the cookies for matching URI
      // Put in comma-separated list
      StringBuilder cookies = new StringBuilder();
      for (Cookie cookie : cache) {
        // Remove cookies that have expired
        if (cookie.hasExpired()) {
          cache.remove(cookie);
        } else if (cookie.matches(uri)) {
          if (cookies.length() > 0) {
            cookies.append(", ");
          }
          cookies.append(cookie.toString());
        }
      }

      // Map to return
      Map<String, List<String>> cookieMap =
        new HashMap<String, List<String>>(requestHeaders);

      // Convert StringBuilder to List, store in map
      if (cookies.length() > 0) {
        List<String> list =
          Collections.singletonList(cookies.toString());
        cookieMap.put("Cookie", list);
      }
        System.out.println("Cookies: " + cookieMap);
    return Collections.unmodifiableMap(cookieMap);
    }
  }

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

   public Cookie(URI uri, String header) {
     String attributes[] = header.split(";");
     String nameValue = attributes[0].trim();
     this.uri = uri;
     this.name = nameValue.substring(0, nameValue.indexOf('='));
     this.value = nameValue.substring(nameValue.indexOf('=')+1);
     this.path = "/";
     this.domain = uri.getHost();

     for (int i=1; i < attributes.length; i++) {
       nameValue = attributes[i].trim();
       int equals = nameValue.indexOf('=');
       if (equals == -1) {
         continue;
       }
       String name = nameValue.substring(0, equals);
       String value = nameValue.substring(equals+1);
       if (name.equalsIgnoreCase("domain")) {
         String uriDomain = uri.getHost();
         if (uriDomain.equals(value)) {
           this.domain = value;
         } else {
           if (!value.startsWith(".")) {
             value = "." + value;
           }
           uriDomain = 
             uriDomain.substring(uriDomain.indexOf('.'));
           if (!uriDomain.equals(value)) {
             throw new IllegalArgumentException(
               "Trying to set foreign cookie");
           }
           this.domain = value;
         }
       } else if (name.equalsIgnoreCase("path")) {
         this.path = value;
       } else if (name.equalsIgnoreCase("expires")) {
         try {
           this.expires = expiresFormat1.parse(value);
         } catch (ParseException e) {
           try {
             this.expires = expiresFormat2.parse(value);
           } catch (ParseException e2) {
             throw new IllegalArgumentException(
               "Bad date format in header: " + value);
           }
         }
       }
     }
  }

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

   public boolean hasExpired() {
     if (expires == null) {
       return false;
     }
     Date now = new Date();
     return now.after(expires);
   }

   public String toString() {
     StringBuilder result = new StringBuilder(name);
     result.append("=");
     result.append(value);
     return result.toString();
   }

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

   public boolean matches(URI uri) {

     if (hasExpired()) {
       return false;
     }

     String path = uri.getPath();
     if (path == null) {
       path = "/";
     } 

     return path.startsWith(this.path);
   }

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

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

   import java.net.*;
   import java.text.*;
   import java.util.*;

   public class Cookie {

     String name;
     String value;
     URI uri;
     String domain;
     Date expires;
     String path;

     private static DateFormat expiresFormat1
         = new SimpleDateFormat("E, dd MMM yyyy k:m:s 'GMT'", Locale.US);

     private static DateFormat expiresFormat2
        = new SimpleDateFormat("E, dd-MMM-yyyy k:m:s 'GMT'", Local.US);
		

     /**
      * Construct a cookie from the URI and header fields
      *
      * @param uri URI for cookie
      * @param header Set of attributes in header
      */
     public Cookie(URI uri, String header) {
       String attributes[] = header.split(";");
       String nameValue = attributes[0].trim();
       this.uri = uri;
       this.name = 
         nameValue.substring(0, nameValue.indexOf('='));
       this.value = 
         nameValue.substring(nameValue.indexOf('=')+1);
       this.path = "/";
       this.domain = uri.getHost();

       for (int i=1; i < attributes.length; i++) {
         nameValue = attributes[i].trim();
         int equals = nameValue.indexOf('=');
         if (equals == -1) {
           continue;
         }
         String name = nameValue.substring(0, equals);
         String value = nameValue.substring(equals+1);
         if (name.equalsIgnoreCase("domain")) {
           String uriDomain = uri.getHost();
           if (uriDomain.equals(value)) {
             this.domain = value;
           } else {
             if (!value.startsWith(".")) {
               value = "." + value;
             }
             uriDomain = uriDomain.substring(
               uriDomain.indexOf('.'));
             if (!uriDomain.equals(value)) {
               throw new IllegalArgumentException(
                 "Trying to set foreign cookie");
             }
             this.domain = value;
           }
         } else if (name.equalsIgnoreCase("path")) {
           this.path = value;
         } else if (name.equalsIgnoreCase("expires")) {
           try {
             this.expires = expiresFormat1.parse(value);
           } catch (ParseException e) {
             try {
               this.expires = expiresFormat2.parse(value);
             } catch (ParseException e2) {
               throw new IllegalArgumentException(
                 "Bad date format in header: " + value);
             }
           }
         }
       }
     }

     public boolean hasExpired() {
       if (expires == null) {
         return false;
       }
       Date now = new Date();
       return now.after(expires);
     }

     public String getName() {
       return name;
     }

     public URI getURI() {
       return uri;
     }

     /**
      * Check if cookie isn't expired and if URI matches,
      * should cookie be included in response.
      *
      * @param uri URI to check against
      * @return true if match, false otherwise
      */
     public boolean matches(URI uri) {

       if (hasExpired()) {
         return false;
       }

      String path = uri.getPath();
       if (path == null) {
         path = "/";
       }

       return path.startsWith(this.path);
     }

     public String toString() {
       StringBuilder result = new StringBuilder(name);
       result.append("=");
       result.append(value);
       return result.toString();
     }
   }

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

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

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

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

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

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

"Java SE" 카테고리의 다른 글

Posted by 1010
01.JAVA/Java2008. 11. 12. 17:23
반응형

Java SE 6.0(코드명 Mustang)에는 Swing JTable의 내용을 훨씬 쉽게 정렬하고 필터링할 수 있게 해주는 몇 가지 기능이 추가된다. (이 기능들이 최종적으로 포함되려면 JCP의 승인을 거쳐야 한다.) 최근의 테이블 중심 사용자 인터페이스는 대부분 사용자가 테이블 헤더를 클릭하여 칼럼을 정렬할 수 있도록 되어 있는데, 이는 Mustang 이전에 Swing JTable 지원을 통해 가능하게 되었다. 이 기능을 필요로 하는 각 테이블에 일일이 수동으로 기능을 추가해 주어야만 하는 불편이 따랐지만 Mustang은 최소한의 노력으로 이 기능을 사용할 수 있도록 해준다. 필터링은 사용자 인터페이스에서 일반적으로 이용할 수 있는 또 다른 옵션으로서, 테이블 내에서 사용자가 제공하는 기준에 부합하는 행만을 디스플레이할 수 있게 해준다. Mustang을 이용하면 JTable 컨텐츠 필터링이 훨씬 용이해진다.

행 정렬하기

Mustang에서 행을 정렬하고 필터링하는 기준이 되는 것이 바로 추상 RowSorter 클래스로서, 이 RowSorter는 두 가지 매핑-JTable 내의 한 행을 기본 모델의 엘리먼트로, 그리고 다시 반대로-을 유지한다. 이는 하나의 행이 정렬과 필터링을 수행할 수 있게 해준다. 이 클래스는 TableModelListModel 모두에 적용될 만큼 포괄적이긴 하지만 TableRowSorter에만 JTable에 적용되는 Mustang 라이브러리가 제공된다.

가장 간단한 경우를 예로 들면, TableModelTableRowSorter 생성자에 패스한 다음 생성된 RowSorterJTablesetRowSorter() 메소드로 패스한다. 다음은 이런 방식을 보여주는 예제 프로그램 SortTable이다.
   import javax.swing.*;
   import javax.swing.table.*;
   import java.awt.*;

   public class SortTable {
     public static void main(String args[]) {
       Runnable runner = new Runnable() {
        public void run() {
           JFrame frame = new JFrame("Sorting JTable");
           frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
           Object rows[][] = {
               {"AMZN", "Amazon", 41.28},
               {"EBAY", "eBay", 41.57},
               {"GOOG", "Google", 388.33},
               {"MSFT", "Microsoft", 26.56},
               {"NOK", "Nokia Corp", 17.13},
               {"ORCL", "Oracle Corp.", 12.52},
               {"SUNW", "Sun Microsystems", 3.86},
               {"TWX",  "Time Warner", 17.66},
               {"VOD",  "Vodafone Group", 26.02},
               {"YHOO", "Yahoo!", 37.69}
             };
           String columns[] = {"Symbol", "Name", "Price"};
           TableModel model =
               new DefaultTableModel(rows, columns) {
             public Class getColumnClass(int column) {
               Class returnValue;
               if ((column >= 0) && (column < getColumnCount())) {
                 returnValue = getValueAt(0, column).getClass();
               } else {
                 returnValue = Object.class;
               }
               return returnValue;
             }
           };

           JTable table = new JTable(model);
           RowSorter<TableModel> sorter =
             new TableRowSorter<TableModel>(model);
           table.setRowSorter(sorter);
           JScrollPane pane = new JScrollPane(table);
           frame.add(pane, BorderLayout.CENTER);
           frame.setSize(300, 150);
           frame.setVisible(true);
         }
       };
       EventQueue.invokeLater(runner);
     }
   } 
Sort Table 1

디스플레이된 테이블의 특정 칼럼을 클릭하고 칼럼의 내용이 재정리되는 것을 살펴본다.

Sort Table 2

커스텀 서브클래스를 생성하느니 차라리 DefaultTableModel을 이용하면 안 되느냐고 질문을 던질지도 모른다. 그 대답은, TableRowSorter가 칼럼 정렬 시 적용되는 일련의 규칙을 가진다는 것이다. 기본값으로, 테이블 내의 모든 칼럼은 Object 타입으로 간주된다. 따라서, toString()을 호출함으로써 정렬이 수행되는 것이다. DefaultTableModel의 기본값 getColumnClass() 비헤이비어를 오버라이드함으로써, RowSorterComparable을 구현하는 것으로 가정하고 해당 클래스의 규칙에 따라 정렬한다. 또한, setComparator(int column, Comparator comparator)를 호출하여 특정 칼럼을 위한 커스텀 Comparator를 설치할 수도 있다.

다음은 정렬과 관련이 있는 SortTable 프로그램 내의 세 가지 주요 라인이다.
           JTable table = new JTable(model);
           RowSorter<TableModel> sorter =
             new TableRowSorter<TableModel>(model);
           table.setRowSorter(sorter);
첫 번째 라인은 모델을 테이블에 연결시키고, 두 번째 라인은 모델에 특정 RowSorter를 생성한다. 세 번째 라인은 RowSorterJTable에 연결시킨다. 이로써 사용자는 칼럼 헤더를 클릭하여 해당 칼럼을 정렬할 수 있다. 같은 칼럼을 두 번 클릭하면 정렬 순서가 반대로 된다.

정렬 순서가 바뀔 때 각자의 액션을 추가하고 싶으면 RowSorterRowSorterListener를 첨부하면 된다. 인터페이스는 다음과 같은 하나의 메소드를 가진다.
   void sorterChanged(RowSorterEvent e)
이 메소드는 상태 바에서 텍스트를 업데이트하거나 몇 가지 추가 태스크를 수행할 수 있게 해준다. 이 액션에 대한 RowSorterEventRowSorter가 뷰 안팎의 행을 필터링한 경우, 정렬 전에 얼마나 많은 행이 존재했었는지 알아낼 수 있게 해준다.

테이블 행 필터링하기

RowFilterTableRowSorter에 연결시켜 테이블의 내용을 필터링하는 데 사용할 수 있다. 예를 들어, RowFilter를 이용하여 이름이 A 자로 시작하거나 주가가 $50를 넘는 행만 테이블에 디스플레이되도록 하는 경우가 그것이다. 추상 RowFilter 클래스의 경우 다음과 같이 필터링에 사용되는 하나의 메소드를 가진다.
   boolean include(RowFilter.Entry<? extends M,? extends I> entry)
RowSorter에 연결된 모델 내의 각 엔트리에 대해, 메소드는 지정된 엔트리가 모델의 현재 뷰에 표시되어야 할지 여부를 알려준다. 대개의 경우 여러분은 각자의 RowFilter 구현을 생성할 필요는 없으나, 대신 RowFilter는 필터 생성을 위한 여섯 개의 정적 메소드를 제공한다.
  • andFilter(Iterable<? extends RowFilter<? super M,? super I>> filters)
  • dateFilter(RowFilter.ComparisonType type, Date date, int... indices)
  • notFilter(RowFilter<M,I> filter)
  • numberFilter(RowFilter.ComparisonType type, Number number, int... indices)
  • orFilter(Iterable<? extends RowFilter<? super M,? super I>> filters)
  • regexFilter(String regex, int... indices)
인덱스의 인자(dateFilter, numberFilter, regexFilter)를 가지는 RowFilter 팩토리 메소드의 경우에는 모델에서 지정된 인덱스에 일치하는 일련의 칼럼만을 확인하고, 지정된 인덱스가 없으면 모든 칼럼에 대해 일치 여부를 확인한다.

dateFilter는 날짜의 일치 여부를 확인할 수 있게 해주고, numberFilter는 일치하는 수를 확인한다. notFilter는 다른 필터를 반전시키는 데 사용된다. 즉, 제공 필터에 포함되지 않는 엔트리를 포함한다는 말인데, 이 필터는 가령 2005년 12월 25일까지 완료되지 않은 엔트리를 찾는다든지 하는 일에 사용할 수 있다. andFilterorFilter는 다른 필터들을 논리적으로 결합하는 데 사용되고, regexFilter는 정규 표현식을 사용하여 필터링을 수행한다. 다음은 regexFilter를 이용하여 테이블의 내용을 필터링하는 프로그램 FilterTable이다.
   import javax.swing.*;
   import javax.swing.table.*;
   import java.awt.*;
   import java.awt.event.*;
   import java.util.regex.*;

   public class FilterTable {
     public static void main(String args[]) {
       Runnable runner = new Runnable() {
         public void run() {
           JFrame frame = new JFrame("Sorting JTable");
           frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
           Object rows[][] = {
             {"AMZN", "Amazon", 41.28},
             {"EBAY", "eBay", 41.57},
             {"GOOG", "Google", 388.33},
             {"MSFT", "Microsoft", 26.56},
             {"NOK", "Nokia Corp", 17.13},
             {"ORCL", "Oracle Corp.", 12.52},
             {"SUNW", "Sun Microsystems", 3.86},
             {"TWX",  "Time Warner", 17.66},
             {"VOD",  "Vodafone Group", 26.02},
             {"YHOO", "Yahoo!", 37.69}
           };
           Object columns[] = {"Symbol", "Name", "Price"};
           TableModel model =
              new DefaultTableModel(rows, columns) {
             public Class getColumnClass(int column) {
               Class returnValue;
               if ((column >= 0) && (column < getColumnCount())) {
                 returnValue = getValueAt(0, column).getClass();
               } else {
                 returnValue = Object.class;
               }
               return returnValue;
             }
           };
           JTable table = new JTable(model);
           final TableRowSorter<TableModel> sorter =
                   new TableRowSorter<TableModel>(model);
           table.setRowSorter(sorter);
           JScrollPane pane = new JScrollPane(table);
           frame.add(pane, BorderLayout.CENTER);
           JPanel panel = new JPanel(new BorderLayout());
           JLabel label = new JLabel("Filter");
           panel.add(label, BorderLayout.WEST);
           final JTextField filterText =
               new JTextField("SUN");
           panel.add(filterText, BorderLayout.CENTER);
           frame.add(panel, BorderLayout.NORTH);
           JButton button = new JButton("Filter");
           button.addActionListener(new ActionListener() {
             public void actionPerformed(ActionEvent e) {
               String text = filterText.getText();
               if (text.length() == 0) {
                 sorter.setRowFilter(null);
               } else {
                 try {
                   sorter.setRowFilter(
                       RowFilter.regexFilter(text));
                 } catch (PatternSyntaxException pse) {
                   System.err.println("Bad regex pattern");
                 }
               }
             }
           });
           frame.add(button, BorderLayout.SOUTH);
           frame.setSize(300, 250);
           frame.setVisible(true);
         }
       };
       EventQueue.invokeLater(runner);
     }
   }
디스플레이는 어딘가에 SUN이라는 문자가 포함된 모든 문자열에 대해 필터를 설정하는데, 이는 문자열 "SUN"’에 의해 명시된다. 일치 여부를 정확하게 검사하려면 문자열의 시작과 끝 각각에 '^'와 '$'의 문자를 이용한다.
Filter Table 1

사용자가 아래쪽의 "Filter" 버튼을 누르면 필터는 자체적으로 Matcher.find()를 사용하여 포함 여부를 검사한다.

Filter Table 2

테이블에 표시된 일련의 행을 변경하려면 필터 텍스트를 변경하고, 테이블 내의 모든 행을 보고 싶으면 필터 텍스트를 삭제한다.

마지막으로 빼놓을 수 없는 것은, 정렬이나 필터링 시 선택은 뷰의 관점에서 이루어진다는 점이다. 따라서, 기본 모델에 매핑할 필요가 있다면 convertRowIndexToModel() 메소드를 호출해야 한다. 마찬가지로, 모델에서 뷰로 전환할 경우에는 convertRowIndexToView()를 사용해야 한다.

RowSorter, TableRowSorter, RowFilter 등에 관한 자세한 내용은 각 클래스에 관한 javadoc을 참조하기 바란다.

RowSorter
TableRowSorter
RowFilter

"Java SE" 카테고리의 다른 글

Posted by 1010
01.JAVA/Java2008. 11. 12. 17:22
반응형

2004년 3월 25일자 테크팁 예외 처리의 Best Practices에서 예외 처리를 위한 몇 가지 선진 기법을 설명한 바 있다. 이번 테크팁에서는 예외를 처리하는 또 다른 방법에 관해 배우게 될 것인데, J2SE 5.0에 추가된 UncaughtExceptionHandler가 바로 그것이다.

이름에서 알 수 있듯이 UncaughtExceptionHandler는 캐치되지 않은 예외를 처리하는 방법인데, 좀더 구체적으로 얘기하면 캐치되지 않은 런타림 예외를 처리하는 것이다. 자바 컴파일러는 모든 비(非) 런타임 예외를 처리할 것을 요구하며, 그렇지 않을 경우 프로그램은 컴파일되지 않는다. 여기서 '처리한다'는 것은 예외가 선언하는 메소드의 throws 절에서 선언되거나 try-catch 블록의 catch 절에서 캐치되는 것을 의미한다.

그럼 예시를 위해 다음 두 가지 예외를 살펴보기로 하자: FileNotFoundExceptionArithmeticException. String이나 File 인자로 FileReader 생성자를 호출할 때, 제공된 장소가 유효한 정상 파일을 가리키지 않을 경우 FileNotFoundException이 throw된다. 컴파일러는 이 생성자들 중 하나를 호출할 때 throw된 예외를 처리할 것을 요구한다.
   FileReader input;
   String filename = ...;
   try {
     input = new FileReader(filename);
   } catch (FileNotFoundException e) {
     processMissingFile(filename, e);
   }
이와 대조적으로, ArithmeticException은 일종의 런타임 예외라 할 수 있는데, 자바 프로그래밍 언어 스펙은(컴파일러도 마찬가지로) 그 런타임 예외를 처리할 것을 요구하지 않는다. 따라서 10에서 0까지의 수를 100으로 나누는 다음 루프의 경우 루프를 통과하는 최종 패스에 ArithmeticException을 throw한다:
   for (int i=10; i >= 0; i--) {
     int div = 100 / i;
   }

   Exception in thread "main" java.lang.ArithmeticException: / 
   by zero
        at Test.main(Test.java:4)
기본 Uncaught Exception Handler의 역할은 스택 트레이스를 프린트하는 것인데, 보통 이 기본값 동작으로 충분하지만 꼭 그렇지 않은 경우도 있다. 스택 트레이스를 시스템 콘솔에 덤프하는 대신 팝업 창에 트레이스를 표시한다고 생각해보자. 이럴 때는 자체의 기본 Uncaught Exception Handler를 설치하면 이 작업을 수행할 수 있다.

Uncaught Exception Handler 설치에는 최소 세 가지 방법이 사용되고 있다. 첫째는 ThreadsetUncaughtExceptionHandler() 메소드를 호출하는 것인데, 이 경우 특정 스레드의 동작을 커스터마이즈할 수 있게 된다. 둘째로, 자체 ThreadGroup을 정의하고 그룹 내에 생성된 스레드의 uncaughtException() 메소드를 오버라이드하여 해당 스레드의 동작을 변경할 수 있다. 셋째로, Thread의 정적 setDefaultUncaughtExceptionHandler() 메소드를 호출하여 모든 스레드의 기본값 동작을 설정할 수 있다.

ThreadsetUncaughtExceptionHandler()setDefaultUncaughtExceptionHandler() 메소드 모두 UncaughtExceptionHandler 인터페이스 인자의 구현을 수용한다. 이 인터페이스는 Thread 클래스의 내부 인터페이스이므로 정식 명칭은 Thread.UncaughtExceptionHandler이며, 하나의 메소드를 가진다.
   void uncaughtException(Thread t, Throwable e)
uncaughtException 메소드의 구현을 인터페이스의 커스텀 구현의 일부로서 또는 ThreadGroup의 오버라이드된 메소드로서 제공하여 커스터마이즈된 동작을 얻을 수 있다. 예시를 위해, 런타임 예외와 마주칠 때마다 텍스트 영역의 말미에 첨부되는 스택 트레이스가 포함된 창을 표시하는 UncaughtExceptionHandler의 구현을 살펴보기로 하자. 예외 사이에 창을 닫을 수 있으며, 창은 다음 예외가 발생할 때 다른 창 앞에 다시 표시된다
   import java.awt.*;
   import java.io.*;
   import javax.swing.*;

   public class StackWindow extends JFrame
       implements Thread.UncaughtExceptionHandler {

     private JTextArea textArea;

     public StackWindow(
      String title, final int width, final int height) {
       super(title);
       setSize(width, height);
       textArea = new JTextArea();
       JScrollPane pane = new JScrollPane(textArea);
       textArea.setEditable(false);
       getContentPane().add(pane);
     }

     public void uncaughtException(Thread t, Throwable e) {
       addStackInfo(e);
     }

     public void addStackInfo(final Throwable t) {
       EventQueue.invokeLater(new Runnable() {
         public void run() {
           // Bring window to foreground
           setVisible(true);
           toFront();
           // Convert stack dump to string
           StringWriter sw = new StringWriter();
           PrintWriter out = new PrintWriter(sw);
           t.printStackTrace(out);
           // Add string to end of text area
           textArea.append(sw.toString());
        }
     });
   }
  }
핸들러를 테스트하기 위해서는 핸들러를 설치한 다음 런타임 예외를 throw하는 프로그램이 필요한데, 다음의 프로그램 DumpTest가 이 작업을 수행한다. 단순성을 감안하여, DumpTest는 2개의 예외만을 생성한다. 사용자 여러분은 더 난해한 코드를 마음대로 프로그램에 추가하여 더 많은 예외가 throw되도록 한다. 이 때, 프로그램은 예외 사이에서 일시 중지됨으로써 사용자가 예외 사이에 예외 덤프 스택 창을 닫을 수 있음을 보여준다.
   import java.io.*;

   public class DumpTest {
    public static void main(final String args[]) 
     throws Exception {
       Thread.UncaughtExceptionHandler handler =
         new StackWindow("Show Exception Stack", 400, 200);
       Thread.setDefaultUncaughtExceptionHandler(handler);
       new Thread() {
         public void run() {
           System.out.println(1 / 0);
         }
       }.start();
       BufferedReader br =
         new BufferedReader(new InputStreamReader(System.in));
       System.out.print("Press Enter for next exception");
       br.readLine();
       new Thread() {
         public void run() {
           System.out.println(args[0]);
         }
       }.start();
       System.out.print("Press Enter to end");
       br.readLine();
       System.exit(0);
     } 
   }
StackWindowDumpTest를 컴파일한다. DumpTest 실행 시 콘솔에 다음과 같은 내용이 표시되어야 한다.
   > java DumpTest
   Press Enter for next exception
아울러, 텍스트 영역의 예외를 위한 스택 트레이스가 포함된 창이 표시된다.

UncaughtExceptionWindow Window

Enter를 누르면 콘솔에 다음과 같은 내용이 표시되어야 한다.
   Press Enter to end
또한, 창 내의 텍스트 영역에 첨부된 또 다른 스택 트레이스가 표시되어야 한다.

UncaughtExceptionWindow Window

캐치되지 않은 예외를 처리하는 작업은 이것 말고도 더 여러 가지가 있다. Modal Dialogs(여러가지 양식의 대화상자)의 경우에는 자체 이벤트 스레드를 요구하며, 따라서 자체 Uncaught Handler가 필요하다. 시스템 속성 sun.awt.exception.handler는 모든 경우를 커버할 수는 있지만 문서화 수준이 아직 미흡한 상태이다. 한편, 속성을 정식 API에 포함시키기 위해 RFE(Request for Enhancement)가 이미 제출된 상태이다.

예외 처리의 Best Practices 외에도, 2002년 5월의 StackTraceElements에 관한 팁에서도 유용한 참고 정보를 얻을 수 있다. 또한, 자바 튜토리얼에 포함된 레슨 Handling Errors Using Exceptions도 함께 참조할 것.

"Java SE" 카테고리의 다른 글

Posted by 1010
01.JAVA/Java2008. 11. 12. 17:21
반응형

정규 expression 또는 regex 지원은 버전 1.4 이후 자바 플랫폼의 일부가 되어 왔다. java.util.regex 패키지에서 발견되는 regex 클래스는 펄 언어가 제공하는 것과 유사한 패턴 매칭을 지원하지만 자바 언어 구문 및 클래스를 사용한다. 패키지 전체는 Pattern, MatcherPatternSyntaxException의 3가지 클래스로 제한된다. 버전 1.5에서는 MatchResult 인터페이스가 소개되었다.

두 클래스 PatternMatcher를 함께 사용한다. Pattern 클래스를 사용하여 정규 표현식을 정의한 다음, Matcher 클래스를 사용하여 입력 소스에 대해 패턴을 검사한다. 표현식에서 패턴에 구문 오류가 있으면 예외가 발생한다.

두 클래스 모두 구성자를 가지지 않는다. 대신, 정규 표현식을 컴파일하여 패턴을 얻은 다음 반환된 Pattern에게 일부 입력 소스를 기반으로 해당 Matcher를 요청한다.

Pattern pattern = Pattern.compile( <regular expression> );
Matcher matcher = pattern.matcher( <input source> );

Matcher를 얻었으면 일반적으로 입력 소스를 처리하여 포함된 모든 매칭을 찾는다. find() 메소드를 사용하여 입력 소스에서 패턴의 매칭을 찾는다. find()에 대한 각 호출은 마지막 호출 위치에서 계속되거나 첫 번째 호출 위치 0에서 계속된다. 그런 다음 매칭되는 항목이 group() 메소드에 의해 반환된다.

while (matcher.find()) {
   System.out.printf"Found: \"%s\" from %d to %d.%n",
       matcher.group(), matcher.start(), matcher.end());
}

다음 코드는 기본적인 정규 표현식 프로그램을 보여 주며 사용자가 정규 표현식과 비교 대상 문자열을 입력하도록 메시지를 표시한다.

import java.util.regex.*;

public class Regex {

   public static void main(String args[]) {
       Console console = System.console();

       // Get regular expression
       String regex = console.readLine("%nEnter expression: ");
       Pattern pattern = Pattern.compile(regex);

       // Get source
       String source = console.readLine("Enter input source: ");
       Matcher matcher = pattern.matcher(source);

       // Show matches
       while (matcher.find()) {
           System.out.printf("Found: \"%s\" from %d to %d.%n",
               matcher.group(), matcher.start(), matcher.end());
       }
   }
}

그러면 정규 표현식의 모양은 정확하게 어떠한가? Pattern 클래스는 보다 세부적인 사항을 제공하지만 기본적으로 정규 표현식은 다른 문자 시퀀스와 일치시킬 문자 시퀀스이다. 예를 들어, "Hello, World" 문자열에서 두 개의 L자("ll") 문자열 리터럴 패턴을 찾을 수 있다. 앞의 프로그램은 시작 위치 2와 끝 위치 4에서 "ll" 패턴을 찾을 것이다. 끝 위치는 일치된 문자열 패턴의 끝 이후에 다음 문자의 위치이다.

"ll" 같은 패턴 문자열은 입력 소스에서 문자적으로 위치하는 지점만을 보고하므로 그리 흥미롭지 않다. 정규 표현식 패턴은 특수 메타 문자를 포함할 수 있다. 메타 문자는 정규 표현식에서 강력한 매칭 기능을 제공한다. 정규 표현식에서는 "([{\^-$|]})?*+."의 15문자를 메타 문자로 사용할 수 있다.

일부 메타 문자는 문자 그룹을 나타낸다. 예를 들어, 대괄호([ 및 ])를 사용하면 대괄호 안의 문자 중 하나가 텍스트에서 발견되는 경우 매칭이 성공하는 일련의 문자를 지정할 수 있다. 예를 들어, "co[cl]a" 패턴은 coca 및 cola라는 단어와 매칭된다. []는 단일 문자를 매칭하는 데만 사용되므로 cocla는 매칭되지 않는다. 몇 가지 매칭을 해 보고 문제가 없으면 곧 수량자에 대해 자세히 살펴보자.

개별 문자의 매칭 이외에 대괄호 문자([ 및 ])를 사용하여 [j-z]로 지정된 j-z의 문자처럼 일정 범위의 문자를 매칭할 수 있다. 이러한 문자 범위는 "foo[j-z]"처럼 문자열 리터럴과 결합할 수도 있다. 여기서 fool을 찾으면 매칭이 성공하고 food를 찾으면 매칭이 실패한다. ljz 사이의 범위 안에 있지만 d는 그렇지 않기 때문이다. ^ 문자를 사용하여 문자열 리터럴 또는 문자 범위의 제외를 나타낼 수도 있다. "foo[^j-z]" 패턴은 foo로 시작하고 j에서 z 사이의 문자로 끝나지 않는 단어를 찾는다. 따라서 이번에는 food라는 문자열이 매칭에 성공한다. [a-zA-Z]처럼 여러 범위를 결합하여 a에서 z 사이의 소문자와 대문자를 나타낼 수도 있다.

정규 표현식을 처음 학습할 때는 문자열 리터럴이 유용하지만 정규 표현식에서 대부분의 사람들이 사용하는 보다 일반적인 요소는 미리 정의된 문자 클래스이다. 여기서 메타 문자 .\가 사용된다. 마침표(.)는 임의 문자를 나타내는 데 사용된다. 따라서 정규 표현식 ".oney"는 money 및 honey와 매칭되며 oney로 끝나는 5자의 어느 단어와도 매칭된다. 반면에 \는 다른 문자와 함께 사용되어 전체 문자 집합을 나타낸다. 예를 들어, 숫자 집합을 나타내기 위해 [0-9]를 사용할 수 있지만 \d를 사용할 수도 있다. 숫자가 아닌 문자 집합을 나타내기 위해 [^0-9]를 사용할 수도 있다. 또는 \D의 미리 정의된 문자 클래스 문자열을 사용할 수 있다. 이러한 모든 문자 클래스 문자열은 모두 기억하기가 어려우므로 패턴 클래스에 대한 자바 플랫폼 문서에 정의되어 있다. 다음은 미리 정의된 특수 문자 클래스의 하위 집합이다.

* \s -- whitespace
* \S -- non-whitespace
* \w -- word character [a-zA-Z0-9]
* \W -- non-word character
* \p{Punct} -- punctuation
* \p{Lower} -- lowercase [a-z]
* \p{Upper} -- uppercase [A-Z]

미리 정의된 문자열과 관련하여 지적해야 할 사항은 즉각 눈에 띄지 않는다. 위의 Regex 프로그램에 이러한 문자열 중 하나를 사용하려면 표시된 대로 입력한다. \s는 공백과 매칭된다. 하지만 자바 소스 파일에서 정규 표현식을 하드 코딩하려면 \ 문자가 특별하게 취급된다는 것을 기억해야 한다. 소스에서 이 문자열을 다음과 같이 이스케이프해야 한다.

String regexString = "\\s";

여기서 \\는 문자열에서 하나의 백슬래시를 나타낸다. 다른 문자열 리터럴을 나타내기 위한 기타 특수 문자열은 다음과 같다.

* \t -- tab
* \n -- newline
* \r -- carriage return
* \xhh -- hex character 0xhh
* \uhhhh -- hex character 0xhhhh

수량자는 정규 표현식을 더욱 흥미롭게 만드는데 문자 클래스 같은 기타 표현식과 결합될 때는 특히 그렇다. 예를 들어, a-z에서 3자의 문자열을 매칭하기 위해 "[a-z][a-z][a-z]" 패턴을 사용할 수도 있지만 그럴 필요가 없다. 문자열을 반복하는 대신 패턴 다음에 수량자를 추가하면 된다. 이 예제의 경우, "[a-z][a-z][a-z]"":[a-z]{3}"으로 나타낼 수 있다. 특정 수량에 대해 숫자가 {} 괄호 안에 들어간다. ?, * 또는 + 문자를 사용하여 0번 또는 한 번, 0번 이상, 한 번 이상을 각각 나타낼 수도 있다.

[a-z]? 패턴은 a-z의 문자와 0번 또는 한 번 매칭된다. [a-z]* 패턴은 a-z의 문자와 0번 이상 매칭된다. [a-z]+ 패턴은 a-z의 문자와 한 번 이상 매칭된다.

수량자는 주의해서 사용한다. 0번 매칭을 허용하는 수량자에는 특별한 주의를 기울여야 한다.

괄호 기호({})를 수량자로 사용할 때는 범위를 지정해야 한다. {3}은 정확히 3번을 의미하지만 {3,}은 적어도 3번을 의미한다. 수량자 {3, 5}3번에서 5번까지의 패턴과 매칭된다.

정규 표현식에는 여기서 살펴본 것보다 훨씬 많은 내용이 있다. 특정 상황에 맞는 정규 표현식을 사용하는 것이 중요하다. 앞의 Regex 프로그램을 사용하여 몇 가지 표현식을 시험해 보고 기대했던 결과가 나오는지 확인해 본다. 여러 가지 수량자를 사용하여 각 차이가 어떻게 나오는지 이해할 수 있도록 한다. 일반적으로 수량자는 가능한 매칭에 대해 최대 수의 문자를 포함하려고 한다.

정규 표현식에 대한 자세한 내용을 살펴보려면 자바 온라인 자습서의 정규 표현식 편을 참고한다.

또한 패턴 클래스에 대한 내용은 javadoc을 참고한다.


"Java SE" 카테고리의 다른 글

Posted by 1010
01.JAVA/Java2008. 11. 12. 17:20
반응형

이러한 팁은 Java SE 6을 사용하여 개발되었습니다. Java SE 6은 Java SE Downloads 페이지에서 다운로드할 수 있습니다.

이번 호 테크팁의 저자는 JZ Ventures사의 사장이자 주요 컨설턴트인 John Zukowski입니다.
이전에 다루었던 CookieHandler를 이용한 쿠키 관리 팁 에서는 J2SE 5.0이 네트워크 연결을 통해 쿠키 관리를 지원한다고 설명했습니다. 5.0 릴리스에서는 이러한 간략한 설명을 다루기 위한 프레임워크를 제공했지만 모든 작업을 수행하는 일은 개발자의 몫으로 남았습니다. 정의된 API를 따르기만 하면 되지만 실제로 프로그램에서 쿠키를 제대로 관리하려면 더 많은 작업이 수반됩니다.

J2SE 5.0 API를 다시 간략하게 살펴 보면 모호한 CookieHandler 클래스를 사용할 수 있게 되지만 실제 구현도, 스토리지 메커니즘도, 스토리지 정책도, 저장할 대상도 아무 것도 존재하지 않습니다. Java SE 6으로 이동하면서 CookieManager 클래스는 이러한 CookieHandler의 구현을 제공합니다. CookieStore는 스토리지 메커니즘이고, CookiePolicy는 쿠키를 수락하거나 거부하는 정책을 제공합니다. 그리고 HttpCookie는 저장할 개체입니다.

J2SE 5.0에서 수행하기 어려웠던 작업들이 Java SE 6에서는 기존 클래스를 활용한 간단한 작업이 되었습니다.

다음은 쿠키 작업을 위해 작동하는 J2SE 5.0 프로그램입니다.

   import java.io.*;
   import java.net.*;
   import java.util.*;

   public class Fetch {
     public static void main(String args[]) throws Exception {
       if (args.length == 0) {
         System.err.println("URL missing");
         System.exit(-1);
       }
       String urlString = args[0];
       CookieHandler.setDefault(new ListCookieHandler());
       URL url = new URL(urlString);
       URLConnection connection = url.openConnection();
       Object obj = connection.getContent();
       url = new URL(urlString);
       connection = url.openConnection();
       obj = connection.getContent();
     }
   }

setDefault 메소드는 중간에 CookieHandler를 호출합니다. ListCookieHandler 클래스는 기본 CookieHandler의 팁 구현이었습니다.

동일한 프로그램의 Java SE 6 버전은 거의 동일하며 다음 행만 다릅니다.

   CookieHandler.setDefault(new CookieManager());

추가 코드는 제공하지 않아도 됩니다. 간단하게 다른 API 사이를 이동할 수 있습니다. 다른 차이점은 있지만 페치(Fetch) 프로그램의 한 행을 변경하면 두 번째 네트워크 연결이 첫 번재 연결에서 반환된 쿠키를 사용합니다.

차이점은 다음과 같습니다. Java SE 6에는 CookiePolicy가 있습니다. 이것은 모든 쿠키를 수락하거나, 쿠키를 수락하지 않거나, 원래 호스트의 쿠키만 수락하는 기능인 CookieManager를 제공합니다. 다른 정책으로 CookieManager를 설정하려면 CookiePolicy 인터페이스의 상수 ACCEPT_ALL, ACCEPT_NONE 또는 ACCEPT_ORIGINAL_SERVER 중 하나를 사용합니다. 아무 것도 설정하지 않을 경우 마지막 옵션은 기본값이 됩니다. 쿠키 관리자를 구성하려면 아래에 표시된 대로 적절한 상수로 setCookiePolicy 메소드를 호출하십시오.

   CookieManager manager = new CookieManager();
   manager.setCookiePolicy(CookiePolicy.ACCEPT_NONE);
   CookieHandler.setDefault(manager);

그리고 CookieStoreCookiePolicy 인수 모두를 수락하는 CookieManager에 대한 두 번째 구성자를 호출할 수 있습니다.

   CookieManager(CookieStore store, CookiePolicy cookiePolicy)

널 저장소에 전달되면 시스템이 사전 정의된 메모리 내장 버전을 사용합니다. 쿠키의 장기 스토리지를 제공하려면 고유한 인터페이스 구현을 정의할 수도 있습니다.

수정된 페치 프로그램 버전을 소개하기 전에 API 차이점을 하나 더 설명하겠습니다. 바로 쿠키 jar, err..., 관리자 등에서 쿠키 목록을 가져오는 방법입니다. CookieManager 클래스에는 CookieStore를 가져오는 getCookieStore 메소드가 있습니다. getCookies 메소드로 저장소에 쿠키 목록을 요청하면 목록 전체를 반복할 수 있습니다.

다음은 Java SE 6 버전의 쿠키 처리 API를 활용하는 이전 프로그램의 수정된 버전입니다. 기본 프로그램은 크게 변경되지 않았지만 지원되는 모든 사용자 정의 클래스는 이제 더 이상 필요하지 않습니다.

import java.io.*;
import java.net.*;
import java.util.*;

public class Fetch {
  public static void main(String args[]) throws Exception {
    Console console = System.console();
    if (args.length == 0) {
      System.err.println("URL missing");
      System.exit(-1);
    }
    String urlString = args[0];
    CookieManager manager = new CookieManager();
    manager.setCookiePolicy(CookiePolicy.ACCEPT_ALL);
    CookieHandler.setDefault(manager);
    URL url = new URL(urlString);
    URLConnection connection = url.openConnection();
    Object obj = connection.getContent();
    url = new URL(urlString);
    connection = url.openConnection();
    obj = connection.getContent();
    CookieStore cookieJar = manager.getCookieStore();
    List<HttpCookie> cookies = cookieJar.getCookies();
    for (HttpCookie cookie: cookies) {
      console.printf("Cookie: %s%n", cookie);
    }
  }
}

특정 URL을 방문할 때 시스템에 쿠키가 남는지 확인하려면 프로그램을 컴파일하고 실행하십시오. ACCEPT_ALL을 사용할 수 없으면 http://www.sun.com/을 방문해도 쿠키가 표시되지 않지만 모두 사용 가능하게 설정하면 세 개가 표시됩니다. 여러분이 실행하면 다른 결과가 생성될 것입니다.

> java Fetch http://www.sun.com
Cookie: JROUTE=9999
Cookie: JSESSIONID=999999995d3f13ffffffffc68433979b7f5b0
Cookie: Starload=star-fep7

그렇습니다. J2SE 5.0 쿠키 처리 메커니즘에서 Java SE 6으로의 이동은 매우 쉽습니다. 기본 기능에 만족하면 추가로 인터페이스를 구현하지 않아도 됩니다.

Posted by 1010
카테고리 없음2008. 11. 12. 17:18
반응형

저자 John Zukowski

매년 시행하고 있는 테크팁 퀴즈를 올해도 진행하도록 하겠습니다. 최근 제공된 두 번의 퀴즈(2006년 6월2005년 9월)는 그전에 제공된 팁과 비법에 대해 이해하기 쉽도록 복습하는 과정으로 이루어졌었습니다. 이번 퀴즈는 5문항입니다. 정답은 아래 부분에서 확인할 수 있습니다.

1. onetwo라는 두 개의 BigDecimal 유형 개체가 있을 경우 three 개체에서 이 둘의 곱을 구하려면 어떻게 해야 합니까?

a. BigDecimal three = one * two;
b. BigDecimal three = one.*(two);
c. BigDecimal three = one.times(two);
d. BigDecimal three = one.multiply(two);

2. System.out.println("Pi = " + PI)라는 행을 작성하기 위해 컴파일러가 Math 클래스에서 PI를 찾도록 하려면 어떤 import 문을 사용해야 합니까?

a. import java.lang.Math;
b. import java.lang.Math.PI;
c. import static java.lang.Math.PI;
d. import final java.lang.Math.PI;

3. 향상된 for 루프(foreach 문이라고도 함)를 사용할 경우 구조가 제대로 컴파일되어 실행되도록 하려면 콜론(:) 다음에 오는 요소가 어떤 인터페이스를 구현해야 합니까?

a. Enumeration
b. Iterable
c. Iterator
d. Collection

4. 두 개의 스윙 구성요소가 표시 영역에서 겹칠 경우, 위에 표시될 구성요소를 어떻게 제어할 수 있습니까?

a. 컨테이너에 먼저 추가된 구성요소가 위에 그려진다.
b. 컨테이너에 나중에 추가된 구성요소가 위에 그려진다.
c. 컨테이너의 setComponentZOrder() 메서드를 호출한다.
d. 각 구성요소에 대해 setComponentZOrder() 메서드를 호출한다.

5. 자바 이미지 I/O API를 사용할 경우 이미지 읽기 진행 상황을 모니터하는 가장 좋은 방법은?

a. FilteredReader 하위 클래스를 만들어 바이트를 센다.
b. ImageReaderProgressMonitorListener를 첨부하여 진행 상황을 보고하도록 한다.
c. ImageReaderIIOReadProgressListener를 첨부하여 진행 상황을 보고하도록 한다.
d. ImageReaderRunnable을 등록하여 실행 빈도를 알려 준다.

정답을 확인하려면 여기를 클릭하십시오.



-----------------------


정답

1. onetwo라는 두 개의 BigDecimal 유형 개체가 있을 경우 three 개체에서 이 둘의 곱을 구하려면 어떻게 해야 합니까? 정답: D
두 개의 BigDecimal 유형 변수의 곱을 구하려면 BigDecimalmultiply() 메서드를 사용합니다. 이 내용은 "BigDecimal의 필요성"(2007년 7월)에 나와 있으며 형식 지정 및 반올림 문제에 대해서도 설명되어 있습니다.

2. System.out.println("Pi = " + PI)라는 행을 작성하기 위해 컴파일러가 Math 클래스에서 PI를 찾도록 하려면 어떤 import 문을 사용해야 합니까? 정답: C
static import 문을 사용하면 해당 클래스를 명시적으로 지정하지 않고도 원하는 상수와 메서드를 컴파일러에 알려 줄 수 있습니다. static import 문에 대한 자세한 내용은 "Using Static Imports for Constants and Methods"(2004년 10월)를 참조하십시오.

3. 향상된 for 루프(foreach 문이라고도 함)를 사용할 경우 구조가 제대로 컴파일되어 실행되도록 하려면 콜론(:) 다음에 오는 요소가 어떤 인터페이스를 구현해야 합니까? 정답: B
인수는 다음과 같이 단일 메서드로 구성된 Iterable 인터페이스를 구현하여 iterator를 얻어야 합니다.

Iterator iterator()

향상된 for 루프에 대한 설명 및 사용 방법은 "The Enhanced For Loop"(2005년 5월)를 참조하십시오. Iterable 사용에 대한 설명은 "클래스에서 향상된 For-Loop 사용"(2007년 9월)을 참조하십시오.

4. 두 개의 스윙 구성요소가 표시 영역에서 겹칠 경우, 위에 표시될 구성요소를 어떻게 제어할 수 있습니까? 정답: C
Z-order는 화면에 표시되는 구성요소의 겹침 순서를 나타냅니다. 여기서 x 및 y 좌표는 각각 수평 위치와 수직 위치입니다. 겹치는 각 구성요소에 대해 ContainersetComponentZOrder() 메서드를 호출하여 다른 구성요소 위에 표시될 구성요소를 명시적으로 제어할 수 있습니다. Z-order 겹침 제어에 대한 자세한 내용은 "Let There Be Z-Order" 팁(2005년 1월)을 참조하십시오.

5. 자바 이미지 I/O API를 사용할 경우 이미지 읽기 진행 상황을 모니터하는 가장 좋은 방법은? 정답: C
Progress Monitor를 사용하여 진행 상황을 표시할 수 있다 하더라도 진행 상황 알림을 설정하는 가장 좋은 방법은 IIOReadProgressListener를 사용하는 것입니다. 진행 상황 알림 및 영역 업데이트 알림에 대한 자세한 내용은 "이미지 I/O 이벤트 모니터하기"(2007년 2월)를 참조하십시오.

이 글의 영문 원본은http://blogs.sun.com/corejavatechtips/e ··· _answers
에서 보실 수 있습니다.

Posted by 1010
01.JAVA/Java2008. 11. 12. 17:17
반응형
저자 John Zukowski

자바 플랫폼에서의 문자열 정렬은 간단한 작업으로 생각할 수 있으나 국제 시장을 대상으로 한 프로그램을 개발할 경우에는 고려해야 할 사항이 많습니다. 영어만을 기준으로 한다면 tomorrow라는 단어가 today 다음에 정렬되므로 프로그램이 제대로 기능하는 것으로 보이며 아무 문제가 없을지도 모릅니다. 그러나 정렬 기능으로 기본적인 String 클래스의 compare() 메서드만 사용했을 경우 스페인어 사용자가 mañana라는 단어를 정렬하려 한다면 ñ 문자가 z 문자가 다음에 오게 되므로 n 문자와 o 문자 사이의 스페인어 문자 정렬 순서가 틀리게 됩니다. 바로 이러한 문제를 해결하기 위해 java.text 패키지의 Collator 클래스가 사용되는 것입니다.

다음과 같은 단어 목록을 예로 들어 보겠습니다.

  • first
  • mañana
  • man
  • many
  • maxi
  • next

기본 정렬 방법인 String의 클래스의 compare() 메서드를 사용할 경우 위의 단어 목록은 다음과 같이 정렬될 것입니다.

  • first
  • man
  • many
  • maxi
  • mañana
  • next

여기서 mañana는 maxi와 next 사이에 옵니다. 그러나 스페인어 알파벳에서 ñ 문자('에녜'로 발음)는 n 다음에 오므로 mañana는 many와 maxi 사이에 와야 합니다. ñ 문자를 처리하는 정렬 루틴을 자체적으로 만들 수도 있지만, 이번에는 또 독일어 사용자가 독일어 발음 부호를 사용하려 하거나 façade 같은 디자인 패턴 목록을 정렬하려 할 수도 있습니다. 이 경우 façade라는 단어를 factory 앞에 오게 정렬하시겠습니까? 아니면 뒤에 오게 정렬하시겠습니까? 실제로 세디유가 붙은 ç 문자는 c 문자와 동일하게 처리되거나 다르게 처리됩니다.

이러한 문제들을 해결하기 위해 Collator 클래스를 사용할 수 있습니다. Collator 클래스는 언어별 정렬 문제를 고려하므로 ASCII/Unicode 문자 값만을 기준으로 단어를 정렬하지 않습니다. Collator 클래스의 기능과 장점을 제대로 활용하려면 먼저 strength라고 하는 특별한 속성에 대해 잘 알고 있어야 합니다. Collator의 strength 설정은 정렬 순서에 사용되는 비교 정도가 얼마나 강하고 약한지를 결정합니다. 이 속성의 값으로 PRIMARY, SECONDARY, TERTIARY, IDENTICAL이라는 4가지 값을 사용할 수 있습니다. 언어에 대해 이러한 값이 일반적으로 어떻게 작용하는지 알아 보겠습니다. 제일 나중에 언급한 IDENTICAL은 말 그대로 정확하게 같아야만 문자가 동일한 것으로 간주됩니다. TERTIARY는 대소문자를 구분하지 않습니다. SECONDARY는 n에 대해 ñ과 같은 발음 부호의 차이를 무시합니다. PRIMARY는 기본적인 문자 비교 방식은 IDENTICAL과 동일하지만 제어 문자 및 악센트 부호를 처리할 때 몇 가지 차이점이 있습니다. 이러한 차이점과 분석 모드 규칙에 대한 자세한 내용은 Collator javadoc을 참조하십시오.

Collator를 사용하기 위해서는 먼저 한 가지 작업을 수행해야 합니다. 기본 로케일에 대해 getInstance()를 호출하여 사용하거나, 제공된 로케일에 대해 특정 LocalegetInstance() 메서드에 전달하여 사용할 수 있습니다. 예를 들어 스페인어의 경우, 다음과 같이 Locale("es")을 새로 사용하여 스페인어 Locale을 만든 다음 getInstance()에 전달해야 합니다.

 Collator esCollator =
   Collator.getInstance(new Locale("es"));

로케일에 기본 Collator 강도(스페인어에 SECONDARY 값 적용)로도 충분하면 다음과 같이 Comparator와 같은 CollatorCollectionssort() 루틴에 전달하여 정렬된 List를 얻을 수 있습니다.

 Collections.sort(list, esCollator);

이렇게 하면 위의 목록이 스페인어 알파벳의 올바른 순서에 따라 다음과 같이 정렬됩니다.

  • first
  • man
  • many
  • mañana
  • maxi
  • next

Collator에 US 로케일을 사용한다면 영어 알파벳에 ñ이라는 문자가 없기 때문에 mañana가 man과 many 사이에 올 것입니다.

다음은 이러한 차이점을 보여 주는 간단한 예입니다.

import java.awt.*;
import java.text.*;
import java.util.*;
import java.util.List; // Explicit import required
import javax.swing.*;

public class Sort {
 public static void main(String args[]) {
   Runnable runner = new Runnable() {
     public void run() {
       String words[] = {"first", "mañana", "man",
                         "many", "maxi", "next"};
       List list = Arrays.asList(words);
       JFrame frame = new JFrame("Sorting");
       frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
       Box box = Box.createVerticalBox();
       frame.setContentPane(box);
       JLabel label = new JLabel("Word List:");
       box.add(label);
       JTextArea textArea = new JTextArea( list.toString());
       box.add(textArea);
       Collections.sort(list);
       label = new JLabel("Sorted Word List:");
       box.add(label);
       textArea = new JTextArea(list.toString ());
       box.add(textArea);
       Collator esCollator = Collator.getInstance(new Locale("es"));
       Collections.sort(list, esCollator);
       label = new JLabel("Collated Word List:");
       box.add(label);
       textArea = new JTextArea(list.toString());
       box.add(textArea);
       frame.setSize(400, 200);
       frame.setVisible(true);
     }
   };
   EventQueue.invokeLater (runner);
 }
}


sorting

마지막으로 정렬 순서에 대해 알아 둘 내용이 있습니다. getInstance() 호출을 통해 반환된 Collator는 지원되는 언어에 대한 RuleBasedCollator의 인스턴스입니다. RuleBasedCollator를 사용하면 정렬 순서를 원하는 대로 정의할 수 있습니다. 이러한 규칙 구문은 클래스에 대한 javadoc에 자세히 설명되어 있습니다. 여기서는 4자 알파벳을 사용하고 있으며 문자 순서가 ACEF가 아닌 CAFE가 되도록 만드는 규칙을 간단히 예로 들어 보겠습니다.

 String rule =
   "< c, C < a, A < f, F < e, E";
 RuleBasedCollator collator = new RuleBasedCollator(rule);

이 규칙은 대소문자를 표시하여 명시적인 순서를 cafe로 정의합니다. ace, cafe, ef, face라는 단어 목록이 있을 경우 새 규칙을 사용하여 정렬하면 cafe, ace, face, ef 순서가 됩니다.

import java.text.*;
import java.util.*;

public class Rule {
 public static void main(String args[]) throws ParseException {
   String words[] = {"ace", "cafe", "ef", "face"};
   String rule ="< c, C < a, A < f, F < e, E";
   RuleBasedCollator collator = new RuleBasedCollator(rule);
   List list = Arrays.asList(words);
   Collections.sort(list, collator);
   System.out.println(list);
 }
}

컴파일 후 실행하면 새 규칙을 사용하여 정렬된 단어를 볼 수 있습니다.

> javac Rule.java
> java Rule
[cafe, ace, face, ef]

javadoc에서 규칙 구문에 대한 내용을 자세히 읽은 후 더 많은 문자와 다양한 발음 부호를 사용하여 연습해 보십시오.

이제 전 세계를 대상으로 하는 글로벌 프로그램을 개발할 경우 다양한 언어를 지원하는 프로그램을 만들 수 있을 것입니다. 이전 팁에서 설명했듯이 문자열을 리소스 번들에 반드시 포함시켜야 합니다.

이전 팁

*********

사용자 삽입 이미지

GlassFish에 연결하여 참여
GlassFish에 참여하여 iPhone 경품의 주인공이 되어 보십시오. 본 행사는 2008년 3월 23일까지 진행됩니다. 지금 바로 참여하십시오.

Foote on Blu-ray Disc Java 이 비디오 인터뷰에서는 썬의 BDJ(Blu-ray Disc Java) 설계자인 Bill Foote가 강력한 기술에 대해 설명하고 BDJ 코드 및 애플리케이션의 몇 가지 예를 보여 줍니다. 코드 다운로드하기

이 글의 영문 원본은 http://blogs.sun.com/corejavatechtips/e ··· _strings
에서 보실 수 있습니다.

"Java SE" 카테고리의 다른 글


Posted by 1010
01.JAVA/Java2008. 11. 12. 17:13
반응형

기고인 : JCO 기술젼략팀장 김홍회(자바스터디 네트워크 대표 운영자)

Database 시장은 오라클, DB2, MS-SQL, MySQL등 많은 경쟁업체들의 각축장이 되어 왔다. BEA를 인수한 오라클은 점차 그 범위를 확장해 가고 있으며 Sun은 MySQL을 인수하며 시장에 합류하였다. 이렇듯이 Dabase는 Database가 시장에 갖는 의미만큼 각 업체에서 사활을 건 싸움을 하고 있다. 그리고 이러한 경쟁 시장에 JavaDB가 도전장을 내밀게 되었다. JavaDB는 제목처럼 경량화 DB를 모토로 만들어 졌으며 이 글은 소개 및 설치하는 방법과 실행 예제 등 기본적인 정보만 다루도록 하겠다.

1. JavaDB의 등장과 기원

여기서 필자는 JavaDB의 등장과 함께 기원이라는 단어를 사용했다. 그럼 JavaDB는 부모가 있다는 것인가? 그렇다. JavaDB의 기원은 1996년으로 거슬러 올라간다. IBM이 Cloudscape라는 프로젝트를 시작 하였으며 1999년에 Informix, 2001년에는 IBM에서 관리 하였으며 2004년에 지금의 Apache에 기부 되었다. Apache는 Apache Derby라는 프로젝트로 오픈 소스 프로젝트를 진행하고 있으며 Apache Derby는 Apache DB subproject라는 이름 또한 가지고 있다. 기존 Database가 무겁고 비싼 것에 비해 Derby는 경량화와 무료 라이센스 그리고 무엇보다 매력적인 오픈 소스를 표방하며 만들어 졌다. 이 프로젝트를 Sun에서 JavaDB라는 이름으로 JavaSE6에 포함시키며 공급하기 시작했다.

2. JavaDB의 특징

JavaDB의 특징은 Apache Derby의 특징과 같다. 이는 기존 Dababase와 성격이 매우 다른데 특징은 다음과 같다.

1) base engine과 JDBC driver 모두 합쳐 2메가바이트
2) 자바와 JDBC, SQL을 기초로 만들어짐
3) client/server 모드를 Derby Network Client JDBC driver and Derby Network Server. 를 통해 지원 가능
4) 설치 및 디플로이, 사용이 편함

또한 JavaDB는 다음과 같은 환경에 적합하다고 소개되어 있다.

1) 자바 애플리케이션 개발 및 테스트 : 사용하기 쉬우며 사용자의 컴퓨터나 메인프레임에서도 잘 돌아감.
아파치 라이센스하에 무료임.
2) 임베디드 애플리케이션
3) 멀티 플랫폼 환경 : 100% 자바이므로 Java DB에서 다른 오픈 스탠더드 데이터베이스로 마이그레이트 가능함.
4) Web 2.0 기반의 브라우져 based 환경
5) PDA와 같이 J2ME CDC에서 돌아가는 애플리케이션

먼저 base engine과 JDBC driver를 모두 합쳐서 2메가바이트라는 획기적이고도 믿을 수 없는 용량을 자랑한다.
또한 이전에 설명했듯이 Pure Java로 만들어 졌으며 설치법 또한 간단하다.

3. JavaDB 설치

JavaDB는 SE6를 설치하면 자동으로 설치되나 수동으로 설치하는 방법도 있다.

(1) homepage(http://developers.sun.com/javadb/downloads/index.jsp)에 접속하여 다운로드 하기
(2) 설치하기

① 윈도우의 경우

     javadb_<version>.ms를 더블 클릭하거나 msiexec /i javadb_<version>.msi 명령 실행

② 솔라리스의 경우
    1) 다운로드 하기
       javadb-<version>-solaris-<arch>-pkg.sh
    2) 권한 확인하기
       
chmod +x javadb-<version>-solaris-<arch>-pkg.sh

    3) 압축풀기
       
./javadb-<version>-solaris-<arch>-pkg.sh

    이 명령을 실행하고 나면 javadb-<version> 디렉토리 밑에 여러 개의 다음의 SVR4 package 디렉토리들이 생성된다

SUNWjavadb-common
SUNWjavadb-client
SUNWjavadb-core
SUNWjavadb-demo
SUNWjavadb-docs
SUNWjavadb-javadoc
SUNWjavadb-service

      4) su – root등을 사용하여 수퍼 권한자로 변경하기
      5) 기존 JavaDB 삭제하기
         기존에 JavaDB가 설치되어 있다면(디폴트 설치 경로는 /opt/SUNWjavadb 이다) 새 버전을 설치하기 전에 제거해야 한다. 현재 돌아가고 있는 패키지를 보는 방법은 다음과 같다.
    
pkginfo | grep SUNQjavadb-

          기존 패키지를 제거하기          
        
pkgrm SUNWjavadb-client SUNWjavadb-core SUNWjavadb-demo SUNWjavadb-docs SUNWjavadb-javadoc SUNWjavadb-service SUNWjavadb-common

      6) 설치하기
        
cd javadb-<version>
pkgadd
-d . SUNWjavadb-common SUNWjavadb-client SUNWjavadb-core SUNWjavadb-demo SUNWjavadb-docs SUNWjavadb-javadoc SUNWjavadb-service

      7) 새 JavaDB 패키지 설치하기
         
cd javadb-<version>
          pkgadd
-d . SUNWjavadb-common SUNWjavadb-client SUNWjavadb-core SUNWjavadb-demo SUNWjavadb-docs SUNWjavadb-javadoc SUNWjavadb-service



③ 리눅스의 경우
   1) 다운로드 하기
      javadb-<version>-linux-rpm.bin
   2) 권한 확인하기
        
chmod +x javadb-<version>-linux-rpm.bin

       3) 압축풀기
          
/javadb-<version>-linux-rpm.bin

    이 명령을 실행하고 나면 javadb-<version> 디렉토리 밑에 여러 개의 RPM 패키지
Sun-javadb-*.i386.rpm이 생긴다.

       4) 수퍼 권한자로 변경하기
       5) 기존 JavaDB 삭제하기
          만약 기존에 JavaDB 설치가 되어 있으면(디폴트 설치 경로는 /opt/sun/javadb 이다)
          이를 새 버전을 설치하기 전에 삭제해야 한다.
          현재 돌아가고 있는 JavaDB 패키지의 리스트를 보는 방법은 다음과 같다.
         
rpm qa | grep sun-javadb-

          기존 패키지를 제거하기       
         
rpm -ev sun-javadb-common sun-javadb-client sun-javadb-core sun-javadb-demo sun-javadb-docs sun-javadb-javadoc


        6) 새 JavaDB 패키지 설치하기

cd javadb-<version>
rpm
-ivh sun-javadb-*.rpm

       
위와 같이 JavaDB를 설치하면 demo, frameworks, javadoc, docs 그리고 lib라는 서브 디렉토리가
생긴다.
이들 서브 디렉토리가 어떤 정보를 가지고 있는지 살펴보자.

1) demo : 2개의 데모 프로그램을 가지고 있으며 database 폴더는 임베디드 애플리케이션을 어떻게 만드는 지에 대한 데모이며 programs 폴더는 클라이언트-서버 환경에서 JavaDB를 사용하는 데모이다. 이 데모는 밑에서 실행해 보도록 하겠다.
2) frameworks : 환경 변수, 데이터베이스 생성 및 작업의 셋팅을 위한 유틸리티를 가지고 있다.
3) javadoc : 예상 했겠지만 API 관련 문서가 있으며 jdbc3와 jdbc4 폴더로 나뉘어져 있다.
4) docs : JavaDB의 셋업 및 어드민, 레퍼런스 가이드가 있다.
5) lib : JAR 파일과 같은 패키지 된 JavaDB 라이브러리가 있다.

4. JavaDB 실행하기

설치를 마쳤으면 이제 JavaDB를 가동시켜 보자. 가동하기 전 현재 환경 셋업이 잘 되었는지 테스트 해야 한다. 테스트 하는 명령어는 다음과 같다.

java org.apache.derby.tools.sysinfo -cp embedded SimpleApp.class


명령어를 실행하면 다음과 같은 결과 화면이 나온다.
 

이 테스트는 먼저 클래스 경로를 찾고 라이브러리와 클래스를 찾는다.
이 테스트가 성공적으로 끝나면 화면과 같은 메시지가 나온다.

그럼 이제 Derby 프로그램을 실행시켜 보자.
우선 실행할 프로그램은 SimpleApp라는 자바 프로그램이다. 이 프로그램은 설치 시 예제로 들어가 있으며 기본적인 커넥션 얻는 법부터 SQL 문을 수행하는 예제이다. 소스는 다음과 같다.

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Properties;

public class SimpleApp
{
   
/* the default framework is embedded*/
   
public String framework = "embedded";
   
public String driver = "org.apache.derby.jdbc.EmbeddedDriver";
   
public String protocol = "jdbc:derby:";

   
public static void main(String[] args)
   
{
       
new SimpleApp().go(args);
   
}

   
void go(String[] args)
   
{
       
/* parse the arguments to determine which framework is desired*/
        parseArguments
(args);

       
System.out.println("SimpleApp starting in " + framework + " mode.");

       
try
       
{
           
/*
               The driver is installed by loading its class.
               In an embedded environment, this will start up Derby, since it is not already running.
             */

           
Class.forName(driver).newInstance();
           
System.out.println("Loaded the appropriate driver.");

           
Connection conn = null;
           
Properties props = new Properties();
            props
.put("user", "user1");
            props
.put("password", "user1");

           
/*
               The connection specifies create=true to cause
               the database to be created. To remove the database,
               remove the directory derbyDB and its contents.
               The directory derbyDB will be created under
               the directory that the system property
               derby.system.home points to, or the current
               directory if derby.system.home is not set.
             */

            conn
= DriverManager.getConnection(protocol +
                   
"derbyDB;create=true", props);

           
System.out.println("Connected to and created database derbyDB");

            conn
.setAutoCommit(false);

           
/*
               Creating a statement lets us issue commands against
               the connection.
             */

           
Statement s = conn.createStatement();

           
/*
               We create a table, add a few rows, and update one.
             */

            s
.execute("create table derbyDB(num int, addr varchar(40))");
           
System.out.println("Created table derbyDB");
            s
.execute("insert into derbyDB values (1956,'Webster St.')");
           
System.out.println("Inserted 1956 Webster");
            s
.execute("insert into derbyDB values (1910,'Union St.')");
           
System.out.println("Inserted 1910 Union");
            s
.execute(
               
"update derbyDB set num=180, addr='Grand Ave.' where num=1956");
           
System.out.println("Updated 1956 Webster to 180 Grand");

            s
.execute(
               
"update derbyDB set num=300, addr='Lakeshore Ave.' where num=180");
           
System.out.println("Updated 180 Grand to 300 Lakeshore");

           
/*
               We select the rows and verify the results.
             */

           
ResultSet rs = s.executeQuery(
                   
"SELECT num, addr FROM derbyDB ORDER BY num");

           
if (!rs.next())
           
{
               
throw new Exception("Wrong number of rows");
           
}

           
if (rs.getInt(1) != 300)
           
{
               
throw new Exception("Wrong row returned");
           
}

           
if (!rs.next())
           
{
               
throw new Exception("Wrong number of rows");
           
}

           
if (rs.getInt(1) != 1910)
           
{
               
throw new Exception("Wrong row returned");
           
}

           
if (rs.next())
           
{
               
throw new Exception("Wrong number of rows");
           
}

           
System.out.println("Verified the rows");

            s
.execute("drop table derbyDB");
           
System.out.println("Dropped table derbyDB");

           
/*
               We release the result and statement resources.
             */

            rs
.close();
            s
.close();
           
System.out.println("Closed result set and statement");

           
/*
               We end the transaction and the connection.
             */

            conn
.commit();
            conn
.close();
           
System.out.println("Committed transaction and closed connection");

           
/*
               In embedded mode, an application should shut down Derby.
               If the application fails to shut down Derby explicitly,
               the Derby does not perform a checkpoint when the JVM shuts down, which means
               that the next connection will be slower.
               Explicitly shutting down Derby with the URL is preferred.
               This style of shutdown will always throw an "exception".
             */

           
boolean gotSQLExc = false;

           
if (framework.equals("embedded"))
           
{
               
try
               
{
                   
DriverManager.getConnection("jdbc:derby:;shutdown=true");
               
}
               
catch (SQLException se)
               
{
                    gotSQLExc
= true;
               
}

               
if (!gotSQLExc)
               
{
                   
System.out.println("Database did not shut down normally");
               
}
               
else
               
{
                   
System.out.println("Database shut down normally");
               
}
           
}
       
}
       
catch (Throwable e)
       
{
           
System.out.println("exception thrown:");

           
if (e instanceof SQLException)
           
{
                printSQLError
((SQLException) e);
           
}
           
else
           
{
                e
.printStackTrace();
           
}
       
}

       
System.out.println("SimpleApp finished");
   
}

   
static void printSQLError(SQLException e)
   
{
       
while (e != null)
       
{
           
System.out.println(e.toString());
            e
= e.getNextException();
       
}
   
}

   
private void parseArguments(String[] args)
   
{
       
int length = args.length;

       
for (int index = 0; index < length; index++)
       
{
           
if (args[index].equalsIgnoreCase("jccjdbcclient"))
           
{
                framework
= "jccjdbc";
                driver
= "com.ibm.db2.jcc.DB2Driver";
                protocol
= "jdbc:derby:net://localhost:1527/";
           
}
           
if (args[index].equalsIgnoreCase("derbyclient"))
           
{
                framework
= "derbyclient";
                driver
= "org.apache.derby.jdbc.ClientDriver";
                protocol
= "jdbc:derby://localhost:1527/";
           
}
       
}
   
}
}

 
이 소스를 실행하면 다른 DB에 접속하는 법과 다른 점은 없다.
 

지금까지 JavaDB의 기본적인 설명을 다루었다. JavaDB를 사용해보면서 느낀 점은 가볍다는
것이었다. 향후 임베디드 시장에서의 활약을 기대해 보지만 이를 뚫기 위해 넘어야 할 난관이
많으며 이 중 오라클에서 Oracle Berkeley DB라는 오픈소스 기반 Lightweight 
데이터베이스와의 경쟁이 그 하나이다. 오라클은 Derby와 Oracle Berkeley DB의 성능을 비교한
문서를 공개하며 성능 이슈를 재기하고 있다. 임베디드 시장에서의 두 데이터베이스간의 활약이
기대하며 기고를 마친다.

참조 문헌 :
JavaDB 설치: http://developers.sun.com/javadb/downlo ··· ons.html
아파치 Derby 소개: http://db.apache.org/derby/docs/dev/getstart/
오라클 버클리 DB 소개: http://www.oracle.com/technology/produc ··· dex.html

"Java SE" 카테고리의 다른 글

Posted by 1010
98..Etc/Etc...2008. 11. 12. 17:12
반응형

많은 J2EE개발자들이 환경 변수(environment entries), DataSource 객체, JMS 메시지 수신지(JMS message destinations) 그리고 엔터프라이즈 빈 홈 인터페이스(enterprise bean home interfaces)를 찾고자 Java Naming and Directory Interface (JNDI)를 이용한다. 하지만 많은 사람들은 JNDI에 대한 진정한 이해 없이 이러한 기능을 하는 코드를 단순히 복사해서 붙이고 고칠 뿐이다. 이 팁은 사용자의 엔터프라이즈 시스템에 배포된 리소스를 액세스하기 위해 JNDI를 사용하는 방법을 소개한다.

엔터프라이즈 애플리케이션들은 그것들의 특성상, 비즈니스 오퍼레이션을 지원하기 위해 여러곳에 배포된 리소스들을 한데 모아야 한다. 새로운 시스템이 생성되었다거나, 기존의 시스템이 업그레이드 되었다거나, 오래된 시스템이 더 이상 작동하지 않을 때 서비스들이 오가게 된다. 애플리케이션 서비스를 서로 분리하는 것은 시스템을 쉽게 유지/확장할 수 있게 한다. 하지만 서비스가 분리되었을 때, 각자의 역할을 제대로 수행하기 위해서는 서로를 찾아낼 수 있어야만 한다. 이 때가 바로 명명 서비스(naming services)와 디렉토리가 유용한 시점이다.

명명 서비스는 이름을 이용해서 객체나 객체에 대한 레퍼런스를 검색하는 방법을 제공한다. 그러한 객체로의 예는 메시지 큐(message queues), 데이터베이스 커넥션 팩토리(database connection factories), 환경 파라미터(environment parameters), 그리고 엔터프라이즈 빈과 같은 분산 컴포넌트(distributed components)등이 있다. 애플리케이션 개발자들은 명명 서비스내의 이름에 객체들을 바인딩해서 객체에 이름을 붙인다. 애플리케이션 코드는 이렇게 바인딩된 이름으로 객체들을 검색하기 위해 명명 서비스를 사용할 수가 있다. 이러한 분리(decoupling)는 네트워크 객체들을 사용하는 시스템 컴포넌트에 대한 어떠한 변경없이 유지 보수를 위해 올리거나 내릴 수 있고, 요청들을 리다이렉트(redirect)시킬 수 있으며, 서비스가 다이나믹하게 재조정될 수 있음을 의미한다.

이미 기존의 명명 서비스에 대해 잘 이해하고 있으리라고 생각된다.

  • DNS (Domain Name Service)는 java.sun.com과 같은 호스트네임을 %nslookup java.sun.com%과 같은 IP주로소 매핑한다.
  • CORBA (Common Object Request Broker Architecture)를 위해 쓰이는 COS (Common Object Services) 명명 서비스 는 CORBA 인터페이스 이름(interface names)을 객체 인터페이스로 매핑한다.

사용자는 컴퓨터의 파일시스템을 파일의 경로(pathname)에 파일의 컨텐츠를 매핑하는 일종의 명명 서비스로 생각할 수 있다.

밑의 그림은 명명 서비스가 서비스 네임을 데이터나 서비스 인터페이스로 매핑하는 방법을 보여주고 있다.

figure 1

이름(name)을 객체로 매핑하는 것을 바인딩이라 부른다. 바인딩은 명명 서비스를 형성하는 사람에 의해 생성된다. 대부분의 명명 서비스는 프로그램이 런타임시에 이름을 객체로 바인딩하거나 해제하는 방법도 제공한다.

컨텍스트는 이름을 객체로 바인딩한 집합이다. 예를 들면, 파일시스템에서 경로 /home 는 흔히 시스템의 유저 디렉토리를 포함하는 컨텍스트이다. 컨텍스트는 다른 컨텍스트를 포함할 수도 있다. /home 컨텍스트의 유저 디렉토리는 그 자체가 유저 파일을 포함하는 컨텍스트이다.

컨텍스트는 최소한 명명 규칙(naming convention)과 검색기능(lookup function)을 갖는다. 예를 들면, DNS는 가장 구체적인 스트링은 왼쪽, 도메인은 오른쪽에 나타내면서 스트링을 점으로 분리하는 명명 규칙을 갖는다. DNS의 검색기능은 nslookup 프로그램을 이용해서 커맨드라인으로부터 액세스가 가능하다.(물론, DNS 명명 서비스에 대한 API도 존재한다.) 컨텍스트는 대게 객체들을 바인딩하고 해제하는 방법과 그것들을 열거하는 방법을 제공한다.

때때로 명명 서비스 의 객체들은 다른 프로그램이 필요로 하는 데이터를 포함한다. 가령, J2EE애플리케이션에서 환경 변수를 나타내는 객체들은 대게 명명 서비스 에 저장된다. 하지만 이외에 명명 서비스의 객체는 객체에 대한 레퍼런스를 나타낸다. 예를 들면, 서버에 레퍼런스를 제공하는 객체는 통상적으로 오픈 서버 커넥션이 아닌 서버에 대한 레퍼런스로서 명명 서비스 에 의해 저장된다.명명 서비스 에 의해 리턴된 레퍼런스 객체는 필요시에 서버 커넥션을 생성하기 위해 사용될 수 있다.

다음 그림은 컨텍스트의 개념도이다. top 컨텍스트는 /top로 불리며 객체, 레퍼런스, 그리고 다른 컨텍스트들을 포함한다. 컨텍스트 /top은 subcontexts b 와 g를 갖는다. 컨텍스트 /top/g 는 subcontext "b"를 갖는다. A라고 불리는 객체가 하나 이상 일지라도, 특정 개체 /top/g/b/a는 그 위치가 컨텍스트 /top/g/b 임이 분명하기 때문에 찾을 수가 있다.

figure 2

바인딩된 객체들에 대한 데이터를 제공하는 명명 서비스를 디렉토리라고 부른다. 가령, 파일시스템 디렉토리는 일반적으로 크기, 타입, 접근 허용 그리고 파일을 생성하고 수정한 날짜에 관한 정보를 제공한다. 몇몇 디렉토리는 이름으로 검색, 애트리뷰트의 조합으로 검색 모두를 허용한다.

각각의 명명 서비스들은 각자의 태스크(task)에 잘 맞도록 되어있지만, 그들이 작동하는 방식은 서로 다르다. 각 명명 서비스는 고유의 명명규칙, 검색기능, 바인딩과 디렉토리 프로토콜(directory protocols)과 객체 서비스 인터페이스(object service interfaces)를 갖는다. JNDI 는 네트워크 서비스를 이름짓고 찾기 위해 일관된 방법을 제공한다.

Java Naming and Directory Interface

JDBC 데이터베이스 커넥션(database connections), JMS 큐(JMS queues) 혹은 엔터프라이즈 빈 홈 인터페이스(enterprise bean home interfaces)와 같은 네트워크 객체에 액세스하기 위해 JNDI 를 사용하는 방법을 이미 알고 있을 것이다. 사실 JNDI 는 이름들을 객체로 매핑하지만, JNDI 는 명명 서비스가 아니다. 그보다도 JNDI는 명명 서비스를 표준적인 방법으로 액세스가능하게 하면서 기존의 명명 서비스를 감추는(wrap) 인터페이스들의 집합이다.

다음 그림에서 보는 것과 같이, 자바 애플리케이션은 JNDI인터페이스를 이용해서 감춰진(underlying) 명명 서비스에 액세스한다.

figure 3

애플리케이션내의 코드는 JNDI 인터페이스 메소드를 호출한다. 이러한 메소드를 구현하는 객체들은 JNDI 인터페이스 호출을 감춰진 명명 서비스에 대한 호출로 매핑한다. 또한 JNDI는 통합된 명명 규칙도 정의한다. JNDI 이름들은 JNDI의 명명 관리자(naming manager)에 의해 감춰진 명명 서비스의 명명 규칙을 따르는 이름으로 매핑된다.

javax.naming 패키지는 다음과 같은 명명과 디렉토리에 관련된 인터페이스들을 포함한다.

  • javax.naming.Context는 컨텍스트를 나타내는데, 이것은 바인딩과 서브컨텍스트를 찾고 관리하는 데에 쓰인다.
  • javax.naming.Name는 명명 서비스의 이름을 추상적으로 표현(abstract representation)하게 해준다.
  • javax.naming.Binding은 명명 서비스 이름과 그 이름에 바인딩된 객체의 표현이다.
  • javax.naming.Reference는 객체의 복사본을 얻어낼 수 있게 해준다.

컨텍스트 찾기

이 팁에 포함된 샘플코드는 JNDI컨텍스트의 컨텐츠를 열거하는 방법을 보여준다. 샘플 서블릿 Oct2003Servlet는 사용자가 입력한 이름에 해당하는 JNDI namespace내의 컨텐츠를 찾고 디스플레이한다.

컨텍스트를 얻는 가장 쉬운 방법은 javax.naming.InitialContext 클래스의 인스턴스를 생성하는 것이다. 샘플 서블릿 메소드 jndiList 는 최초의 컨텍스트를 생성하고 명명된 객체를 찾을 때 그것을 사용한다.

   InitialContext ic = new InitialContext();
   Object objFound = ic.lookup(name);

여기에서 name은 사용자가 HTML페이지에서 입력한 HTTP GET 혹은 POST 변수명(vriable name)이다. 만약 리턴된 객체가 Context 라면 jndiListlistContext 메소드를 호출하고, ListContext메소드는 주어진 이름에 해당하는 컨텍스트의 컨텐츠를 열거한다. 객체가 DataSource이면, jndiList 는 명명된 데이터 소스에 관한 정보를 출력한다.

listContext 메소드는 주어진 JNDI 컨텍스트의 컨텐츠를 하나의 테이블로 출력한다. 이를 위해서는 Context 메소드 listBindings 를 이용하는데, 이는 NamingEnumeration 를 리턴한다.

      NamingEnumeration ne = context.listBindings("");

NamingEnumerationjava.util.Enumeration를 구현한 것이다. NamingEnumeration.next 메소드는 javax.naming.Binding 타입의 객체를 리턴하고, 이는 객체의 이름과 객체의 클래스 이름 그리고 저장된 객체 자체를 포함한다.

      
      while (ne.hasMore()) {
         Binding ncp = (Binding)ne.next();
         String objName = ncp.getName();
         String objClass = ncp.getClassName();
         Object objObj = ncp.getObject();

         ...
     }

단순히 Context내의 이름들과 클래스이름을 보고자 한다면 Context.list 메소드를 이용할 수 있다. Context.listNamingEnumeration를 리턴하지만, 그것이 담고있는 컬랙션은 Binding이 아닌 NameClassPair 타입이다. NameClassPair는 이름과 객체 클래스 이름만을 포함한다.

샘플애플리케이션을 배포하고 실행하는 방법은 샘플코드 실행하기 를 참고한다.

애플리케이션을 실행하면, 다음과 같은 시작페이지를 보게 된다.

jndichoice

컨텍스트를 입력하거나, 텍스트 필드를 빈 상태로 놔두고 명명된 컨텍스트의 컨텐츠를 보기 위해 List버튼을 클릭한다. 예를 들면 jdbc 의 엔트리는 다음과 같은 화면을 디스플레이한다.

jndilist

JNDI 에 관한 더 자세한 정보는 JNDI 튜토리얼를 참고한다.

"Java EE" 카테고리의 다른 글

Posted by 1010
98..Etc/Etc...2008. 11. 12. 17:12
반응형

JavaServer Pages (JSP pages)의 커스텀 태그는 HTML 태그와 유사하다. 하지만 커스텀 태그는 태그와 관련된 핸들러 클래스에 의해 런타임시에 텍스트 형식의 출력값으로 대체된다. 지난 테크팁인 Using Custom Tags에서 이와 같은 클래스의 생성방법에 대해 이야기했다. 이번달의 첫번째 팁은 커스텀 태그를 구현하는 더 새롭고 쉬운 방법을 설명한다.

JSP 2.0이전에는 커스텀 태그를 생성하려면 자바 태그 핸들러 클래스로 구현하는 것이 유일한 방법이었다. 또한 핸들러 클래스 개발자는 태그를 웹 컨테이너에게 설명하는 역할을 하는 TLD파일을 생성해야만 했다. 커스텀 태그의 힘은 강력하지만 이를 이용하려면 프로그래밍 스킬이 요구되고 JSP 페이지가 어떻게 HTML로 변환하는지를 확실하게 알고 있어야 한다.

JSP 2.0 태그 파일의 새로운 기능은 비개발자도 재사용이 가능한 커스텀 태그를 작성할 수 있게 할 뿐만 아니라 프로그래머도 더 쉽게 작업할 수 있도록 도와준다. JavaServer Pages Standard Tag Library (JSTL) 과 expression language (EL)으로 이루어진 JSP 2.0 구문을 이용하면, 자바 코드를 작성할 필요없이 커스텀 태그를 생성할 수 있다.

태그 파일은 JSP 페이지에서 재사용이 가능한 컴포넌트이다. 이를 사용하면 다음과 같은 이점이 있다.

  • Scriptlets을 숨기거나 제거하는데 사용될 수 있다.
  • 자르기와 붙이기가 아닌, 레퍼런스로를 이용하여 코드를 재사용가능하도록 만든다.
  • JSP 페이지를 작성하기 쉽고, 매우 논리적이며, 유지하기 쉽도록 만든다.
  • 비개발자에 의해 작성될 수 있다.
  • 태그 파일의 구성은 자바가 아닌 HTML에 더 가깝다. 따라서 JSP 페이지는 단일 언어로 작성된 것처럼 보인다.
  • 태그 파일은 하이 레벨 컴포넌트를 형성하기 때문에 생산성을 높이고 개발속도를 빠르게 한다.
  • 커스텀 태그에 의해 사용되는 TLD파일은 흔히 자동적으로 생성된다.
  • 현존하는 페이지를 리팩토링하기 위해 사용될 수 있다. 코드의 공통 부분은 애플리케이션 뷰간에 공유되는 태그 파일로 통합될 수 있다.

태그 파일이 커스텀 태그 핸들러 클래스를 완벽하게 대체하지는 못한다. 태그 파일은 재사용이 가능한 컨텐츠를 관련된 레이아웃과 프리젠테이션으로 캡슐화할 때 적합하다. 반면 커스텀 태그는 JSP페이지에서 애플리케이션 로직(logic)을 재사용할 때 더 효과적이다. 가령, 페이지 헤더와 풋터(꼬리말)는 태그 파일을 위한 최적의 애플리케이션이다. 이와 비교해서 JSTL의 커스텀 태그는 자바 언어 핸들러 클래스로서 구현된다.

태그 파일의 특징

태그 파일은 사실 태그 핸들러 클래스로 번역/컴파일된다. 태그 핸들러와 태그 핸들러 클래스의 단 한가지 다른 점은 태그 핸들러는 JSP 구문으로 쓰여지고 태그 핸들러 클래스는 자바 언어로 작성된다는 점이다.

JSP 2.0-compliant 컨테이너는 웹 아카이브 디렉토리인 WEB-INF/tags내에서 태그 파일을 찾게 된다. 태그 파일은 WEB-INF/lib의 JAR 파일안에 묶일 수도 있다. JSP 페이지를 구현할 때 웹 컨테이너가 태그 파일과 관련된 태그를 마주치게 되면, 태그 파일의 JSP컨텐츠 출력값은 계산되어서 응답 스트림에 포함되게 된다. 태그 파일은 애트리뷰트를 정의할 수 있고 JSP 2.0 expression language (EL)에 대한 접근이 가능하다. 또한 태그 파일은 실행이 완료된 후에도 존재하는 EL 변수들을 생성할 수 있다.

태그 파일은 애트리뷰트 지시어를 이용해서 애트리뷰트를 선언하게 된다. 다음은 이 팁의 샘플코드로부터 발췌한 예제이다. 태그 파일은 단일 애트리뷰트 지시어로 시작한다.

   <%@ attribute name="format" required="false" %>

날짜를 생성하는 태그는 위의 라인을 이용해서 컨테이너에게 가능한 "format" 애트리뷰트를 찾으라고 명령한다. 필수 애트리뷰트일 경우에는 지시어의 "required" 애트리뷰트가 "true"로 설정된다. 이 지시어들은 전개시에 웹 컨테이너가 고유의 TLD 파일을 생성하도록 해준다.

태그는 애트리뷰트를 통해서 입력값을 받는다. 태그 파일에 의해 생성되는 출력 텍스트이외에도 태그는 EL변수를 생성함으로써 데이터를 "output" 할 수 있다. 태그 파일은 다음과 같이 값을 태그 파일이 호출되었던 페이지에 값을 리턴할 수도 있다.

   <%@ variable name-given="filesincluded" scope="AT_END" %>

"name-given"은 태그가 수행된 후, 페이지내에 설정되는 변수의 이름을 제공한다. "AT_END"는 태그 파일이 완성되면 변수가 설정된다는 것을 말한다.

태그 파일 예제

이번 샘플 코드는 지난 2002/9월의 테크팁 "Using Custom Tags"에서 작성했던 커스텀 태그를 재구현하는 태그 파일을 포함하고 있다. 오리지널 태그는 다음 세 가지 중 한가지 방법으로 서버에 날짜의 포맷을 지정한다.

  • 만약 포맷이 설정되지 않았거나 비어있다면, 태그는 디폴트 포맷으로 날짜를 프린트한다.
  • 포맷 파라미터가 $사인으로 시작되면, 태그는 스트링의 나머지 부분을 환경 엔트리의 이름으로 사용한다. 태그는 지명된 환경 엔트리를 찾아서 그것의 값을 포맷으로 사용하게 된다.
  • java.text.SimpleDateFormat과 호환되는 "format" 파라미터가 포맷 스트링을 포함하고 있다면, 스트링은 날짜의 포맷을 지정하는데 쓰이게 된다.

태그 파일에 의해 지정된 태그는 약간 다르게 작동한다. 포맷 파라미터가 $으로 시작하면, 태그는 환경 엔트리 대신 서블릿 컨텍스트 초기화 파라미터를 찾는다. (JSTL 1.0의 expression language는 환경 엔트리를 액세스할 수 있는 빌트 인 서포트를 포함하지 않았다.)

샘플 태그 파일인 date.tag는 파일이 요구하는 애트리뷰트를 정의하는 몇몇 지시어로 시작한다. 또한 이 태그가 사용하는 다른 태그 라이브러리를 위한 namespaces를 식별해내기도 한다.

   <%@ attribute name="format" required="false" %>
   <%@ taglib uri=
     "http://java.sun.com/jsp/jstl/core" prefix="c" %>
   <%@ taglib uri=
     "http://java.sun.com/jsp/jstl/fmt" prefix="fmt" %>
   <%@ taglib uri=
     "http://java.sun.com/jsp/jstl/functions" 
     prefix="fn" %>

날짜 태그 파일의 다음 블록은 포맷 스트링을 결정하기 위해 <c:choose>태그를 사용한다. <c:choose>는 JSP페이지에서 if/then/else기능을 선택하기 위한 태그이다. 처음 "when"구문은 포맷이 null이거나 비어있을 때 EL변수 "format"을 디폴트 값으로 설정하기 위한 것이다.

   <c:choose>
     <%-- If format is blank, set default --%>
     <c:when test="${format == null or format == ''}">
       <c:set var="format" 
         value="EEEE, d MMMM yyyy 'at' kk:mm:ss z"/>
     </c:when>
   ...

"format"이 비어있는 상태가 아니라면, 그것은 "$"사인으로 시작하거나 또는 "$"으로 시작하지 않는다. 첫번째 경우에서, <choose> 태그의 <otherwise>절은 첫번째 "$"을 없애버리고 주어진 이름을 이용해서 "format" 변수의 값을 컨텍스트 파라미터의 컨텐츠로 대체하게 된다.

     <c:otherwise>
       <%-- Else if format starts with "$", 
           look up in context param,
           and set "format" to its value. --%>
       <c:if test="${fn:substring(format,0,1) == '$'}">
         <c:set var="format_name" 
           value="${fn:substringAfter(format,'$')}"/>
         <c:set var="format" 
           value="${initParam[format_name]}"/>
       </c:if>

       <%-- Otherwise leave it as it is --%>
     </c:otherwise>
   </c:choose>

코멘트에 적혀있듯이, "format"이 "$"로 시작하지 않으면, 그것의 값은 변하지 않는다.

페이지의 이 지점에서, EL "format"변수의 값은 날짜의 포맷을 지정하기 위해 사용될 스트링으로 설정된다. useBean라인은 현재 시간을 나타내는 Date객체를 생성하고 fmt:formatDate 메소드는 이 날짜를 주어진 "format"에 설정하게 된다.

   <%-- Now actually create and format the date --%>
   <jsp:useBean id="now" class="java.util.Date"/>
   <fmt:formatDate value="${now}" pattern="${format}"/>

이것으로 태그 파일이 완성되었다.

태그 파일을 이용하는 것은 더욱 간단하다. 샘플 페이지 DatePage.jsp 의 윗부분에 있는 지시어는 /WEB-INF/tags 디렉토리에 있는 모든 태그가 접두사 "mytags"를 이용해서 액세스 가능하다는 것을 말한다. 지시어는 다음과 같다.

    <%@ taglib tagdir="/WEB-INF/tags" prefix="mytags" %>

자, 이제 JSP 태그 파일을 다른 커스텀 태그처럼 간단하게 사용할 수 있다. 샘플 페이지 DatePage.jsp에서 발췌한 코드이다.

   The time and date at the server in the default format
      are <b><mytags:date/></b>.<br>
   The time zone at the server is 
      <b><mytags:date format="zzzz"/></b>.<br>
   The server date is 
      <b><mytags:date format="M/d/yyyy"/></b>.<br>
   The server time is 
      <b><mytags:date format="hh:mm:ss a"/></b>.<br>

각각의 는 태그 파일을 호출하는 결과를 낳고, 태그는 서버에 해당 시간과 날짜를 출력하게 된다.

이 예제는 JSP태그 파일을 간단히 소개하는 것에 그친다. 사실 JSP태그 파일은 여기서 언급한 것 이상의 많은 옵션을 갖고 있다. JSP태그 파일에 대한 자세한 정보는 J2EE 1.4 튜토리얼의 "Custom Tags in JSP Pages"장을 참고하기 바란다.

"Java EE" 카테고리의 다른 글

Posted by 1010
01.JAVA/Java2008. 11. 12. 17:10
반응형

많은 사람들이 알고 있는 것처럼, JDBC는 자바 프로그램으로부터 관련된 데이터베이스에 이식가능한 액세스를 할 수 있는 메커니즘이다. 관련된 데이터베이스에 액세스하기 위해 사용자의 J2EE 애플리케이션의 JDBC를 사용할 수 있다. 이번 테크팁은 JDBC를 사용하는 간단한 애플리케이션을 보여주고 이식성과 유연성을 위한 몇 가지 유용한 JDBC 실습법을 소개한다.

JDBC의 소개

JDBC를 이용해서 데이터에 액세스하는 절차는 간단하다.

  1. 드라이버를 로드한다. 대부분의 경우 클래스이름으로 되어있다.

  2. 데이터베이스에 Connection 접속한다. 이름으로 DataSource 를 위한 JNDI를 호출하고 DataSource 로부터 Connection을 얻어서 접속할 수 있다. 이 절차는 동일한 서블릿 메소드인 getConnection 에서 실제적으로 볼 수 있을 것이다.

  3. Connection로부터 Statement 객체를 받는다. 각각의 Statement는 단일 SQL 커맨드를 나타낸다. StatementStatement의 출처가 되는 Connection에 묶여 있기 때문에 다른 Connection을 위해 재사용될 수가 없다.

  4. updateQuery(데이터를 업데이트하지만 결과는 리턴하지 않음)나 executeQuery(선정된 데이터의 ResultSet를 리턴함) 와 같은 Statement 메소드를 이용해 비즈니스 기능을 수행해 보자.

  5. ResultSet를 받았다면 다른 비즈니스 기능을 수행하기 위해 쿼리의 결과를 반복해서 실행해 보자.

이 팁의 샘플 코드에서는 jdbcservlet라고 불리는 서블릿을 사용하고 있다. 이 서블릿은 데이터베이스에 접속하여 임의의 SQL 구를 실행하도록 한다. 그리고 리턴되는 결과를 테이블의 형태로 보여주고, SAVEDQUERIES라고 불리는 테이블 안에 지명된 쿼리들을 저장할 수 있게 한다.

데이터베이스 생성하기

설명을 계속하기 전에 이 애플리케이션에 번들된 데이터 로딩 스크립트가 연결하고자 하는 데이터베이스 내의 테이블을 드롭하고 생성한다는 것을 기억해야 한다. 필요한 데이터를 포함하고 있는 데이터베이스에 연결하고 있지 않다는 것을 확실히 해야 한다.

안전한 데이터베이스에 연결하고 있다는 것을 확실히 확인할 수 있는 가장 좋은 방법은 작업이 끝났을 때 삭제해 버릴 수 있는 것을 하나 생성해 놓는 것이다. 이 팁은 썬으로부터 배포된 표준 J2EE 1.4에 탑재된 Java technology relational database management system (RDBMS)인 PointBase 를 사용한다. 이는 통합된 PointBase 드라이버에 부수되어 배포되기 때문에 가장 사용하기 쉬운 방법이다. 원한다면 고유의 데이터베이스를 생성하기 위해 제공자가 지정한 툴을 사용할 수도 있다. (이 경우, 배포 시에 몇 가지 사항만 변경하면 된다.) 데이터베이스를 생성하기 위해서는 "샘플 코드 실행하기"의 "팁 2: 데이터베이스 생성하기" 부분을 참고하기 바란다.

쿼리 실행하기

샘플 애플리케이션에 의해 디스플레이되는 메인 페이지에서 SQL 쿼리를 동반하는 텍스트 박스를 볼 수 있다.

이 쿼리를 실행하려면 Execute SQL 버튼을 클릭한다.

쿼리 결과의 일부분만을 편집한 것이다.

결과를 나타내는 폼은 3가지 영역을 갖는다.

  • 왼쪽 윗부분, 쿼리 박스
  • 오른쪽 윗부분, 저장된 쿼리의 리스트
  • 이하부분, 쿼리를 실행했을 때 나타나는 결과를 나타내는 테이블

데이터베이스의 데이터 출처는 퍼블릭 도메인 소스이다. 이는 이세상에 존재하는 나라들과 그곳에 사는 사람들에 관한 기초적인 통계를 보여준다.

Name 박스에 이름을 타이핑하고 Save Query 버튼을 클릭하여 흥미있는 쿼리를 저장할 수 있다. 이 쿼리는 오른쪽 Saved 쿼리 박스에 나타날 것이다. 링크를 클릭하여 저장된 쿼리를 실행해보자.

샘플 코드

이하의 샘플은 보기에는 복잡하지만 사실은 매우 간단하다. 이 코드의 핵심을 간추려보았다.

1. 드라이버 로드하기

드라이버를 로드하는 코드는 서블릿을 위한 init 메소드 내에 있다.

    public void init(ServletConfig config)
       throws ServletException {
       _config = config;

       // Load driver. If this fails, the rest won't work.
       try {
          String driver =
                   _config.getInitParameter("driver");
          Class.forName(driver).newInstance();
       } catch (Exception e) {
          throw new ServletException(e);
       }
    }

드라이버 클래스 이름은 서블릿 초기화 파라미터에서 왔다는 것을 기억하자. 이 파라미터는 웹 애플리케이션 배포 디스크립터인 web.xml에 설정되어 있다.

   <servlet>
     <description>Sample servlet</description>
     <display-name>Apr2004Servlet</display-name>
     <servlet-name>Apr2004Servlet</servlet-name>
     <servlet-class>
       com.elucify.tips.apr2004.Apr2004Servlet
     </servlet-class>
     <init-param>
       <description>
         This is the name of the JDBC driver
         used to connect to the database.
       </description>
       <param-name>driver</param-name>
       <param-value>
         com.pointbase.jdbc.jdbcUniversalDriver
       </param-value>
     </init-param>
     ...

드라이버 클래스의 이름을 배포 정보에 삽입하는 것은 좋은 습관이다. 드라이버 클래스를 변경해야 한다면(가령 다른 데이터베이스를 사용하고 있기 때문에), 코드를 변경하고 그것을 다시 컴파일하는 것 대신에 이를 배포 툴에서 해결할 수 있다.

데이터베이스 이름, 사용자, 패스워드의 경우 모두 동일하다.

2. 데이터베이스에 접속한다.

다음 메소드는 데이터베이스에 대한 Connection을 얻기 위해 사용된다.

    protected Connection getConnection() throws Exception {
       String dbname = _config.getInitParameter("dbname");
       String user = _config.getInitParameter("user");
       String pass = _config.getInitParameter("pass");

       InitialContext ic = new InitialContext();
       DataSource ds = (DataSource)ic.lookup(dbname);
       Connection conn = ds.getConnection(user, pass);

       return conn;
    }

Init 파라미터를 정의하고 지정하기

이와 같은 init 파라미터를 세팅하는 배포 디스크립터는 다음과 같다.

     <init-param>
       <description>
         This is the JDBC name of the database.
       </description>
       <param-name>dbname</param-name>
       <param-value>jdbc/PointBase</param-value>
     </init-param>

    <init-param>
      <description>
        The name of the database user.
      </description>
      <param-name>user</param-name>
      <param-value>PBPUBLIC</param-value>
    </init-param>

    <init-param>
      <description>
        The password for the user named above.
      </description>
      <param-name>pass</param-name>
      <param-value>PBPUBLIC</param-value>
    </init-param>

또 다시, 이러한 정보를 배포 디스크립터에 삽입하면 컴파일링을 다시 하지 않고서도 정보를 변경할 수 있게 해준다.

일반적으로 제작과정에서는 배포 디스크립터 파일에 패스워드를 저장하지 않는다. 인증을 잘 관리하기 위해서는 조직의 보안 책임자와 상담하는 것이 대부분이다. 위의 "jdbc/PointBase" 스트링값은 데이터베이스의 이름이다. 이 JDBC리소스는 Sun One 애플리케이션 서버 내에 미리 형성되어서 존재한다. 만약 다른 애플리케이션 서버를 사용한다면 데이터베이스의 이름을 찾기 위해 서버 다큐먼테이션을 보면 된다.(서버 다큐먼트를 직접 생성해야 할 수도 있다.)

3. 데이터베이스를 로드한다.

dataload 메소드는 WAR 아카이브 내의 파일(data/data.sql)로부터 sql 문을 읽어 들인다. 그리고 파일이 처리될 때까지 항목들을 하나씩 차례대로 실행한다. 이 메소드 코드의 대부분은 단순한 스트링 처리이다. 이러한 스티링 처리의 결과는 sqlcmd 라고 불리는 스트링 변수이다. sqlcmd StringBuffer 로 구축되었다. 이 메소드는 Connection 을 생성하고 Connection 으로부터 Statement를 받아서 sqlcmd 쿼리 스트링을 구축한다. 그리고나서 완전한 커맨드를 갖으면 sqlcmd.executeUpdate 에 그 스트링 값을 넘겨주게 된다.

   StringBuffer sbcmd = new StringBuffer();
   String line = "";
   Connection conn = null;

   try {
      // Get connection and statement
      conn = getConnection();
      Statement stmt = conn.createStatement();

      while ((line = in.readLine()) != null) {
         linenumber++;

         // ... line splitting, cleaning, joining, etc.
         // ... result is in sbcmd

          String sqlcmd = new String(sbcmd);
         
          // ... more housekeeping

          stmt.executeUpdate(sqlcmd);

   } catch (Exception e) {
      // ... blah blah
   } finally {
      if (conn != null) {
         try {
            conn.close();
         } catch (SQLException exc) {
            // Not much we can do about it here except...
            System.err.println
              ("Failed closing connection in dataload");
         }
      }
   }

dataload 메소드의 코드는 파일로부터 읽어 들인 SQL 구를 구축하고 Statement.executeUpdate 가 실행될 수 있도록 이를 넘겨준다. 이 메소드는 executeQuery 대신에 executeUpdate 를 사용하는데, 이유는 executeUpdate가 결과를 리턴하지 않고 데이터 로드 스크립트는 SELECT 구를 포함할 수 없기 때문이다.

메소드 dataload의 코드(method dataload code)내의 try/finally블록을 주의 깊게 살펴보자. 이 코드 블록은 중요한 연습방법을 따르고 있는데, 그것은 사용자가 커넥션에 관한 작업을 마쳤을 때 언제나 커넥션들이 닫혀져 있어야 한다는 것이다. 이렇게 하지 않았을 때 심한 리소스 누출이 생길 수 있다. 이런 경우, 서버의 커넥션 풀 안의 모든 커넥션은 닫혀있지 않은 커넥션에 의해 계속된다. 때문에 항상 try/finally블록에 Connection객체의 사용을 enclose하고 필요할 시 커넥션을 닫아야 한다.

서버-사이드 커넥션 관리에 있어 또 다른 주의 사항은, 만약 서버가 커넥션을 갖지 않는다는 사실을 파악하지 못했다면 커넥션 풀을 구현하려는 시도를 하지 말아야 한다는 것이다. 서버가 이미 커넥션을 갖고 있는 상태라면 커넥션 풀을 구현하려는 것은 시간 낭비일 수 있기 때문이다. 또한 서버 커넥션 풀과 애플리케이션 레벨의 풀 간에 경합은 애플리케이션의 퍼포먼스를 떨어뜨릴 수 있다. 말하자면 커넥션 풀링은 애플리케이션 프로그래머가 아닌 서버 프로그래머의 일이라는 것이다.

웹 애플리케이션에서 대부분의 경우처럼 서버가 커넥션 풀을 갖고 있다면 HTML 호출을 리턴하기 전에 커넥션을 항상 해제해야 한다. 만약 서버가 이미 공유되는 커넥션을 효율적으로 관리하고 있다면 이 공유 커넥션에 시간을 투자할 이유는 없다. 또한 서블릿이 불필요하게 데이터베이스 커넥션에 매달려있다면 애플리케이션의 속도를 늦출 수 있다.

4. SQL 문 실행하기

브라우저로부터 받은 SQL문은 데이터를 리턴하는 SELECT 문이거나 데이터를 리턴하지 않는 다른 문장일 수 있다. evalprint 메소드는 이후의 SQL이 SELECT 문이라면 Statement.executeQuery를 사용하고 그렇지 않으면 executeUpdate를 사용한다. 입력 서식에서 받은 사용자의 SQL을 실행시키는 코드는 다음과 같다.

   try {
      // Get connection and statement
      conn = getConnection();
      Statement stmt = conn.createStatement();
      ResultSet rs;

      // Execute posted sqlcmd
      if (sqlcmd.trim().toUpperCase().indexOf("SELECT ") == 0) {
         rs = stmt.executeQuery(sqlcmd);
         pw.println("<div class=\"results\">");
         pw.println("<h2>Query Results</h2>");
         printResultSet(pw, rs);
         pw.println("</div>");
      } else {
         stmt.executeUpdate(sqlcmd);
      }

5. 출력 결과

printResultSet 메소드는 HTML 테이블 형식으로 ResultSet객체를 출력한다. ResultSet객체를 위한 데이터 액세스 인터페이스는 SQL 결과를 다루기 쉽게 만든다.

   protected void printResultSet(PrintWriter pw, 
           ResultSet rs)
      throws SQLException {
      ResultSetMetaData rsmd = rs.getMetaData();
      int cols = rsmd.getColumnCount();
      int nrows = 0;

      pw.println("<TABLE class=\"tbl\">");
      pw.println("<TR class=\"hdrtr\">");
      for (int n = 1; n <= cols; n++) {
         pw.print
           ("<TD class=\"hdrcol hdrcol" + n + "\">");
         pw.print(rsmd.getColumnLabel(n));
         pw.println("</TD>");
      }
      pw.println("</TR>");

      while (rs.next()) {
         ++nrows;
         pw.println
           ("<TR class=\"datatr datatr" + nrows + "\">");
         for (int i = 1; i <= cols; i++) {
            pw.print("<TD class=\"datacol datacol" + 
                     i + "\">");
            pw.print(rs.getObject(i).toString());
            pw.println("</TD>");
         }
         pw.println("</TR>");
      }
      pw.println("</TABLE>");

      pw.println("<div class=\"rowcount\">");
      pw.println(nrows + " rows returned.");
      pw.println("</div>");
   }

JDBC에 관한 더 많은 정보는 자바 튜토리얼의 "JDBC Basics" 과 Fisher, Ellis, Bruce 의 "JDBC API Tutorial and Reference, Third Edition" 를 참고하기 바란다.

샘플코드 실행하기

주의: 2번째 팁의 샘플 코드를 실행하기 위해서는 J2EE서버를 시작하기 전에 PointBase 데이터베이스 서버를 먼저 시작해야 한다.

이 팁을 위한 샘플 아카이브를 다운로드 받자. 애플리케이션의 컨텍스트 루트는 ttapr2004이다. 다운로드된 ear 파일은 샘플을 위한 완성된 코드를 포함한다. deploytool 프로그램이나 어드민 콘솔을 이용해서 J2EE 1.4 Application Server에 애플리케이션 아카이브(ttapr2004.ear) 를 배포할 수 있다. 혹은 다음과 같은 asadmin 커맨드로 이를 배포할 수도 있다.

   asadmin deploy install_dir/ttapr2004.ear

install_dir를 war 파일을 인스톨한 디렉토리로 대체하자.

http://localhost:8000/ttapr2004에서 애플리케이션에 액세스할 수 있다.

Application Server이외의 J2EE 1.4에 호환되는 구현을 원한다면 플랫폼에 애플리케이션을 배포할 때 J2EE 제품의 배포툴을 이용하면 된다.

애플리케이션의 실행을 위한 설명은 index.jsp welcome 파일을 참고하기 바란다.

팁 1: JAR의 다운로드와 압축출기

팁 1의 샘플 코드는 apr2004-tip1.jar라고 불리는 JAR 파일 내의 애플리케이션 아카이브에 있다. apr2004-tip1.jar파일을 다운로드 받는 가장 쉬운 방법은 위에서 언급한 애플리케이션 EAR 파일을 배포하고 메인 페이지를 방문하여 Web 페이지의 첫번째 부분에 있는 다운로드 링크를 클릭하는 것이다.

또 다른 방법은 ttapr2004.ear 에서apr2004.war 를 처음 추출해낼 때 JAR 유틸리티를 이용하는 것이다. 그리고나서 apr2004.war 에서 apr2004-tip1.jar를 추출해낸다.

jar파일을 확보했다면 다음을 이용해서 내용물들을 모두 추출하자.

   $ jar xvf apr2004.jar

결과는 "apr2004"라고 불리는 디렉토리이고 코드와 컴파일된 클래스 지원 데이터 파일등과 함께 소스 파일을 포함하고 있다.

팁 2: 데이터베이스 생성하기

프로그램을 처음으로 실행할 때 데이터를 데이터베이스안에 로드해야 한다. 데이터베이스 로드 스크립트는 애플리케이션 아카이브 내에 있다. 데이터를 로드하기 위해 페이지에서 적절한 링크를 클릭하자. 데이터베이스가 6000줄이 넘기 때문에 시간이 좀 걸릴 수 있다.

데이터가 로드되었다면 텍스트 입력란에 SQL문을 입력하고 Execute SQL 버튼을 이용해서 그것을 실행한다.

이 글 뿐만 아니라 썬에서 제공하는 다른 샘플코드로부터 샘플 테이터를 볼 수 있다.

리스트에 저장된 쿼리를 클릭하면 이 쿼리를 위한 SQL문과 쿼리를 실행한 결과를 보게 된다.

가령, CountryBorders 를 클릭하면 다음과 같은 결과를 보게 된다.

"Java EE" 카테고리의 다른 글

Posted by 1010
반응형

SOAP 기반 XML 웹서비스에 아무리 주의를 기울이고 있다 하더라도 SOAP과 호환되지 않는 수많은 유용한 서비스와 컨텐츠가 있다는 사실을 잊기 쉽다. 이번 테크팁에서는 웹 티어로부터 기존의 웹 리소스들을 사용하기 위해 서블렛을 어떻게 사용하는 지 보여준다. 이 경우에 기존 웹 서비스란 비표준 기반 XML 서비스를 말한다.

기존 리소스 호환

적어도 지금까지는 웹의 대부분의 컨텐츠를 표준 기반 웹 서비스 형식에서 사용할 수 없었다. HTML 페이지, 텍스트 파일, PDF나 워드 프로세서 같은 문서 파일, ftp 사이트 안의 파일들과 이미지들 형식에서 많은 정적 컨텐츠가 존재한다. 어떤 서비스들은 HTTP POSTGET 요구에 따라 활발하게 XML을 생산하기도 하며, RSS feeds 같은 다른 서비스들은 정적인 URL을 가지고 있으나, 그들의 데이터가 동적이어서 보통 직접 사용자에 의해 사용되기 보다는 프로그램에 의해 사용되기 때문에 서비스의 역할을 한다.

엔터프라이즈Java 테크놀로지는 조직 내 인트라넷과 일반 인터넷에서 모두 기존 리소스들과 훌륭히 호환된다. 엔터프라이즈Java 테크놀로지는 새로운 시스템에 기존 정보 자산을 통합하기 위해 어느 티어에서나 사용될 수 있다. SOAP 기반 웹 서비스들이 기존 리소스들을 통합하는 새로운 시스템을 생성하는 것을 기다릴 필요가 없는 것이다.

다음은 통합적인 애플리케이션을 제공하기 위해 엔터프라이즈 Java 서버가 다양한 기존 웹 리소스들을 통합하는 구성도이다.

업무에서의 검색엔진

The National Library of Medicine은 미국방부의 건강 관련 기관 중 하나이다. PubMed*는 NLM의 서비스 중 하나로, 생체의학분야의 출판물 요약본 DB를 검색 가능하도록 제공한다. PubMed의 검색엔진은 Entrez로 불리며, 주소창에pubmed라고 입력하거나 http://www.ncbi.nlm.nih.gov/entrez/query.fcgi 로 들어가서 이 Entrez를 사용해볼 수 있다.

다음의 그림은 PubMed 사이트에서"neanderthal dna" 를 검색한 결과 중 일부이다.

PubMed는 또한 e-uilities라고 불리는 서비스를 제공한다. 이 서비스는 검색엔진에 프로그램적 인터베이스를 제공하는데, 클라이언트로부터 HTTP GET 요구를 받아 검색 결과를 나타내는 XML을 리턴할 수 있다.

이 테크팁을 수행하는 샘플 코드는 e-utilities 중 두가지에 접근하는 서블렛이다.

  • esearch. 검색을 실행하고 ID 리스트를 리턴한다.
  • efetch. 다양한 형식으로 요청된 문서를 불러온다.

esearch 이나 efetch로부터 리턴된 XML을 브라우저에 표시할 수 있게끔 XSLT스타일시트에 의해 변환되도록 지정하는 옵션도 있다. 서블렛은 클라이언트가 어떤 매개변수를 제공하는가에 따라 적절한 스타일시트를 사용할 것이다.

서블렛 개요

Jul2004Servlet라 불리는 샘플코드 서블렛은 새로운 기능을 제공하기 위해 기존 웹 서비스인 esearchefetch를 사용한다. 서블렛은 입력 매개변수를 GET URLs로 변환하여, 분석하고 표시하기 위해 데이터를 검색하는데 이를 사용한다. 서블렛은 먼저 검색을 실행하기 위해 esearch를 호출하고, 그 후 결과를 검색하기 위해 efetch를 호출한다.

다음은 서블렛이 결과를 도출하기 위해 진행되는 과정을 보여주는 순서도이다. 서블렛이 결과를 도출하기 위해 다양한 HTTP GETs의 결과를 사용하였다는 것에 주의하기 바란다.

  1. 서블렛은 유저 형식로부터 POST 매개변수를 수신하고 esearch가 요청하는 매개 변수로 HTTP GET URL를 구축한다.
  2. 서블렛은 URL로 HTTP GET을 실행한다. esearch e-utility는 서버용 결과 집합을 식별하는 데이터를 가진 XML을 리턴한다.
  3. 서블렛은 esearch로부터 리턴된 XML을 파싱하고, WebEnvQueryKey를 검색하기 위해 DOM API를 호출한다. 그 후 이 값들을 이용해 URL을 구축하여 데이터를 얻는다.
  4. 서블렛은 이번엔 efetch에 또다른 HTTP GET를 실행시킨다. 이는 결과 집합을 식별하고 원래의 요청에서 지정된 매개변수들을 형식화한다.
  5. 서블렛은 efetch로부터 요청된 문서 데이터를 수신하여 데이터를 XSL로 임의 변환하고 브라우저에 결과를 표시한다.

데이터베이스 검색

Entrez 서버에서 문서정보를 검색하는 첫번째 단계는 애플리케이션의 index.html에 있는 입력폼의 몇가지 매개 변수에 대해 esearch를 실행시키는 것이다. esearch는 Entrez 서버를 조회하여 조회 결과에 부합되는 데이터를 리턴한다. HTTP GET 쿼리 문자열이 "usehistory=y"라는 매개변수를 포함하고 있다면, Entrez 서버는 WebEnv 문자열과 QueryKey 라는 두가지 데이터 아이템을 추가하여 리턴한다. WebEnv 문자열은 Entrez 서버 안의 세션에서 유저 상태(과거에 있었던 쿼리와 그 결과 집합을 포함)에 대한 독자적인 식별자이며, QueryKey는 세션 안의 특정 쿼리를 식별하는 작은 정수이다. 종합하자면, WebEnvQueryKey를 합하면 서버에서의 쿼리 결과를 나타내게 된다. 서블렛은 애플리케이션의 index.html 페이지에 있는 입력폼로부터 몇 가지 매개 변수를 수신한다. 다음은 입력폼 샘플이다.

서블렛은 애플리케이션의 index.html 페이지에 있는 입력폼로부터 몇 가지 매개 변수를 수신한다.

다음은 입력폼 샘플이다.

서블렛 코드는URL을 구축하기 위해 다음과 같이POST매개 변수를 사용한다.

   AbstractMap paramMap = new TreeMap();
   
   res.setContentType("text/html");
   OutputStream os = res.getOutputStream();
   
   // Get parameters
   String query = req.getParameter("query");
   
   // Execute esearch
   // db=PubMed: search PubMed database
   // usehistory=y: return key to server-side result set
   // term=$query: search for "$term" in PubMed
   paramMap.put("db", "PubMed");
   paramMap.put("usehistory", "y");
   paramMap.put("term", query);
   
   // Create the URL and get its content
   String surl = buildUrl(BASEURL_ESEARCH, paramMap);
   InputStream is = getContent(surl);

여기서 사용된 buildUrl메서드는 기본 URL을 받아서 맵 상의 각각의 키와 컨텐츠 쌍에 대해 key=content라는 키를 생성함으로써 HTTP GET URL을 만든다. 컨텐츠는 URL로 인코딩 되어있으며 매개 변수는 스트링"&"와 결합하여 HTTP GET URL을 생성한다. 예를 들어 형식 쿼리가 "neanderthal DNA"이면 쿼리 문자열은 다음과 같다.

   db=PubMed&term=neanderthal+dna&usehistory=y

getContent 메서드는 요청된 URL에게 간단히 HttpUrlConnection을 개방하고, 결과 컨텐츠에 대해 다음과 같이 InputStream을 리턴한다.

   protected InputStream getContent(String surl)
      throws ServletException {
      Object content = null;

      try {
         // Connect to URL
         URL url = new URL(surl);
         HttpURLConnection conn =
            (HttpURLConnection)url.openConnection();
         conn.connect();

         // Get content as stream
         content = conn.getContent();
      } catch (Exception e) {
         throw new ServletException(e);
      }
      return (InputStream)content;
   }

esearch 요청에 대한 입력 스트림은 다음과 같은 XML 문서를 포함한다.

   <?xml version="1.0"?>
   <!DOCTYPE eSearchResult PUBLIC
     "-//NLM//DTD eSearchResult, 11 May 2002//EN"
     "http://www.ncbi.nlm.nih.gov/entrez/query/DTD/eSearch_020511.dtd">
   <eSearchResult>
      <Count>19</Count>
      <RetMax>19</RetMax>
      <RetStart>0</RetStart>
      <QueryKey>1</QueryKey>

      <WebEnv>0ed8yFoq_CFyEEP6hW9aZ9UoTCVrrPm2w343S9MRNaT9MQmwbnjI
      </WebEnv>
      <!-- additional data removed for brevity -->
   </eSearchResult>

서블렛은 이 XML 문서에서 QueryKeyWebEnv 요소의 컨텐츠를 추출해야하며, 이 컨텐츠를 efetch 후속 호출에 포함시켜야한다. 그러면 디스플레이를 위한 문서 데이터를 리턴하게 될 것이다.

결과 도출

efetch 결과가 작기 때문에 in-memory DOM 트리로 파싱이 가능하다. esearch로 리턴된 스트링을 파싱하는 서블렛은, 파싱하는 동안 Document오브젝트를 메모리로 가져오기 위해 DocumentBuilder를 사용한다.

   // Create DOM parser and parse search result
   DocumentBuilderFactory dbf =
   DocumentBuilderFactory.newInstance();
   DocumentBuilder db = dbf.newDocumentBuilder();
   Document esearchDoc = db.parse(is);
   
   // Get WebEnv, Count, and QueryKey from result
   // WebEnv - result key
   // QueryKey - history index
   // Count - result set length
   String webenv = getElementString(esearchDoc, "WebEnv");
   String count = getElementString(esearchDoc, "Count");
   String querykey = getElementString(esearchDoc, "QueryKey");

getElementString메서드는 주어진 이름(파일명)의 노드를 찾고 그 노드의 첫번째 Text 하위노드를 리턴하는 간단하고 편리한 기능을 한다. 서블렛은 파싱된 DOM 문서로부터 WebEnvQueryKey 를 추출한다.

이 때에, 서블렛은 서버에서 대기하는 결과 집합을 포함하고 있다. 다음 단계는 efetch를 사용하여 데이터를 검색하고 포맷하는 것이다.

Fetch 매개변수 지정

Efetch에서는 esearch의 결과를 식별하기 위해 단지 몇가지의 매개변수만을 필요로한다.

  • db. 데이터베이스 식별("PubMed")
  • WebEnv. 세션 식별
  • query_key. 세션 안의 쿼리 식별

추가 매개 변수들은 사이즈를 제한하거나, 리턴되는 데이터의 포맷 제어하는 역할을 한다.

  • retstartretmaxefetch 가 결과 중 일부 집합을 리턴하도록 명령한다. 이 때 이 결과치는 retstart에서 시작하고 retmax 레코드보다 작은 값을 리턴한다. 이 매개 변수들이 없으면 efetch는 종종 몇만 메가바이트가 되곤 하는 전체 결과를 리턴하고 만다.
  • retmode는 XML, HTML, text, ASN.1 중 어떤 형식으로 데이터를 생성할 것인지 지정한다. 기본값은 ASN.1, PubMed 의 네이티브 스토리지, 그리고 교환 포맷이다.
  • rettype은 각 레코드에서 어떤 것을 리턴할 지 명령한다. efetch은 기본값으로 abstracts을 리턴한다.

서블렛은 esearch에서와 같이 요청된 매개변수들의 Map을 생성한다. 서블렛이 esearch에서 검색한 WebEnvQueryKey스트링을 사용하고, 또한 원래 형식에서 수신한 몇가지 매개변수들을 포함한다. getParameter메서드는, 매개변수가 설정되지 않았을 때 기본값으로 대체하는 요청으로부터 간단히 매개변수를 얻는다.

   paramMap = new TreeMap();
   paramMap.put("WebEnv", webenv);
   paramMap.put("query_key", querykey);
   paramMap.put("db", "PubMed");
   paramMap.put("retstart", getParameter(req, "start", "0"));
   paramMap.put("retmax", getParameter(req, "max", "20"));
   paramMap.put("retmode",
   retmode = getParameter(req, "retmode", "xml"));
   paramMap.put("rettype", getParameter(
           req, "rettype", "abstract"));
   
   // Create URL and get its content
   surl = buildUrl(BASEURL_EFETCH, paramMap);
   is = getContent(surl);

서블렛은 요청된URL을 구축하기위해 맵을 사용하며, 요청 결과로 InputStream 을 얻기 위해 getContent 을 사용한다.

문서 변환

요청된 데이터 형식이 XML이 아니거나 유효한 스타일시트가 없다면 서블렛은 efetch로 리턴된 데이터를 복사하여 브라우저에 표시한다. 이 기능은 e-utilities로 직접 실험할 때 유용하다.

데이터가 XML형식이고 비어있지 않은 유효한 스타일시트가 있다면 서블렛은 이 스타일시트를 데이터에 적용하여 결과를 브라우저에 리턴한다. 브라우저에 리턴엔 XML은 각 레코드에 대해 약간씩의 데이터를 포함하고 있다. 다음은 리턴된 데이터를 발췌한 것이다.

    <?xml version="1.0"?>
    <!DOCTYPE PubmedArticleSet PUBLIC "...">
    <PubmedArticleSet>
     <PubmedArticle>
      <MedlineCitation Owner="NLM" Status="In-Process">
       <!-- ... -->
       <Article>
       <!-- ... -->
        <ArticleTitle>The thermal history of human fossils
        and the likelihood of successful DNA amplification.
        </ArticleTitle>
       <!-- ... -->

이 데이터는 결과를 도출하기 위해 수행되는 스타일시트의 컨텐츠이다.

적용할 스타일시트를 지정하는 데에는 두가지 방법이 있다. 첫번째로는, 사용자형식에는 라디오 버튼으로 되어있는 "isFile" 사용자 형식인제, 변환하기 위해 어떤 시트를 사용할 것인지 지정한다. IsFile이 1이면 매개변수 stylesheet는 웹애플리케이션 아카이브(WAR file)의 스타일시트 이름을 포함하게된다. IsFile이 0이면 매개변수 sstext는 사용자가 형식 안의 TEXTAREA에 넣는 스타일시트를 사용한다. 서식화된 레코드를 보고자 할 때 이 기능을 사용하면 된다. 또한 텍스트 에디터로부터 바로 형식으로 XSL을 카피하여 새로운 레포트를 생성할 때도 사용할 수 있다.

변환을 수행하는 코드는 다음을 얻는다.

  • efetch 결과로부터 읽어드린 입력 스트림
  • 서블렛 결과에 작성되는 결과 스트림
  • 매개 변수 isFile
  • 스타일시트의 이름이나 스타일시트 안의 텍스트 자체를 포함하는 스트링

이 메서드는 스타일시트로부터 Transformer 오브젝트를 구축한 후 소스와 수신자로서 입력과 출력 스트림을 전달하는 Transformer의 transform메서드를 호출한다.

   // Create XSLT transformer for output.
   TransformerFactory tf = TransformerFactory.newInstance();
   Transformer t = null;
   
   // Use identity transform if no transformer was specified
   if (stylesheet == null || stylesheet.equals("")) {
      t = tf.newTransformer();
   } else {
      StreamSource xslsource = null;

      if (isFile) {
         // Read XSL stylesheet from app archive and wrap it as
         // a StreamSource. Then use it to construct 
         // a transformer.
         InputStream xslstream = _config.getServletContext().
            getResourceAsStream(stylesheet);
         xslsource = new StreamSource(xslstream);
      } else {
         // Read XSL stylesheet from string
         xslsource = new StreamSource(
                 new StringReader(stylesheet));
      }
      t = tf.newTransformer(xslsource);
   }
   
   // Do transform from input (e-utilities result) to
   // output (servlet writer)
   StreamSource ss = new StreamSource(is);
   StreamResult sr = new StreamResult(os);
   t.transform(ss, sr);

변환이 완료되면 출력 스트림에 결과 HTML을 작성하게 되며, 서버는 결과 컨텐츠를 브라우저에 표시한다.

결과 보기

애플리케이션 아카이브는 두가지 스타일시트를 포함한다.

  • titles.xsl는 문서의 타이틀만을 표시한다.

다음은 타이틀 형식의 결과 집합의 일부를 출력한 것이다.

  • extended.xsl는 국립의학 도서관에서 지정한 표준 추천 도서목록에서 각 레코드를 서식화한다. 덧붙여 실제 인용 정보(저널 종류, 출판 날짜, 쪽수 등)가 PubMed 사이트의 전체 요약문으로 하이퍼링크된다.

따라서 다음의 출력물을 보면 이전과 같은 결과이지만 좀 더 확장된 형식을 보여줌을 알 수 있다.

입력폼에 삽입된 스타일시트를 이용하여 사용자의 기호에 맞는 스타일을 정의할 수도 있다. TEXTAREA 스타일시트는 스타일시트의 골격을 제공한다. 서블렛을 이용하여 간단한 검색을 실행해보자. 스타일시트를 지정하지 않은 채 결과 XML을 파일에 복사하고 이를 텍스트 에디터에서 새로운 스타일시트를 생성하기 위한 XML예제로 사용한다. 스타일시트 전체를 복사하여 form에 붙인 후 "Using custom XSL style:" 라디오 버튼을 클릭하여 Search를 선택한다. 스타일시트가 유효하다면 서블렛은 검색결과를 포맷하기 위해 이 스타일시트를 사용하게 될 것이다.

이 예제 서블렛을 확장하여 좀 더 유용하게 만드는 것도 어려운 일은 아니다. 예를 들어 PubMed로부터 대형 데이터를 다운로드 받아 분석하거나, 링크된 DNA 시퀀스를 표시하는 바이오정보 애플리케이션을 작성하기 위해 e-utilities의 다른 데이터베이스를 사용할 수도 있을 것이다.

"Java EE" 카테고리의 다른 글

Posted by 1010
98..Etc/Etc...2008. 11. 12. 17:09
반응형

기존 시스템과 J2EE 애플리케이션 서버를 통합하는 작업을 해본 경험이 있다면 그 작업이 결코 쉽지 않다는 것을 알고 있을 것이다. 커넥션을 관리하고 상이한 데이터 타입들을 변환시켜주는 작업, 애플리케이션 서버와 기업 정보 시스템(EIS) 사이의 단순한 메시지 전송조차 자바 프로그래머의 머리를 아프게 만들 수 있다. 하지만 고맙게도 이제 J2EE 커넥터 아키텍처 1.5가 J2EE 1.4 버전에 포함되어 J2EE 플랫폼을 이종 EIS들과 접속시키기 위한 표준 아키텍처를 제공한다.

J2EE 커넥터 아키텍처를 사용하면 EIS 벤더들이 여러 애플리케이션 서버들을 위한 커넥션 소프트웨어를 직접 제작할 필요가 없다. 또한 애플리케이션 서버 벤더들도 다양한 EIS 패키지들을 위해 자신들의 코드를 다시 작성할 필요가 없다. 이제 벤더들은 단순히 J2EE 커넥터 아키텍처 기준에 맞는 "중간" 코드만 패키지 시켜주면 된다. 이 코드는 애플리케이션 서버와 EIS 자원 간의 통신에 필요한 데이터와 메시지를 위해 표준화된 인터페이스와 클래스를 구현하거나 확장 시켜 준다. 이 중간 코드는 애플리케이션 서버에 직접 연결되며 자원 어댑터라 불린다.

자원 어댑터 모듈은 흔히 자원 어댑터 아카이브 (RAR)이라는 명칭을 가지고 있으며 단순한 JAR 파일이다. 보통 RAR은 다음과 같은 사항들을 포함하고 있다.

  • 자원 어댑터 컨트렉트와 클라이언트 API를 구현하는 자바 클래스 및 인터페이스

  • EIS와의 통신을 위해 필요한 모든 고유 라이브러리

  • 애플리케이션 서버가 자원 어댑터를 설정하기 위해 필요한 설치 정의 파일인 ra.xml. 이 파일은 META-INF 디렉토리에 위치

다음 그림은 애플리케이션 서버와 EIS에서 사용되는 자원 어댑터를 도표로 설명한 것이다.

각각의 구성원들이 다른 요소들과 통신하는 방식을 살펴보기 바란다.

J2EE 커넥터 아키텍처를 구성하는 데는 세 가지의 중요한 통신 브릿지가 있다. 각각에 대해 살펴보도록 하자.

클라이언트 API와 공통 클라이언트 인터페이스

애플리케이션 서버에 연결되는 애플리케이션 컴포넌트들은 반드시 자원 어댑터와의 통신 방식을 가지고 있어야 하며, 공통 클라이언트 인터페이스 (CCI)는 이런 통신 방식을 위해 권장되는 API이다. CCI는 javax.resource.cci 패키지에 구현된 API이다. 이 API를 권장하는 이유는 사용하기가 쉬우면서 필수적이지는 않다는 점 때문이다. 만약 CCI가 제공하는 기능이 부족하다면 자원 어댑터 구현자들은 자유롭게 자신만의 클라이언트 API를 제작하여 애플리케이션 컴포넌트와 통신을 할 수도 있다.

컨테이너-컴포넌트 컨트렉트

컨테이너-컴포넌트 컨트렉트는 애플리케이션 서버와 애플리케이션 컴포넌트들 간의 표준 통신 방식이다. JCA에 관련된 내용은 거의 없는데 예를 들어, 만약 웹 애플리케이션이 Enterprise JavaBeans(EJB) 컴포넌트들을 사용한다면 EJB specification은 곧 각각의 EJB 컴포넌트가 애플리케이션과 통신하는 방법을 정의하는 컨테이너-컴포넌트 컨트렉트가 된다.

시스템 컨트렉트

시스템 컨트렉트는 J2EE 커넥터 아키텍처의 주요 부분이다. 이 아키텍처는 자원 어댑터가 애플리케이션 서버와 적절히 통신하기 위해 따라야 하는 몇 가지의 시스템 컨트렉트를 정의하고 있다. 시스템 컨트렉트를 위한 API는 javax.resource.spi 패키지에 구현되어 있다. J2EE 커넥터 아키텍처의 1.0 버전에서는 세 가지의 컨트렉트를 제시하고 있다.

  • 접속 관리 컨트렉트: EIS와 애플리케이션 서버 간의 접속을 가능하게 하는 인터페이스와 클래스 제공하며 애플리케이션 서버가 필요에 따라 이러한 커넥션들을 풀링할 수 있도록 해준다.

  • 트랜젝션 관리 컨트렉트: EIS에 트랜젝션의 모든 부분을 관리할 수 있는 기능을 제공한다. 이 컨트렉트를 이용하여 EIS를 자원 어댑터 혹은 트랜젝션 관리자로 관리할 수 있다. 그러나 두 가지 방법 중 한 가지 방법으로만 사용할 수 있다. 트랜젝션은 반드시 EIS 외부에서 이루어져야 하며 이를 EIS로 전달해야 한다. 그 반대는 성립되지 않는다.

  • 보안 컨트렉트: 애플리케이션 서버와 EIS 간의 적절한 인증이 이루어져 있는지 확인한다.

아키텍처의 1.5 버전은 자원 어댑터의 기능을 확장 시켜 줄 수 있는 4 가지의 새로운 컨트렉트를 제공한다. 이 새로운 컨트렉트들은 다음과 같다.

  • 생명 주기 관리 컨트렉트: 애플리케이션 서버가 깨끗하게 자원 어댑터를 시작 혹은 종료 시킬 수 있는 방법 제공

  • 작업 관리 컨트렉트: 자원 어댑터가 애플리케이션 서버에서 작업을 실행시킬 수 있도록 인터페이스와 클래스 제공

  • 트랜젝션 유입 컨트렉트: EIS에서 애플리케이션 서버로 트랜젝션 전달 가능 (트랜젝션 관리 컨트렉트의 반대 기능) 또한 EIS가 오작동할 경우를 대비하여 트랜젝션 복구 보조

  • 메시지 유입 컨트렉트: 자원 어댑터가 애플리케이션 서버 내에서 동기 혹은 비동기 메시지를 엔드포인트로 전달 가능

이 4 가지의 새로운 기능들을 좀더 자세히 살펴보도록 하자.

생명 주기 관리 및 작업 관리 컨트렉트

javax.resource.spi 패키지 안의 ResourceAdapter 인터페이스가 바로 자원 어댑터이다. ResourceAdapter 인터페이스에서 생명 주기 관리를 하는 메소드는 start(), stop()의 두 가지가 있다. start() 메소드는 애플리케이션 서버가 자원 어댑터를 시작하려 할 때(예:자원 어댑터를 배치할 때) 호출되며, stop() 메소드는 애플리케이션 서버가 자원 어댑터를 해제할 때 (예:자원 어댑터를 배치 해제할 때) 호출된다.

작업 관리 컨트렉트는 javax.resource.spi.work 패키지의 Work 인터페이스를 확장하는 객체를 생성함으로써 자원 어댑터가 애플리케이션 서버에 작업을 전달할 수 있도록 해준다. Work 인터페이스는 Runnable 인터페이스의 확장이다. Work 인터페이스에는 Runnable 인터페이스에서 상속받고 자신의 쓰레드에서 실행시키는 run() 메소드뿐만 아니라 release() 메소드도 포함되어 있다. 애플리케이션 서버는 release() 메소드를 사용하여 쓰레드가 완료되면 그 자원을 가능하면 빨리 해제할 수 있도록 해준다.

다음은 ResourceAdapter 인터페이스를 사용하여 생명 주기 관리 및 작업 관리 컨트렉트를 사용하는 예시이다.

   public class NexesResourceAdapterImpl implements 
           ResourceAdapter {

      // Ten seconds
      public static final long WORK_START_TIMEOUT = 10000L;
      
      public NexesResourceAdapterImpl() {    }
      public void start(BootstrapContext ctx)
      throws ResourceAdapterInternalException
      {
 
          WorkManager workManager = ctx.getWorkManager();
          Work nexesWorkJob = new NexesWorkImpl();
          WorkListener workListener = 
                  new NexesWorkListenerImpl();

          try {

              // Unlike scheduleWork() or doWork(), this call 
              // blocks until the work has started. If it takes 
              // longer than 10 seconds for the work to start, 
              // the call throws a WorkRejectedException.

              workManager.startWork
                      (nexesWorkJob, WORK_START_TIMEOUT, 
                      new ExecutionContext(), workListener);

          } catch (WorkException e) {
              //  Handle the exception
          }
        }

       public void stop()
       {
          //  Do whatever you need to do here to close down the
          //  resource adapter.
       }  
       
          // Transaction Inflow contract methods omitted. 
          // See the section "Message Inflow and Transaction 
          // Inflow Contracts"

      }

BootstrapContext 인터페이스를 구현하는 start() 메소드로 객체가 전달되는 것을 볼 수 있다. 이 객체는 EIS가 트랜젝션 정보와 작업을 애플리케이션 서버에 전달할 수 있도록 해주는 매우 중요한 객체이다. BootstrapContext 인터페이스에 관련된 상세한 정보는 "메시지 유입 및 트랜젝션 유입 컨트렉트" 섹션을 참조하시기 바란다.

또한 WorkListener 인터페이스에 대한 참조도 확인하기 바란다.

   WorkListener workListener = 
           new NexesWorkListenerImpl();

and the startWork method:

   workManager.startWork
           (nexesWorkJob, WORK_START_TIMEOUT, 
           new ExecutionContext(), workListener);

만약 애플리케이션 서버가 처리 중인 작업에 대한 진행 상황을 보고 받고 싶다면 javax.resource.spi.WorkListener 인터페이스를 구현하는 객체를 생성하면 된다. 그 다음 이 객체를 애플리케이션 서버 상의 WorkManager 객체의 startWork() 메소드를 사용하여 등록하면 된다. WorkManager 인터페이스는 Work 인스턴스를 실행 시킬 수 있는 기능을 제공한다. 객체를 등록함으로써 작업이 거절 혹은 수락되었는지, 수락되었다면 언제 작업이 시작되었고 종료되었는지를 서버가 자원 어댑터에게 알려줄 수 있다. 또한 WorkAdapter 클래스를 확장시킬 수 있는데, 이 클래스는 WorkListener 인터페이스를 구현하고 각각에 대해 빈 메소드를 제공한다. 다음은 WorkListener를 구현하는 간략한 코드이다.

   public class NexesWorkListenerImpl implements WorkListener {

      public void workAccepted(WorkEvent e) {
          //  myAppServerLog.log("Work instance " + e + 
          //      " has been accepted."); 
      }

      public void workRejected(WorkEvent e) {
          //  myAppServerLog.log("Work instance " + e + 
          //      " has been rejected."); 
      }

      public void workStarted(WorkEvent e) {
          //  myAppServerLog.log("Work instance " + e + 
          //      " has been started."); 
      }

      public void workCompleted(WorkEvent e) {
          //  myAppServerLog.log("Work instance " + e + 
          //      " has been completed."); 
      }

}

메시지 유입 및 트랜잭션 유입 컨트렉트

메시지 유입 컨트렉트는 애플리케이션 서버가 메시지 엔드포인트를 활성화/비활성화 시키는 호출에 대해 자원 어댑터가 반응할 수 있도록 해준다. ResourceAdapter 인터페이스의 endpointActivation() 메소드가 엔드포인트 활성화시 호출되며, 이는 자원 어댑터가 메시지 엔드포인트에 메시지를 전달할 때 필요한 설정을 하도록 한다. ResourceAdapterendpointDeactivation() 메소드는 메시지 엔드포인트가 비활성화될 때 호출되며, 이는 리소스 어댑터가 메시지 엔드포인트에게 메시지를 전달하는 것을 멈추게 한다. javax.resource.spi.endpoint 패키지의 MessageEndpointFactory 객체가 endpointActivation 메소드에 전달된다. 자원 어댑터는 이 객체를 사용하여 메시지 엔드포인트를 생성하게 되며, 이러한 엔드포인트들에 대한 모든 정보는 endpointDeactivation() 메소드가 호출되면 자원 어댑터에서 제거되야 한다. 마지막으로 ResourceAdaptergetXAResources() 메소드는 시스템에 오류가 생길 경우에 트랜잭션 자원을 복구하는데 사용된다. endpointActivation(), endpointDeactivation(), getXAResources()메소드들은 ResourceAdapter 인터페이스에 위임된다.

   public class NexesResourceAdapterImpl implements 
          ResourceAdapter {

      //  Lifecycle Contract methods from earlier omitted.

      public XAResource[] getXAResources(ActivationSpec[] specs)
      throws ResourceException
      {
          // This method should either return an array of 
          // XAResource objects that uniquely correspond to the 
          // resource manager given the ActivationSpecs passed 
          // in, or null if it does  not support this par 
          // of the Message Inflow contract.
        
          return null;
      }

      public void endpointActivation(MessageEndpointFactory mef, 
          ActivationSpec as) 
      throws NotSupportedException
      {
          // This is also part of the Message Inflow contract.
          // The idea here is to create a message endpoint 
          // using the MEF's createEndpoint() method, which is 
          // then stored in the ActivationSpec class. This binds 
          // the EIS and the application server together so that 
          // the two can communicate independently of the 
          // messaging style of the EIS.
      }

      public void endpointDeactivation(MessageEndpointFactory mef, 
          ActivationSpec as)
      {
          //  This removes any resources that were created by the 
          //  endpointActivation() method above for the specified
          //  messaging endpoint. The resource adapter must notify
          //  any message providers that the endpoint is no longer
          //  valid
      }

   }

ResourceAdapter 메소드에 전달되는 ActivationSpec 클래스는 JavaBean으로써, 다양한 속성들의 정보를 입수/설정하는 메소드들을 구현한다. 이러한 입수/설정 메소드들 이외에도 구현은 반드시 validate() 메소드를 제공하여 모든 속성들이 적법하게 설정되어 있는지 확인해야 한다. 만약 속성이 적절히 설정되어 있지 않다면 메소드는 InvalidPropertyException을 반환하게 된다. ActivationSpec 객체가 equals()를 오버라이드할 수 없다는 점에 유의하기 바란다.

   public class MyActivationSpec implements ActivationSpec, 
           Serializable {

      public void setMyProperty(MyProperty s) { }
      public MyProperty getMyProperty() { }

      public void validate() throws InvalidPropertyException { }

   }

J2EE 커넥터 아키텍처 버전 1.0의 경우, 자원 어댑터는 자기 자신 혹은 외부 트랜잭션 관리자로부터 EIS에만 트랜잭션 정보를 전달할 수 있었다. 그러나 1.5 버전에 포함된 트랜잭션 유입 컨트렉트에 의해 자원 어댑터가 EIS 트랜잭션 요구를 애플리케이션 서버에 전달할 수 있게 됨은 물론, 생명주기 컨트렉트의 start() 메소드에 전달된 BootstrapContext 오브젝트 또한 사용할 수 있게 되었다. BootstrapContext 인터페이스는 생명주기 관리 contract의 부분에서 잠깐 언급된바 있다. 다음은 BootStrapContext 인터페이스 내의 메소드이다.

   public class NexesBootstrapContextImpl implements 
           BootstrapContext {

      public WorkManager getWorkManager() {
          //  Get the work manager from the application server
      }

      public XATerminator getXATerminator() {
          return new NexesXATerminatorImpl();
      }

      public Timer createTimer() {
          return new Timer();
      }

}

이제 XATerminator 인터페이스에 대해 자세히 알아보자. BootStrapContext 인터페이스의 getXATerminator() 메소드의 반환값에 유의하기 바란다. XATerminator 인터페이스는 트랜잭션 터리를 위해 5가지의 간단한 메소드를 제공한다

   public class NexesXATerminatorImpl implements XATerminator {

      public void commit(Xid xid, boolean onePhase)
              throws XAException { }
      public void forget(Xid xid) throws XAException { }
      public int prepare(Xid xid) throws XAException { }
      public Xid[] recover(int flag) throws XAException { }
      public void rollback(Xid xid) throws XAException { }

   }

자원 어댑터 파일

자원 어댑터 디스크립터 파일인 ra.xml은 매우 쉽게 생성할 수 있다. 이는 단순히 ResourceAdapter 인터페이스를 구현하는 클래스를 파일에 가리키는 것만으로 이루어 진다. 그러면 애플리케이션 서버가 이 객체에 접근하게 된다. J2EE Connector Architecture 1.5 specifications을 참조하면 수신 메시지 클래스를 ActivationSpec 클래스와 묶는 방법 등을 포함하여 자원 어댑터의 배치 디스크립터에 대한 상세한 정보를 얻을 수 있다. 모든 배치 디스크립터와 마찬가지로 ra.xml 파일은 WAR 파일의 WEB-INF 디렉토리 내에 위치하고 있어야 한다.

   <?xml version="1.0" encoding="UTF-8"?>
   <connector xmlns="http://java.sun.com/xml/ns/j2ee"
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee
     http://java.sun.com/xml/ns/j2ee/connector_1_5.xsd" 
     version="1.5">

     <display-name>Skeleton Resource Adapter</display-name>
     <vendor-name>Sun Microsystems, Inc.</vendor-name>
     <eis-type>Unknown</eis-type>
     <resourceadapter-version>1.0</resourceadapter-version>

     <resourceadapter>
       <resourceadapter-class>
         com.nexes.ra.NexesResourceAdapterImpl
       </resourceadapter-class>
     </resourceadapter>

   </connector>

J2EE 커넥터 아키텍처에 대한 상세한 정보를 위해서는 J2EE Connector Architecture page를 참조하기 바란다.

"Java EE" 카테고리의 다른 글

Posted by 1010
01.JAVA/Java2008. 11. 12. 17:08
반응형

트랜잭션은 반드시 원자적으로 수행되어야하는 작업들의 모음이다. 다시 말해, 트랜잭션 전체가 성공적이기 위해서는 각각의 작업이 반드시 성공적 이어야 한다는 뜻이며, 어느 한 작업이라도 성공적이지 못하면 전체 트랜잭션이 실패하게 된다. 실패했을 경우에는, 앞서 성공적으로 수행된 작업들은 반드시 원상태로 되돌아가, 작업을 시작하기 이전의 상태와 일치해야한다.

예를 들어, A 은행계좌(계좌번호12345-1)에서 B 은행계좌(계좌번호 12345-2)로 $50를 송금하길 원하는 사람이 있다고 하자. 이 트랜잭션의 절차는 다음과 같이 "pseudo code(역자 주: 프로그램이 실행되기 전 기계 코드로 번역될 필요가 있는 것)"로 나타낼 수 있다.

   BOUNDARY: START TRANSACTION A
   SUBTRACT 50 DOLLARS FROM SAVINGS ACCOUNT 12345-1
   ADD 50 DOLLARS TO THE CHECKING ACCOUNT 12345-2
   BOUNDARY: END TRANSACTION A

트랜잭션을 구동하기 위해서는 다음의 코드가 필요하다.

   PREPARE (RUN) TRANSACTION A
   IF TRANSACTION A SUCCEEDED
       COMMIT TRANSACTION A
   ELSE
       ROLLBACK TRANSACTION A

A은행계좌에 $50을 송금하기 충분한 자금이 있다고 가정하고, 트랜잭션의 첫번째 단계가 성공했다고 치자. 컴퓨터가 50 달러를 B계좌에 전송하려고 시도했으나 B 계좌가 사용정지 중이어서 두번째 단계에서 실패했다면, 이 두번째 단계가 실패했기 때문에 전체 트랜잭션은 실패한 것이 된다. 결과적으로 첫번째 단계의 작업은 초기화 되어야 한다. 즉, 50달러는 A계좌로 다시 돌아가야하는 것이다. 이는 매우 중요한 사항인데, 그렇지 않으면 컴퓨터가 전송을 시도할 때마다 A계좌에서는 돈이 빠져나가는 셈이기 때문이다!

전체 트랜잭션이 성공한 경우에는 모두 실행되고 그 결과가 지속된다.

전 과정이 완성되기 위해서는 두 과정이 실행된다. 첫번째로, 트랜잭션이 오류 없이 구동 되었는지를 확인하는 검사가 실행된다. 오류가 없다면 두번째로 넘어가며, 에러가 있다면 트랜잭션은 초기화된다. 이런 일반적인 트랜잭션 방법은 두 과정 실행 프로토콜(two-phase commit protocol)이라고 불린다.

J2EE 환경에서의 트랜잭션

트랜잭션에는 일반적으로 다음의 세가지가 관계되어 있다. 1. 애플리케이션: 트랜잭션 요청 착수, 2. data store(데이터베이스 등):트랜잭션 구동, 3. API(드라이버 등):애플리케이션과 데이터 창고 간의 통신. J2EE 플랫폼에서는 J2EE- compliant 애플리케이션 서버에 의해 API(또는 드라이버)가 제공된다. 이 애플리케이션 프로그램은 애플리케이션 서버를 호출하여 트랙잭션을 수행한다.

JTA(Java Transaction API)는 J2EE 플랫폼에 포함되어 있다. 이API는 분포된 트랙잭션을 수행할 수 있도록 한다. 즉, 애플리케이션은 이 API를 사용하여 한번에 네트워크 상에 있는 여러 개의 data store에서 트랙잭션을 수행할 수 있게 된다. 그러나 이를 효율적으로 수행하기 위해서는 애플리케이션 서버에서 기능하는 또다른 컴포넌트인 J2EE 트랜잭션 매니저가 도움이 될 것이다. 트랜잭션 매니저는 애플리케이션 서버에서 생성되는 많은 수의 트랜잭션을 효율적으로 스케쥴링하여 실행하도록 돕는다.

많은 수의 데이터베이스 벤더들은 각 회사 고유의 트랜잭션 매니저를 제공하고 있다. 그러나 특정 DBMS의 트랜잭션 매니저는 다른 벤더들의 데이터베이스에서 작동하지 않을 수 있다. 이런 여러 종류의 데이터베이스에서 작업하고 싶다면(ex. 여러 벤더사의 각종 데이터베이스를 업데이트), JTA 트랜잭션과 이에 수반되는 J2EE 트랜잭션 매니저를 사용할 것을 권장한다. JTA 설명서에는, "JTA 트랜잭션은 J2EE 트랜잭션 매니저로 관리됩니다"라고 나와있다. J2EE 트랜잭션 매니저에는 한가지 한계가 있는데, J2EE 트랜잭션은 수평적이라는 것이다. J2EE에서는 중첩된 트랜잭션이 지원되지 않는다. 즉, 먼저 시작한 트랜잭션이 끝나기 전에는 J2EE 트랜잭션 매니저가 다음 트랜잭션을 시작할 수 없다.

J2EE를 사용하는 애플리케이션 서버는 JTS(Java Transaction Service)를 이용하여 트랜잭션 매니저를 구현한다. JTA는 낮은 레벨의 JTS 절차에 호출할 API를 제공한다. JTA와 JTS를 헷갈리지 않도록 주의하기 바란다.

JTA 습득하기

트랜잭션을 수행하기 위해 JTA와 사용할 수 있는 인터페이스는 세가지가 있으며, 각 인터페이스는 트랜잭션을 핸들링하는 각각의 고유 방법이 있다. 다음은 그 인터페이스들이다.

  • javax.transaction.UserTransaction : 트랜잭션 매니저를 우회하는 트랜잭션 영역을 지정할 수 있다.
  • javax.transaction.TransactionManager : J2EE 트랜잭션 매니저가 각종 트랜잭션 작업과 함께 트랜잭션의 영역도 결정할 수 있도록 한다.
  • javax.transaction.xa.XAResource : 이 인터페이스는 트랜잭션 수행을 위해 써드파티 XA-compliant 트랜잭션 매니저를 이용하는 X/Open CAE Specification (Distributed Transaction Processing: The XA Specification) 표준과 매핑된다.

대부분의 J2EE 프로그래머들이 첫번째 인터페이스만을 사용하기 때문에 이 인터페이스에 특히 초점을 맞추어 설명하도록 하자.

EJB 트랜잭션: 컨테이너 & 빈 관리 트랜잭션

J2EE 환경에서 트랜잭션을 수행하는 가장 논리적인 장소는 EJB(Enterprise JavaBeans) 기술 컴포넌트(엔터프라이즈 빈이라고도 불림) 내부이다. 엔터프라이즈 빈을 이용하여 트랜잭션을 수행하는 데에는 두가지 방법이 있는데, 첫번째로 EJB 컨테이너로 트랜잭션 영역을 관리하는 것이다. 컨테이너 관리 트랜잭션(container-managed transactions)이라고 불리며, 이 방법으로 프로그래머에게 부담을 좀 줄여줄 수 있다. 두번째 방법을 이용하여 엔터프라이즈 빈 코드의 트랜잭션 영역을 명확히 지정해 줌으로써 프로그래머가 좀 더 자유로울 수 있다. 이는 빈 관리 트랜잭션(bean-managed transactions)이라고 불린다.

컨테이너 관리 트랜잭션은 세션 빈, 엔터티 빈, 메시지 드리븐 빈 중 어떤 엔터프라이즈 빈과도 함께 이용할 수 있다. 컨테이너 관리 트랜잭션에서는 EJB컨테이너가 트랜잭션 영역을 설정한다. 일반적으로 개별적인 트랜잭션으로 빈에 한 개 이상의 메소드를 선정하면서 작업이 완료된다. 컨테이너는 메소드 시간부분 바로 전부터 메소드가 끝나기 바로 전까지 영역을 설정한다. 그러나, 컨테이너 관리 트랜잭션에서는 각각의 메소드만이 하나의 트랜잭션이 될 수 있다(다중 트랜잭션은 허용되지 않는다.) 빈을 배치할 때 빈의 어떤 메소드가 트랜잭션과 관계될 것인지 지정해야한다. 트랜잭션 속성을 설정하여 지정할 수 있다.

트랜잭션 속성은 엔터프라이즈 빈 메소드가 다른 엔터프라이스 빈 메소드를 호출할 때 트랜잭션의 범위를 조절한다. JTA 설명서에서는 엔터프라이즈 빈 메소드가 EJB 배포 기술자의 6개의 트랜잭션 속성 중에서 선정할 수 있다고 명시하고 있다.

트랜잭션 속성은 트랜잭션이 수반될 때 EJB 컨테이너가 클라이언트 엔터프라이즈 빈에 의해 호출된 메소드를 어떻게 처리해야하는지를 보여준다.

   <ejb-jar>
     ...
    <enterprise-beans>
    ... 
     </enterprise-beans>
     <assembly-descriptor>
       <container-transaction>
         <method>
           <ejb-name>BankBean</ejb-name>
           <method-intf>Remote</method-intf>
           <method-name>transferMoney</method-name>
           <method-params>
             <method-param>java.lang.String</method-param>
             <method-param>java.lang.String</method-param>
             <method-param>java.lang.double</method-param>
           </method-params>
         </method>
         <trans-attribute>Required</trans-attribute>
       </container-transaction>
     </assembly-descriptor>
   </ejb-jar>

다음은 6개의 트랜잭션 속성에 대해 스펙에 적혀있는 내용이다.

  • Required - 클라이언트가 트랙잭션 하에 구동되고 있을 때 엔터프라이즈 빈의 메소드를 호출하면, 그 메소드는 클라이언트의 트랜잭션 내에서 실행된다. 만약 이 클라이언트가 트랜잭션에 관계 없다면, 컨테이너는 메소드를 구동하기 전에 새로운 트랙잭션을 시작한다. 대부분의 컨테이너 관리 트랜잭션이 Required를 사용한다.
  • RequiresNew - 클라이언트가 트랙잭션 하에 구동되고 있을 때 엔터프라이즈 빈의 메소드를 호출하면 컨테이너는 그 클라이언트의 트랜잭션을 잠시 보류하고 새로운 트랜잭션을 시작하며 메소드에 호출 권한을 위임한다. 메소드가 완료되면 클라이언트의 트랜잭션을 다시 시작한다. 클라이언트가 트랜잭션에 관계 없다면, 컨테이너는 메소드를 구동하기 전에 새로운 트랜잭션을 시작한다.
  • Mandatory - 클라이언트가 트랜잭션하에 구동될 때 엔터프라이즈 빈의 메소드를 호출하면 그 메소드는 그 클라이언트 트랜잭션 하에서 실행된다. 클라이언트가 트랜잭션과 관계 없다면 컨테이너는 TransactionRequiredException을 던지게 된다. 엔터프라이즈 빈의 메소드가 반드시 클라이언트의 트랜잭션을 이용해야할 때 Mandatory 속성을 이용하자.
  • NotSupported - 클라이언트가 트랜잭션하에 구동될 때 엔터프라이즈 빈의 메소드를 호출하면 컨테이너는 메소드를 활성화하기 전데 클라이언트의 트랜잭션을 일시 중지하며, 메소드가 완료되면 컨테이너는 클라이언트 트랜잭션을 재시작한다. 클라이언트가 트랜잭션과 관련이 없다면 컨테이너는 메소드를 구동하기 전에는 새로운 트랜잭션을 시작하지 않는다. 트랜잭션을 필요로하지 않는 메소드에 대해 code>NotSupported 속성을 사용하자. 트랜잭션이 전체를 포함하기 때문에 이 속성은 성능 향상을 가져다줄 것이다.
  • Supports - 클라이언트가 트랜잭션 하에서 구동될 때 엔터프아리즈 빈의 메소드를 호출하면 메소드는 그 클라이언트 트랜잭션 하에서 실행된다. 클라리언트가 트랜잭셕과 관계 없다면 컨테이너는 메소드를 시작하기 전에 새로운 트랜잭션을 시작하지 않는다. 메소드의 트랜잭션 비헤이비어가 변할 수 있기 때문에, Supports 속성을 사용할 때는 주의가 필요하다.
  • Never - 클라이언트가 트랜잭션 하에서 구동될 때 엔터프라이즈 빈의 메소드를 호출하면 컨테이너는 RemoteException를 던진다. 클라이언트가 트랜잭션과 관계 없다면 컨테이너는 메소드를 구동하기 전에 새로운 트랜잭션을 시작하지 않는다.

컨테이너 관리 트랜잭션을 처음상태로 돌리는 데에는 두가지 방법이 있다. 시스템 예외상황이 던져지면 컨테이너는 자동적으로 트랜잭션을 원상태로 돌리게 된다. 또한 EJBContext 인터페이스의 setRollbackOnly() 메소드를 호출하여 원상태로 만들 수도 있다. 이 메소드는 컨테이너에게 트랜잭션을 초기화하도록 지시한다. 엔터프라이즈 빈이 애플리케이션 예외상황을 던지면, 자동적으로 초기화 되지는 않지만 setRollbackOnly()를 호출함으로써 초기화에 착수할 수는 있다. 컨테이너 관리 트랜잭션을 사용할 때에는 JTA 메소드들을 호출할 수 없다는 것을 명심하기 바란다. 세 메소드가 빈 관리 트랜잭션을 이용하기 위해 예약되어 있기 때문이다. 이 세 메소드는 다음과 같다.

  • 트랜잭션적 의미와 충돌되는 리소스 지정 기능; java.sql.Connectioncommit(), setAutoCommit(), rollback() 메소드 등
  • javax.ejb.EJBContextgetUserTransaction() 메소드
  • javax.transaction.UserTransaction의 모든 메소드

빈 관리 트랙잭션에서 세션 빈 또는 메세지 드리븐 빈에서의 코드는 트랜잭션 영역을 명확하게 지정한다. 엔터티 빈은 빈 관리 트랜잭션을 가질 수 없으며, 반드시 컨테이너 관리 트랜잭션을 이용해야한다. 세션 빈 또는 메시지 드리븐 빈을 위해 빈 관리 트랜잭션을 코딩할 때는 일반적으로 JDBC 또는 JTA 트랜잭션을 사용할 수 있다. JDBC 트랜잭션은 J2EE 트랜잭션 매니저가 아닌 데이터베이스 관리 시스템의 트랜잭션 매니저에 의해 관리된다. JDBC 트랜잭션을 수행하기 위해서는 java.sql.Connection 인터페이스의 commit() 메소드와 rollback() 메소드를 이용한다. 트랜잭션의 시작에는 가장 최근의 commit(), rollback(), connect() 문을 따르는 첫번째 SQL 명령문으로 시작된다. JTA 트랜잭션을 위해서는 javax.transaction.UserTransaction 인터페이스의 begin(), commit(), rollback() 메소드를 호출할 수 있다. begin() 메소드와 commit() 메소드는 트랜잭션 영역을 지정한다. 트랜잭션 작업이 실패했을 경우, 일반적으로 예외상황 핸들러가 rollback() 메소드를 호출하여 EJBException을 던진다. 다음의 코드에서는 javax.transaction.UserTransaction 인터페이스를 이용하여 빈 관리 트랜잭션을 수행하는 법을 보여준다.

      UserTransaction ut = context.getUserTransaction();

      try {
         ut.begin();
         // Do whatever transaction functionality is necessary
         ut.commit();
      } catch (Exception ex) {
          try {
             ut.rollback();
          } catch (SystemException syex) {
              throw new EJBException
                 ("Rollback failed: " + syex.getMessage());
          }
          throw new EJBException 
             ("Transaction failed: " + ex.getMessage());
       }   

JDBC 빈 관리 트랜잭션을 수행하는 코드도 이와 비슷하다. 그러나 그 코드는 데이터베이스 자동 연결 기능이 되지 않음을 주의해야한다. 이 방법으로 데이터베이스는 수반되는 모든 작업을 하나의 트랜잭션으로 취급한다.( commit() 메소드가 호출될 때까지)

   try {            
        Connection con = makeDatabaseConnection();
        con.setAutoCommit(false);           
        //  Do whatever database transaction functionality
        //  is necessary           
        con.commit();          
    } catch (Exception ex) {
        try {
            con.rollback();
        } catch (SQLException sqx) {
            throw new EJBException("Rollback failed: " +
                sqx.getMessage());
        }
    } finally {
        releaseDatabaseConnection();
    }

JTA 스펙에 나와있는 몇가지 조항을 소개한다.

빈 관리 트랜잭션을 이용하는 비상태 유지 세션 빈(stateless session bean)에서는 비즈니스 메소드가 반드시 실행되거나 트랜잭션을 초기화한 후 리턴되야한다 그러나 상태 유지 세션 빈(stateful session bean)에서는 이러한 제한이 없다. JTA 트랜잭션을 이용하는 상태 유지 세션 빈에서는 빈 인스턴스와 트랜잭션 사이의 연관성이 다중 클라이언트 호출을 망라하여 유지된다. 심지어는 클라이언트에 의해 호출된 각각의 비즈니스 메소드가 데이터베이스 연결을 통제할 때는 인스턴스가 트랜잭션을 완료할 때까지 관계가 유지된다. JDBC 트랜잭션을 이용하는 상태 유지 세션 빈에서는 다중 호출 시 JDBC 연결이 빈 인스턴스와 트랜잭션 사이의 관계를 유지한다. 연결이 끊기면, 관계는 유지되지 않는다.

JTA 빈 관리 트랜잭션을 사용할 때 한가지 메소드 제한이 있다. EJBContext 인터페이스의 getRollbackOnly() 메소드와 setRollbackOnly()메소드를 호출하면 안된다.(이 메소드들은 컨테이너 관리 트랜잭션에만 사용되어야한다.) 빈 관리 트랜잭션을 위해서는 UserTransaction 인터페이스의 getStatus() 메소드와 rollback() 메소드를 사용하기 바라며 또한, 트랜잭션적 의미와 충돌되는 resource-specific 기능을 사용하지 말기 바란다.

JTA와JTS에 대한 좀 더 자세한 정보는 J2EE Transaction page를 참고하기 바란다.

Java Transaction API에 대한 예제 코드 구동하기

  1. Java 트랜잭션 API 팁을 위한 예제 압축파일을 다운로드 받으세요.
  2. 예제 압축파일을 다운로드할 디렉토리를 변경하고,예제 압축파일을 위한 JAR file의 압축을 푸세요.
          jar xvf  ttJan2005jta.jar
    
    jta라는 이름의 디렉토리와 소스 코드, 컴파일된 클래스들, 다른 지원 파일들이 나타납니다.
  3. 애플리케이션 서버를 시작합니다. J2EE 1.4 SDK는 Sun Java System Application Server Platform Edition 8을 포함합니다. 정확하게 작동하기 위해서는 Sun Java Application Server 8.1 4Q2004또는 그 이후 버전이 필요합니다.
  4. PointBase 데이터베이스 서버를 시작합니다.
  5. jta 디렉토리로 변경합니다. Ant 스크립트(build.xml)를 사용자의 J2EE 홈 디렉토리에 지정하여 편집합니다.
  6. 명령창에 명령어를 입력합니다.
          ant create-db
    
    데이터베이스가 생성되고 "account" 테이블로 채워집니다. 테이블을 삭제하려고 할 때 데이터베이스의 시간이 지연되면, 컴퓨터를 재부팅하고 PointBase를 다시 시작하기 바랍니다.
  7. 명령어를 입력합니다.
          ant build
    
    ejb라는 디렉토리가 생성되고 두개의 JAR 파일(bank-client.jar, bank-ejb.jar)로 채워집니다.
  8. Deploy bank-ejb.jar를 배치하세요. Sun Java System Application Server Platform Edition 8 Admin Console을 이용하거나(Application->EJB Modules), 또는 Autodeploy 디렉토리에 파일을 복사하여(이 옵션 없이 서버를 인스톨 했을 때) 실행할 수 있습니다.
  9. ejb 디렉토리를 변경하고 다음의 명령어를 입력하면,
          appclient -client bank-client.jar
    

다음과 같은 결과가 나타납니다.

   --OUTPUT FROM APP CLIENT BEGIN--
   Using Bean Transaction Management (Database Transaction 
   Manager)...
   
   Balance of 12345-01 is: 100.0
   Balance of 12345-02 is: 0.0
   
   Now transferring 23.43 from 12345-01 to 12345-02
   
   Balance of 12345-01 is: 76.57
   Balance of 12345-02 is: 23.43
   
   Now transferring 23.43 from 12345-01 to fictional account 
   number 12345-10
   Balance should be the same as before...
   
   Balance of 12345-01 is: 76.57
   Balance of 12345-02 is: 23.43
   
   
   Now Using JTA Container Transaction Management...
   
   
   Now transferring 23.43 from 12345-01 to fictional account 
   number 12345-10
   Exception was caught.
   Balance should again be the same as before...
   
   Balance of 12345-01 is: 76.57
   Balance of 12345-02 is: 23.43
   
   --OUTPUT FROM APP CLIENT END-- 

"Java EE" 카테고리의 다른 글

Posted by 1010
01.JAVA/Java2008. 11. 12. 17:07
반응형

저자 Greg Murray

AJAX는 'Asynchronous JavaScript and XML'의 머리글자를 딴 것으로, 웹 애플리케이션이 웹 페이지에 대한 사용자 인터랙션을 효율적으로 처리할 수 있도록 하는 수단을 제공한다(사용자 인터랙션이 이루어질 때마다 페이지를 리프레시(새로 고침)하거나 전체 페이지를 리로드하는 번거로움을 덜어줌). 이는 또한 브라우저를 이용한 리치 비헤이비어(rich behavior)를 가능케 해준다(데스크톱 애플리케이션 또는 플러그인 기반 애플리케이션의 경우와 유사). AJAX 인터랙션은 백그라운드에서 비동기적으로 처리되고, 그 동안 사용자는 페이지에 대한 작업을 계속할 수 있다. AJAX 인터랙션은 웹 페이지 내의 JavaScript에 의해 시작되는데, AJAX 인터랙션이 완료되면 JavaScript는 페이지의 HTML 소스를 업데이트한다. 변경 작업은 페이지 리프레시 없이 즉시 이루어진다. 이 AJAX 인터랙션은 서버측 논리를 이용한 폼 엔트리 검증(사용자가 입력하는 동안), 서버의 상세 데이터 검색, 페이지 상의 데이터에 대한 동적 업데이트, 그리고 페이지에서 폼을 부분적으로 제출하는 등의 작업에 이용될 수 있다.

여기서 특히 흥미를 끄는 부분은 AJAX 애플리케이션이 별도의 플러그인을 요구하지 않으며 플랫폼/브라우저 중립적 특성을 지니고 있다는 점이다. 첨언하자면, 구형 브라우저에서는 AJAX가 충분히 지원되지 않으며, 브라우저간의 차이를 유발하는 클라이언트측 스크립트를 작성할 때는 주의를 기울여야 한다. 따라서 브라우저의 차이를 추상화(abstract)하는 JavaScript 라이브러리를 사용하거나 경우에 따라서는 대체 인터랙션 기법을 이용하여 구형 브라우저를 지원하는 것도 좋은 방법이 될 수 있다. 자세한 내용은 자바 개발자를 위한 AJAX FAQ(영문)를 참조할 것.

자바 기술은 어떤 작업에 적합한가?

자바 기술과 AJAX는 서로 궁합이 잘 맞는다. 자바 기술은 AJAX 인터랙션을 위한 서버측 프로세싱 기능을 제공하는데, 이는 서블릿, JSP(JavaServer Pages) 기술, JSF(JavaServer Faces) 기술, 웹 서비스 등을 통해 제공될 수 있다. AJAX 요청 처리를 위한 프로그래밍 모델은 종래의 웹 애플리케이션에서 사용하던 것과 동일한 API를 사용한다. JSF 기술은 클라이언트측 JavaScript와 그에 대응하는 서버측 AJAX 프로세싱 코드를 작성하는 재사용 가능 컴포넌트를 생성하는 데 사용될 수 있다. 이제 AJAX와 서블릿의 활용 예제를 살펴보기로 하자.

자동 완성(autocomplete) 예제

사용자가 종업원에 관한 정보를 검색할 수 있는 웹 페이지를 상상해보자. 이 페이지에는 사용자가 종업원의 이름을 입력할 수 있는 필드가 포함되어 있다. 이 예제에서 엔트리 필드는 자동 완성(autocomplete) 기능을 가지고 있는데, 다시 말해 사용자가 종업원 이름의 일부를 입력하면 웹 애플리케이션은 입력한 문자로 이름이나 성이 시작되는 모든 종업원을 열거하여 이름을 자동으로 완성하게 된다. 자동 완성 기능은 사용자가 종업원의 정식 이름을 일일이 기억하거나 다른 페이지에서 이름을 찾아보아야 하는 번거로움을 덜어준다.

autocomplete example

검색 필드의 자동 완성 기능은 AJAX를 이용해서 구현될 수 있으며, 이를 위해서는 클라이언트와 서버 상에 코드를 제공해야 한다.

클라이언트 상에서

먼저, 사용자가 브라우저에 의해 로드되는 페이지의 URL을 지정한다. 한편, 이 예제에서는 JSF 컴포넌트, 서블릿, 또는 JSP 페이지에 의해 생성되는 HTML 페이지가 사용되었다고 가정하자. 페이지에는 JavaScript 함수 doCompletion()의 이름으로 된 onkeyup 속성을 가지는 폼 텍스트 필드가 포함되고, 이 함수는 폼 텍스트 필드에서 키를 누를 때마다 호출된다.

    <input type="text"
          size="20"
          autocomplete="off"
          id="complete-field"
                      name="id"
          onkeyup="doCompletion();">

사용자가 폼 텍스트 필드에 문자 "M"을 입력한다고 가정해보자. 그에 대한 응답으로 doCompletion() 함수가 호출되고, doCompletion() 함수는 다시 XMLHttpRequest 오브젝트를 초기화한다.

   function initRequest(url) {
       if (window.XMLHttpRequest) {
           return new XMLHttpRequest();
       } else if (window.ActiveXObject) {
           isIE = true;
           return new ActiveXObject("Microsoft.XMLHTTP");
       }
   }

   function doCompletion() {
       if (completeField.value == "") {
           clearTable();
       } else {
           var url = "autocomplete?action=complete&id=" + 
                   escape(completeField.value);
           var req = initRequest(url);
           req.onreadystatechange = function() {
               if (req.readyState == 4) {
                   if (req.status == 200) {
                       parseMessages(req.responseXML);
                   } else if (req.status == 204){
                       clearTable();
                   }
               }
           };
           req.open("GET", url, true);
           req.send(null);
       }
   }

XMLHttpRequest 오브젝트는 현재 표준 JavaScript에 포함되지는 않지만(표준화를 위한 노력이 진행중임), 사실상의 표준이자 AJAX의 핵심이라 할 수 있다. 이 오브젝트는 HTTP를 통해 서버측 컴포넌트(이 경우에는 서블릿)와 상호 작용하는 부분을 담당한다.

XMLHttpRequest 오브젝트 생성 시 URL, HTTP 메소드(GET 또는 POST), 그리고 상호작용의 비동기 여부 등 세 가지 매개변수가 지정된다. XMLHttpRequest 예제에서 매개변수는 다음과 같다.

  • URL autocomplete 및 전체 필드(complete-field)의 텍스트(M 문자):
         var url = "autocomplete?action=complete&id=" + 
                 escape(completeField.value);
    
  • GET(HTTP 인터랙션이 GET 메소드를 사용함을 의미) 및 true(인터랙션이 비동기적임을 의미):
         req.open("GET", url, true);
    

비동기 호출을 이용할 때는 callback 함수를 설정해야 하는데, XMLHttpRequestreadyState 속성이 변경될 경우 이 callback 함수는 HTTP 인터랙션 과정의 특정 포인트에서 비동기적으로 호출된다. 예제에서 callback 함수는 processRequest()이며, 함수에 대해 XMLHttpRequest.onreadystatechange 속성으로 설정된다. readState가 "4"’일 경우 parseMessages 함수에 대한 호출에 주목할 것. "4"의 XMLHttpRequest.readyState는 HTTP 인터랙션이 성공적으로 완수되었음을 나타낸다.

XMLHttpRequest.send()가 호출되면 HTTP 인터랙션이 시작되고, 인터랙션이 비동기적이면 브라우저는 계속해서 페이지의 이벤트를 처리한다.

서버 상에서

XMLHttpRequest는 URL 자동 완성에 대해 HTTP GET을 요청하고, autocomplete라 불리는 서블릿으로의 매핑이 수행된다. 그리고, AutoComplete 서블릿의 doGet() 메소드가 호출된다. 다음은 doGet() 메소드의 형태이다.

   public void doGet(HttpServletRequest request, 
           HttpServletResponse response) 
        throws IOException, ServletException { 
       ... 
       String targetId = request.getParameter("id"); 
       Iterator it = employees.keySet().iterator(); 
       while (it.hasNext()) { 
           EmployeeBean e = (EmployeeBean)employees.get(
                   (String)it.next()); 
           if ((e.getFirstName().toLowerCase().startsWith(targetId) || 
              e.getLastName().toLowerCase().startsWith(targetId)) 
              && !targetId.equals("")) { 
              sb.append("<employee>"); 
              sb.append("<id>" + e.getId() + "</id>"); 
              sb.append("<firstName>" + e.getFirstName() + 
                      "</firstName>"); 
              sb.append("<lastName>" + e.getLastName() + 
                      "</lastName>"); 
              sb.append("</employee>"); 
              namesAdded = true; 
           } 
       } 
       if (namesAdded) { 
           response.setContentType("text/xml"); 
           response.setHeader("Cache-Control", "no-cache"); 
           response.getWriter().write("<employees>" + 
                   sb.toString() + "</employees>"); 
       } else { 
           response.setStatus(HttpServletResponse.SC_NO_CONTENT); 
       } 
    }

이 서블릿을 보면 알 수 있듯이, AJAX 처리를 위해 서버측 코드 작성 방법을 배우는 데 필요한 새로운 내용은 전혀 나와있지 않다. XML 문서를 교환하고자 할 경우에 대비해서 응답 컨텐트 유형을 text/xml로 설정해야 하는데, AJAX의 경우에는 평문(plain text) 또는 심지어 클라이언트 상의 callback 함수에 의해 평가되거나 실행될 수 있는 JavaScript의 단편도 교환이 가능하다. 일부 브라우저는 결과를 캐시할 수 있으므로 Cache-Control HTTP 헤더를 no-cache로 설정할 필요가 있을 수 있다는 점에도 역시 유의할 것. 이 예제에서 서블릿은 이름이나 성이 문자 M으로 시작되는 모든 종업원을 포함하는 XML 문서를 생성한다. 다음은 호출을 한 XMLHttpRequest 오브젝트로 반환되는 XML 문서의 예제이다.

   <employees>
      <employee>
        <id>3</id>
        <firstName>George</firstName>
        <lastName>Murphy</lastName>
      </employee>
      <employee>
        <id>2</id>
        <firstName>Greg</firstName>
        <lastName>Murphy</lastName>
      </employee>
      <employee>
        <id>11</id><firstName>Cindy</firstName>
        <lastName>Murphy</lastName>
        </employee>
      <employee>
        <id>4</id>
        <firstName>George</firstName>
        <lastName>Murray</lastName>
      </employee>
      <employee>
        <id>1</id>
        <firstName>Greg</firstName>
        <lastName>Murray</lastName>
     </employee>
   </employees>

다시 클라이언트로

처음 호출을 한 XMLHttpRequest 오브젝트가 응답을 받을 경우, parseMessages() 함수가 호출된다(자세한 내용은 이 예제의 앞 부분에 있는 XMLHttpRequest의 초기화를 참조). 다음은 parseMessages() 함수의 모습이다.

   function parseMessages(responseXML) {
       clearTable();
           var employees = responseXML.getElementsByTagName(
                   "employees")[0];
       if (employees.childNodes.length > 0) {
           completeTable.setAttribute("bordercolor", "black");
           completeTable.setAttribute("border", "1");
       } else {
           clearTable();
       }
    
       for (loop = 0; loop < employees.childNodes.length; loop++) {
           var employee = employees.childNodes[loop];
           var firstName = employee.getElementsByTagName(
                   "firstName")[0];
           var lastName = employee.getElementsByTagName(
                   "lastName")[0];
           var employeeId = employee.getElementsByTagName(
                   "id")[0];
           appendEmployee(
                   firstName.childNodes[0].nodeValue,
                   lastName.childNodes[0].nodeValue, 
                   employeeId.childNodes[0].nodeValue);
       }
   }

parseMessages() 함수는 AutoComplete 서블릿이 반환한 XML 문서의 오브젝트 표현을 매개변수로 수신하는데, 이 함수는 XML 문서를 프로그램적으로 traverse한 다음 결과를 이용하여 HTML 페이지의 컨텐츠를 업데이트한다. 이 작업은 XML 문서 내의 이름에 대한 HTML 소스를 ID가 "menu-popup"’인 <div> 엘리먼트로 inject함으로써 수행된다.

   <div style="position: absolute; 
      top:170px;left:140px" id="menu-popup">

사용자가 문자를 더 많이 입력할수록 목록 길이는 줄어들게 되고, 이어서 사용자는 여러 이름 중 하나를 클릭할 수 있다.

이제 여러분은 AJAX가 단순히 페이지의 백그라운드에서 HTTP를 통해 정보를 교환하고 결과를 토대로 해당 페이지를 동적으로 업데이트한다는 것을 알게 되었을 것이다. AJAX와 자바 기술에 관한 자세한 내용은 테크니컬 아티클 Asynchronous JavaScript Technology and XML (AJAX) With Java 2 Platform, Enterprise Edition(영문)을 참조하기 바란다. 아울러 AJAX BluePrints 페이지(영문)와 Greg Murray의 블로그에 실려 있는 AJAX FAQ for the Java Developer(영문)의 내용도 함께 참조할 것.

예제 코드 실행하기

본 팁에는 본문에서 다루어진 기법을 예시하는 예제 패키지가 첨부되어 있는데, Servlet 2.4 이상의 API를 지원하는 웹 컨테이너라면 모두 예제 패키지 설치가 가능하다. 예제를 설치하고 실행하려면 다음 단계를 따르도록 한다.

  1. 먼저 GlassFish Project 페이지(영문)에서 GlassFish를 다운로드해야 하는데, GlassFish는 Servlet 2.5와 JSTL(JSP Standard Tag Library)을 곧바로 지원한다. 만약 J2EE 1.4 또는 Servlet 2.4 컨테이너를 사용하고 있다면 JSTL JAR 파일을 web/WEB-INF/lib 디렉터리에 포함시켜야 할 수도 있다.
  2. 다음의 환경 변수를 설정한다.
    • GLASSFISH_HOME. This should point to where you installed GlassFish (for example C:\Sun\AppServer) GLASSFISH_HOME. GlassFish 설치 장소를 가리켜야 한다(가령 C:\Sun\AppServer).
    • ANT_HOME. ant 설치 장소를 가리켜야 한다. ant는 다운로드한 GlassFish 번들에 포함되어 있다. (Windows의 경우에는 lib\ant 서브디렉터리 내에 위치함.)
    • JAVA_HOME. 시스템에서의 JDK 5.0 위치를 가리켜야 한다.

    아울러, ant 위치를 PATH 환경 변수에 추가한다.

  3. 예제 파일을 다운로드하여 압축을 해제한다. 이제 새로 추출된 디렉터리가 <install_dir>\ajax-autocomplete로 표시되어야 하는데, 예를 들어 Windows 컴퓨터의 C:\에 압축을 풀었다면 새로 생성된 디렉터리는 C:\ajax-autocomplete가 되어야 한다

  4. ajax-autocomplete 디렉터리로 이동해서 build.properties 파일에 build.properties.sample을 복사한다.

  5. build.properties 파일을 열고 Servlet 2.4 이상의 API를 포함하는 JAR 파일에 servlet.jar 속성을 설정한다. GlassFish의 경우 JAR 파일은 <gf_install_dir>/glassfish/lib/javaee.jar인데, 여기서 <gf_install_dir>은 GlassFish가 설치된 곳이다. javaee.autodeploy를 웹 컨테이너가 애플리케이션을 자동 설치할 디렉터리로 설정한다. GlassFish의 경우 이 디렉터리는 <gf_install_dir>/glassfish/domains/domain1/autodeploy이다.

  6. 6. 다음 명령어를 입력하여 GlassFish를 시작한다.
    <GF_install_dir>\bin\asadmin start-domain domain1
    
    이 때, <GFinstall_dir>은 Glassfish가 설치된 디렉터리이다.

  7. ant 툴을 이용하여 애플리케이션을 구축하고 설치한다. Glassfish는 /glassfish/bin/asant 디렉터리에 ant의 사본을 가지고 있고, 또한 Apache Ant Project 페이지(영문)에서 ant를 다운로드할 수도 있다.

    애플리케이션을 구축하려면 다음 명령어를 입력한다.
          ant
    
    애플리케이션을 설치하려면 다음 명령어를 입력한다.
          ant deploy
    
  8. 브라우저를 다음의 URL로 연다: http://localhost:8080/ajax-autocomplete/.

    NetBeans 4.1 이상을 사용하고 있다면 예제가 포함되어 있을 것이고, Help -> BluePrints Solutions Catalog를 선택하여 툴에서 이를 실행할 수 있다. 여기서 AJAX -> Autocomplete 예제를 선택하면 된다. 이제 NetBeans 내에서 예제를 실행할 수 있으며 원할 경우 수정도 가능하다.

    예제를 실행하면 다음과 같은 모습이 된다.
    example autocomplete

    목록의 이름을 클릭하면 종업원에 관한 정보가 표시된다.
    example autocomplete - Employee Info

필자 소개

서블릿 스펙 분야를 주도하고 있는 Greg Murray는 Java BluePrints 팀의 전 멤버로, 웹 티어 권고(recommendations) 부문을 담당한 바 있으며, 현재 그는 BluePrints 팀의 도움을 받아 썬의 AJAX 프로젝트를 이끌고 있습니다. 또한 Greg은 국제화, 웹 서비스, J2SE 독립형 클라이언트, AJAX 기반 웹 클라이언트 등의 분야에서 다양한 경력을 쌓기도 했습니다.

"Java EE" 카테고리의 다른 글

Posted by 1010