'Programing/Java - J2SE'에 해당되는 글 9건
- 2006.01.10 DEQUES
- 2006.01.10 필터링 JLIST 모델
- 2005.11.10 J2ME Mobile 2D Graphics 시작하기
- 2003.09.09 AFFINETRANSFORM 이해하기
- 2003.09.09 SOCKETCHANNELS을 이용해서 작업하기
- 2003.08.19 클래스의 UNLOADING 과 RELOADING
- 2003.08.19 VARIABLE CONTENT로 메세지 포맷하기
- 2003.08.05 SWING COMPONENTS의 저장과 재구성
- 2003.08.05 MIDI SOUND 생성하기
DEQUES
![]() | ||||||||
| ||||||||
Deque는 double-ended queues의 줄임말이다(디큐가 아니라 데크로 발음함). 큐는 한쪽 엔드에서 추가하고 다른 쪽 엔드에서 제거할 수 있는 반면, 양쪽 엔드에서 추가와 제거가 가능한 double-ended queues는 스택과 큐가 결합된 것처럼 작동한다. Deque 인터페이스는 J2SE 5.0에 도입된 Queue 인터페이스에서 확장되는데, 이 기능은 최근에 Java SE 6의 Java Collections Framework에 추가되었다(이 기능이 최종적으로 포함되려면 JCP의 승인을 받아야 함). 인터페이스 구현에는 LinkedList, ArrayDeque와 이에 수반되는 LinkedBlockingDeque가 포함된다.
LinkedList는 아마도 deque의 가장 전형적인 용례일 것이다. LinkedList는 제한 없이 확장이 가능하고 양쪽 엔드에서 신속하게 추가 및 제거 작업이 가능하다. ArrayDeque는 용량 제한이 없는 또 하나의 전형적인 구현으로, 성능을 최상으로 유지하기 위한 wraparound index 구현을 제공한다. 모든 베이스 컬렉션 구현이 그렇듯이, 어느 쪽도 threadsafe하지 않다. (Vector나 Hashtable 같은 역사적인 컬렉션은 threadsafe하지만 고도의 동시발생적인 액세스를 위해 설계되지는 않았다.) 쓰레드 안전이 필요한 경우에는 LinkedBlockingDeque를 이용하면 되는데, LinkedBlockingDeque 클래스는 Deque에서 확장되는 새로운 BlockingDeque 인터페이스를 구현한다. 이 클래스는 사이즈가 제한되거나 제한되지 않을 수도 있다. 용량이 지정되지 않은 경우, 사이즈 제한은 Integer.MAX_VALUE이다.
다음 메소드 중 하나로 deque에 엘리먼트를 추가할 수 있다.
void addFirst(E e)
void addLast(E e)
boolean add(E e)
add() 메소드는 addLast()와 동등하다고 볼 수 있다. deque의 용량이 부족하면 IllegalStateException이 throw된다. 또한 다음 메소드 중 하나를 통해 추가될 엘리먼트를 제공할 수도 있다.
boolean offer(E e)
boolean offerFirst(E e),
boolean offerLast(E e)
addXXX() 메소드로 엘리먼트를 추가하는 경우와 달리, offerXXX() 메소드가 제공되었을 때 항목을 추가할 수 없으면 메소드는 false를 반환한다.
이 외에도 엘리먼트 제거를 위한 한 쌍의 메소드 세트가 있다.
remove(), removeFirst(), and removeLast()
poll(), pollFirst(), and pollLast()
deque가 비어있을 경우 removeXXX() 메소드는 NoSuchElementException를 throw하고, pollXXX() 메소드는 null을 반환한다. 또한, 아래의 메소드 중 한 가지를 사용하여 특정 개체를 제거할 수 있다(deque가 엔드에서의 추가/제거의 용도로만 의도된 경우라도).
boolean remove(Object o)
boolean removeFirstOccurrence(Object o)
boolean removeLastOccurrence(Object o),
Deque는 엘리먼트를 검사하기 위한 6개의 메소드를 가진다.
element()
getFirst()
getLast()
peek()
peekFirst()
peekLast()
element()는 오래된 Queue 인터페이스로부터 상속한 인터페이스 메소드이기 때문에 get() 메소드가 없다. get 메소드는 removeXXX()와 유사하며 deque가 비어있는 경우 NoSuchElementException을 throw하는데, 이와 대조적으로 peek 메소드는 비어있는 경우 null을 반환한다. 이는 물론 Deque가 null 값의 추가를 허용할 경우 deque의 엔드에 있는 null 항목과 ‘nobody on deck’ 간의 차이를 알 수 없다는 것을 의미한다. 그러나 이 경우에는 size() 메소드를 활용할 수 있다.
Deque는 개념상 이중으로 링크되므로, 어떤 순서로도 엘리먼트들을 traverse할 수 있다. 앞에서 뒤로 traverse하려면 iterator()를 이용하고 역순, 즉 뒤에서 앞으로 traverse하려면 descendingIterator()를 이용한다. 하지만 위치별로 엘리먼트에 액세스할 수는 없다?적어도 Deque 인터페이스로는 불가능하다. LinkedList는 Deque의 구현이지만 함께 구현되는 List 인터페이스를 통해 색인 액세스(indexed access)를 지원한다. 랜덤 액세스 요건이 없다면 Deque 구현이 더 효과적으로 이루어질 수 있다.
왜 deque를 사용하는 것일까? deque는 maze 또는 parsing 소스를 통한 검색과 같은 반복적인 문제에 특히 유용한 데이터 구조로서, path를 따라 이동하면서(path가 양호하다고 판단되는 한) "good" spot을 저장하고 계속해서 데이터를 추가할 수 있다. path가 bad를 반환할 경우에는 bad 비트를 pop off하여 마지막 good spot으로 복귀한다. 이 경우 스택과 같은 동일한 엔드에서 추가와 제거를 수행하게 된다. 일단 길을 찾으면 처음부터 다시 시작하여 반대쪽 엔드에 해당하는 솔루션을 밝혀낸다. 또 다른 전형적인 예로, 운영체제 스케줄러, 그리고 사람들을 속이기를 좋아하는 악질 카드 딜러 등을 들 수 있을 것이다.
다음 프로그램 Blocked는 Deque의 용례, 보다 구체적으로는 용량 제한이 있는 LinkedBlockingDeque를 보여주고 있다. 이는 물론 최상의 deque 용례는 아니지만 API와 용량 제한에 도달했을 때의 상황을 예시해준다. 프로그램은 23개의 달 이름(짧은 이름과 긴 이름 모두)을 취하여 이를 6-엘리먼트 블로킹 deque의 헤드에 한번에 하나씩 추가한다. 또 다른 스레드에서는, 현재 컬렉션 내에 있는 엘리먼트의 수를 토대로 엘리먼트를 deque의 헤드와 테일에서 제거한다.
import java.io.*;
import java.util.*;
import java.util.concurrent.*;
public class Blocked {
public static void main(String args[]) {
Calendar now = Calendar.getInstance();
Locale locale = Locale.getDefault();
final Console console = System.console();
final Map
Calendar.MONTH, Calendar.ALL_STYLES, locale);
console.printf("Starting names: %s%n", names);
final Deque
new LinkedBlockingDeque
// Add one at time to beginning of deque
new Thread() {
public void run() {
Set
Iterator
String element = null;
while (itor.hasNext() || element != null) {
if (element == null) {
element = itor.next();
console.printf("MapGot: %s%n", element);
}
console.printf("Offering: %s%n", element);
if (deque.offerFirst(element)) {
console.printf("MapRemoving: %s%n", element);
itor.remove();
element = null;
} else {
try {
Thread.sleep(250);
} catch (InterruptedException ignored) {
}
}
}
// Done. Give time to process rest.
try {
Thread.sleep(3500);
} catch (InterruptedException ignored) {
}
System.exit(0);
}
}.start();
while (true) {
if ((deque.size() % 2 == 1)) {
// remove head
console.printf(
"Remove head: %s%n", deque.pollFirst());
} else {
// remove tail
console.printf(
"Remove tail: %s%n", deque.pollLast());
}
// Sleep between loops
try {
Thread.sleep(500);
} catch (InterruptedException ignored) {
}
}
}
}
아래에서 보듯이, 프로그램 실행 시 printf 선언문 때문에 많은 아웃풋이 생성된다. 엘리먼트를 소스 맵에서 가져오거나, 소스 맵에서 제거하거나, deque에 제공하거나, deque에서 제거할 때마다 아웃풋 행이 생성된다. deque가 full 상태일 때 어떻게 제공(offering) 동작이 복수로 발생하는지 유의할 것.
>> java Blocked
Starting names: {Jun=5, March=2, December=11, April=3,
November=10, September=8, October=9, Sep=8, Aug=7, Apr=3,
May=4, June=5, Feb=1, Dec=11, Oct=9, Jan=0, Mar=2, Jul=6,
August=7, January=0, February=1, July=6, Nov=10}
Remove tail: null
MapGot: Jun
Offering: Jun
MapRemoving: Jun
MapGot: March
Offering: March
MapRemoving: March
...
Remove tail: Jul
Remove head: Nov
Remove tail: August
Remove head: July
Remove tail: January
Remove head: February
Remove tail: null
여기서 두 가지 사실에 주목할 필요가 있다. 첫째, 24개가 아닌 23개의 짧고 긴 달 이름이 있다(5월인 "May"의 경우에는 짧은 이름과 긴 이름 모두에 해당하기 때문에 23개가 된다). getDisplayNames() 메소드는 Map을 반환하므로 "May"는 짧은 이름과 긴 이름 등 2개 엔트리에 대한 key가 될 수 없다. 둘째는, 한쪽 엔드에서 추가하고 다른 쪽 엔드에서 제거하는 것이 작업의 전부라면 Deque 대신 컬렉션 프레임워크에서 Queue 구현을 이용하는 편이 더 나을 것이라는 점이다.
최소한, STL(Standard Template Library)에서의 유용한 deque/vector 비교 자료를 보려면 An In-Depth Study of the STLDeque Container를 참조할 것(여기서는 두 컨테이너 타입의 성능 차이에 대해 다루고 있음). 자바 플랫폼 상의 실제 수에는 차이가 있지만 전반적인 개념은 나름대로 정확하다고 볼 수 있다.
필터링 JLIST 모델
![]() | ||||||||
| ||||||||
2005년 12월 20일자 테크 팁 테이블 정렬 및 필터링에서는 Java SE 6의 새로운 기능을 이용하여
JTable
을 정렬하고 필터링을 하는 방법을 살펴보았다. Java SE 6은 JList
정렬과 필터링을 위한 새로운 기능을 제공하지는 않지만 본 팁에서는 J2SE 5.0에서 JList
에 대한 유사한 작업을 수행하는 방법을 배우게 될 것이다.사용자에게 긴 리스트에서 엘리먼트를 필터링하도록 프롬프트하는 데 일반적으로 사용되는 기법으로는 리스트에
JTextField
를 표시하는 경우를 들 수 있다. 사용자가 JTextField
에 입력을 하면 리스트에 표시된 엘리먼트가 일련의 매칭 엔트리로 단축된다.
JList
를 위한 이런 종류의 기능을 구현하려면 일부 텍스트를 토대로 엘리먼트를 필터링하는 모델과 사용자가 필드에 입력할 때 필터링 동작을 트리거하는 텍스트 컴포넌트 등의 두 가지 지원 엘리먼트가 필요하다. 둘 중에서 인풋 필드를 구현하는 쪽이 더 쉬우므로 이를 먼저 살펴보기로 한다.
Swing
컴포넌트 세트에서 JTextField
를 위한 모델은 Document
이다. Document
에 대한 인풋을 모니터하려면 모델에 DocumentListener
를 첨부한다. 이 리스너에는 이벤트의 인풋, 제거, 변경에 대해 서로 다르게 반응할 수 있도록 해주는 다음의 세 가지가 메소드가 있다.public void insertUpdate(DocumentEvent event)
public void removeUpdate(DocumentEvent event)
public void changedUpdate(DocumentEvent event)
changeUpdate()
메소드는 모델 내의 속성 변경과 관련이 있는데, 이는 무시해도 무방하다. 다른 두 메소드에 대해서 동일한 필터링 동작이 이루어져야 하기 때문에, 이들은 커스텀 모델에서 생성되어야 할 동일한 메소드를 호출해야 한다. 다음은 필터링 JList
에 연결되어야 할 JTextField
를 정의한 것이다.JTextField input = new JTextField();
String lastSearch = "";
DocumentListener listener = new DocumentListener() {
public void insertUpdate(DocumentEvent event) {
Document doc = event.getDocument();
lastSearch = doc.getText(0, doc.getLength());
((FilteringModel)getModel()).filter(lastSearch);
}
public void removeUpdate(DocumentEvent event) {
Document doc = event.getDocument();
lastSearch = doc.getText(0, doc.getLength());
((FilteringModel)getModel()).filter(lastSearch);
}
public void changedUpdate(DocumentEvent event) {
}
};
input.getDocument().addDocumentListener(listener);
JList에 의해 생성되는 JTextField에 사용을 제한하는 대신, 리스너를 주어진 컴포넌트에 연결하는 installJTextField() 메소드와 리스너를 제거하기 위한 언인스톨 메소드가 제공된다. 이 경우, 필터링 리스트 사용자는 기본값을 생성하는 대신 자체의 JTextField를 제공할 수 있게 된다.
public void installJTextField(JTextField input) {
input.getDocument().addDocumentListener(listener);
}
public void unnstallJTextField(JTextField input) {
input.getDocument().removeDocumentListener(listener);
}
다음으로 필터링 모델에서는 DocumentListener에 의해 filter() 메소드가 요구되는데, 이 경우에는 단지 소스 리스트와 필터링된 리스트의 2개 엘리먼트 리스트를 유지하면 된다. AbstractListModel의 도움으로 다음과 같은 메소드를 구현해야 한다.
- 생성자(constructor)
- 엘리먼트를 모델에 추가하기 위한 추가 메소드
- 사이즈를 얻기 위한
getSize()
- 엘리먼트를 다시 얻기 위한
getElementAt()
생성자는 2개의 List 오브젝트를 생성한다. List 내에 어떤 엘리먼트가 있는지는 문제가 되지 않으므로 다음과 같이 Object 타입의 List로 생성하면 된다.
List
J2ME Mobile 2D Graphics 시작하기
모바일 애플리케이션 개발자들에게 있어서 벡터 그래픽은 컴팩트하면서도 확장성이 높다는 두 가지 큰 이점을 가지고 있다. 먼저 아래의 간단한 이미지를 살펴보자.

GIF 포맷인 경우 이 이미지에는 7,386바이트가 사용되는 반면, 벡터 포맷인 경우에는 사이즈가 693바이트로 전자 경우의 1/10에 불과하다. 그렇다면 이런 축소는 어떻게 가능한 것일까?
GIF 같은 래스터(Raster) 기반 이미지 포맷은 이미지를 구성하는 직사각형 영역의 각 픽셀에 대한 색상 정보를 인코딩하지만 벡터 기반 이미지에서는 픽셀의 색상을 결정하는 드로잉 명령만 포함된다. 이미지의 벡터 표현은 훨씬 더 컴팩트하기 때문에 리소스의 제약을 받는 모바일 장치에 상당히 유리하다.
확장성은 또 하나의 중요한 이점으로, 벡터 이미지는 드로잉 명령어가 해상도의 영향을 받지 않기 때문에 매끄럽게 변환될 수 있다. 그림 2는 앞의 이미지를 축소하고, 뒤집어(flip) 회전시킨 것이다.

벡터 이미지의 측정 단위는 추상적(arbitrary)이고 상대적이며, 몇 가지 상수 요소로 곱하거나 나누어 이미지를 화면 크기에 맞도록 변환할 수 있다. 즉, 좌표에 간단한 수학적 연산을 적용함으로써 뒤집기, 회전, 확대, 기울이기(skewing) 등을 포함한 거의 무한한 효과를 낼 수 있다. 또한, 애플리케이션 개발자가 벡터 그래픽을 이용할 경우 화면의 해상도가 서로 다른 장치를 지원하기 위해 별도의 이미지 및 아이콘 세트를 생성할 필요가 없다.
벡터 그래픽의 간결성과 확장성은 애니메이션 분야에서 특히 빛을 발한다. 래스터 기반 애니메이션의 경우에는 웹 페이지의 애니메이션 GIF나 MIDP 게임의 애니메이션 스프라이트처럼 애니메이션의 각 프레임마다 수고스럽게 전체 이미지를 포함시켜야 하지만 벡터 기반 애니메이션에는 변화될 특정 이미지 요소와 그 시점을 지시하는 명령어만 포함하면 된다. 벡터 이미지가 래스터 이미지보다 한 등급 작다면, 결국 벡터 애니메이션이 래스터 애니메이션보다 두 등급이 더 작은 것이기 때문에 모바일 장치에서 멀티미디어 컨텐츠를 전송하고 표시하는 데 이상적이다.
특허권이 있는 Macromedia Flash 플레이어를 기반으로 한 벡터 그래픽과 애니메이션이 웹 상에서 인기를 얻었다면, SVG(Scalable Vector Graphics) 파일 포맷은 그 밖의 분야에서 개방 표준에 특허권이 없는 대안으로 부상한 바 있다. 이 포맷은 W3C(World Wide Web Consortium)에 의해 표준화되며, SVG-Tiny는 SVG의 전체 기능 중에서 모바일 장치에 맞게 조정된 일부 기능을 제공한다. JSR 226은 J2ME 벡터 그래픽을 위한 공식 파일 포맷으로 SVG-Tiny 버전 1.1을 채택하고 있다. W3C는 SVG-Tiny를 ‘프로파일’이라고 부르지만 여기서는 J2ME 프로파일과의 혼동을 피하기 위해 ‘포맷’이라는 용어를 사용하고자 한다.
SVG-Tiny 표준은 또한 애니메이션을 지원하는 구현을 가능케 해준다. 이미지 파일 자체에 내장된 명령어는 타이머 이벤트와 사용자 인풋에 응답하여 이미지 요소들의 위치와 속성을 수정할 수 있다.
좀더 복잡한 상호작용을 위해, JSR 226은 SVG 1.2의 풀 DOM(Document Object Model)의 서브세트인 microDOM과 호환이 되는 API를 채택하는데, 자바 기반 프로그램은 이 API를 이용하여 벡터 이미지 및 애니메이션을 즉석에서 생성/수정할 수 있다.
SVG는 public schema로 잘 문서화되고 코드화된 XML(eXtensible Markup Language) 포맷이며, SVG-Tiny 파일은 작기는 하지만 여전히 SVG 파일이므로 당연히 XML을 포함하고 있다. 다음은 그림 1 ‘brave world’ 이미지를 풀 SVG-Tiny로 표현한 것이다.
<?xml version="1.0" standalone="no"?> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1 Tiny//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11-tiny.dtd"> <svg preserveAspectRatio="xMidYMid meet" viewbox="10 10 130 55" width="150.0" height="75.0"> <rect x="25" y="25" transform="translate(75 36.375) rotate(15) translate(-70 -35.375)" fill="#00007E" width="100" height="22.75" stroke="#000000" stroke-width=".5"/> <rect x="25" y="25" fill="#FF9800" width="108.5" height="22.75" stroke="#000000" stroke-width=".5"/> <text x="30" y="40" fill="#00007E" stroke="#FFFFFF" stroke-width=".33" xml:space="preserve">Hello brave world.</text> </svg>
원본 XML 텍스트는 그다지 컴팩트한 포맷은 아니지만 이 문서는 여전히 압축된 래스터 표현보다도 훨씬 작을 뿐 아니라 육안 판독도 가능하다. XML 텍스트는 압축이 잘 되기 때문에 SVG 문서는 애플리케이션의 JAR 파일에서 상대적으로 작은 공간을 차지한다. JSR 226은 gzip으로 압축된 SVG 문서를 지원하는 구현을 필요로 하며, 확장자 .svgz는 gzip 압축문서를 나타낸다.
Graphics2D
클래스가 제공하는 것과 같은
일반 2D 드로잉 툴킷으로 보기는 어렵다. 분명한 것은 M2G API가 SVG 컨텐츠의 재생과 런타임 조작에 초점을 맞추고 있다는 점이다.
M2G는 벡터 이미지 생성 및 렌더링을 위한 하이 레벨 클래스와 벡터 이미지의 XML 컴포넌트를 DOM(Document Object Model) 트리의 일부로 조작하기 위한 로우 레벨 클래스로 구성된다. 벡터 이미지는
ScalableImage
의 인스턴스로, 여러분은 이 인스턴스를 해당 클래스의 정적
createImage()
메소드로부터 얻고, ScalableGraphics
의 인스턴스를 이용하여
MIDP Graphics 컨텍스트 상에 ScalableImages
를 그릴 수 있다.
SVGImage
는 이벤트 핸들링을 위한 후크(hooks)와 기본적인 DOM 문서에 대한 액세스를 제공하는
ScalableImage
의 서브클래스이다. SVGAnimator
클래스는 애니메이션
SVG 컨텐츠를 렌더링하는 커먼 케이스(common case)를 더 편리하게 해준다. MIDP 애플리케이션의 경우,
SVGAnimator
는 애니메이션 이벤트에 응답하는 화면 업데이트와 이미지에 대한 프로그램적 수정을 자동으로 처리하는
Canvas 오브젝트를 생성?제어하고, 아울러 애니메이션의 재생을 제어하기 위해 비디오 플레이어와 같은 인터페이스를 제공한다. 이 클래스들은 표 1에 요약한
javax.microedition.m2g
및
org.w3c.dom.svg
패키지에 정의되어 있으며 JSR
226 specification에서 상세히 다루고 있다. 한편, 표준 org.w3c.dom
및
org.w3c.dom.events
패키지에 정의된 기본 DOM 클래스와 인터페이스는 SVG DOM 클래스에 의해
확장된다.
Class or Interface |
Description | |||
---|---|---|---|---|
![]() | ||||
javax.microedition.m2g | ||||
ScalableGraphics |
2D 렌더링의 기본이 되는 클래스 | |||
SVGAnimator |
이 클래스는 SVGImage의 업데이트와 애니메이션을 대상 사용자 인터페이스 컴포넌트로 자동
렌더링한다. | |||
SVGEventListener |
이 인터페이스는 플랫폼별 이벤트를 애플리케이션으로 전송하는 데 사용된다. | |||
SVGImage |
이 클래스는 W3C SVG Tiny 1.1 Profile 에 따른 SVG 이미지를 나타낸다.
| |||
ExternalResourceHandler |
이 인터페이스는 SVG 컨텐츠 로드에 필요한 외부 리소스를 동기적으로 로드하는데
사용된다. | |||
org.w3c.dom.svg | ||||
SVGAnimationElement |
이 인터페이스는 애니메이션 요소를 나타내며 애니메이션의 타이밍을 제어하는 메소드를 포함한다 .
| |||
문서 트리의 SVG 요소를 나타낸다 . | ||||
SVGLocatableElement |
이 인터페이스는 외형 (shape), 이미지 , 텍스트 등의 그릴 수 있는 SVG 요소를 나타낸다 .
| |||
SVGMatrix |
이 인터페이스는 번역을 수반한 선형 변환에 해당하는 어파인 변환 (affine transformation)
a€“ 에 의해 식별되는 ‘ SVG matrix ’ 데이터 유형을 나타낸다 . | |||
SVGPath |
경로 지오메트리를 정의하는 데 사용되는 ‘ SVG path ’ 데이터 유형을 나타낸다 .
| |||
SVGPoint |
x 및 y 컴포넌트에 의해 식별되는 ‘ SVG point ’ 데이터 유형을 나타낸다 .
| |||
SVGRect |
최소 X, 최소 Y, 폭 , 높이 값으로 구성되는 ‘ SVG rectangle ’ 데이터 유형을 나타낸다 .
| |||
SVGRGBColor |
적 , 녹 , 청 컴포넌트로 구성되는 ‘ SVG RGB color ’ 데이터 유형을 나타낸다 .
| |||
SVGSVGElement |
SVG 문서 트리의 요소를 나타낸다 . | |||
![]() |
아울러, SVG-Tiny 컨텐츠가 필요하다. 이 글에서는 위의 ‘brave world’ 이미지를 사용하지만 모빌리티 개발 툴메이커 TinyLine에서 더 많은 SVG-Tiny 예제를 구할 수 있다. 여러분은 또한 Sketsa라 불리는 자바 기반의 상용 애플리케이션으로 직접 컨텐츠를 작성할 수도 있는데, 이 애플리케이션은 일러스트레이션 툴처럼 작동하며 SVG를 파일 포맷으로 사용한다.
SVG 컨텐츠를 화면에 표시하는 방법으로는 여러 가지가 있는데, 이중 가장 간단한 방법은 이미지에 대한
SVGAnimator
를 생성하고 그 Canvas를 화면에 표시하는 것이다. 또 다른 방법은
ScalableImage
인스턴스를 생성하고 ScalableGraphics
의 인스턴스를 이용하여
이를 자체 Canvas
또는 CustomItem
의 Graphics 컨텍스트 상에 드로잉하는
것이다. 이렇게 하면 프레젠테이션을 제어하기가 더 쉽다는 장점이 있지만 애니메이션 또는 사용자 인터랙션의 결과로 인풋 이벤트와
리페인트(repaints)를 처리해야 하는 책임이 따르게 된다. createAnimator()
메소드로부터 SVGAnimator
의 인스턴스를
획득하여 자신만의 SVGImage
를 제공할 수 있다. 애니메이터는 동적 컨텐츠를 위한 모든 사용자 인터랙션과 애니메이션을
처리하기 때문에 SVGAnimator
를 이용하는 것은 탁월한 선택이 될 수 있다. play()
,
pause()
, stop()
, setTimeIncrement()
메소드는
애니메이션의 재생을 프로그램적으로 제어할 수 있게 해주는데, 이 구현을 통해 동적 행동을 지시하기 위한 원시(native) 사용자 인터페이스를
제공할 수도 있다. 일단
SVGAnimator
를 획득하면 이를 애플리케이션에 끼워 넣기는 어렵지 않다.
getTargetComponent()
를 호출하면 화면에 표시할 수 있는 Canvas
의 인스턴스를
반환한다. 다음은 관련 예제이다. public void animationTest() { // create an animator to load the content SVGAnimator animator = SVGAnimator.createAnimator( image ); // add our custom event listener animator.setEventListener( new CustomEventListener( animator ) ); // get the Canvas for this player; requires a cast Canvas canvas = (Canvas) animator.getTargetComponent(); // add a "back" command canvas.addCommand( backCommand ); canvas.setCommandListener( this ); // show it Display.getDisplay(this).setCurrent( canvas ); // start it animator.play(); }
getTargetComponent()
로부터 반환되는 값은 MIDP Canvas인데, 비(非)
MIDP 환경에서는 이 결과가 native windowing 시스템에 적합할 것으로 사료된다(예: AWT 환경에서
Component
). 환경에 적합한 선택이 둘 이상인 경우에는 원하는 클래스의 이름을
createAnimator()
에 대한 두 번째 아규먼트로 제공하도록 한다. 그림 3은 폼이 화면에 표시된 모습인데, 구현 방식에 따라 그리고 스타일러스 또는 기타 포인팅 디바이스의 사용 여부에 따라 애니메이터는 사용자와 컨텐츠 간의 상호작용을 허용할 수도 있다. ‘brave world’ 예제의 경우, 동적 컨텐츠는 없지만 텍스트는 선택이 가능하다는 점에 유의한다.

SVGAnimator
에 의해 제어되는
Canvas
애니메이터와의 상호작용
JSR 226의 DOM 지원은 프로그램이 이미지의 구조를 변경할 수 있게 해준다.
SVGImage
도 다른 XML 문서와
마찬가지로 DOM 오브젝트 트리로 표현되며, M2G API는 친숙한 XML 조작 기법을 사용하여 이미지를 수정할 수 있게 해준다. 트리의 각
노드는 SVGElement
이며 인접한 부모 및 자식 노드로 네비게이트하기 위한 메소드를 제공한다. 노드를 수정하려면 자식
노드를 추가/제거하거나, 해당 요소 클래스에 캐스팅하고 그 클래스의 메소드를 호출하여 변경하면 된다.
SVGEventListener
인터페이스는 애플리케이션이 SVGAnimator
의
Canvas
로부터 사용자 인풋 이벤트를 수신할 수 있게 해준다. 다음 예제는 DOM API를 이용하여 사용자의 화살표
키를 통한 인풋에 응답하여 이미지를 확대/축소하고 회전시키는 경우이다. { int gameAction = canvas.getGameAction( keyCode ); switch ( gameAction ) { case Canvas.UP: scale( 0.10f ); break; case Canvas.LEFT: rotate( -10.0f ); break; case Canvas.DOWN: scale( -0.10f ); break; case Canvas.RIGHT: rotate( 10 ); break; default: // leave unchanged } } private void rotate( final float delta ) { // put ourselves on the animator's thread animator.invokeLater( new Runnable() { public void run() { // execute the transformation Document document = svgImage.getDocument(); SVGSVGElement root = (SVGSVGElement) document.getDocumentElement(); root.setCurrentRotate( root.getCurrentRotate() + delta ); } } ); } private void scale( final float delta ) { // put ourselves on the animator's thread animator.invokeLater( new Runnable() { public void run() { // execute the transformation Document document = svgImage.getDocument(); SVGSVGElement root = (SVGSVGElement) document.getDocumentElement(); root.setCurrentScale( root.getCurrentScale() + delta ); } } ); }
SVGImage.getDocument()
는 애니메이터가 렌더링하는 문서에 레퍼런스를 제공한다. 플레이어가
애니메이션을 실행하거나 다른 동적 컨텐츠를 업데이트 중일 수도 있기 때문에, 변경내용을 애니메이션 쓰레드와 동기화할 때 주의하지 않으면 예기치
않은 장애가 발생할 수도 있다. SVGAnimator
's invokeAndWait()
및
invokeLater()
메소드는 애니메이션 쓰레드 상에서 코드가 실행되도록 하기 위해 AWT의 경우와 비슷하게
작동한다. ScalableGraphics
클래스를 이용하여
Canvas
, Layer
, 또는 CustomItem
인스턴스의
paint()
메소드로 전달된 Graphics 오브젝트 상에 직접 ScalableImage
를
드로잉할 수 있다. 정적
createInstance()
메소드를 인보크하여
ScalableGraphics
의 인스턴스를 생성한다. 드로잉 전에 먼저 bindTarget()
메소드를 이용하여 확장 그래픽스 인스턴스를 페인트 메소드로 전달된 그래픽스 오브젝트에 바인딩해야 한다. 작업이 완료된 후에는
releaseTarget()
을 호출하는 것을 잊지 않도록 하고, 일단 바인딩이 설정되면
render()
메소드를 이용하여 각각의 이미지를 드로잉하면 된다. 아래의 예제에서는
render()
에 대한 각 호출 전에 setViewportHeight()
및
setViewportWidth()
메소드를 호출하여 ‘brave worl’ 이미지를 다양한 크기로 반복해서 드로잉한다.
private static class M2GCanvas extends Canvas { // retain a reference the specified image ScalableImage scalableImage; // retain an instance of a scalable graphics ScalableGraphics scalableGraphics; public M2GCanvas( ScalableImage inImage ) { scalableImage = inImage; // create the scalable graphics instance scalableGraphics = ScalableGraphics.createInstance(); } public void paint( Graphics g ) { // clear the display g.setColor( 255, 255, 255 ); g.fillRect( 0, 0, getWidth(), getHeight() ); // bind our scalable graphics to the given graphics scalableGraphics.bindTarget( g ); // render at fixed position and size scalableImage.setViewportWidth( 50 ); scalableImage.setViewportHeight( 75 ); scalableGraphics.render( 5, 50, scalableImage ); // again at different position and size scalableImage.setViewportWidth( 100 ); scalableImage.setViewportHeight( 150 ); scalableGraphics.render( 80, 5, scalableImage ); // again at size that varies with the canvas size scalableImage.setViewportWidth( getWidth()-20 ); scalableImage.setViewportHeight( getHeight()-20 ); scalableGraphics.render( 0, 0, scalableImage ); // release the graphics context scalableGraphics.releaseTarget(); } }

ScalableImages
확대/축소하기 렌더링하고 있는 이미지가 동적인 경우에는 사용자와 이미지 간 상호작용을 허용해야 한다.
SVGAnimator
없이 렌더링하는 경우에는 사용자의 포인터 이벤트를 탐지, 이미지를 어느 위치에서 클릭하는지
확인하고, SVGImage
상의 dispatchMouseEvent()
를 호출하여 스크립트된 행동을
트리거해야 한다. CustomItem
의 경우에는 form traversal이 SVGImage
의
activate()
및 focusOn()
메소드를 이용하여 활성화를 허용하고 이미지 내의 요소들에
대한 네비게이션을 포커스해야 한다. 프로그램은 기존
SVGImage
의 DOM을 조작할 수 있을 뿐 아니라
스크래치로부터 이미지를 구축할 수도 있다. SVGImage
의 정적
createEmptyImage()
메소드는 비어있는 문서 뼈대를 가지는 이미지를 반ㅇ환하는데, 여기 상에 프로그램적으로
생성되는 외형과 변환(shapes and transformations)을 나타내는 SVGElements
를 바로
파퓰레이트할 수 있다. 한편, 이 기법을 ScalableGraphics
와 연계하여 사용하면 일반화된 2D 드로잉 툴킷의
효과를 얻을 수는 있지만, 앞에서 지적했듯이 이러한 용도로 활용하는 것을 권장하지는 않는다. AFFINETRANSFORM 이해하기
사용자가 유사변환(affine transformation)을 수행할 수 있도록 해주는
java.awt.geom.AffineTransform
클래스는 Java 2D 클래스에 속한다. 유사변환은 2차원 이미지의
좌표를 평행선이 평행을 유지하는 것과 같은 방법으로 변환한다. 가령, 사용자가 직사각형으로 시작하더라도, 유사변환을 이용해서 다른 위치에
평행사변형을 생성할 수가 있다. 이때에 라인은 라인을 남기면서 평행관계는 유지되게 된다. 유사변환을 이용하면 변환(translations),
회전(rotations), (플립을 포함한) 크기조정(scaling), 전단(shears)이 가능하다.
AffineTransform
의 documentation을 보면,
그러한 좌표변환은 마지막 행에 [0 0 1]을 수반하는 3행3열 매트릭스로 나타낼 수가 있다. 이 매트릭스는 원시좌표(source coordinates)와 목적좌표(destination coordinates)를 열벡터(column vector)로 만든 후 다음과 같이 매트릭스를 좌표벡터에 곱해가는 방법으로 원시좌표(x,y)를 목적좌표인 (x',y')로 변환한다. [ x'] [ m00 m01 m02 ] [ x ] [ m00x + m01y + m02 ] [ y'] = [ m10 m11 m12 ] [ y ] = [ m10x + m11y + m12 ] [ 1 ] [ 0 0 1 ] [ 1 ] [ 1 ]
이번 테크팁에서는 사용자가 2차원 공간의 유사변환을 표현하기 위해서 3행3열 매트릭스를 이용해야 하는 이유를 설명하면서, 기본변환을 위한
매트릭스를 AffineTransform
클래스에서의 함수호출과 연관시키고자 한다.
이 글에서는 사용자가 AffineChecker
라는 다음의 프로그램을 실행시키면서
AffineTransform
변환을 행렬의 곱(matrix multiplications)과 비교하게 될 것이다. 이
프로그램에서 Point2D.Double
의 3개 인스턴스 변수들을 잘 보아두자. 이 변수들은 (2,3)의 좌표를 갖는
시작점과 각각의 변환으로부터 생긴 점들에 해당한다.
import java.awt.geom.Point2D; import java.awt.geom.AffineTransform; public class AffineChecker { private static Point2D startingPoint = new Point2D.Double(2,3); private static Point2D affineResult = new Point2D.Double(); private static Point2D matrixResult = new Point2D.Double(); public static void matrixMultiply( double m00, double m01, double m10, double m11){ double x = m00 * startingPoint.getX() + m01 * startingPoint.getY(); double y = m10 * startingPoint.getX() + m11 * startingPoint.getY(); matrixResult.setLocation(x,y); } public static void perform(AffineTransform at){ at.transform(startingPoint,affineResult); } public static void report(){ System.out.println("Affine Result = (" + affineResult.getX() + " , " + affineResult.getY() + ")"); System.out.println("Matrix Result = (" + matrixResult.getX() + " , " + matrixResult.getY() + ")"); if (affineResult.distance(matrixResult)<.0001){ System.out.println(" No real difference"); } } }
AffineChecker
의 matrixMultiply()
메소드는 표준 행렬의
곱(standard matrix multiplication)을 보여준다.
[ m00 m01 ] [ x ] [ m00x + m01y] [ m10 m11 ] [ y ] = [ m10x + m11y]
이 프로그램의 report()
메소드는 변환의 결과와 행렬의 곱(matrix multiplication)의 결과값을
출력하고, 결과들이 본질적으로 같다고 볼 수 있을 만큼 충분히 가까운지를 비교한다.
첫번째 테스트로, 시작점에 아무것도 하지말고 비교를 해보자. 이를 위해 다음 프로그램을 돌려본다.
import java.awt.geom.AffineTransform; public class NothingChecker { public static void main(String[] args){ System.out.println( "The result of doing nothing:"); AffineChecker.perform( AffineTransform.getRotateInstance(0)); AffineChecker.matrixMultiply(1,0, 0,1); AffineChecker.report(); } }
NothingChecker
는 새로운 회전변환을 생성하고, 스태틱 메소드 호출을 이용해서 회전되기 위해서
각도(degree)를(이 예제의 경우엔 0) 넘겨준다.
AffineTransform.getRotateInstance(0)
그리고 나서 NothingChecker
는 AffineChecker
의
perform()
메소드를 통해 transform()
을 리턴된
AffineTransform
에 호출한다. 여기서 주목할 만한 것은 점(2,3)을 변환하고
affineResult
에 결과점(resulting point)을 저장하기 위해 startingPoint
과 affineResult
를 transform()
메소드에 넣어주고 있다는 것이다.
다음으로 NothingChecker
는 Affine Result
를 2행2열 항등행렬(identity
matrix)을 곱한 값과 비교한다.
[ 1 0 ] [ 0 1 ]
NothingChecker
을 실행시킬 때, 사용자는 이하의 내용을 보게 되고,
The result of doing nothing: Affine Result = (2.0 , 3.0) Matrix Result = (2.0 , 3.0) No real difference
사용자는 두 변환의 결과가 모두 점(2,3)이라는 것을 발견하게 된다.
x와 y방향으로의 크기조정은 AffineTransform
의 스태틱 메소드인
getScaleInstance()
을 이용한다. X방향으로 4, y 방향으로 5, 이렇게 두 요소를 삽입하여 크기조정을
해보자. 여기 크기조정의 방법을 보여주는 행렬의 곱(matrix multiplication)이 있다.
[ 4 0 ] [ x ] [ 4x ] [ 0 5 ] [ y ] = [ 5y ].
다음의 프로그램은 getScaleInstance()
호출을 보여주는데, 사용자는 변환과 행렬의 곱(matrix
multiplication)을 비교하기 위해 다음 프로그램을 실행할 수 있다.
import java.awt.geom.AffineTransform; public class Scale4Xand5YChecker { public static void main(String[] args) { System.out.println( "The results of scaling four " + "times in the x direction " + "and five times in the y:"); AffineChecker.perform( AffineTransform.getScaleInstance(4,5)); AffineChecker.matrixMultiply(4,0, 0,5); AffineChecker.report(); } }
결과는 다음과 같다.
The results of scaling four times in the x direction and five times in the y: Affine Result = (8.0 , 15.0) Matrix Result = (8.0 , 15.0) No real difference
결과값이 또 다시 같은 것을 볼 수 있다.
사용자는 크기조정 계수 중의 하나에 음수를 넣어봄으로써 x와 y축에 대해서 이해할 수가 있는데, 예를 들면,
[ -1 0 ] [ x ] [ -x ] [ 0 1 ] [ y ] = [ y ]
여기에서 x방향의 값이 바뀐 것을 볼 수 있다. 밑의 프로그램에서는 위의 값을 넣어 수행하고 그것을 행렬의 곱(matrix multiplication)과 비교하고 있다.
import java.awt.geom.AffineTransform; public class FlipChecker { public static void main(String[] args) { System.out.println( "The results of flipping over" + "the y-axis:"); AffineChecker.perform( AffineTransform.getScaleInstance(-1,1)); AffineChecker.matrixMultiply(-1, 0, 0, 1); AffineChecker.report(); } }
결과는,
The results of flipping over the y-axis: Affine Result = (-2.0 , 3.0) Matrix Result = (-2.0 , 3.0) No real difference
회전을 시키기 위해선 디그리(degree)가 아닌 라디안(radian)인 인수(argument)를 넣어 줘야 한다. 물론 이미 알고 있겠지만, 디그리는 pi를 곱하고 이것을 180디그리로 나눔으로써 라디안으로 쉽게 전환할 수가 있다. 예를 들면 30도는 pi/6과 같고, Pi/6라디안만큼 회전시키기 위해서는 다음과 같은 회전행렬 (rotation matrix)을 곱한다.
[ cos pi/6 -sin pi/6 ] [ sin pi/6 cos pi/6 ]
사용자는 회전행렬(rotation matrices)의 결정요소가 1이라는 것을 생각해 낼 수가 있다. 이하와 같은 방법으로 결과를 체크하자.
import java.awt.geom.AffineTransform; public class RotateThirtyChecker { public static void main(String[] args) { System.out.println( "The results of a thirty degree" + " counter clockwise rotation:"); AffineChecker.perform( AffineTransform.getRotateInstance( Math.PI/6)); AffineChecker.matrixMultiply( Math.cos(Math.PI/6), -Math.sin(Math.PI/6), Math.sin(Math.PI/6), Math.cos(Math.PI/6)); AffineChecker.report(); } }
RotateThirtyChecker
을 실행하면 다음과 같은 결과를 얻을 수 있다.
The results of a thirty degree counter clockwise rotation: Affine Result = (0.23205080756887764 , 3.598076211353316) Matrix Result = (0.23205080756887764 , 3.598076211353316) No real difference
전단변환은 변환된 객체의 형태를 바꾼다. 계수 shx는 y좌표의 요소로서 x 방향(direction)의 변화량을, shy는 x좌표의 요소로서 y 방향(direction)의 변화량을 나타내는데, 이것이 바로 직사각형을 평행사변형으로 바꾸는 변환이고 행렬의 형태는 다음과 같다.
[ 1 shx ] [ x ] [ x + y (shx) ] [ shy 1 ] [ y ] = [ y + x (shy) ].
변환을 하고 그것을 행렬의 곱(matrix multiplication)과 비교하는 코드는 다음과 같다.
import java.awt.geom.AffineTransform; public class ShearChecker { public static void main(String[] args) { System.out.println( "The results of shearing:"); AffineChecker.perform( AffineTransform.getShearInstance(5,6)); AffineChecker.matrixMultiply(1, 5, 6, 1); AffineChecker.report(); } }
ShearChecker
을 실행하면,
The results of shearing: Affine Result = (17.0 , 15.0) Matrix Result = (17.0 , 15.0) No real difference
이제 남아있는 것은 고정량에 의해 형태를 옮기는 가장 쉬운 변환이다. x방향의 tx와 y 방향의 ty를 움직이기 위해 다음 덧셈을 수행하자.
[ tx ] [ x ] [ tx + x ] [ ty ] + [ y ] = [ ty + y ].
하지만 문제는 지금까지 모든 변환을 2행2열 매트릭스의 곱셈으로 표현했다는 것이다. 때문에 위의 매트릭스를 이제까지의 아이디어에 충족하도록 만드는 한가지 방법은 모든 변환들을 다음과 같이 적는 것이다.
[ m00 m01 ] [ x ] [ tx ] [ m00x + m01y + tx ] [ m10 m11 ] [ y ] + [ ty ] = [ m10x + m11y + ty ]
여기에서 다음과 같은 3번째 행을 추가 시키면 사용자는 좌측의 내용을 좀 더 간단하게 적을 수 있을 것이다.
[ m00 m01 tx ] [ x ] [ m00x + m01y + tx ] [ m10 m11 ty ] [ y ] = [ m10x + m11y + ty ] [ 0 0 1 ] [ 1 ] [ 1 ]
이상의 모든 구성요소변환(building block transformations)은 6개의 계수 m00, m01, m10, m11, tx,
and ty, 로 환산하여 쓰여질 수 있다. (순서에 주의하자.) 이것은 왼쪽 위의 2행2열 매트릭스를 적용해서 이동을 수행한 것에 해당한다.
다음의 메소드를 AffineChecker
에 추가하자.
private static void translate(double tx, double ty){ matrixResult.setLocation( matrixResult.getX() + tx, matrixResult.getY() + ty); } public static void matrixTransform( double m00, double m01, double m10, double m11, double tx, double ty){ matrixMultiply(m00,m01,m10,m11); translate(tx,ty); }
자, 이제 matrixTransform()
를 사용하면 어떤 변환이라도 호출할 수가 있다. 가령,
ShearChecker
에서 matrixMultiply(1,5,6,1)
호출을 새로운 메소드인
matrixTransform(1,5,6,1,0,0)
호출로 대체할 수가 있다는 것이다. 뿐만 아니라 다음과 같은 방법으로
matrixTransform()
메소드를 호출할 수도 있다.
import java.awt.geom.AffineTransform; public class TranslationChecker { public static void main(String[] args) { System.out.println("The results of translating " + "by (-2,5):"); AffineChecker.perform( AffineTransform.getTranslateInstance(-2,5)); AffineChecker.matrixTransform(1,0,0,1,-2,5); AffineChecker.report(); } }
TranslationChecker
을 실행하면 다음과 같은 결과를 얻게 된다.(위에서 설명한 2개의 함수가
AffineChecker
에 추가되었는지 확인하자.)
The results of translating by (-2,5): Affine Result = (0.0 , 8.0) Matrix Result = (0.0 , 8.0) No real difference
이러한 기본적인 연산을 결합해서 AffineTransform
클래스의 간편한 메소드안에 포착되는 복잡한 결과값을
얻어낼 수 있다.
AffineTransform
에 대한 더 자세한 정보는 자바튜토리얼 "Transforming
Shapes, Text, and Images"에서 찾을 수 있다.
SOCKETCHANNELS을 이용해서 작업하기
Web Services API나 혹은 높은 수준의 API까지 다뤄야 하는 네트워킹 태스크를 해야 할 일이 생긴다면, 소켓을 이용해서
태스크를 적은 비용으로 좀 더 간단하게 완수할 수 있는 방법을 찾아보자. 이번 Tech Tip에서는 네트워크화 된 간단한 애플리케이션을 생성하기
위해 java.nio package
의 SocketChannel
과
ServerSocketChannel
클래스를 사용하게 될 것이다.
사용자가 특정 웹 페이지를 보기 위해 브라우저를 띄울 때, 페이지의 세부정보는 감춰지게 된다. 브라우저를 열고 다음을 입력하자.
http://developer.java.sun.com/developer/JDCTechTips/
결과적으로, 이 엔트리는 사용자를 웹 사이트의 80포트에 연결시켜주며, 보고자 하는 페이지의 이름을 포함하는 특정(particular) HTTP 요구(request)도 보내게 된다
자 이제 SocketChannel
을 이용해서 다음의 액션을 명시적으로 실행해보자.
TechTipReader
프로그램은 페이지를 검색하기 위해서 SocketChannel
를 사용한다.
이 프로그램의 getTips()
메소드를 주목해서 보면, 먼저 SocketChannel
을 실행하고
난 후, 그것을 developer.java.sun.com
의 80포트에 연결하기 위해서 인스턴스를 사용하는 것을 알 수
있다. 그리고 나서 JDCTechTips 페이지를 검색하기 위해 표준(standard) HTTP
"GET
"요구(request)를 보낸다.
서버로부터의 응답이 standard out에 출력된다.
import java.nio.channels.SocketChannel; import java.nio.charset.Charset; import java.nio.ByteBuffer; import java.net.InetSocketAddress; import java.io.IOException; public class TechTipReader { private Charset charset = Charset.forName("UTF-8"); private SocketChannel channel; public void getTips() { try { connect(); sendRequest(); readResponse(); } catch (IOException e) { e.printStackTrace(); } finally { if (channel != null) { try { channel.close(); } catch (IOException e) {} } } } private void connect() throws IOException { InetSocketAddress socketAddress = new InetSocketAddress( "developer.java.sun.com", 80); channel = SocketChannel.open(socketAddress); } private void sendRequest() throws IOException { channel.write(charset.encode("GET " + "/developer/JDCTechTips/" + "\r\n\r\n")); } private void readResponse() throws IOException { ByteBuffer buffer = ByteBuffer.allocate(1024); while ((channel.read(buffer)) != -1) { buffer.flip(); System.out.println(charset.decode(buffer)); buffer.clear(); } } public static void main(String[] args) { new TechTipReader().getTips(); } }
TechTipReader
프로그램을 자세히 들여다보면, connect()
메소드가
InetSocketAddress
를 생성할 때, 도메인과 표준(standard) HTTP 80포트를 나타내는 스트링,
이렇게 두 개의 매개변수(parameters)를 받고 있는 것을 발견하게 될 것이다. 그리고 스태틱 메소드인
SocketChannel.open()
을 호출하면 InetSocketAddress
를
매개변수(parameter)로 갖는데, 이 호출은 다음 2개의 과정에 상당한다.
channel = SocketChannel.open(); channel.connect(socketAddress);
sendRequest()
메소드는 첫번째 라인에 "GET
/developer/JDCTechTips/
", 두번째와 세번째 라인은 비어있는3줄짜리 요구(request)를 보낸다.
channel.write()
호출은 ByteBuffer
를 인수(argument)로 갖기 때문에
사용자는 스트링을 변환해야 한다. 사실 SocketChannels
을 이용해서 작업할 때,
ByteBuffers
를 변환하는 데에 대부분의 시간을 할애하게 된다. 변환하는 방법에는 여러 가지가 있을 수 있지만 이
예제에서는 java.nio.charset.Charset
클래스의 encode()
메소드를 사용한다.
readResponse()
메소드는 SocketChannel
로 리턴되는 응답을 처리하는 역할을
하며, SocketChannel
을 통해서 ByteBuffer
를 읽어들인다. 전송되는 파일의 끝에
도달하지 않는 한, readResponse()
메소드는 버퍼를 읽기상태에서 쓰기준비 상태로 뒤집는다.
ByteBuffer
의 내용은 charset.decode()
메소드에 의해 스트링으로 변환되고, 이
스트링은 standard out으로 보내진다.
TechTipReader
프로그램을 실행할 때, 디스플레이 된 페이지의 HTML을 봐야 한다.
자 이제 두 번째 예제를 보자. 클라이언트와 서버를 동반한 이 예제에서는 2개의 숫자가 더해진다. 클라이언트가 더해질 2개의 숫자를 서버에
보내면 서버는 덧셈을 수행하고 합계를 낸다. 이 예는 SocketChannel
API을 기반으로 하지만, 웹서비스나
RMI 혹은 서블릿을 이용하는 다른 솔루션도 가능하다.
여기에서 ByteBuffer
는 2개의 int값을 갖는데 이들은 사용자가 ByteBuffer
을
8byte로 제한하고, ByteBuffer
에 대한 뷰(view)의 역할을 하는 IntBuffer
을
생성하도록 한다.
private ByteBuffer buffer = ByteBuffer.allocate(8); private IntBuffer intBuffer = buffer.asIntBuffer();
SumClient
라는 다음의 프로그램은 예제의 클라이언트부분인데 이를 실행하기 위해서는 이 글의 뒷부분에서 보게 될
예제의 서버부분(SumServer)을 먼저 시작해야 한다.
import java.nio.channels.SocketChannel; import java.nio.ByteBuffer; import java.nio.IntBuffer; import java.io.IOException; import java.net.InetSocketAddress; public class SumClient { private SocketChannel channel; private ByteBuffer buffer = ByteBuffer.allocate(8); private IntBuffer intBuffer = buffer.asIntBuffer(); public void getSum(int i, int j) { try { channel = connect(); sendSumRequest(i, j); receiveResponse(); } catch (IOException e) { // add exception handling code here e.printStackTrace(); } finally { if (channel != null) { try { channel.close(); } catch (IOException e) { // add exception handling code here e.printStackTrace(); } } } } private SocketChannel connect() throws IOException { InetSocketAddress socketAddress = new InetSocketAddress("localhost", 9099); return SocketChannel.open(socketAddress); } private void sendSumRequest(int i, int j) throws IOException { buffer.clear(); intBuffer.put(0, i); intBuffer.put(1, j); channel.write(buffer); System.out.println("Sent request for sum of " + i + " and " + j + "..."); } private void receiveResponse() throws IOException { buffer.clear(); channel.read(buffer); System.out.println( "Received response that sum is " + intBuffer.get(0) + "."); } public static void main(String[] args) { new SumClient().getSum(14, 23); } }
SumClient
의 getSum()
메소드는 SocketChannel
을
미리 예정된 주소로 연결한다. connect()
메소드는 TechTipReader
예제에서 보았던
것과 본질적으로 동일하다. connect()
메소드를 호출하면, getSum()
메소드는 2개의
int값(14, 23)을 차례대로 sendSumRequest()
메소드로 넘기는데, 이 때
sendSumRequest()
는 ByteBuffer
의 내용을
SocketChannel
에 입력하는 역할을 한다. 그러면 receiveResponse()
메소드는
리턴된 메소드의 내용을 가지고 와서 그것을 standard out에 출력하게 된다.
사용자가 SumClient
를 실행시키면, 다음 사항이 화면에 표시된다. (SumServer
를
먼저 실행해야 하는 것을 잊지 말자.)
Sent request for sum of 14 and 23... Received response that sum is 37.
SumClient
를 자세히 보면, sendSumRequest()
가 같은
ByteBuffer
의 2개의 다른 뷰(view)를 사용하는 것을 알 수 있다. 먼저,
buffer.clear()
는 사실상 버퍼를 비우고 ByteBuffer
는
intBuffer
핸들의 사용으로 인해 2개의 int값을 갖는 버퍼로 간주된다.
sendSumRequest()
의 첫번째 파라미터로 넘겨진 int값은
intBuffer.put(0,i)
을 이용하여 첫번째 슬롯에 놓여지고, 두번째 파라미터 또한 유사한 방법으로 두번째 슬롯에
놓여진다. SocketChannel
의 write()
메소드는
ByteBuffer
를 갖기 때문에 버퍼의 첫번째 뷰(view)가 필요하다.
receiveResponse()
메소드는 sendSumRequest()
와
같다(parallel). 버퍼의 내용이 또다시 비워지고 SocketChannel
의 내용은 버퍼의
ByteBuffer
뷰(view)로 해석된다. IntBuffer
는 첫번째 위치에서 int값을
도출하기 위해 사용된다.
솔루션의 서버부분인 SumServer
로 넘어가기 전에, SocketChannel
객체의
read()
와 write()
메소드를 호출하는 장소에 추가적인 안전장치를 넣길 원할 수도 있다.
이 메소드들은 읽히거나 혹은 입력되는 바이트의 수와 함께 long값을 리턴하는데, 이번의 경우에는 8바이트라고 생각하면 된다.
sendSumRequest()
의 다음 라인을 찾아보고:
channel.write(buffer);
입력되는 버퍼의 사이즈를 확인하는 다음의 내용으로 바꾸자.
if (channel.write(buffer)!= 8){ throw new IOException("Expected 8 bytes."); }
receiveResponse()메소드의
channel.read() 호출주변에 비슷한 가드를 넣자.
이제 SumServer
를 보자. 이 예제를 실행하기 위해서, 사용자는 동의한 위치에
착신요구(incomingrequest)를 처리하게 될 서버를 셋업해야 한다. 밑의 openChannel()
메소드에서는
9099포트가 사용되었다. ServerSocketChannel
의 static method인
open()
이 호출되면, 그 다음 라인은 명시된 포트에 소켓을 묶는다. 그리고
channel.isOpen()
메소드가 트루(true)를 리턴하는대로 확인메시지가 standard out으로 보내진다.
import java.nio.ByteBuffer; import java.nio.IntBuffer; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; import java.io.IOException; import java.net.InetSocketAddress; public class SumServer { ByteBuffer buffer = ByteBuffer.allocate(8); IntBuffer intBuffer = buffer.asIntBuffer(); ServerSocketChannel channel = null; SocketChannel sc = null; public void start() { try { openChannel(); waitForConnection(); } catch (IOException e) { e.printStackTrace(); } } private void openChannel() throws IOException { channel = ServerSocketChannel.open(); channel.socket().bind( new InetSocketAddress(9099)); while (!channel.isOpen()) { } System.out.println("Channel is open..."); } private void waitForConnection() throws IOException { while (true) { sc = channel.accept(); if (sc != null) { System.out.println( "A connection is added..."); processRequest(); sc.close(); } } } private void processRequest() throws IOException { buffer.clear(); sc.read(buffer); int i = intBuffer.get(0); int j = intBuffer.get(1); buffer.flip(); System.out.println("Received request to add " + i + " and " + j); buffer.clear(); intBuffer.put(0, i + j); sc.write(buffer); System.out.println("Returned sum of " + intBuffer.get(0) + "."); } public static void main(String[] args) { new SumServer().start(); } }
사용자가 SumServer
를 시작하면, 다음 메시지가 나타난다.
Channel is open... Sent request for sum of 14 and 23... Received response that sum is 37.
SumClient
를 실행시키면, SumServer
는 이하의 내용을 화면에 표시한다.
A connection is added... Received request to add 14 and 23 Returned sum of 37.
waitForConnection()
메소드에서는 ServerSocketChannel
에
접속하려는 요구가 도착할 때까지 애플리케이션이 주기를 갖고 반복한다. 이 요구가 충족되면,
channel.accept()
는 SocketChannel
을 리턴하기 때문에 변수 sc는 더 이상
null이 아니다. 착신요구(incoming request)가 처리되면 SocketChannel
는 종료되고, 서버는 다시
SumServer
를 시작해서 다른 접속요청을 대기한다. 이 간단한 예제에서 요구(requests)들은 한번에 완전히
처리된다는 것을 상기하자. 동시다발적인 요구를 수행할 수 있는 좀 더 심화된 서버를 위해서는 다른 구조가 필요하지만, 이것은 이번 테크팁이
커버하는 범위를 벗어나기 때문에 다루지 않겠다.
processRequest()
메소드는 SumClient
의 대응
메소드(corresponding methods)와 매우 흡사하다. 버퍼가 지워지면 채널이 버퍼로 읽혀지고 IntBuffer
뷰(view)는 버퍼에서 파생된 2개의 int값을 검색하는데 사용된다. 사용 후 버퍼가 제거되고 합계는
IntBuffer
뷰(view)의 사용 중에 로드되는데, 그리고 나서 버퍼는 SocketChannel
로 다시 쓰여지게 된다.
SocketChannel
관한 자세한 정보는 테크니컬 아티클인 "New I/0
Functionality for Java 2 Standard Edition 1.4"을 참고한다.
클래스의 UNLOADING 과 RELOADING
지난 2003/7/22 Tech Tip인 "Compiling Source Directly From a Program"에서는 자바 프로그램으로부터 소스 파일을 직접적으로 컴파일하는 방법에 대해 설명하며, 클래스의 이름을 짓고 소스를 제공할 수 있는 간단한 편집기를 소개했다. 이 접근방법에는 별 문제가 없지만,여기서 제공된 클래스는 프로그램의 실행 중에 사용자가 컴파일하고 로드했던 클래스가 아니라는 것을 알아야 한다. 만약 이미 클래스를 컴파일하고 로드한 상태라면, 지난 Tech Tip 에서 소개했던 접근방법을 이용해서 클래스를 편집하고 리컴파일하는 것은 불가능하다. 이번 Tech Tip 에서는 underlying class loader 에 관해서 이야기 하고자 한다. 시스템 클래스 로더를 통해서 로드된 모든 클래스는 절대로 unload될 수 없기 때문에 프로그램 상에서 클래스의 소스를 재편집한 후, 리컴파일(recompile)하고 재생하고자 한다면, 다른 클래스 로더(class loader)를 사용해야 한다. 이 글에서는 이미 컴파일된 클래스(또는 어떤 클래스라도)를 로드하게끔 해주는 커스텀 클래스 로더의 생성법을 알아보고, 클래스를 컴파일하고, 이 컴파일된 클래스가 로드되었을 때, 기존의 클래스를 버리는 방법에 대해서 배우게 될 것이다.
모든 클래스의 로딩을 책임지는 것은 java.lang
패키지 내에 있는
ClassLoader
클래스의 한 인스턴스이다. 시스템 클래스의 경우에는 ClassLoader
의
getSystemClassLoader
메소드를 통해서 클래스 로더를 사용할 수 있고, 사용자 클래스의 경우에는 이미
클래스가 로드되어 있는 상태라면, Class
클래스의 getClassLoader
메소드를 통해
ClassLoader
에게 요청할 수가 있다.
지난 Tech Tip의 RunIt
프로그램에서는 클래스의 인스턴스를 만들어 내는 클래스 데이터를 로드하기 위해
forName
메소드를 사용하였다. 하지만 forName
메소드가 아닌 다른 클래스 로더를
사용하고자 한다면, ClassLoader
의 loadClass
메소드를 사용할 수 있다.
다시 말해서,
String className = ...; Class aClass = Class.forName(className);
위의 코드 대신에,
다음과 같이 코딩한다.
String className = ...; Class aClass = loader.loadClass(className);
위의 두 코드가 같은 클래스 로더를 통해 로딩되었을 때에는 기능적으로 동일하지만, 첫번째 블럭은 코드가 위치해 있는 로더를 통해서 클래스를
로드하는 반면, 두번째 블럭은 로더 변수가 명시한 클래스 로더를 통해 클래스를 로드하게 된다. 이 두 경우 모두, 클래스의 인스턴스를 생성하기
위해서는 newInstance
와 같은 메소드를 호출해야 한다.
호출하는 클래스의 클래스 로더가 변하지 않는다는 가정하에, Class.forName
메소드를 반복호출하면 같은
클래스를 로드하게 된다. 마찬가지로 loader.loadClass
를 반복적으로 호출해도 같은 클래스를 로드한다. 하지만
두번째 블럭은 클래스를 리로드할 수 있게 한다. 이를 위해서 새로운 ClassLoader
를 생성한다.
String className = ...; // create new loader instance ClassLoader loader = ...; Class aClass = loader.loadClass(className);
위의 코드 블럭에서는 loadClass
의 호출들 사이에 새로운 로더가 만들어진다. 만약 호출 중에
className
의 클래스 정의가 바뀐다면 두번째 호출에서는 클래스의 새로운 버전이 로드된다.
이 메카니즘을 사용하기 위해 RunIt
프로그램을 변경하면, 프로그램을 컴파일, 실행한 후에 소스를 편집하는 것이
가능하고, 이 때 출력값은 이전의 소스가 아닌 새로운 소스에 부합하는 값이다.
이제 남은 것은 어디에서 추상 클래스인 ClassLoader
를 받느냐 하는 것이다. permission
support가 추가된 java.security
의 SecureClassLoader
와
java.net
의 URLClassLoader
는 미리 정의된 로더들이다. 이렇게 미리 정의된
두개의 로더 중에서 URLClassLoader
만이 그것의 생성자(constructor) 또는 static
newInstance
메소드를 통해서 public construction을 지원하게 된다.
URLClassLoader
에 관한 자세한 정보를 원하면 documentation을
참고한다.
URLClassLoader
를 생성하는 것은 URL
객체들의 배열의 생성을 수반한다. 이
URL
객체들은 커스텀 클래스 로더가 클래스를 찾기 위해 사용하는 장소의 역할을 한다. 배열의 요소를 지정할 때에는
CLASSPATH
환경변수의 패스 요소(path elements)를 지정했던 것과 비슷한 방법을 써야 한다. (이때 패스
요소는 윈도우에서는 a ;
, 유닉스에서는 a :
로 분리된다.) URL
배열의 개개의 요소는 지역적으로 혹은 원거리 호스트에 위치시킬 수 있다. "/
"으로 끝나는 모든 것들은 디렉토리로 추정할
수 있으며, 이 외의 모든 것들은 JAR 파일로 추정된다.
가령, 현재 디렉토리에서만 찾아지는 디폴트 클래스패스(classpath)와 같은 역할을 하는 ClassLoader
를
만들고 싶다면, 다음과 같이 코딩하면 된다.
File file = new File("."); ClassLoader loader = new URLClassLoader( new URL[] {file.toURL()} );
이 코딩의 첫번째 라인은 현재 디렉토리를 참고해서 File
객체를 만들고, 두번째 라인은
URLClassLoader
생성자를 호출하고 있다. 이렇게 넘겨진 생성자는 URL
객체(File
에 대한URL
)의 배열이 생성자의 인수로 넘겨진다.
RunIt
프로그램에 다음의 코드를 포함하여 리컴파일하고 실행시키면, RunIt
프로그램은 이를
실행하고 리로드하는 중에 로드된 클래스를 버릴 것이다. 지난 Tech Tip에서 소개했던 RunIt
프로그램과는 다르게
밑의 코드는 메인메소드를 불러내는 클래스의 인스턴스를 생성하지 않는다. 왜냐하면 메인메소드가 static이기 때문에 클래스의 인스턴스를 만들
필요가 없기 때문이다.
// Create new class loader // with current dir as CLASSPATH File file = new File("."); ClassLoader loader = new URLClassLoader( new URL[] {file.toURL()} ); // load class through new loader Class aClass = loader.loadClass(className.getText()); // run it Object objectParameters[] = {new String[]{}}; Class classParameters[] = {objectParameters[0].getClass()}; Method theMethod = aClass.getDeclaredMethod( "main", classParameters); // Static method, no instance needed theMethod.invoke(null, objectParameters);
다음은 RunItReload
라고 새롭게 이름붙인 코드이다.
import java.awt.*; import java.awt.event.*; import javax.swing.*; import java.io.*; import java.net.*; import java.lang.reflect.*; public class RunItReload extends JFrame { JPanel contentPane; JScrollPane jScrollPane1 = new JScrollPane(); JTextArea source = new JTextArea(); JPanel jPanel1 = new JPanel(); JLabel classNameLabel = new JLabel("Class Name"); GridLayout gridLayout1 = new GridLayout(2,1); JTextField className = new JTextField(); JButton compile = new JButton("Go"); Font boldFont = new java.awt.Font( "SansSerif", 1, 11); public RunItReload() { super("Editor"); setDefaultCloseOperation(EXIT_ON_CLOSE); contentPane = (JPanel) this.getContentPane(); this.setSize(400, 300); classNameLabel.setFont(boldFont); jPanel1.setLayout(gridLayout1); compile.setFont(boldFont); compile.setForeground(Color.black); compile.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { try { doCompile(); } catch (Exception ex) { System.err.println( "Error during save/compile: " + ex); ex.printStackTrace(); } } }); contentPane.add( jScrollPane1, BorderLayout.CENTER); contentPane.add(jPanel1, BorderLayout.NORTH); jPanel1.add(classNameLabel); jPanel1.add(className); jScrollPane1.getViewport().add(source); contentPane.add(compile, BorderLayout.SOUTH); } public static void main(String[] args) { Frame frame = new RunItReload(); // Center screen Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize(); Dimension frameSize = frame.getSize(); if (frameSize.height > screenSize.height) { frameSize.height = screenSize.height; } if (frameSize.width > screenSize.width) { frameSize.width = screenSize.width; } frame.setLocation( (screenSize.width - frameSize.width) / 2, (screenSize.height - frameSize.height) / 2); frame.show(); } private void doCompile() throws Exception { // write source to file String sourceFile = className.getText() + ".java"; FileWriter fw = new FileWriter(sourceFile); fw.write(source.getText()); fw.close(); // compile it int compileReturnCode = com.sun.tools.javac.Main.compile( new String[] {sourceFile}); if (compileReturnCode == 0) { // Create new class loader // with current dir as CLASSPATH File file = new File("."); ClassLoader loader = new URLClassLoader(new URL[] {file.toURL()}); // load class through new loader Class aClass = loader.loadClass( className.getText()); // run it Object objectParameters[] = {new String[]{}}; Class classParameters[] = {objectParameters[0].getClass()}; Method theMethod = aClass.getDeclaredMethod( "main", classParameters); // Static method, no instance needed theMethod.invoke(null, objectParameters); } } }
이 프로그램을 컴파일하고 실행할 때는 지난 Tech Tip 의 RunIt
프로그램에서 했던 것과는 약간 다른 방법을
써야 한다. 왜냐하면 커스텀 클래스 로더 는 현재 디렉토리를 리로드 가능한 클래스들의 출처가 되는 장소로 사용하고 있기 때문에 사용자가 같은
클래스패스로부터 실제 RunItReload
클래스를 로드할 수가 없다. 그렇지 않으면, 시스템클래스 로더는 같은
위치로부터 온 클래스 로더와 컴파일된 클래스를 로드하게 될 것이다. 따라서 사용자는 컴파일러가 RunItReload
를
위해서 컴파일된 클래스를 다른 위치로 보낼 수 있도록 해줘야 한다. 또한 ".
"을 포함하지 않은 클래스패스의 지점에서
프로그램을 실행시켜야 한다. 클래스패스를 컴파일하기 위해서는 tools.jar
파일을 포함시켜야 함을 잊지 말자. 밑의
명령문은 RunItReload
의 컴파일된 클래스 파일들을 XYZ
서브디렉토리로 보내고 있다.
여기서 subdirectory name은 마음대로 지정해 줘도 상관없다. (여기서는 커맨드 라인이 여러줄로 보이지만 실제로는 한줄에 기입되도록
한다.)
윈도우에서는,
mkdir XYZ javac -d XYZ -classpath c:\j2sdk1.4.2\lib\tools.jar RunItReload.java
유닉스에서는,
mkdir XYZ javac -d XYZ -classpath /homedir/jdk14/j2sdk1.4.2/lib/tools.jar RunItReload.java
homedir
을 실제 홈 디렉토리로 대체하라.
이 때 시스템이 지정된 패스를 찾을 수 없다는 에러가 뜨면, 컴파일 전에 XYZ
디렉토리를 생성했었는지 확인하라.
방금 전에 했던 것처럼 런타임 클래스패스에 tools.jar
파일을 포함시키고 실제
RunItReload
프로그램을 위한 XYZ
디렉토리 또한 포함시켜라. 프로그램을 실행하기
위해서는, 다음과 같은 커맨드를 첨부한다. (다시, 여기서는 커맨드 라인이 여러줄로 보이지만 실제로는 한줄에 기입되도록 한다.)
윈도우에서는,
java -classpath c:\j2sdk1.4.2\lib\tools.jar;XYZ RunItReload
유닉스에서는,
java -classpath /homedir/jdk14/j2sdk1.4.2/lib/tools.jar: XYZ RunItReload
XYZ 디렉토리는 이전의 javac
과정으로부터 영향을 받는다. 따라서 컴파일하기 위한 타겟
디렉토리(-d
뒤에 명시된 디렉토리)는 반드시 런타임 클래스패스와 매치되어야 한다.
이 프로그램은 실행되면서 GUI를 디스플레이하기 때문에 다음과 같은 사항을 할 수가 있다.
JTextField
안에 클래스를 컴파일하고자 한다면,Sample2
와 같은 클래스의 이름을 입력한다.JTextArea
에 소스코드를 입력한다. 여기Sample2
의 소스코드를보자.public class Sample2 { public static void main(String args[]) { System.out.println(new java.util.Date()); // System.out.println("Hello, World!"); } }
- Go 버튼을 클릭한다.
출력값은 콘솔창으로 보내진다. 가령 Sample2
는 다음과 같은 출력값을 산출한다.
Tue Aug 19 11:25:16 PDT 2003
날짜를 프린트하는 라인을 주석에 달고, "Hello World
"를 프린트하는 라인의 주석을 없앤다. 그리고
Go버튼을 클릭하면 콘솔창에 다음을 보게 된다.
Hello, World!
이전에 로드된 클래스를 unload한 새로운 클래스 로더가 생성되었기 때문에 여러분은 다른 라인이 디스플레이되는 것을 보게 되는 것이다.
VARIABLE CONTENT로 메세지 포맷하기
국제화(Internalization)는 다양한 언어와 지역에서 애플리케이션이 작동할 수 있도록 디자인하는 프로세스이다. 이것은 text label을 다른 언어로 번역하는 것 뿐만 아니라 날짜나 시간과 같은 정보를 특정지역의 형식에 맞게 표시하는 것을 뜻한다.
text label과 메시지를 국제화 하는 첫번째 과정은 모든 정보를 리소스 번들로 옮기는 것이다. 이 때 각각의 인용된 스트링을 사용자가 볼 수 있게 하기 위해서는, 우선 리소스 번들에 엔트리를 만들고, text label과 메시지가 사용자가 위치한 지역에 따라 다이내믹하게 변하도록 코드를 바꿔줘야 한다. 이것을 제대로 했을 때, 미국에 있는 사용자는 Help를, 스페인에 있는 사용자는 Ayuda를 도움 메뉴를 나타내는 라벨로 이해할 것이다.
이러한 기법은 전체가 하나의 메시지로 되어 있어서 text에서 text로의 번역이 가능한 경우에는 완벽하게 들어맞지만, 여러 개의 메시지로 이루어진 하나의 긴 메시지의 경우에는 그렇지 않다. 다음의 예를 보자.
Hello, John. Good luck.
사용자는 여기에서 단순하게 연결된 스트링(string concatenation)을 떠올리며, 다음과 같이 여러 개의 스트링을 덧붙이는 방법으로 합성된 메시지를 만들려고 할 것이다.
System.out.println( "Hello, " + name + ". Good luck.")
혹은, Hello
와 Good luck
스트링을 리소스 번들로 옮겨 가면서 메시지를
합성시키려는 생각을 할 지도 모른다. 사실 이 방법은 잘 돌아갈 수도 있다. 하지만 인사말의 형태가 다음과 같이 나타나는 현상이 일어난다.
Hello and good luck, John.
그렇다면 이번엔 리소스 번들 스트링을 이름 앞, 뒤의 접두사와 접미사 부분으로 구별해서 쪼개는 방법을 생각해 보자. 하지만 이 때에는 번역가가 어떤 것들이 같이 쓰이는지 그 용법을 모두 알고 있어야만 하기 때문에 일을 더욱 복잡하게 만들 뿐이다. 따라서 이보다 나은 접근방법은 가운데에 이름 변수를 넣을 수 있는 텍스트 스트링(text string)을 만들어 주는 것이다.
영어의 경우에는 다음과 같다.
Hello, {0}. Good luck.
이렇게해서 생성된 스트링을 본다면, 스페인어 번역가는 다음과 같이 hello 와 good luck 을 함께 넣어버리는 것이 더 낫다는 것을 깨닫게 될 것이다.
Hola y buena suerte, {0}.
이 메시지가 실제적으로 디스플레이 될 때에는 java.text
패키지의
MessageFormat
클래스가 변수들을 대체하기 위해 사용된다. 이 때
MessageFormat
은 한 세트의 객체들을 가져와서 그것들을 포맷하고, 포맷된 스트링을 "Hello,
{0}. Good luck.
" 과 같은 패턴에 삽입한다.
MessageFormat
을 사용하기 위해서는 우선 formatter를 생성해야 한다.
String pattern = ...; // from bundle Locale aLocale = ...; // the Locale MessageFormat formatter = new MessageFormat(pattern); formatter.setLocale(aLocale);
각각의 패턴을 만들기 위해서는 그에 대응하는 각각의 다른 MessageFormat
객체를 생성할 수도 있지만,
새로운 패턴 템플릿에 applyPattern
메소드를 호출함으로써 또 다른 패턴을 가진
MessageFormat
객체를 재사용 할 수도 있다. 이것은 지역정보(locales)를 바꾼 후에 해야 하는 작업임을
기억하자.
formatter.setLocale(aNewLocale); formatter.applyPattern(aPatternForNewLocale);
포맷터를 만든 후에는 출력 메시지를 생성해야 한다. 이를 위해서는 인수(argument)들의 배열(array)을 넘겨 주어야 하는데,
배열안의 인덱스값에 따라서 패턴안의 각 {#}을 이 인수들의 배열이 교체해주게 된다. "Hello, {0}. Good
luck.
" 이라는 패턴을 위해서는 하나의 요소를 갖는 배열이 필요하고, 이 배열안의 요소는 스트링의 위치0에 삽입될 텍스트를
포함한다는 것이다. 앞서 설명한 패턴에 "John
" 이라는 스트링값을 삽입한 한개의 요소를 갖는 배열의 예가 있다.
Object messageArgs[] = {"John"};
출력값을 생성하기 위해선, message arguments을 지정하여 MessageFormat
의 포맷 메소드를
호출한다.
System.out.println(formatter.format(messageArgs));
아래의 HelloGoodLuck
프로그램은 MessageFormat
이 어떻게 사용되는지
보여주고 있으며, 여기에서는 프로그램을 간단하게 유지하기 위해 리소스 번들을 사용하지 않는다.
import java.text.*; import java.util.*; public class HelloGoodLuck { public static void main(String args[]) { String pattern = "Hello, {0}. Good luck."; Locale aLocale = Locale.US; MessageFormat formatter = new MessageFormat( pattern, aLocale); Object messageArgs[] = {"John"}; System.out.println( formatter.format(messageArgs)); // Pass in command line args if (args.length != 0) { System.out.println(formatter.format(args)); } } }
커맨드 라인에 이름값을 주면 HelloGoodLuck
프로그램은 두번째 메시지를 산출한다.
예를 들어, 다음과
같은 명령을 주고 프로그램을 실행했을 때,
java HelloGoodLuck Spot
이와 같은 출력값을 보게 될 것이다.
Hello, John. Good luck. Hello, Spot. Good luck.
MessageFormat
을 사용하면 텍스트를 대입하는 기능뿐만 아니라, NumberFormat
이나 DateFormat
클라스를 사용하지 않고서도 숫자나 날짜를 포맷할 수가 있다. javadoc은
MessageFormat
클라스에서 가능한 모든 지원에 대해 설명하고 있으니 이를 참고하길 바란다.
{#}
의 argument index 뒷부분에는 포맷의 타입과 스타일을 컴마로 구분해서 지정할 수 있다. 예를 들어
날짜의 경우, short, medium, long, full 등의 타입을 지정하여 DateFormat
상수(constants)에 매핑할 수 있다. 만약 이때 인수의 타입이 Date
이고,
MessageFormat
이 "{1,date,long}
"을 인수로 갖는다면, 출력값으로 지역에 맞는
포맷에 long 타입으로 디스플레이 된 날짜를 보게 될 것이다. 또한 똑같은 short, medium, long, full 옵션을 이용해서
날짜를 "시간"과 함께 디스플레이할 수도 있다. 여기에서 숫자값에는 정수(integer), 통화(currency), 퍼센트(percent)
스타일도 가능하다. 만약 이렇게 미리 짜여진 스타일이 싫다면, 또 SimpleDateFormat
과
DecimalFormat
의 패턴 스트링을 이미 알고 있다면, 정보를 직접 명시해서 사용해도 된다.
시간, 날짜, 숫자를 사용하는 MessageFormat
패턴의 예를 보자.
At the tone, the time is now {0, time, short} on {0, date, long}. You now owe us {1, number, currency}.
입력 인수로 Date
와 Number
를 주었다면, 미국과 독일 지역에 맞는 출력값을 보게 될
것이다.
다음은 위의 출력값을 생성한 프로그램이다. 단순한 프로그램을 위해서는 리소스 번들을 사용하지 말고, 대신에 패턴변수와
germanPattern
변수의 스트링을 리소스 번들에 위치시켜야 한다.
import java.text.*; import java.util.*; import java.io.*; import java.awt.*; import javax.swing.*; public class ExtendedFormat { public static void main(String args[]) { String pattern = "At the tone, the time is now {0, time, short}" + " on {0, date, long}." + " You now owe us {1, number, currency}."; String germanPattern = "Beim Zeitton ist es {0, time, short} Uhr" + " am {0, date, long}." + " Sie schulden uns jetzt {1, number, currency}."; StringWriter sw = new StringWriter(100); PrintWriter out = new PrintWriter(sw, true); MessageFormat formatter = new MessageFormat(pattern, Locale.US); Object messageArgs[] = {new Date(), new Double(9000.12)}; out.println(formatter.format(messageArgs)); formatter.setLocale(Locale.GERMAN); // Need to reset pattern after changing locales formatter.applyPattern(germanPattern); out.println(formatter.format(messageArgs)); out.close(); // Put output in window JFrame frame = new JFrame("Extended Format"); frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE); JTextArea ta = new JTextArea(sw.toString()); JScrollPane pane = new JScrollPane(ta); frame.getContentPane().add( pane, BorderLayout.CENTER); frame.setSize(500, 100); frame.show(); } }
MessageFormat
을 사용하는 것 말고도 애플리케이션을 국제화하는 방법은 매우 많다. 스트링 패턴의 출처인
리소스 번들의 사용에 관한 자세한 사항은 "Resource
Bundles" 1998/5/21 Tech Tip을, 날짜와 시간 스트링 포맷에 관한 자세한 정보는 "Internationalizing
Dates, Times, Months, and Days of the Week" 2003/6/24 Tech Tip을 참고하길 바란다.
SWING COMPONENTS의 저장과 재구성
J2SE 1.4 의 릴리즈 이전에는 모든 스윙 컴포넌트는 "이후 릴리즈 될 스윙은 long term persistence 을 지원할 것이다."라고 약속했다. 이는 "모든 자바빈의 long term storage에 대한 지원은 1.4버전에서 java.beans 패키지에 추가되었다."는 메세지로 바뀌었다.
java.beans
패키지의 XMLEncoder
클래스는 XML 파일에 자바빈을 존속시키고
스윙 컴포넌트를 저장하는 방법을 제공한다. 스윙 컴포넌트를 저장한 후에는 java.beans
패키지의
XMLDecoder
클래스를 이용해서 컴포넌트를 재구성할 수가 있다.
FrameCreator
프로그램은 JFrame
을 생성하고 사용자 설정을 하고 있다.
main()
메소드는 XMLEncoder
객체를 생성하고, 이것을 이용해서
JFrame
의 XML버전을 Frame.xml
파일에 저장한다.
import javax.swing.*; import java.awt.*; import java.awt.event.ActionListener; import java.beans.XMLEncoder; import java.beans.EventHandler; import java.io.BufferedOutputStream; import java.io.FileOutputStream; public class FrameCreator { public static JFrame createFrame() { JFrame jFrame = new JFrame(); jFrame.setTitle("My Frame"); jFrame.setSize(200, 100); JPanel jPanel = new JPanel(); jFrame.getContentPane().add(jPanel); Controller controller = new Controller(); JButton button1 = new JButton("Hello, world"); button1.addActionListener( (ActionListener) EventHandler.create( ActionListener.class, controller, "helloWorld")); JButton button2 = new JButton( "Goodbye, cruel world"); button2.setBackground(Color.RED); button2.addActionListener( (ActionListener) EventHandler.create( ActionListener.class, controller, "exit")); jPanel.add(button1); jPanel.add(button2); jFrame.setVisible(true); return jFrame; } public static void main(String[] args) throws Exception { XMLEncoder encoder = new XMLEncoder( new BufferedOutputStream( new FileOutputStream("Frame.xml"))); encoder.writeObject(createFrame()); encoder.close(); } }
FrameCreator
는 non-GUI클래스인 Controller.java
에 정의된 메소드를
호출한다. 제어 Controller
클래스는;
public class Controller { public void helloWorld() { System.out.println("Hello, world"); } public void exit() { System.exit(0); } }
Frame.xml
컨텐츠를 보면 FrameCreator
클래스에 설정되어 있는 속성들을 쉽게
확인할 수 있다. JFrame
의 디멘존(dimension)이 저장되는 것을 주목할 필요가 있다.
JFrame
안에 저장된 JPanel
에 관한 정보들과 JPanel
의 구성
요소인 2개의 Jbuttons
(Controller
클래스의 인스턴스에 링크되어 있다.)이 파일안에
저장되어 있다.
<?xml version="1.0" encoding="UTF-8"?> <java version="1.4.1_01" class="java.beans.XMLDecoder"> <object class="javax.swing.JFrame"> <void property="size"> <object class="java.awt.Dimension"> <int>200</int> <int>100</int> </object> </void> <void property="contentPane"> <void method="add"> <object class="javax.swing.JPanel"> <void method="add"> <object class="javax.swing.JButton"> <string>Hello, world</string> <void method="addActionListener"> <object class="java.beans.EventHandler" method="create"> <class>java.awt.event.ActionListener</class> <object id="Controller0" class="Controller"/> <string>helloWorld</string> </object> </void> </object> </void> <void method="add"> <object class="javax.swing.JButton"> <string>Goodbye, cruel world</string> <void property="background"> <object class="java.awt.Color"> <int>255</int> <int>0</int> <int>0</int> <int>255</int> </object> </void> <void method="addActionListener"> <object class="java.beans.EventHandler" method="create"> <class>java.awt.event.ActionListener</class> <object idref="Controller0"/> <string>exit</string> </object> </void> </object> </void> </object> </void> </void> <void property="name"> <string>frame0</string> </void> <void property="title"> <string>My Frame</string> </void> <void property="visible"> <boolean>true</boolean> </void> </object> </java>
이를 이용해 Frame.xml
파일로부터 형성된 JFrame
를 놀랍도록 쉽게 재구성할 수
있다. 다음의 FrameRecreator
프로그램을 보면 XMLDecoder
가
Frame.xml
파일을 읽고 있는 것을 알 수 있고, 이를 실행하면 FrameCreator
에서
생성/ 구성되었던 것과 같은 JFrame
를 보게 된다.
import java.beans.XMLDecoder; import java.io.BufferedInputStream; import java.io.FileInputStream; public class FrameRecreator { public static void main(String[] args) throws Exception { XMLDecoder decoder = new XMLDecoder( new BufferedInputStream( new FileInputStream("Frame.xml"))); decoder.readObject(); decoder.close(); } }

XMLEncoder
와 XMLDecoder
을 이용해서 JavaBeans을 저장하고 복구하는
방법을 살펴보았다. 하지만 XMLEncoder
와 XMLDecoder
는 이 예제에세 다루는 것보다
훨씬 더 광범위하게 사용된다. XMLEncoder
는 모든 프리미티브 데이터타입, 날짜, 스트링, 어레이,
리스트,hashmaps(primitive data types, dates, strings, arrays, lists, )그리고
JavaBeans이 아닌 J2SE SDK 의 클래스들을 저장하기 위해 미리 구성된다. JavaBeans이 아니더라도, 사용자만의 자바 클래스들을
저장하기 위해 XMEncoder
를 형성할 수도 있다. 이 테크닉에 대해 좀 더 알고 싶다면, 스윙 커넥션의 다음 아티클을
참고한다.
사용자의 persistence techniques에 XMLEncoder
를 추가하려면, Core Java
Technologies Tech Tips의 "Using the Preferences API" July 15 issue에서
설명했던 Preferences API를 참고한다.
MIDI SOUND 생성하기
자바 데스크탑 애플리케이션에 오디오 큐(audio cues)를 추가하면 세련된 느낌을 줄 뿐만 아니라 사용성을 극적으로 증가시킬 수가
있다. Musical Instrument Digital Interface (MIDI)는 장치간에 뮤지컬 이벤트를 주고 받을때 사용되는
커뮤니케이션 프로토콜이다. MIDI파일은 실제 오디오가 아닌 오디오 명령어를 포함한다. MIDI 가 사운드를 재현하기 위해 사운드 엔진에
명령어를 기술하는 반면, 오디오는 사운드의 디지탈 표현(digital representation)이다. 이 TechTip에서는 J2SE
1.3이후의 자바 플랫폼에서 지원하는 javax.sound.midi
패키지를 사용하여 MIDI사운드를 생성하는 3가지
방법을 소개한다.
첫번째 테크닉은, MidiChannel
객체를 직접 호출해서 사운드를 생성하는 것이다. 이 객체는 MIDI전송을 위한
하나의 MIDI 채널을 나타낸다. 채널을 통해서 연주를 시작하기 위해서는 MidiChannel
객체의
noteOn()
메소드를 사용하고, 멈추기 위해서는 noteOff()
메소드를 사용한다.
noteOn()
메소드는 2개의 int값을 필요로 한다. 첫번째 int는 연주될 음을 지정하는데, 다음
SingleNoteChannel
프로그램에서 noteOn()
메소드에 넘겨진 int 60은 가운데
도(middle C)를 위한 표준 MIDI 음의 번호이다. 위아래의 정수는 반음에 해당하고 12 반음은 한 옥타브를 이룬다.
noteOn()
메소드의 두번째 매개변수는 키가 눌려질 속도를 지정한다. 사실 이 매개변수는 흔히 속도를 나타내지만,
그것을 제어볼륨(volume control)으로 봐도 무방하다. SingleNoteChannel
에서
noteOn()
메소드의 두번째 매개변수는 70이다. 속도를 나타내는 70을 다른 숫자로 바꿔서 실험을 해보자.
noteOff()
메소드는 두개의 signature를 갖는데, 하나는 noteOn()
에서와
동일한 두개의 매개변수이고, 다른 하나는 연주될 음을 나타내는 번호이다.
음을 연주하기 전에, SingleNoteChannel
생성자에 필요한 셋업을 해야한다. 사운드를 생성하기 위해서는
Synthesizer
객체가 필요하다. 이 객체는 MIDI 1.0 명세(specification)에서 규정하는 각각의
16개 채널을 말하는 MidiChannel
들의 컬랙션을 보여준다.
SingleNoteChannel
생성자는 MidiSystem
클래스의 팩토리 메소드를 이용해서
Synthesizer에 대한 핸들을 받고, Synthesizer
의 open()
메소드를 호출한 후,
getChannels()
메소드를 호출함으로써 가능한 MidiChannel
s의 array를
얻어낸다. 그리고는 index 0자리에 첫번째 MidiChannel
을 넣는다.
import javax.sound.midi.MidiChannel; import javax.sound.midi.Synthesizer; import javax.sound.midi.MidiSystem; import javax.sound.midi.MidiUnavailableException; public class SingleNoteChannel { private MidiChannel channel; public SingleNoteChannel() { try { Synthesizer synth = MidiSystem.getSynthesizer(); synth.open(); channel = synth.getChannels()[0]; } catch (MidiUnavailableException e) { e.printStackTrace(); } } public void playNote(int note) { channel.noteOn(note, 70); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } channel.noteOff(note, 70); } public static void main(String[] args) { new SingleNoteChannel().playNote(60); } }
SingleNoteChannel
프로그램의 playNote()
메소드는
MidiChannel noteOn()
메소드를 호출해서 음을 연주하기 시작한다. 연주는 쓰레드가 휴면하는 1초동안
지속되다가 MidiChannel noteOff()
메소드가 호출됨으로 해서 끝나게 된다. 또한 버튼이나 키 혹은
on-screen event를 누르거나 떼서 특정 음들을 결합시킬 수도 있다.
MIDI사운드를 생성하기 위한 두번째 테크닉은, Synthesizer
로 결합된
Receiver
객체를 사용하는 것이다. 이 때에 특정 MidiChannel
대신
Receiver
에 대한 핸들을 받아야 한다. 사용자는 ShortMessage
타입의 MIDI메세지를
만들고 사용자설정을 한 후에 Receiver
객체의 send()
메소드를 호출해서 연주를 시작할 수
있다.
ShortMessage
객체는 많아야 2데이터 바이트를 갖는 MIDI 메세지를 포함한다. MIDI 메세지의 매개변수를
설정하거나, 채널 메세지를 위한 메세지 매개변수를 설정하기 위해서 setMessage()
메소드를 사용할 수 있다.
SingleNoteSynthesizer
프로그램의 setMessage()
메소드는 채널 메세지를 위한
메세지 매개변수를 설정하기 위해서 4개의 int값을 포함하는 signature를 갖는다. 첫번째 int는 NOTE_ON
와 NOTE_OFF
중 보내질 코맨드를, 두번째 int는 타겟 채널을 지정한다. (앞의 예제와 일관되게,
index 0을 조건으로 지정했다.) 마지막 두개의 int값은 음 식별자와 속도이다.
SingleNoteSynthesizer
에서 noteOn()
과
noteOff()
에 넘겨지는 int값은 SingleNoteChannel
예제에서와 동일하다.
import javax.sound.midi.ShortMessage; import javax.sound.midi.InvalidMidiDataException; import javax.sound.midi.Receiver; import javax.sound.midi.Synthesizer; import javax.sound.midi.MidiSystem; import javax.sound.midi.MidiUnavailableException; public class SingleNoteSynthesizer { private ShortMessage message = new ShortMessage(); private Receiver receiver; private SingleNoteSynthesizer() { try { Synthesizer synth = MidiSystem.getSynthesizer(); synth.open(); receiver = synth.getReceiver(); } catch (MidiUnavailableException e) { e.printStackTrace(); } } public void playNote(int note) { setShortMessage(note, ShortMessage.NOTE_ON); receiver.send(message, -1); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } setShortMessage(note, ShortMessage.NOTE_OFF); receiver.send(message, -1); } private void setShortMessage( int note, int onOrOff) { try { message.setMessage(onOrOff, 0, note, 70); } catch (InvalidMidiDataException e) { e.printStackTrace(); } } public static void main(String[] args) { new SingleNoteSynthesizer().playNote(60); } }
지금까지는 디폴트 악기에서 하나의 음을 연주하는 예만을 살펴보았다. 그렇다면 다른 악기에서의 연주는 어떨까? 세번째 테크닉에서 이 물음의 답을 보게 될 것이다. 실험을 통해 시작해보자. 연주 가능한 악기의 리스트를 생성하는 메소드가 있다.
public void listAvailableInstruments(){ Instrument[] instrument = synth.getAvailableInstruments(); for (int i=0; i<instrument.length; i++){ System.out.println(i + " " + instrument[i].getName()); } }
위의 메소드를 호출하면 다음과 같은 아이템 리스트를 보게 된다.
0 Piano 1 Bright Piano 2 Electric Grand 3 Honky Tonk Piano 4 Electric Piano 1 5 Electric Piano 2 6 Harpsichord 7 Clavinet 8 Celesta 9 Glockenspiel 10 Music Box 11 Vibraphone 12 Marimba 13 Xylophone 14 Tubular Bell 15 Dulcimer 16 Hammond Organ 17 Perc Organ 18 Rock Organ 19 Church Organ 20 Reed Organ 21 Accordion 22 Harmonica 23 Tango Accordion 24 Nylon Str Guitar 25 Steel String Guitar 26 Jazz Electric Gtr 27 Clean Guitar 28 Muted Guitar 29 Overdrive Guitar 30 Distortion Guitar
SingleNoteSynthesizer2
는 다른 악기들을 사용하여 하나 이상의 음을 연주하기 위해
SingleNoteSynthesizer
를 보강한 프로그램이다. 하지만 이를 실행해보면, 시간을 맞추기 위해
Thread
가 여전히 휴면하는 것을 볼 수 있다. 이는 음악을 만들고 연주하기 위한 좋은 방법이 아닐 뿐더러, AWT
Event Dispatch thread에서 이를 실행했다면 GUI가 이벤트를 처리하거나 보수하는 것을 막게 될 것이다. 이 부분은 마지막
테크닉을 설명할 때 다루기로 하겠다.
SingleNoteSynthesizer2
를 보면, 사용자가 만든 악기 리스트에서 사용하고 싶은 악기의 int값을
찾아 MidiChannel
의 programChange()
메소드에 넣고, 이를 호출해서 연주될
악기를 바꿀 수가 있다. 예에서 교회 오르간을 나타내는 int는 19이다.
startNote()
와 stopNote()
메소드가 각각
NOTE_ON
명령어와 NOTE_OFF
명령어를 이용해서 메세지를 생성할 수 있도록 코드를 추가한
것을 주목하자. playNote()
메소드는 startNote()
를 호출하고, duration에
지정된 밀리초(milliseconds)동안 휴면한 후, stopNote()
를 호출한다.
import javax.sound.midi.ShortMessage; import javax.sound.midi.Synthesizer; import javax.sound.midi.Receiver; import javax.sound.midi.MidiSystem; import javax.sound.midi.MidiUnavailableException; import javax.sound.midi.InvalidMidiDataException; public class SingleNoteSynthesizer2 { private ShortMessage message = new ShortMessage(); private Synthesizer synth; private Receiver receiver; public SingleNoteSynthesizer2() { try { synth = MidiSystem.getSynthesizer(); synth.open(); receiver = synth.getReceiver(); } catch (MidiUnavailableException e) { e.printStackTrace(); } } public void startNote(int note) { setShortMessage(ShortMessage.NOTE_ON, note); receiver.send(message, -1); } public void stopNote(int note) { setShortMessage(ShortMessage.NOTE_OFF, note); receiver.send(message, -1); } public void playNote(int note, int duration){ startNote(note); try { Thread.sleep(duration); } catch (InterruptedException e) { e.printStackTrace(); } stopNote(note); } public void setInstrument(int instrument){ synth.getChannels()[0].programChange( instrument); } private void setShortMessage( int onOrOff, int note) { try { message.setMessage(onOrOff, 0, note, 70); } catch (InvalidMidiDataException e) { e.printStackTrace(); } } public void playMajorChord(int baseNote){ playNote(baseNote,1000); playNote(baseNote+4,1000); playNote(baseNote+7, 1000); startNote(baseNote); startNote(baseNote+4); playNote(baseNote+7,2000); stopNote(baseNote+4); stopNote(baseNote); } public static void main(String[] args) { SingleNoteSynthesizer2 synth = new SingleNoteSynthesizer2(); synth.setInstrument(19); synth.playMajorChord(60); } }
주요 화음은 한개의 기본 음, 기본음으로부터 4개 반음 올린 음, 3개 반음 선간(spaces)을 올린 음으로 구성된다.
SingleNoteSynthesizer2
에서, playMajorChord()
메소드는 먼저 하나씩
3개의 음를 연주한 뒤, 이들을 동시에 연주한다. 타이밍은 Thread
s를 이용해서 맞추는데, 타이밍이 중요할 경우에는
Sequencer
를 이용하는 것이 좋다.
Sequencer
객체는 MIDI 이벤트의 타이밍을 좀 더 효과적으로 컨트롤 할 수 있는 메소드를 포함한다. 전에는
MidiSystem
의 팩토리 메소드를 사용했던 반면, 여기서는 Sequencer
를 이용한다.
Sequencer
를 사용한 예는 다음 예제인 SequencerSound
에서 볼 수 있다. 앞선
예와 비교해서 여기서는 코드에 몇몇 주요한 변경이 있음을 유의해야 한다. 먼저 startNote()
와
stopNote()
메소드는 생성되는 메세지가 일어나는 시점을 나타내는 int값을 갖는다.
createTrack()
메소드는 8분음표마다 4번의 똑딱 소리를 내서 하나의 Sequence
를
생성한다. 예를 들면, 4/4박자는 4개의 8분음표 혹은 16번의 똑딱 소리로 나뉘어 질 수 있는 것처럼, 박자를 나눌 때 이 메소드를 사용할
수 있다.
setShortMessage()
메소드는 setMessage()
를 호출한후,
ShortMessage
에 기반하고, 똑딱 소리로 결합된 MidiEvent
객체를 생성한다. 그리고는
마지막으로 트랙에 MidiEvent
를 추가한다. 트랙을 재생할 때,
startSequencer()
메소드는 새롭게 생성된 Sequence
를 기존
Sequence
에 할당하고, start()
메소드를 호출해서 재생시킬수가 있다. 또한
setTempoInBPM()
메소드를 이용해서 템포를 분당 16 비트(BPM)으로 설정할 수가 있다.
import javax.sound.midi.Track; import javax.sound.midi.Sequencer; import javax.sound.midi.Sequence; import javax.sound.midi.MidiSystem; import javax.sound.midi.MidiUnavailableException; import javax.sound.midi.InvalidMidiDataException; import javax.sound.midi.ShortMessage; import javax.sound.midi.MidiEvent; public class SequencerSound { private Track track; private Sequencer sequencer; private Sequence sequence; public SequencerSound() { try { sequencer = MidiSystem.getSequencer(); sequencer.open(); } catch (MidiUnavailableException e) { e.printStackTrace(); } createTrack(); makeScale(20); startSequencer(); } private void startSequencer() { try { sequencer.setSequence(sequence); } catch (InvalidMidiDataException e) { e.printStackTrace(); } sequencer.start(); sequencer.setTempoInBPM(60); } private void createTrack() { try { sequence = new Sequence(Sequence.PPQ, 4); } catch (InvalidMidiDataException e) { e.printStackTrace(); } track = sequence.createTrack(); } public void startNote(int note, int tick) { setShortMessage( ShortMessage.NOTE_ON, note, tick); } public void stopNote(int note, int tick) { setShortMessage( ShortMessage.NOTE_OFF, note, tick); } private void setShortMessage( int onOrOff, int note, int tick) { ShortMessage message = new ShortMessage(); try { message.setMessage(onOrOff, 0, note, 90); MidiEvent event = new MidiEvent( message, tick); track.add(event); } catch (InvalidMidiDataException e) { e.printStackTrace(); } } public void makeScale(int baseNote) { for (int i = 0; i < 13; i++) { startNote(baseNote + i, i); stopNote(baseNote + i, i + 1); startNote(baseNote + i, 25 - i); stopNote(baseNote + i, 26 - i); } } public static void main(String[] args) { new SequencerSound(); } }
이 글은 MIDI 파일을 생성하고 재생하는 방법을 설명했다. 개발자는 사운드뱅크가 J2SE SDK에 따라서 자동적으로 인스톨되기 때문에 음악을 들을 수가 있다. 사운드 뱅크는 악기 설정을 보여주고, 사운드를 종합하기 위해 필요하다. (사운드뱅크에 대해서는 후에 더 자세히 다루도록 하겠다.) 비 개발자들은 생성한 파일을 들으려면 사운드뱅크를 따로 설치해야 될 것이다. J2SE 1.4.1 혹은 이후버전에 JRE를 인스톨할 때, 사용자는 사운드뱅크를 포함시키도록 설정해야 한다.
MIDI파일의 저장과 재생관련 정보는 javax.sound.midi
의
documentation를, 이 팁에 대한 자세한 내용은 Java Sound API home
page를 참고한다.