원숙미 넘치는 기능으로 거듭나는 JDK 1.4 ①

일반입력 :2001/08/07 00:00

하동욱

지난 95년, JDK 1.0 알파버전이 첫 선을 보인 이래 자바는 수많은 기능 향상을 통해 자신의 모습을 끊임없이 발전시켜왔다. 또한 인터넷이라는 거대한 조류를 타고 세상에 알려지더니 급기야는 엔터프라이즈 버전, 스탠더드 버전, 마이크로 버전으로 구분해야 할만큼 자바의 활용분야도 다양해졌다.

이런 자바가 자바원 2001 행사를 며칠 앞둔 지난 5월말, 다시 우리들 앞에 새로운 모습으로 다가왔다. JDK 1.4의 첫 공식 베타버전이 발표된 것이다. 자바원 2001 행사에서 공개된 JDK 1.4, 코드명 멀린의 새로운 면모를 윈도우용 JDK 1.4를 중심으로 하나하나 살펴보자.

기능 추가보다 품질에 중점

자바원 2001에서 썬은 멀린이 자바 환경의 품질에 가장 큰 비중을 두었음을 밝혔다. 마이너 업그레이드를 무려 1.1.8까지 진행했던 자바 1.1에 이어, JDK 1.2는 1.2.2(Cricket)까지, JDK 1.3(kestrel)은 1.3.1(Ladybird)까지 나오면서 자바도 원숙미를 느낄 만큼 발전해 가고 있다(보통 이러한 마이너 업그레이드는 한 두 번 정도 나오며, 1.4도 2001년 연말의 정식 발표 후 2002년 상반기에 한 번의 마이너 업그레이드가 예정돼 있다).

썬은 이런 기본 컨셉을 일명 RAS(Reliability, Availability, Serviceability)라고 말하고 있다. 그것에 추가해 확장성과와 성능, 배포(Deployment)도 강조하고 있다. 이는 썬도 그 간의 경험을 통해 불안정한 모습의 자바 환경이 불필요한 비용을 지출하게 해서 근본적으로 자바의 세계를 뒤흔들 수 있음을 자각해서 나온 조치로 보인다. JDK 1.4 멀린의 향상된 기능들이 뛰어난 안정성을 제공한다면 개발자로서 무척 고무적인 일이 될 것이다.

새롭게 추가된 강력한 기능들

JDK 1.4 멀린은 여러 다양하고 새로운 기능을 갖고 있다. JDK 1.4 멀린을 디자인함에 있어 썬은 여러 가지 경로를 통해 의견을 받아들였다. 자바 디벨로퍼 커넥션(Java Developer Connection)의 RFE(Request For Enhancements)로 JDK의 리눅스 포팅이나 어서션 기능 추가 등의 개발자들 의견을 받아들인다든가, 이것을 JCP(Java Community Process)의 JSR(Java Specification Requests)을 통해 개발자와 업계의 비중있는 요구를 체계적으로 자바에 반영하는 개방된 모습을 보여주었다(아직 미흡하지만 이런 노력들이 자바를 가장 인기있는 언어로 이끄는 원동력이 되지 않았나 싶다).

빌드 64까지 비공개로 진행되면서 현재 베타 1인 빌드 65 버전까지의 면면을 살펴보면 많은 패키지와 기능들이 추가돼 왔음을 알 수 있다. 아직 정식버전이 나오지 않은 상태라 최종적인 모습은 아니지만 2회에 걸쳐 JDK 1.4 베타 1의 새로운 모습들을 통해 JDK 1.4 멀린이 추구하고자 하는 방향을 가늠해보자.

자바 VM

JDK 1.4에는 1.3까지의 VM과 비교해서 많은 변화와 발전이 있다. JDK 1.3에선 클래식 VM과 핫스팍 클라이언트 VM이 있었는데 JDK 1.4에서는 클래식 VM이 빠지면서 핫스팍 서버 VM이 추가됐다. 이는 더 이상 클래식 VM을 지원하지 않아도 될 정도로 핫스팍 VM의 성능과 안정성이 좋아졌다는 것으로 해석할 수 있다. 현재 VM의 버전정보를 알아보면 다음과 같다.

[C:] java -version

java version 1.4.0-beta

Java(TM) 2 Runtime Environment, Standard Edition (build 1.4.0-beta-b65)

Java HotSpot(TM) Client VM (build 1.4.0-beta-b65, mixed mode)

만약, 자바 핫스팍 서버 VM을 쓰고 싶다면 -server라는 옵션을 사용하면 된다.

[C:] java -server -version

java version 1.4.0-beta

Java(TM) 2 Runtime Environment, Standard Edition (build 1.4.0-beta-b65)

Java HotSpot(TM) Server VM (build 1.4.0-beta-b65, mixed mode)

64비트 CPU에 최적화된 핫스팍 서버 VM도 제공하는데 현재는 스팍 칩에서 돌아가는 솔라리스에서 64비트를 지원하고 있다. OS가 64비트의 어드레싱 공간을 이용해 16TB 이상의 버추얼 메모리를 제공할 수 있으므로 자바에서의 힙 메모리도 많이 늘어나게 된다. 단, 사용자가 기존 32비트 환경을 그대로 사용할 때를 위해 32비트와 64비트를 선택할 수 있는 옵션(-d32, -d64)을 제공한다.

기존의 자바 프로그램을 64비트용으로 실행할 때는 그대로 사용할 수 있으며, JNI를 통한 네이티브 코드를 만들어 쓰는 경우에는 이를 64비트 환경에서 재컴파일하면 된다.

인텔에서 제공하는 64비트 CPU 환경인 IA-64를 지원하는 기능도 JDK 1.4에서 제공할 예정인데 64비트 CPU를 지원하는 윈도우가 먼저 출시돼야 할 것이므로 시간이 걸릴 것이다. 하지만 이 글을 쓰는 7월 현재 윈도우 2000 64비트 버전의 프리뷰가 나오고 있는 시점이어서 빨리 지원되지는 않을 듯 싶다. 그밖에, 스팍용 리눅스에서는 쉽게 지원할 수 있을 것이라는 전망이 나오고 있다.

클래스 파일의 버전도 올라갔는데 뒤에 나올 어서션 지원 등을 포함해 47.0에서 48.0으로 올라갔다. 지난 JDK 1.0 대에서도 45.3 버전이던 것이 JDK 1.2에서 46.0으로 올라갔고, 다시 JDK 1.3 에서 47.0으로 올라간 적이 있다.

자바 웹 스타트

이것은 원래 별도의 프로그램이었는데 JDK 1.4에 포함됐다. 자바 웹 스타트는 JNLP(Java Network Launch Protocol)를 기반으로, 네트워크의 자바 프로그램을 별다른 세팅없이 웹브라우저나 탐색기 등을 사용해 실행시킬 수 있는 프로그램이다. 기존의 애플릿이나 jar 파일의 MANIFEST.MF 파일과는 달리 JNLP를 통해 정교하게 프로그램 실행환경을 지정할 수 있다.

동작원리는 간단한데 자바 웹 스타트를 설치하면, jnlp라는 확장자에 대해 javaws.exe(자바로 된 자바 웹 스타트 프로그램을 띄우는 역할을 함)를 띄우도록 설정된다. 이어서 jnlp 파일을 실행하면 연결된 자바 프로그램이 뜬다. 이 프로그램이 jnlp 파일안에 적힌 실행하고자 하는 자바 프로그램의 상세 내용대로 프로그램을 별도로 다운받아 실행시키는 것이다(jnlp의 MIME 타입은 application/x-java-jnlp-file이다).

이런 시도는 예전부터 웹브라우저에서 소프트웨어를 실행시키기 위한 방법으로, 여러 소프트웨어 회사에서 나름대로 개발, 이용해온 것인데 자바 웹 스타트를 통해 자바 프로그램을 위한 체계적인 표준이 만들어진 것으로 볼 수 있다.

JDK 1.4 베타를 설치했다면 자바 웹 스타트도 같이 설치됐을 것이다. http://java.sun.com/products/javawebstart/demos.html나 http://java.sun.com/products/jfc/tsc/articles/jcanyon/에 가서 직접 실행해보길 바란다.

jnlp 파일은 XML이며 이 파일을 열어보면 무슨 의미를 가진 내용인지 대략 파악할 수 있다. JNLP에 대해 더 자세히 알고 싶은 독자는 http://java.sun.com/products/javawebstart/developers.html을 방문하면 자료를 얻을 수 있다

어서션

어서션(assertion)은 프로그램 실행 중 당연하다고 가정되는 부분에서의 오류를 검증하기 위한 방법이다. 이것의 목적은 디자인을 코드 내에 명시화하는 효과를 주고 프로그램 디자인 오류를 잡아내고자 하는 것이다. 그리고 개발자가 스펙을 제대로 이해하고 있는지와 코드에 대한 신뢰성을 높여서 개발 속도를 높이고자 하는 데에 있다(원래 자바가 만들어질 때 어서션 기능을 포함하려고 했으나 구현기능 검증과정에서 빠졌다고 전해진다).

다른 추가기능과는 다르게 어서션은 기능을 위한 키워드가 추가됐는데, assert라는 키워드를 이용해 불린(boolean) 식을 평가한 후 결과 값이 false이면 java.lang.AssertionError를 발생시킨다. 다음은 어서션을 사용하는 예문이다.

assert returnValue < 100 : returnValue;

assert resultBoolean==true;

첫째 예문은 returnValue가 100보다 작은 값을 가져야 하는 곳에 사용될 수 있다. 만약, 100 이상되는 값이 만들어졌다면 이는 디자인과 맞지 않으며 AssertionError 클래스의 생성자중 int를 받는 생성자에 returnValue를 넣어 원인이 뭔지 명시하도록 AssertionError 객체를 만들어 던진다.

둘째 예문은 returnBoolean이 반드시 true이어야 하는 곳에 사용될 수 있다. false로 되었다면 이 경우 detail message를 따로 정해주지 않았으므로 AssertionError가 났다는 정도만 알 수 있다. detail mesage는 필요한 경우에 지정해주면 된다.

어서션을 사용하려면 거쳐야 할 절차가 있다. 원래 assert라는 키워드는 1.4 버전 이전에는 없었으므로 assert 라는 변수명을 이미 사용하고 있는 자바 클래스의 경우 충돌이 있을 수 있다. 그래서 컴파일과 실행시에 이에 대한 옵션을 주도록 되어 있다.

컴파일의 경우 -source 1.4라는 옵션을 줘야하며 주지 않으면 이전 버전처럼 assert를 키워드로 취급하지 않는데, 1.4에서는 키워드가 됐으므로 사용할 수 없다는 경고 메시지가 뜬다.

실행시에는 -ea(enable assertions) 옵션을 줘야 어서션 기능을 사용할 수 있다. 이렇게 한 이유는 어서션이 프로그램 실행에 부하를 주므로 개발시에만 사용하도록 하기 위함이며, assert라는 변수를 사용했을 수도 있는 이전 버전의 클래스를 실행할 때와 구분하기 위한 이유다.

추가로 JDK 1.4의 라이브러리 클래스에 대해서도 어서션을 수행하도록 할 수 있는데 -esa(enable system assertions) 옵션이 그것이다. -ea와 -esa는 같이 써도 되고 둘 중 하나만 써도 된다. 커맨드 라인에서 다음 예문과 같이 사용하면 된다.

java -source 1.4 AssertTest.java

java -ea AssertTest

객체직렬화

객체직렬화(Serialization)를 사용하면 객체를 직렬화해서 저장해두고 나중에 다시 풀어서 객체화할 수 있다. JDK 1.3까지는 int 같은 기본형의 래퍼(Wrapper) 클래스인 java.lang.Integer 등을 객체직렬화해서 저장한 뒤 이를 풀어내는 과정에서 ObjectInputStream.resolveClass() 메쏘드가 적절한 처리를 하지 못하는 버그가 있었다. 이를 resolveClass() 메쏘드를 고쳐 해결했다.

컬렉션 프레임워크

컬렉션 프레임워크(Collections Framework)는 JDK 1.3에서는 큰 변화를 보이지 않았는데 JDK 1.4에서 많은 발전이 있었다.

java.util.Collections에 추가된 메쏘드

rotate(List list, int distance)는 list의 내용을 distance만큼 시계방향으로 돌려주는 메쏘드다. 예를 들어 {1, 2, 3} 인 list를 2만큼 로테이트(rotate) 시켜주면 {2, 3, 1}이 되며 다시 -1 만큼 로테이트 시켜주면 시계반대 방향으로 돌려주므로 {3, 1, 2}가 된다.

List.subList() 메쏘드와 같이 쓰면 재미있는 일을 할 수 있는데, 이 메쏘드가 리턴하는 리스트 객체는 모체가 된 리스트 객체의 엘리먼트를 같이 공유하므로 subList() 메쏘드를 통해 얻어낸 리스트에 변형을 가하면 모체 리스트에도 그대로 반영된다. 그러므로 긴 리스트의 일부분에 대해 로테이트하려면 다음과 같이 하면 된다.

Collections.rotate(list.subList(3, 10), 5);

이렇게 하면 전체 리스트에서 3번째부터 9번째(10번째는 포함되지 않는다)를 대상으로 시계방향으로 5칸 돌리게 된다. 간단한 내용이지만 GUI나 수학, 과학 계산에서는 많이 사용될 수 있다고 한다.

replaceAll(List list, Object oldVal, Object newVal)는 리스트 전반에 걸쳐 oldVal에 해당되는 것이 있으면 이를 newVal로 대치시킨다. 이 과정은 Element e에 대해 다음의 식을 행한다.

(oldVal==null ? e==null : oldVal.equals(e))

프로그래머가 간단히 구현해서 쓸 수도 있는 내용이지만 JDK 1.4에서 구현돼 있으므로 속도가 더 빠르다.

indexOfSubList(List source, List range)는 소스에서 range에 해당하는 부분의 첫 번째 인덱스를 찾는 메쏘드다. 없으면 -1을 리턴하며, 이 메쏘드는 brute force라는 방법을 이용해서 탐색한다. lastIndexSubList()는 뒤에서부터 탐색하는 메쏘드다. 보통 텍스트 프로세싱(Text processing)에서 많이 쓰일 수 있다.

java.util 패키지에 추가된 인터페이스

RandomAccess는 리스트에서 탐색을 할 때, 기존에는 Iterator 인터페이스를 이용해 순차적으로 검색했다. 이는 작은 용량의 리스트에선 문제가 없었지만, 큰 용량의 리스트에서는 매우 비효율적이었다. 그래서 RandomAccess를 구현하는 리스트 객체의 경우, 순차적인 방법이 아니라 아래의 방법 중 하나의 방법으로 탐색하게 된다.

BINARYSEARCH

REVERSE

SHUFFLE

FILL

ROTATE

COPY

REPLACEALL

INDEXOFSUBLIST

RandomAccess 인터페이스 자체는 아무 필드나 메쏘드도 갖지 않으며 다만, 리스트가 RandomAccess를 활용하고 있는지 아닌지 나타내는 데만 쓰인다. 다음 예는 JDK 문서에 나와있는 예다. 기존의 Iterator 인터페이스를 쓰는 것보다 더 빠름을 나타낸 것이다.

/* RandomAccess 인터페이스를 활용할 경우: */

for (int i=0, n=list.size(); i

list.get(i);

/* Iterator 인터페이스를 이용할 경우: */

for (Iterator i=list.iterator(); i.hasNext(); )

i.next();

로깅 API

java.util.logging 패키지가 추가돼 그동안 엔터프라이즈 환경에서 개발하던 개발자에게 환영받을 것으로 보인다. 로깅 API(logging API)는 여러 곳에서 나름대로 정의, 개발을 해서 내놓고 있었는데 이번에 JDK 1.4에 추가된 API도 JLog나 Log4j에서 많은 도움을 받았다고 밝히고 있다. 이런 표준 API를 사용하면 개발자나 사용자, 관리자, 유지 보수자 모두 같은 데이터 형식을 이용해 로그를 볼 수 있는 장점이 있다.

기본적인 동작구조는 간단하다. 로거를 통해 로그 데이터를 보내면 로거에서 필터가 있으면 이를 적용한 뒤 핸들러에게 전해진다. 핸들러도 필터가 있다면 이를 적용하고 포맷터를 써서 메시지를 가공한 다음 로그 파일 등에 기록한다. 이때 핸들러는 다른 핸들러로 연결될 수도 있어 다양한 결과를 내는 유연한 로깅을 할 수 있다.

핸들러는 JDK 1.4에 포함된 것이 몇 가지 있는데 MemoryHandler, StreamHander가 있고 StreamHander를 상속하는 ConsoleHandler, FileHandler, SocketHandler가 있다.

포맷터는 SimpleFormatter와 XMLFormatter가 있는데 이들 핸들러와 포맷터 정도면 일반적인 용도로는 충분할 것이다.

로거는 Loggers.getLogger(String loggerName) 메쏘드로 얻을 수 있는데, loggerName은 보통 사용되고 있는 클래스의 패키지 이름까지를 포함한 이름이나 패키지 이름 자체가 쓰인다. 예를 들어 com.mamoru.avalon 클래스가 있다면 Logger.getLogger(com.mamoru.avalon); 라는 문장으로 로거를 얻을 수 있다. 이렇게 하면 그 패키지 어디에서나 같은 로거를 공유할 수 있다.

물론, 그 안에서 특정 클래스는 클래스 이름까지 포함하는 이름으로 로거를 만들어 써도 된다. 패키지 이름을 사용하는 것은 강제사항은 아니지만 그것이 가장 좋은 방법이다. 왜냐면 로거들은 LogManager에 의해 NameSpace에서 계층구조를 가지며 관리되기 때문이다. 예를 들면 a.b.c 로거는 a.b의 하위 로거이며 그 특성도 상속받게 된다.

이렇게 얻어온 로거는 LogManager에서 지정하는 로그 레벨을 갖게 된다. 로그 레벨은 로그 메시지들의 우선순위를 지정해서 그 우선순위보다 같거나 높은 우선순위의 메시지만 로깅할 때 사용된다. 예를 들어 심각한 버그의 메시지만 기록에 남기게 하고 싶을 때는 평범한 진행사항을 담고 있는 로그 메시지는 무시해도 되는 것이다.

ALL과 OFF라는 레벨도 있는데 모든 메시지를 다 로깅하거나 모두 하지 않거나 할 때 쓰인다.

로거에서 로깅을 위해 사용할 수 있는 메쏘드는 여러 가지가 있는데 log() 메쏘드를 통해 레벨과 로그 메시지를 전해주거나 레벨에 맞는 메쏘드을 직접 이용하는 방법이다. 예를 들어 warning 레벨의 로그 메시지를 남기고 싶다면, log() 메쏘드에 Level.WARNING이라는 로그 레벨을 전해주거나 warning() 메쏘드를 직접 이용하는 것이다. 제공되는 LogTest 예제를 실행하면 다음과 같은 실행결과를 얻을 수 있다.

level : INFO

filter : null

handlers : 1

2001-07-09 오전 4:17:04 LogTest main

INFO: info

2001-07-09 오전 4:17:04 LogTest main

WARNING: warning

2001-07-09 오전 4:17:04 LogTest main

SEVERE: severe

앞에서 말했듯이 기본 레벨은 LogManager에서 지정된다. 여기서는 INFO로 지정된 것을 알 수 있으며 LogManager.setLevel() 메쏘드를 사용하면 원하는 로거에 대해 레벨을 바꿀 수 있다.

java.util.logging.config.class나 java.util.logging.config.file이라는 시스템 프로퍼티가 지정돼 있으면 그 클래스 파일이나 프로퍼티 파일에서 LogManager의 기본 값을 세팅하게 된다. 그 어느 것도 지정되지 않으면 JRE 디렉토리의 lib/logging.properties를 읽어들여 세팅한다.

프레퍼런스 API

많은 프로그램들은 프로그램 시작 전에 선택사항을세팅해둘 필요가 있다. 또 전반적인 실행환경을 조율할 필요도 있다. 이런 능력을 API로 정리해둔 것이 프리퍼런스 API다.

보통 java.util.Properties를 이용해서 이런 기능을 구현하는데 이 방법은 프레퍼런스 데이터를 백업하거나 다른 컴퓨터로 전해주거나 저장공간을 지정해줘야 하는 등의 불편이 있다. 이제 그럴 필요없이 프레퍼런스 API만 사용하면 간편하게 처리 가능하다.

java.util.prefs 패키지를 통해 제공되는 이 API는 유저와 시스템의 프레퍼런스와 컨피규레이션 데이터를 저장하고 가져오는 기능을 제공한다.

저장할 때는 비동기적으로 동작한다. 즉, 저장하라고 지시했다고 하더라도 그 즉시 저장동작이 일어난다는 보장은 없다. 그러므로 Preferences.flush()라는 메쏘드를 불러줘야 한다. 프레퍼런스 데이터는 플fot 파일, OS-specific registries, JNDI 같은 디렉토리 서버나 SQL 등에 저장된다. 그럼 사용하는 방법에 대해서 알아보자.

먼저 Preferences.systemRoot() 메쏘드는 시스템에 대한 프레퍼런스 객체를 얻어온다. 윈도우의 경우 '내 컴퓨터HKEY_LOCAL_MACHINESOFTWAREJavaSoftPrefs'를 키 값으로 문자열 데이터를 만든다. 그리고 Preferences.systemNodeForPackage(Object obj) 메쏘드는 앞서 말한 것과 같은 키에 obj에 해당하는 패키지 정보를 하위 키로 만들어서 문자열 데이터를 넣는다.

Preferences.userRoot() 메쏘드는 '내 컴퓨터HKEY_CURRENT_USERSoftwareJavaSoft'를 키 값으로 문자열 데이터를 만든다. 역시 앞선 경우와 마찬가지로 Preferences.userNodeForPackage(Object obj) 메쏘드는 같은 키에 obj 에 해당하는 패키지 정보를 하위 키로 만들어서 문자열 데이터를 넣는다.

이렇게 시스템과 유저로 구분하는 이유는 컴퓨터를 혼자 독점적으로 사용한다는 보장이 없기 때문이다. 예를 들어 전산실습실에서 자바용 교육 프로그램을 사용한다면 그 프로그램의 기본 세팅은 시스템 프레퍼런스가 될 것이고, 거기에 더해 실습하는 사람들의 개별적인 세팅 내용은 유저 프레퍼런스가 될 것이다. 로그인하는 유저별로 유저 프레퍼런스가 제공되므로, 로그인한 유저가 세팅한 내용 그대로 가져올 수 있게 된다.

제공되는 PreferencesTest 예제를 실행한 다음 각각 'HKEY_LOCAL_MACHINESOFTWAREJavaSoftPrefs'와 '내 컴퓨터HKEY_CURRENT_USERSoftwareJavaSoftPrefs' 키를 regedit.exe 프로그램을 실행해 확인해보면 값이 저장되어 있는 것을 확인할 수 있을 것이다.

AWT

JDK 1.4 부터는 AWT(Abstract Windows Toolkit)에 정말 많은 변화가 생겼다. 각 항목별로 간단하게 정리해보자.

뉴 포커스 서브시스템

포커스 관리 시스템이 완전히 바뀌었다. 플랫폼간 이질성 문제나 AWT와 스윙간의 일관되지 못한 문제점 등 모든 버그들이 포커스 관리 시스템을 교체하면서 해결됐다. 물론, 이렇게 되면서 이전 포커스 관리 시스템과 약간의 차이점이 생겼지만, 새로운 시스템은 이에 대한 충분한 보상을 제공한다.

새로운 포커스 관리 시스템에서 KeyboardFocusManager는 아주 중요한 클래스다. 예를 들어 탭 키를 이용해서 포커스를 이동할 경우, 어떤 컴포넌트들의 순으로 이동해야 하며 JInternalFrame이나 윈도우간에 어떻게 전환이 이뤄져야 하는지를 말해준다.

이런 것에 대한 기본 룰을 정해놓은 것이 DefaultKeyboardFocusManager이며, 프로그래머가 나름대로의 KeyboardFocusManager를 제공하는 것도 가능해졌다.

또 Input 메쏘드와 같이 포커스를 갖고 있는 컴포넌트에게 오는 키 입력을 중간에서 가로채는(왜냐하면 한글의 경우 글자를 조합해야 하므로) 컴포넌트가 조합되기 전의 키도 알 수 있도록 해줄 수 있다.

포커스가 이동할 때는 컴포넌트가 속한 컨테이너에 대해서도 이벤트가 발생하는데 포커스를 잃으면 컴포넌트와 컨테이너 순으로 잃었음이 통보되고, 새로 얻은 컴포넌트에게는 컨테이너, 컴포넌트 순으로 포커스를 얻었음이 통보된다.

그리고 한 컨테이너에서 컴포넌트간의 포커스 이동을 위한 키를 정의할 수 있을 뿐만 아니라 상위나 하위 컨테이너로 이동하기 위한 키도 정의할 수 있다. 하지만 윈도우의 toFront()나 toBack() 종류의 메쏘드의 경우는 플랫폼간의 윈도우 시스템의 차이 때문에 포커스의 이동은 플랫폼 종속적이 된다.

포커스 관리 시스템에 대한 내용은 너무 많기 때문에 지금까지 설명한 간략한 수준에서 마치기로 하겠다. 하지만 이것으로도 새로운 포커스 관리 시스템이 왜 필요한 것인지는 알 수 있을 것이다.

Headless 지원

만약 모니터나 키보드, 마우스 등이 없는 환경에서 이를 필요로 하는 프로그램이 돈다면 어떻게 될까. JDK 1.4부터는 GraphicsEnvironment.isHeadless() 메쏘드를 통해 모든 Top-level 컴포넌트들이 이를 검사한다. 그래서 만약 모니터, 키보드, 마우스 지원이 안되는 시스템이라면 HeadlessException을 던진다. 이것은 RuntimeException의 하위 클래스이므로 반드시 캐치할 필요는 없다.

새로운 풀스크린 독점 모드 API

모니터 스크린 전체를 사용하는 윈도우 객체를 만들어 쓸 수 있다. 게임이나 프리젠테이션의 용도로 쓰임새가 있는데 물론 기존 윈도우 객체의 크기를 화면 사이즈에 맞춰 사용해도 되지만, 그 방법은 불편하고 제한적인 요소가 많다.

OS에 따라 다르겠지만 윈도우의 경우, 다이렉트X의 지원 이후 전체화면을 이용하는 프로그램이 많이 생겨났다. 예를 들어 풀스크린 게임이라던지 파워포인트라든지 IE의 창모드 같은 경우다. 이런 프로그램을 만들 수 있도록 JDK 1.4부터는 풀스크린 형태의 윈도우 객체를 지원한다.

이런 특별한 용도의 윈도우 상태는 기존의 윈도우 시스템에 대해 배타적이기 때문에 독점 모드(Exclusive Mode)라고 말한다.

이것의 특징은 기존의 윈도우 시스템에 구애받지 않고 비디오 카드에 직접 액세스하는 방법을 택하기 때문에 속도가 빠른 장점이 있다. 기능의 특성상 모든 OS에서 지원되지는 못할 수 있으므로, GraphicsDevice의 isFullScreenSupported() 메쏘드로 지원여부를 알아낼 수 있다. 전체화면으로 전환하기 위해서 다음의 메쏘드를 사용한다. FullscreenTest.java라는 소스코드를 제공하니 테스트해보기 바란다.

GraphicsDevice.setFullScreenWindow(Window myWindow)

이때 또 하나 참고할 만한 사항으로 패시브 랜더링(Passive rendering)과 액티브 랜더링(Active rendering)이 있다. 패시브 랜더링은 여러분들이 이때까지 자바 프로그래밍을 해왔던 방식으로 paint(Graphics g) 메쏘드가 호출될 때 드로잉 작업을 하는 방식이다. 그런데 풀스크린에서는 액티브 랜더링 방식을 쓰는 것이 좋은데 Fullscreen 에서의 paint(Graphics g) 메쏘드 호출은 완벽한 신뢰를 주지 못하기 때문이다(물론, 기존방식대로 하면 안된다는 것은 아니다).

액티브 랜더링을 하려면 getGraphics() 메쏘드를 이용해 그래픽을 직접 얻어와서 드로잉 작업을 한 다음 dispose() 해주는 방식이다. 풀스크린에 대한 예제는 www.javasoft.com/docs/books/tutorial/extra/fullscreen/를 참조하기 바란다.

버퍼링

캔버스나 윈도우에서 효과적인 버퍼링 구현을 지원한다. 버퍼링을 하는 방법은 두 가지가 있다. 첫째로, Bit-blit를 사용하는 방법이다. 이 방법은 백킹 버퍼(backing buffer)에 원하는 드로잉 작업을 한 다음 화면에 보여지는 버퍼 에 일부분, 혹은 전체를 그대로 카피하는 방법이다.

둘째로, 플리핑(Flipping)이 있다. 이는 화면에 보여지는 버퍼를 여러 개 준비하고 이들 버퍼의 포인터를 필요할 때 마다 비디오 포인터로 전환하는 것이다. 첫 번째 버퍼의 포인터가 비디오 포인터로 지정돼 있을 때, 두 번째 버퍼 에 드로잉 작업을 하는 식이다.

이런 버퍼의 개수는 프로그래머가 지정할 수 있으며 실행시에 Component.FlipBufferStrategy가 먼저 시도되고, 안되면 Component.BltBufferStrategy가 선택돼 사용된다. 이것도 실패하면 가속되지 않은 채로 Bit-blit를 하게 된다. 버퍼링을 하기 위한 간단한 예를 보자. 윈도우 객체가 있을 때, createBufferStrategy() 메쏘드를 부르고, BufferStrategy 객체를 얻는다.

window.createBufferStrategy(2);

BufferStrategy strategy=window.getBufferStrategy();

렌더링 루프에서 BufferStrategy 객체의 getDrawGraphics()를 통해 그래픽을 얻고 드로잉 작업을 한 다음 show() 메쏘드를 호출한다.

while(condition)

{

Graphics g=strategy.getDrawGraphics();

.....(드로잉 작업)

strategy.show();

}

다 쓰고 난 다음엔 디스포즈 해줘야 메모리 낭비 등을 줄일 수 있다.

window.setVisible(false);

window.dispose();

디스플레이 모드의 전환

그래픽 카드가 현재의 디스플레이 기기에서 표시할 수 있는 표시 모드를 알 수 있고 그들간의 전환을 할 수 있는 능력이 추가됐다. 이런 능력은 그래픽 프로그램이나 프리젠테이션 프로그램을 만들 때 필요할 수 있다.

GraphicsEnvironment의 getScreenDevices() 메쏘드를 이용해 GraphicsDevice 객체들을 얻은 다음, 이 객체에서 getDisplayModes() 메쏘드를 이용해서 DisplayMode들을 얻어낸다. 이들을 다시 GraphicsDevice에 setDisplayMode() 메쏘드의 인자로 이용하면 된다. 제공되는 DisplayTest.java 소스코드를 참조하기 바란다.

언데코레이티드 프레임(Undecorated Frames)

프레임이나 다이얼로그에 OS의 윈도우 시스템이 만드는 타이틀바나 스크롤바, 테두리 등을 그리지 않게 할 수 있게 되었다. 이로써 그동안 윈도우를 써서 불편하게 작업했던 어려움이 사라지게 되었으며, 여러 플랫폼에서 똑같이 보일 수 있는 프레임이나 다이얼로그를 만들 수 있게 되었다. 다음 메쏘드를 프레임이나 다이얼로그에서 사용하면 된다.

public void setUndecorated(boolean undecorated)

public boolean isUndecorated()

휠마우스 지원

드디어 자바에서도 휠마우스를 지원한다. 이를 위해 다음의 인터페이스와 클래스가 추가됐다. 이를 통해 프로그래머는 휠이 어느 방향으로 어느 만큼 움직였는지를 알 수 있게 됐다.

java.awt.event.MouseWheelListener

java.awt.event.MouseWheelEvent

최대화를 위한 프레임 추가

이전 버전에서 프레임에 추가됐던 것 중의 하나가 프레임을 아이콘 상태로 만들거나 다시 원래대로 복귀시키는 것이었다. 하지만 최대화시킬 방법이 없었는데 확장 상태(extended state)를 다룰 수 있도록 수정되면서, 다음 메쏘드가 프레임에 추가됐다.

public int getExtendedState()

public void setExtendedState(int state)

확장 상태를 다루기 위해 스테이트가 integer 값을 갖게 되었다. integer 값의 각 비트가 상태를 나타내는데 이 값은 프레임에 정의되어 있다.

public static final int MAXIMIZED_HORIZ

public static final int MAXIMIZED_VERT

public static final int MAXIMIZED_BOTH

참고로 윈도우에서는 가로나 세로로만 최대화되는 상태는 존재하지 않는다. 이런 최대화 상태를 알기 위해 java.awt.event.WindowStateListeer 인터페이스가 추가되었다.

또 최대 범위(Maximum bounds)도 따로 지정할 수가 있어 최대화되었을 때의 크기도 지정할 수 있다. 다음 메쏘드가 프레임에 추가됐다.

public synchronized void setMaximizedBounds(Rectangle bounds)

public Rectangle getMaximizedBounds()

역동적인 레이아웃으로 크기 조정

윈도우에서는 윈도우의 크기를 바꾸면 그 안의 컴포넌트들도 알아서 동적으로 재배치되는 능력이 있다. IE의 경우를 보면 쉽게 알 수 있다. 이런 것들을 자바에서 안정적으로 제어할 수 있는 API가 생겼는데 java.awt.Toolkik에 다음 메쏘드들이 추가됐다.

public void setDynamicLayout(boolean dynamic)

public boolean isDynamicLayoutActive()

컴포넌트 리스너 리스트로의 접근

JDK 1.3에 각 컴포넌트에 해당하는 EventListener의 리스트를 얻을 수 있는 메쏘드가 추가된 적이 있다. 이런 EventListener들이 해당 컴포넌트에 추가되거나 제거될 때, 이를 알 수 있는 PropertyChangeListener을 얻을 수 있는 메쏘드가 JDK 1.4에서 툴킷 클래스에 추가됐다. 그 메쏘드들은 다음과 같다.

public PropertyChangeListener[] getPropertyChangeListeners()

public synchronized PropertyChangeListener[] getPropertyChangeListeners(String propertyName)

public AWTEventListener[] getAWTEventListeners()

public AWTEventListener[] getAWTEventListeners(long eventMask)

현재 윈도우 매니저 조회

이건 유닉스에만 해당되는 얘기인데, 유닉스에서는 X윈도우에서 동작하는 윈도우 매니저들의 종류가 굉장히 많다. 이들 매니저에 따라 AWT 컴포넌트의 그리는 데이터가 조금씩 다른데, 하나의 매니저를 이용해서 쓰다가 다른 매니저로 바꾸게 되면 바뀐 상황을 제대로 인식하지 못해서 잘못된 결과를 내는 버그를 잡았다.

일치하지 않는 DLL에 대한 경고

VC++에서 JDK 1.4가 개발되고 있는데 같은 이름을 가진 DLL이라 할지라도 버전 차이나 각 언어별 차이로 인해 제대로 동작되지 않는 경우가 있을 수 있다.

이것은 DBCS 윈도우에 영문 프로그램(예를 들어 비주얼 C++)을 설치했을 경우, Riched32.dll 파일의 비호환성 때문에 TextArea에 이상이 나타나는 증상에 따라 이런 경고를 하게 된 것이다.

DrawingSurface API 제거

sun.awt.DrawingSurface 클래스가 없어졌다. 이 기능은 AWT 컴포넌트에서 내이티브 코드로 된 드로잉 능력을 제공해주는 것인데, 이미 JAWT라는 표준 방법이 JDK 1.3부터 제공되기 때문에 없어진 것이다. 내부 패키지이긴 하지만, 이를 이용해서 개발한 코드는 JAWT 로 수정돼야 JDK 1.4에서 제대로 돌아갈 것이다.

스윙

스윙은 새로운 컴포넌트도 추가되고 기존의 기능들도 보강되면서 더 유용해 졌다.

JProgressBar

현재 JProgressBar가 어떤 값을 표시해야 할지 모를 경우에도 사용가능 하도록 수정되었다. 마치 넷스케이프 브라우저에서 막대가 왔다갔다하는 모습을 가졌는데 다음 메쏘드가 추가됐다.

public void setIndeterminate(boolean newValue)

public boolean isIndeterminate()

JTabbedTable

JTabbedTable에서 한 가지 아쉬운 기능이 있었다면, 탭들을 스크롤시키는 것이었다. 필자도 예전에 이것을 만들어본 적이 있는데 JDK 1.4부터는 기본 지원된다. 추가된 필드와 메쏘드는 다음과 같다.

public static final int WRAP_TAB_LAYOUT = 0

public static final int SCROLL_TAB_LAYOUT = 1;

public void setTabLayoutPolicy(int layoutPolicy)

public boolean getTabLayoutPolicy()

JSpinner

Spinner 컴포넌트가 추가됐다. 입력창이 하나 있어서 직접 입력을 받을 수도 있고, 오른쪽에 있는 위아래 화살표버튼으로 그 값을 조절할 수도 있다.

JFormattedTextField

TextField의 기능이 확장되었는데, 포맷터를 적용해서 날짜나 특정 숫자, 문자를 형식화해 표현하거나 가져올 수 있게 됐다. 만약, 위아래 화살표 키로 어떤 한계 값까지 다다랐다면 버튼이 disable 되기도 한다.

생성자에 인자로 SimpleDateFormat 등의 java.text.Format 객체를 넣으면 된다. 그 다음 원하는 객체를 setValue() 메쏘드로 지정해주면 된다. 예를 들어 SimpleDateFormat 객체를 포맷터로 지정한 다음, Date 객체를 setValue() 해주면 지정된 형식대로 날짜가 표시된다.

Popup과 PopupFactory

애플에서 MaxOS X용으로 아쿠아 L&F(Look & Feel)를 구현했는데, 애플의 UI 특성상 팝업에 대한 퍼블릭 접근이 필요하게 됐다. 그래서 Popup과 PopupFactory를 퍼블릭 접근 제한자로 제공하게 됐다.

윈도우 L&F 지원강화

윈도우 L&F는 느리지만, 많은 변화를 가져온 윈도우 2000까지의 UI를 제대로 반영하지 못하고 있었다. 하지만 JDK 1.4에서는 윈도우 2000의 UI도 최대한 지원하도록 작성됐다.

타이틀 바에 기울기(Gradient) 효과를 준다거나, 한글 사용자에게는 별 의미 없긴 하지만 단축키를 키를 누르기 전까지 감추게 한다거나 하는 기능이 추가됐다. 그리고 윈도우의 색깔 등의 지정된 값을 제대로 가져다가 표시해준다. 파일 선택자(File chooser)와 UNC도 지원하고 모양도 동일하게 지원하게 되었다. 메뉴가 부드럽게 열리고 닫히는 기능도 포함된다.

XML 프로세싱

자바는 XML에 많은 관심이 있다. 바로 XML이 포터블 데이터를 가능하게 해주기 때문인데 자바의 특징인 포터블 코드 특성과 함께 강력한 솔루션을 구성할 수 있다. 이번 JDK 1.4에 추가된 XML 관련 API는 현재 진행되고 있는 몇 개의 XML 관련 API 중 하나인 JAXP 1.1(Java API for XML Processing)이다(이 API는 이미 J2EE 1.3 베타에도 포함돼 있다). 다른 XML 관련 API도 있는데 이중에서 JDK 1.4 정식버전에 더 추가될 가능성이 있는 것도 있지만 워낙 관련 표준이 빠르게 발전하기 때문에 외부추가 라이브러리로 공급될 가능성이 더 높다고 한다. JAXP는 자바에서 XML을 처리하기 위해 DOM과 SAX, XSLT를 지원한다.

DOM(Document Object Model)은 W3C에서 만든 문서객체 모델로 문서를 구성하는 요소를 정의해 문서 처리를 가능하게 만든 것이다. 이렇게 만들면 모두 메모리에 모델링돼 있기 때문에 문서의 여기 저기를 자유자재로 다룰 수 있다는 장점이 있는 반면에, 메모리와 처리시간이 많이 든다는 단점이 있다.

DOM은 레벨 2가 나와 있으며 레벨 3이 한창 만들어지고 있는데 JAXP는 레벨 2까지 지원한다. org.w3c.dom 패키지를 통해 제공한다.

SAX(Simple API for XML)는 XML을 처리하기 위한 API를 정의하는 것은 DOM과 같지만 그 접근방법이 완전히 다르다. DOM이 문서의 형태를 모두 파악하고 나서 문서객체를 다루는 것인데 반해 SAX는 문서를 처음부터 하나 하나 읽어 나가면서 어떤 문서요소가 있는지를 이벤트 객체를 통해 알려준다. 그래서 SAX를 Event-based(혹은, Event-driven, Callback-driven) XML 파싱 API라고 하며, DOM을 트리 기반(Tree-based)이라고 말하기도 한다.

이런 SAX의 장점은 빠르게 처리할 수 있다는 점이 있는 반면에 일일이 문서의 요소를 모델링해야 하기 때문에 전반적인 요소를 다루는 데는 어려움이 따른다. SAX 2.0이 나와있고 JAXP는 이를 지원한다. SAX는 org.xml.sax 패키지를 통해 제공된다.

XSLT(XSL Transformations)는 XML 문서를 다른 XML 문서나 그밖의 형태의 문서로 바꾸기 위해 사용된다. javax.xml.transform 패키지를 통해서 제공된다.

앞서 얘기한 DOM과 SAX를 지원하기 위한 파서는 javax.xml.parsers 패키지에서 제공되는 기본내장 파서가 있다. 하지만 이것만 사용할 수 있는 것은 아니다. JAXP는 Plugability 레이어를 갖고 있는데 앞서 말한 javax.xml.parsers 패키지는 사실 이 레이어를 위한 패키지다. 이 레이어만 지원한다면 어떠한 파서라도 가져다 기본내장 파서를 대신해서 쓸 수 있는데, 기존의 javax.xml.parsers 패키지를 이용해 개발한 코드는 전혀 고칠 필요가 없음은 당연한 얘기다. XSLT도 마찬가지로 Plugability 레이어만 갖고 있다면 다른 것을 가져다 쓸 수 있으며 코드도 고칠 필요가 없다.

DOM

DOM 파서를 쓰는 방법부터 간단하게 알아보자. 먼저 다음 두 개의 패키지를 임포트한다.

import javax.xml.parsers.*;

import org.w3c.dom.*;

DocumentBuilderFactory를 만든다. 이 객체는 지정된 DOM 파서로 DocumentBuilder를 만들기 위해 필요한데 자세한 것은 나중에 Plugability Layer를 얘기할 때 다루겠다.

DocumentBuilderFactory factory=DocumentBuilderFactory.newInstance();

DocumentBuilderFactory 객체를 만들었으면 여기에서 DocumentBuilder 객체를 만든다.

DocumentBuilder builder=factory.newDocumentBuilder();

DocumentBuilder 객체를 만들었으면 parse() 메쏘드를 이용해서 XML 파일을 파싱하면 된다. 다음 예제는 WML 파일을 파싱하게 했는데 WML 파일은 모바일 기기에서 사용되는 XML 애플리케이션 파일로 이 역시 XML 파일이므로 XML 파서로 파싱할 수 있다.

Document doc=builder.parse(http://my.netian.com/~handan/test.wml);

XML 데이터의 입력을 위해 전체 URL을 적어줘도 되고 InputStream 객체를 주거나 파일 객체를 줄 수도 있다. 결과로 나오는 도규먼트 객체는 org.w3c.dom.Document 객체로 DOM을 다루기 위해 가장 기본이 되는 객체이다.

다음은 이 도규먼트 객체에서 루트 엘리먼트와 노드 등을 얻는 예제다. DOMTest.java라는 소스코드를 첨부했으니 참고하기 바란다.

Element rootElement=doc.getDocumentElement();

Node node=rootElement.getFirstChild();

NodeList list=doc.getElementsByTagName(card);

SAX

이제 SAX(Simple API for XML)를 쓰는 법을 알아보자. SAX는 이벤트 드리븐(Event-driven)이라고 했으니 이벤트를 받는 객체가 필요할 것이다. 이런 역할을 하는 것이 핸들러 계열 클래스다. 소스코드를 살펴보자. 다음 두 개의 패키지를 임포트해야 한다.

import java.xml.parsers.*;

import org.xml.sax.*;

DOM 때와 마찬가지로 SAXParserFactory 객체를 만든다.

SAXParserFactory factory=SAXParserFactory.newInstance();

SAXParser 객체를 만든다.

SAXParser parser=factory.newSAXParser();

이렇게 만들어진 SAXPaser의 parse() 메쏘드를 이용해 파싱하면 된다.

parser.parse(http://my.netian.com/~handan/test.wml, handler);

이때 이벤트를 받을 핸들러를 지정해주는데 이 객체는 DefaultHandler라는 클래스로 만든다. 이 클래스는 다음의 인터페이스를 기본적으로 구현하고 있으며 필요에 따라 상속받아 부분적으로 메쏘드를 오버라이딩 해주면 된다. 그 인터페이스들은 다음과 같다.

DTDHandler, ContentHandler, ErrorHandler

DOM과 SAX를 따로 다루기는 했지만 내부적으로는 DOM도 SAX 파서를 통해 전체문서를 파싱한 다음 도규먼트 객체를 만들어낸다.

XSLT

이제 XSLT를 다루는 법을 알아보자. XSLT는 XML로 된 소스를 만들어 원하는 형식의 Result를 규정한 뒤, Transformer.transform() 메쏘드를 이용해 변환하는 것이 기본형식이다. DOM을 화면에 출력하는 다음 예제소스를 보자. XSLT를 위해 필요한 패키지를 임포트한다.

import javax.xml.transform.*;

다음은 DOM을 위한 작업이다.

DocumentBuilder builder=factory.newDocumentBuilder();

document=builder.parse(http://my.netian.com/~handan/test.wml);

이제 TransformerFactory를 만들고 Transformer 객체를 만든다.

TransformerFactory factory=TransformerFactory.newInstance();

Transformer transformer=factory.newTransformer();

DOM이 입력이고 화면출력이 결과이니 다음과 같이 소스와 Result를 만든다.

DOMSource source=new DOMSource(document);

StreamResult result=new StreamResult(System.out);

이어서 변환을 실행한다.

transformer.transform(source, result);

실제로 이 내용이 문서를 변환하거나 하지는 않는다. 이를 위해서는 XSL까지 설명해야 하는데 이는 이 글의 범위를 벗어나므로, 관심있는 독자는 XSL을 공부해 직접 테스트해 보기 바란다.

XML에 대한 기본이해가 없는 독자는 읽기가 힘들 것이다. 그런 독자는 썬에서 제공하는 XML 튜토리얼도 있으니 http://java.sun.com/xml/jaxp-1.1/docs/tutorial/index.html을 찾아가 보기 바란다.

Misc

쓰레드

쓰레드에 괜찮은 메쏘드 하나가 추가됐다. 쓰레드의 static 메쏘드인데 다음을 보자.

public static boolean holdsLock(Object obj)

이 메쏘드에 인자로 객체를 넣어서 호출하면 호출하는 쓰레드가 인자로 넘어온 객체의 유일무이하게 락(lock)을 갖고 있는지 아닐지의 여부를 불린 값으로 리턴해 준다. 이것의 장점은 현재 쓰레드가 이미 락을 갖고 있는지 아닌지를 assert 해볼 수 있다는 것이다.

Reflection

java.lang.reflect 패키지는 객체에서 객체의 클래스에 대한 정보를 얻어오는 패키지다. 이 패키지의 전반적인 성능이 좋아졌는데 특히 java.lang.reflect.Field, java.lang.reflect.Method.invoke(), java.lang.reflect.Constructor.newInstance(), java.lang.reflect.Class.newInstance()의 경우 코드를 완전히 새로 만들면서 성능이 몇 배정도 향상됐다.

소수

BigInteger에 소수를 구하는 메쏘드가 추가됐다. 가능한 수의 비트 범위와 난수를 지정해주면 비트 범위내에서 가능한 소수 중 하나를 리턴해주는 메쏘드다. 이 메쏘드의 프로토타입은 다음과 같다.

public static BigInteger probablePrime(int bitLength, Random rnd)

bitLength는 비트 범위로 예를 들어 5를 주면 이진수로 10000에서 11111 즉, 최소 17에서 최대 31까지의 범위에서 소수를 찾아 리턴한다. 이때, 난수를 발생시키기 위한 랜덤 객체도 필요하다. 다음은 probablePrime() 메쏘드 사용 예다.

import java.math.*;

import java.util.*;

public class BigIntegerTest

{

public static void main(String[] args)

{

Random rd=new Random(System.currentTimeMillis());

for (int i=0; i<10; i++)

{

BigInteger bi=BigInteger.probablePrime(7, rd);

System.out.println(bi);

}

}

}

마치면서

이번호에서는 새로운 기능을 중심으로 JDK 1.4를 소개했다. 하지만 워낙 방대한 양이라 다음호에도 이어서 소개해야 할 것이다. 다음호에는 New I/O, 시큐리티, 자바 2D, 이미지 I/O, 자바 프린트 서비스, JDBC, 네트워크 등에 대해 살펴볼 것이다. 여름이 가고 가을이 올 무렵 또 한 번의 베타 릴리즈가 기다리고 있다고 한다. 아마 이 글이 끝나는 9월쯤이 되면 베타 2가 나올 모양이니 또다시 새로워진 JDK 1.4 베타 2를 맞이할 준비를 해야 할 것이다. @