반응형

자바에서 각 이벤트는 하나의 이벤트 타입에 속하게 됩니다.

사용자가 JButton 컴포넌트를 클릭할 때 발생하는 actionPerformed 이벤트는 ActionEvent라고 하는 이벤트 타입에

속합니다. 여기에서 이벤트 타입은 EventObject 클래스에서 파생되는 클래스로 정의됩니다.

그리고 각 이벤트 타입에 속하는 모든 이벤트는 리스너 인터페이스의 멤버가 됩니다.

예를 들어 ActionEvent 이벤트 타입에 속하는 actionPerformed 이벤트는 ActionListener라고 하는 리스너

인터페이스의 멤버가 되는 것이지요.

 

결국 하나의 이벤트 타입에 대하여 하나의 리스터 인터페이스가 있는 셈입니다.

 

Interface

Class

ActionListener

ActionEvent

AdjustmentListener

AdjustmentEvent

AWTEventListener

AWTEvenetListenerProxy

ComponentListener

ComponentAdapter

ContainerListener

ComponentEvent

FocusListener

ContainerAdapter

HierachyBoundsListener

ContainerEvent

HierachyListener

FocusAdapter

InputMethodListener

FocusEvent

ItemListener

HierachyBoundsAdapter

KeyListener

HierachyEvent

MouseListener

InputEvent

MouseMotionListener

InputMethodEvent

MouseWheelListener

InvocationEvent

TextListener

ItemEvent

WindowFocusListener

KeyAdapter

WindowListener

KeyEvent

WindowStateListener

MouseAdapter

 

MouseEvent

 

MouseMotionAdapter

 

MouseWheelEvent

 

PaintEvent

 

TextEvent

 

WindowAdapter

 

WindowEvent

 

 

하나의 리스너 인터페이스에 여러 메소드 즉, 여러 이벤트가 포함될 수 있습니다.

예를 들어 창과 관련된 이벤트에서는 windowActivated, windowClosed, windowClosing, windowDeactivated,

windowDeiconified windowIconified, windowOpened 등이 있으며, 이들 이벤트는 모두 windowListener라고 하는

리스너 인터페이스의 멤버가 됩니다.

 

이러한 리스너 인터페이스를 구현하는 클래스를 어댑터(Adapter)라고 하며, 어댑터의 인스턴스를 리스너라고 합니다.

그리고 리스너 인터페이스를 구현하고 있는 리스너를 이벤트 소스에 등록시켜야 합니다.

이벤트 소스란 이벤트를 발생시키는 객체입니다.

public class MyPanel extends Jpanel implements ActionListener{

public void actionPerformed(ActionEvent e){

JoptionPane.showMessageDialog(

this,

getSource().ToString() + “ 를 클릭했습니다.”,

“actionPerformed 이벤트”,

JoptionPane.OK_OPTION);

}

public MyPanel(){

jButton = new Jbutton(“클릭하세요.”);

jButton.addActionListener(this);

}

}

 

public class MyPanel extends Panel{

ActionAdapter actionAdapter = new ActionAdapter();

public MyPanel(){

jButton = new Jbutton(“클릭하세요.”);

jButton.addActionListener(actionAdapter);

}

}

 

class WindowClosing extends WindowAdapter{

    public void windowClosing(WindowEvent e){

        System.exit(0);

    }

}

 

class MyFrame extends JFrame{

    public MyFrame(){

        WindowClosing listener = new WindowClosing();

        addWindowListener(listener);

    }

}

 

class MyFrame extends JFrame{

    public MyFrame(){

        addWindowListener(new WindowAdapter(){

            public void windowClosing(WindowEvent e){

                System.exit(0);

            }

        });

    }

}

 

* “예전에 누가 Adapter를 사용하면 리스너에 있는 method()를 모두 구현하지 않아도

되며 필요한 method()구현하면 된다고 했었는데, 사실은 Adapter 클래스를

상속받아서 새로운 리스너를 구현하는 것입니.”

반응형

Method Categories and Interface

 

Category

Interface Name

Methods

Action

ActionListener

actionPerformed(ActionEvent)

Item

ItemListener

itemStateChanged(ItemEvent)

Mouse

MouseListener

mousePressed(MouseEvent)

mouseReleased(MouseEvent)

mouseEnterd(MouseEvent)

mouseExited(MouseEvent)

mouseClicked(MouseEvent)

Mouse Motion

MouseMotionListener

mouseDragged(MouseEvent)

mouseMoved(MouseEvent)

Key

KeyListener

keyPressed(KeyEvent)

keyReleased(KeyEvent)

keyTyped(KeyEvent)

Focus

FocusListener

focusGained(FocusEvent)

focusLost(FocusEvent)

Adjustment

AdjustmentListener

adjustmentValueChanged(AdjusmentEvent)

Component

ComponenetListener

componentMoved(ComponentEvent)

componentHidden(ComponentEvent)

componentResized(ComponentEvent)

componentShown(ComponentEvent)

Window

WindowListener

windowClosing(WindowEvent)

windowOpened(WindowEvent)

windowIconified(WindowEvent)

windowDeiconified(WindowEvent)

windowClosed(WindowEvent)

windowActivated(WindowEvent)

windowDeactivated(WindowEvent)

Container

ContainerListener

componentAdded(ContainerEvent)

componentRemoved(ContainerEvent)

Text

TextListener

textValueChanged(TextEvent)

 

 

 

 

 

AWT Components

 

Component Type

Description

Button

A named rectangular box used for receiving mouse clicks

Canvas

A panel used for drawing

Checkbox

A component allowing the user to select an item

ChekboxMenuItem

A checkbox within a menu

Choice

A pull-down static list of items

Component

The parent of all AWT components, except menu components

Container

The parent of all AWT containers

Dialog

The base class of all modal dialog boxes

Frame

The base class of all GUI windows with window manager controls

Label

A text string component

List

A component that containers a dynamic set of items

Menu

An element under the menu bar, which containers a set of menu items

MenuItem

An item within a menu

Panel

A basic container class used most often to create complex layouts

Scrollbar

A component that allows a user to “select from a range of values”

ScrollPane

A container class that implements automatic horizontal and vertical

Scrolling for a single child component

TextArea

A component that allows the user to enter a block of text

TextField

A component that allows the user to enter a single line of text

Window

The base class of all GUI windows, without window manager controls

  

Component Events

 

Component Type

Act

Adj

Cmp

Cnt

Foc

Itm

Key

Mou

MM

Text

Win

Button

V

 

V

 

V

 

V

V

V

 

 

Canvas

 

 

V

 

V

 

V

V

V

 

 

Checkbox

 

 

V

 

V

V

V

V

V

 

 

CheckboxMenuItem

 

 

 

 

 

V

 

 

 

 

 

Choice

 

 

V

 

V

V

V

V

V

 

 

Component

 

 

V

 

V

 

V

V

V

 

 

Container

 

 

V

V

V

 

V

V

V

 

 

Dialog

 

 

V

V

V

 

V

V

V

 

V

Frame

 

 

V

V

V

 

V

V

V

 

V

Label

 

 

V

 

V

 

V

V

V

 

 

List

V

 

V

 

V

V

V

V

V

 

 

MenuItem

V

 

 

 

 

 

 

 

 

 

 

Panel

 

 

V

V

V

 

V

V

V

 

 

Scrollbar

 

V

V

 

V

 

V

V

V

 

 

ScrollPane

 

 

V

V

V

 

V

V

V

 

 

TextArea

 

 

V

 

V

 

V

V

V

V

 

TextField

V

 

V

 

V

 

V

V

V

V

 

Window

 

 

V

V

V

 

V

V

V

 

V

 

Act – ActionListener

Adj – AdjustmentListener

Cmp – ComponentListener

Cnt – ContainerListener

Foc – FocusListener

Itm – ItemListener

Key – KeyListener

Mou – MouseListener

MM – MouseMotionListener

Text – TextListener

Win – WindowListener

 

반응형
IBM 소프트웨어 엔지니어 (마음은 게이머)인 Scott Clee가 테트리스 게임 모델을 재사용 가능한 자바 빈 컴포넌트로 포장하는 간단한 방법을 소개한다. 일단 게임의 구성 요소들이 자바 객체들로 나누어지면 완전한 게임 모델 빈을 형성하도록 재조립될 수 있고, 실제로 어떤 테트리스 GUI에도 결합될 수 있다. forum에서 이 글에 대한 여러분의 생각을 저자와 다른 독자들과 공유하기 바란다.

-참조 : http://www.ibm.com/developerworks/kr/library/j-tetris/

테트리스 게임의 구성 요소들을 자바 객체로 나누어 재사용 가능한 자바 게임 컴포넌트로 만드는 방법

한 친구가 자신은 새로운 프로그래밍 언어를 배울 때마다 그 언어를 사용해 테트리스 게임을 작성하는데 도전한다고 말한 적이 있다. 이러한 전통하에, 내가 처음 자바 언어를 사용한 프로그래밍 법을 배웠을 때 나도 같은 시도를 해 보기로 했다. 나의 첫번째 시도는 하나의 완전한 게임이긴 했지만 매우 간단하고 흉한 것이었다. 시간이 가고 내가 자바 설계와 개발에 더 많은 경험을 얻음에 따라 나는 GUI에서 게임 모델을 분리시켜 (Swing 컴포넌트와 유사하게) 테트리스 빈을 만들 수 있다는 사실을 알았다. 그래서 나는 그것을 해 보기 시작했다.

이 글에서 나는 테트리스 빈을 구축하고 구현하는 방법을 안내하겠다.

테트리스는 자바 객체로 표현될 수 있는 몇 개의 구성 요소를 가지고 있다.:

  • The Tetris pieces 테트리스 조각
  • 조각을 가지고 있는 테트리스 판
  • 판 위의 조각 제어, 점수 관리 등을 하는 게임

이 요소들 각각을 좀 더 자세히 살펴보자.

  • 각 요소는 정확히 네 개의 블록으로 구성되어 있다.
  • 조각 내의 각 블럭은 테트리스 판 내에 (x.y) 좌표를 가지고 있다.
  • 각 조각은 0, 2, 4의 회전율을 가지고 있다.
  • 각 조각은 L, J, S, O, I, Z, 혹은 T 모양을 지닐 수 있다.


그림 1. 각 테트리스 조각에 네 요소가 필요함 : 블록, (x,y) 좌표, 회전율 및 모양
Four elements of each Tetris piece
테트리스 빈으로 무엇을 할 수 있을까?

다음은 테트리스 빈을 만든 후 내가 수행한 몇 가지 일들이다.
  • 빈의 두 인스턴스를 연결시켜 대결 게임을 만들다.
  • Netris라는 이름을 가진, 최초의 Psion Netpad용 테트리스 게임을 만들다
  • 빈을 애플릿에 통합시켜 브라우저와 호환되는 테트리스 게임을 만들다.

첫번째 두 아이템은 매우 간단한 시스템을 사용해 구현될 수 있다. 각 조각에 대해 중앙 블록을 선택하고 이 블록의 (x,y) 좌표를 저장하면, 조각 내의 나머지 블록들을 이 블록을 중심으로 한 상대 좌표로 저장할 수 있다. 이 방식은 중앙 조각에 대한 상대 블록으로 모양을 저장함으로써 조각의 어떤 형태도 기술할 수 있게 해준다. 중앙 지점은 java.awt.Point로 저장될 수 있고 상대 좌표는 java.awt.Point 배열에 저장될 수 있다. 좌표 계산을 쉽게 하기 위해, 중앙 조각을 (0,0) 상대 좌표를 가진 블록으로 저장할 수 있다.

이 시스템은 또한 조각의 회전을 계산할 때 더 쉽다. 간단한 행렬 조작을 사용하여 단순히 y좌표를 x 좌표로 바꾸고 x 좌표는 y 좌표의 마이너스 값으로 바꾸면 조각을 시계방향으로 90도 회전시킬 수 있다. 우리는 중앙 지점을 중심으로 상대 좌표를 사용하고 있기 때문에, 여기에서도 마찬가지로 할 수 있다:



temp = x;
x = -y;
y = temp;

여러분은 또한 시계방향 회전을 3번 적용하면 조각을 시계 반대 방향으로 90도 회전시킬 수 있다. (믿지 못하겠다면 한 번 해보기 바란다.)

마지막으로, 모든 조각이 동일한 회전율을 가지는 것은 아니므로, 이 문제를 보충하기 위해 회전 기법을 체크해 보아야 할 것이다.

우리는 모든 유형의 조각을 표시하는데 동일한 TetrisPiece 클래스를 사용하기 때문에 이들을 구분할 방법이 필요하다. 이를 위해 우리는 몇 개의 정적인 int 생성자를 사용하여 다른 유형들을 표시하고 지역 변수가 조각의 유형을 저장하도록 한다. 다음은 이 생성자들 중 하나의 예이다:



public static final int L_PIECE = 0;

조각을 테트리스 판 위에서 이동시킬 것이므로 이를 위한 이동 메소드를 제공해야 한다. 몇 가지 이동 (이미 가능한 한 제일 오른쪽 끝에 와 있는데 다시 오른쪽으로 가려는 시도)은 불법적일 수 있다. 따라서 우리는 모든 이동 요청을 확인할 필요가 있다. 우리는 참조를 저장할 테트리스 판에서 이것을 구현할 것이다. 따라서 우리 클래스의 생성자는 여기에서 두 가지 매개 변수를 가질 것이다: 첫번째는 만들어진 조각의 유형이고, 두번째는 테트리스판에 대한 참조이다. 생성자에서 우리는 initalizeBlocks()이라는 private 유틸리티 메소드를 호출할 것인데, 이 메소드는 조각의 상대 좌표값을 각각의 조각 유형에 설정할 것이다.

이동이 합법적인지를 체크하는 간단한 방법은 판에서 조각을 떼내어 원하는 방향으로 이동시킨 후 맞는지 보는 것이다. 맞으면 조각을 보드의 새 위치에 둔다. 그렇지 않으면 이동을 취소하고 원래 있던 자리에 다시 둔다. 반환되는 값은 그 결과에 따라 true (이동이 맞으면)나 false(이동이 맞지 않으면)가 될 것이다.

좀 더 주의를 기울여야 할 이동의 한 유형은 조각이 떨어지는 경우이다. 떨어진다는 것은 조각이 판의 제일 아래쪽으로 바로 내려간다는 의미이다. 이를 위해 우리는 더 이상 움직일 수 없을 때가지 조각을 아래로 계속 이동시키는 while 루프가 필요하다. 그러면 조각은 그 위치에 배치될 것이다.

조각에 적용될 수 있는 다양한 이동들을 구별하기 위해 우리는 다음 예제에 나타난 것과 같이 몇 가지 추가적인 static int 생성자를 사용할 것이다.:



public static final int LEFT = 10;

조각이 맞는지 보기 위해 나중에 willFit() 메소드가 TetrisBoard 클래스에서 구현된 것이다.

마지막으로 TetrisPiece 클래스를 포장하기 위해 우리는 중앙 지점과 상대 좌표와 같은 몇 가지 변수에 대한 getters와 setters, 그리고 무작위 유형의 TetrisPiece 인스턴스를 반환할 getRandomPiece()라는 정적인 메소드를 가진다.

TetrisPiece 클래스를 포함한 완성된 소스를 참고 자료에서 다운로드받을 수 있다.

테트리스 판은 빈 블록과 색깔 있는 블록을 가지고 있는 2D 격자판으로 생각할 수 있다. 다양한 유형의 테트리스 조각들이 int 생성자에 의해 구별되기 때문에, 우리가 해야 할 일은 빈 블록의 값을 정의하는 것 뿐이고 우리는 판을 2D int 배열로 저장할 수 있다. 이 방식을 사용하면 판 내의 빈 블록은 다음에 의해 표시될 것이다.:



public static final int EMPTY_BLOCK = -1;

유연성을 유지하기 위해 판의 크기를 가변적으로 하겠지만 이것을 생성자 내에 정의할 것이다. 따라서 생성자는 열과 행의 수를 나타내는 두 ints를 받아들일 것이다. 그리고 나서 2D 배열 내의 모든 값을 기본적으로 빈 블록으로 만드는 resetBoard() 메소드를 호출할 것이다.

조각들이 판에 추가되고 제거되기 때문에 우리는 addPiece()removePiece()메소드를 제공한다. addPiece() 메소드는 TetrisPiece()를 취하고 판에서 이 메소드가 차지하는 모든 위치의 값을 자신의 유형으로 설정함으로써 작동한다. removePiece() 메소드는 판의 값이 빈 블록의 값으로 설정된다는 점을 제외하면 비슷하다.

판에 변화가 있을 때 사용자가 알 수 있도록 하기 위해, 조각이 추가되거나 이동되었을 때 BoardEvent가 구동될 것이다. 이 이벤트를 듣는 클래스들에 대해 우리는 BoardListener 인터페이스가 필요한데, 이벤트가 구동되었을 때 이 인터페이스의 boardChange()메소드가 호출된다. 이 이벤트들은 화면 수정이 필요할 때 통지되도록 테트리스 판 GUI에 의해 사용될 수 있다. listener를 저장하기 위해 우리는 java.util.Vector를 사용할 것이고, listener를 추가/삭제하고 이벤트를 구동시키기 위한 관련 메소드를 제공할 것이다.

때때로 여러분이 조각을 추가하고 삭제할 때 BoardEvents를 구동시키는 것이 부적절할 수 있다. 조각이 떨어져야 할 때 (이 이동은 조각을 떨어뜨리기 위해 while 루프를 사용한다는 것을 기억하라)를 예로 들 수 있다. 이 경우 조각이 바닥에 부딪쳤을 때만 이벤트가 필요하다. 이를 용이하게 하기 위해 우리는 boolean 매개변수를 취하도록 addPiece() 메소드를 만들어 값이 true일 경우에만 이벤트가 구동되도록 할 것이다.

테트리스 게임의 중요 요소 중 하나는 한 행이 완료되면 그 행은 사라지고 그 위의 모든 행들이 내려온다는 것이다. 이를 위해 우리는 삭제될 행의 지수를 매개변수로 취하는 removeRow() 메소드를 제공할 것이다. 행이 없어진 후에 BoardEvent가 구동될 것이다.

private 변수들에 접근하기 위해 필요한 getter와 setter들 외에도, 우리는 하나의 메소드가 더 필요하다. 앞에서 설명한 willFit()가 그것이다. 이 메소드는 TetrisPiece를 매개변수로 취해 그 조각이 판에 맞는지 결정하기 위한 boolean 값을 돌려준다. 맞는다는 것은 그 조각이 판의 경계 안에 있고 판에서 그 조각이 맞춰질 곳에 있는 블록의 값이 비어 있다고 설정되어 있음을 의미한다. 이런 경우 true 값이 반환된다.

이제 TetrisBoard 클래스가 완성되었다.이 클래스를 포함한 완성된 소스를 참고 자료에서 다운로드받을 수 있다.

100 피트 벽을 가진 테트리스?

어느날 나는 이 빈을 타워 블록의 점등 시스템에 연결시키고 빌딩의 측면을 따라 내려가면서 테트리스 게임을 하고 싶어졌다. 나는 누군가가 이렇게 했다는 것을 신문에서 읽은 후 내내 이것을 하고 싶어해 왔다.

이제 테트리스 게임에서 사용되는 두 개의 주 컴포넌트를 만들었으므로, 이들을 모아 게임 로직을 만들면 된다.

게임의 흐름을 제어하기 위한 좋은 방법은 이것을 java.lang.Thread를 확장하는 내부 클래스에 내장시키는 것이다. 이 방식의 한 가지 장점은 게임 속도를 제어하기 위해 스레드 sleep 호출을 추가할 수 있다는 것이다. 또 다른 장점은 현재 주 애플리케이션 스레드가 자유롭기 때문에 하나의 GUI가 첨부될 때 색칠 문제가 없어진다는 것이다. 이 문제는 주 스레드가 계속 묶여 있어 색칠할 시간이 없을 때 때때로 발생할 수 있다.

스레드 내의 로직은 run() 메소드 내의 while 루프 속에 구현될 것이다. 루프는 계속해서 조각을 만들어 내고 조각을 더 이상 맞출 수 없을 때까지 이들을 게임판으로 떨어뜨릴 것이다. 이 때 fPlaying이라는 지역 boolean 변수가 false로 설정되어 루프를 끝내고 GameEvent를 구동시켜 게임이 종료되었음을 표시할 것이다.

while 루프 내에 fPaused의 boolean 값을 체크하는 if 절이 있다. 이 값이 true로 설정되었을 경우 루프는 계속 실행되겠지만 모든 게임 로직이 무시되어 종료되는 느낌을 줄 것이다. Boolean이 false로 다시 바뀌면 게임이 계속될 것이다.

우리는 한 번에 하나씩 떨어지는 조각에만 관심을 가지고 있으므로, 여기에 대한 참조를 저장할 fCurrPiece라는 변수를 만들 것이다. 이 변수가 null 값으로 설정되면 이전의 조각이 더 이상 아래로 내려갈 수 없으며 판의 최종 위치에 도착했음을 의미한다. 이 때 우리는 새 조각을 만들어 판의 맨 위 중앙에 둔다. fCurrPiece 변수가 null값이 아닌 모든 경우에 우리가 해야 할 일은 그 것을 한 위치로 떨어뜨리고 주어진 시간 동안 스레드를 휴면 상태로 만드는 것이다.

한 조각이 더 이상 움직일 수 없게 되었을 때 우리는 행이 완성되었는지 보아야 한다. 이를 위한 손쉬운 방법은 한 쌍의 중첩 for 루프를 사용하는 것이다. 이 for 루프의 바깥 쪽 루프는 행의 지수를 따라 작업하며, 안쪽의 루프는 지수 전체에 걸쳐 확인 작업을 수행한다. 만일 우리가 완성된 행을 발견하면, TetrisBoard 클래스에 구현된 removeRow() 메소드를 호출하며 완성된 행의 지수를 전달할 수 있다. 이제 제거된 행 위의 모든 행들이 하나씩 내려올 것이기 때문에 우리는 이들을 다시 체크해야 할 것이다. 여러 행을 한번에 완성하도록 장려하기 위해 우리는 완성된 행의 개수를 저장하고 각각 더 높은 점수를 줄 것이다.

테트리스 게임의 또 다른 주요 요소는 더 많은 행이 완성될수록 조각이 더 빨리 내려온다는 것이다. 이 기능은 지금까지 완성된 행의 개수를 체크하고 이에 따라 스레드의 휴면 주기를 점차 감소시켜가는 방식으로 구현될 수 있다.

GameThread 내부 클래스를 만들기 위해 필요한 것은 이것이 전부지만, 구현해야 할 또 다른 내부 클래스가 있다. 다수의 이벤트가 구동될 것이고 이들은 listerners가 저장되도록 요구할 것이므로, 이들을 모두 한 장소에 두는 것이 좋을 것이다. 우리는 EventHandler 내부 클래스를 사용하여 이를 수행할 것이다.

EnvetnHandler 내부 클래스
이 클래스는 우리가 구동하는 이벤트에 관심이 있는 listener들에 대한 참조를 저장할 것이다. listener들에 대한 addremove 메소드를 제공할 뿐 아니라 이벤트들을 구동하기 위한 유틸리티 메소드도 있을 것이다.

이 클래스는 다음 유형의 이벤트들을 다룬다. :

  • GameEvent : 게임이 시작되거나 멈출 때마다 구동된다. 게임 START 혹은 END를 표시하기 위한 값을 가지고 있다.

  • BoardEvent : 게임판에 변경 사항이 있을 때 구동된다. EventHandler 클래스에서 추가/삭제 listener 호출이 TetrisBoard 클래스로 전달된다.

  • ScoreEvent: 점수가 바뀔 때 구동된다.

구동될 수 있는 많은 다른 유형의 이벤트들이 있지만, 간편성을 위해 나는 위에서 설명한 이벤트들만 사용하였다. 우리가 구현할 수 있는 다른 이벤트에는 LineEvent가 있는데, 한 행, 혹은 여러 행이 완성되었을 때 구동되고 화면 애니메이션을 일으키는데 사용될 수 있다.

TetrisGame 마무리하기

이제 내부 클래스들을 완성하였으므로 TetrisGame 클래스의 나머지 부분을 설명해야 한다. 모든 자바 빈과 마찬가지로 우리는 매개 변수 없는 생성자가 필요하다. 이 생성자에서 우리는 EventHandler 클래스와 TetrisBoard 클래스의 인스턴스를 만들 것이다. TetrisBoard 클래스 10x20이라는 기본 사이즈를 가질 것이다.

게임 상태를 제어하기 위해 우리는 개시, 중지, 일시 중지 메소드를 사용할 것이다. startGame() 메소드는 모든 게임 변수를 리셋하고 ScoreEvent (이제 0으로 리셋됨)와 GameEvent (START라는 매개 변수 유형을 가짐)를 구동시킬 것이다. 또한 GameThread를 생성하여 개시할 것이다. stopGame() 메소드는 fPlaying 변수를 false로 바꾸어 GameThread가 끝나도록 하고 END라는 매개변수 유형으로 GameEvent를 구동시킨다. setPause() 메소드는 한 게임을 일시 중지시키는 역할만 한다.

필요한 모든 getters와 setters와 별도로, 구현할 메소드가 하나 더 있는데, move() 메소드가 그것이다. 이 메소드는 이동 방향을 매개변수로 취하는데, 이것은 TetrisPiece 클래스에서 나오는 생성자이다. move() 메소드는 게임이 진행중이며 일시 중지 상태가 아니라고 가정하고 이동하려고 시도한다. 그 이동이 아래로 떨어지는 요청인데 성공하지 못한다면 fCurrPiece가 null 값으로 설정되고 조각은 게임판 내의 현재 위치에 남아 있을 것이다. 그러면 GameThread는 새로운 조각을 생성한다.

TetrisGame에 대해서는 이게 전부이다. 이 클래스를 포함한 완성된 소스를 참고 자료에서 다운로드 받을 수 있다.

이제 완벽함을 위해 우리는 BeanInfo 클래스를 만들고 이 클래스들을 적절한 파일들로 채울 수 있지만, 여기에서는 그럴 필요가 없다. 우리가 필요한 것은 우리의 빈을 테스트할 간단한 GUI이고 참고 자료에 하나가 제공되고 있다. 이것은 테트리스 게임판을 그리기 위해 한 개의 내부 클래스를 사용하고 키 조작을 조정하기 위한 몇 가지 로직을 포함하고 있는 간단한 클래스이다. GUI는 javax.swing.JFrame에 표시되며, 선택적으로 java.applet.Applet이 될 수도 있다.

우리의 테트리스 빈을 테스트하기 위해 소스 파일을 푸는데, 디렉토리 구조를 그대로 두기 바란다. (왜냐하면 내가 빈 클래스들이 TetrisBean 디렉토리에 있도록 이들을 TetrisBean 패키지에 두었기 때문에 때문이다.) 여러분이 소스 파일을 푼 경로를 자바 클래스 경로에 추가하고 파일을 컴파일한다. 이제 여러분이 해야 할 일은 "java Scottris"를 실행시키는 것 뿐이다.

나는 이러한 테트리스 빈을 구현할 수 있는 많은 방법이 있다는 것을 알고 있다. 내가 이 글에서 소개한 것은 아주 간단한 방법이며, 이것이 여러분의 창조력에 불을 붙일 수 있기를 바란다. 나는 여러분이 이것을 자유롭게 개선시키기 바란다.




위로


참고자료




위로


필자소개

Photo of Scott Clee

Scott Clee는 현재 IBM의 CICS 제품에 대한 FV Tester로 일하고 있다. 4년간 자바 프로그래머로 일했으며 자바와 관련된 재미있는 프로젝트를 취미 삼아 수행하는 것을 즐긴다.





위로
반응형

자바를 공부하면서 윈도우 종료 스케쥴러를 한번 만들어 봤습니다.

단순하게 작성된 것이라 공개하기에는 부족한 점이 많지만

사용상 편리하리라 생각되어 올리게 되었습니다.

설치버전과 압축버전으로 나누어져 있습니다.

설치버전은 jsmooth 와 install factory 2.7 버전을 이용하여 만들었습니다.

영화나 파일 다운시 윈도우 종료 스케쥴러로 사용하려고 만들었습니다.

사용자 삽입 이미지
위에 모습이 실행시 UI화면 이구요 trayicon을 사용하려고 윈도우 닫기 버튼을 누르면

화면에서 모습은 사라지게 됩니다. tray에 상주하게 되구요.

프로그램 종료는 트레이에서 마우스 오른쪽 버튼을 누르고 exit을 클릭하시면 됩니다.

분단위로 시간 설정하시고 setting 버튼 클릭.

예약 설정을 취소하시고 싶으시면 cancel 버튼 클릭하시면 됩니다.

참고로 java 프로그램을 사용하시려면 JRE가 설치 되어 있어야 합니다.

JRE 설치는 www.java.com
반응형
소프트웨어 사용자들은 결코 메뉴얼을 안본다. 아무리 어려운 프로그램이라 해도 사용자는 자기 스스로 방법을 찾아내려고 한다. 정말 혼자서 해결하지 못한다 해도 다른 사람에게 물어볼지언정 메뉴얼은 안본다.

따라서 개발자는 프로그램을 사용하기 쉽고 직관적으로 이용할 수 있도록 만들어야 한다. 결코 메뉴얼을 보지 않는 사용자들에게 어떻게 프로그램을 이해시키고 도움을 줄 수 있을지, 몇가지 방법을 알아보자.

사용자 가이드하기
가장 자주 쓰이는 방법은 각 인터페이스에 간단한 힌트나 길잡이 문구를 넣는 것이다. 스윙(Swing) 프레임워크는 툴팁 형태로 이러한 기능을 적용시키도록 해준다. setToolTipText 메소드를 호출함으로써 어떤 스윙 컴포넌트에 대해서도 툴팁을 설정할 수 있다.

툴팁이 설정되면 마우스가 해당 컴포넌트 위로 위치할 경우 텍스트로 된 간단한 설명이 작은 윈도우에 표시된다(마우스가 지나가면 사라진다). 이 설명에는 컴포넌트의 기능이나 개발자가 유용하다고 생각하는 정보 등이 담겨있다.

툴팁은 간결하고 명확한 방법이지만 단점도 있다. 아주 소량의 정보만 전달할 수 있으며, 인터페이스 위에 겹쳐져서 표시되기 때문에 오히려 사용에 방해가 되기도 한다. 따라서 프로그램의 메뉴에 사용하기에는 적합하지 않은 면이 있다.

툴팁 외에 상태표시줄에 설명을 표시하는 방법이 있다. 기본 개념은 툴팁과 동일하다. 마우스가 어떤 요소 위에 있을 동안에만 설명이 나타난다. 다만 프로그램 윈도우의 상태표시줄에 설명이 보여지므로, 툴팁처럼 프로그램 사용에 방해가 되는 일은 없다.

마우스-오버 방식 적용하기
스윙에서는 상태표시줄에 설명을 보여주는 기능을 기본 제공되지 않지만, 어렵지 않게 직접 구현할 수 있다. 설명을 보여주고자 하는 각 항목을 '마우스 감지기(mouse listener)'와 연결하고, MouseListener 인터페이스의 mouseEntered와 mouseExited 메소드를 적용하면 된다.

mouseEntered 메소드는 설명을 보여주는 역할을, mouseExited 메소드는 설명을 사라지게 하는 역할을 한다. 이 이벤트 소스를 사용하면 항목을 식별해 자동으로 해당되는 설명을 보여주기 때문에 각 항목마다 일일이 마우스 연결을 하지 않아도 된다.

MouseOverHintManager(소스보기1)는 재사용이 가능한 상태표시줄 설명 기능을 보여준다. 사용법은 아주 간단하다. JLabel을 거치는 MouseOverHintManager의 인스턴스를 설정한 후, addHintFor 메소드를 호출해 각 항목에 대한 설명을 설정한다. 탑컨테이너(프로그램 창, 프레임, 대화상자 등)에 대해서 enableHints 메소드를 호출한다.

소스보기 1
import java.awt.*;
import java.awt.event.*;
import java.util.*;
import javax.swing.*;
import javax.swing.table.*;


public class MouseOverHintManager implements MouseListener {
  private Map hintMap;
  private JLabel hintLabel;

  public MouseOverHintManager( JLabel hintLabel ) {
    hintMap = new WeakHashMap();
    this.hintLabel = hintLabel;
  }

  public void addHintFor( Component comp, String hintText ) {
    hintMap.put( comp, hintText );
  }

  public void enableHints( Component comp ) {
    comp.addMouseListener( this );
    if ( comp instanceof Container ) {
      Component[] components = ((Container)comp).getComponents();
      for ( int i=0; i<components.length; i++ )
       enableHints( components[i] );
    }
    if ( comp instanceof MenuElement ) {
      MenuElement[] elements = ((MenuElement)comp).getSubElements();
      for ( int i=0; i<elements.length; i++ )
       enableHints( elements[i].getComponent() );
    }
  }

  private String getHintFor( Component comp ) {
    String hint = (String)hintMap.get(comp);
    if ( hint == null ) {
      if ( comp instanceof JLabel )
       hint = (String)hintMap.get(((JLabel)comp).getLabelFor());
      else if ( comp instanceof JTableHeader )
       hint = (String)hintMap.get(((JTableHeader)comp).getTable());
    }
    return hint;
  }


  public void mouseEntered( MouseEvent e ) {
    Component comp = (Component)e.getSource();
    String hint;
    do {
      hint = getHintFor(comp);
      comp = comp.getParent();
    } while ( (hint == null) && (comp != null) );
    if ( hint != null )
      hintLabel.setText( hint );
  }

  public void mouseExited( MouseEvent e ) {
    hintLabel.setText( " " );
  }

  public void mouseClicked( MouseEvent e ) {}
  public void mousePressed( MouseEvent e ) {}
  public void mouseReleased( MouseEvent e ) {}
}


MouseOverHintManager를 적용하는 것도 상기한 절차와 유사하다. addHintFor 메소드는 변수로 컴포넌트 레퍼런스와 이에 대응하는 설명을 받고, 이를 맵(Map)에 저장한다. 설명은 WeakHashMap 인스턴스에 저장되기 때문에 대응 항목에 대한 참조가 더 이상 없을 경우 자동적으로 가비지-콜렉트(garbage-collected) 처리된다. 따라서 설명을 사라지게 하기 위한 별도의 메소드는 필요 없다.

enableHints 메소드는 변수인 탑컨테이너의 모든 항목, 하부항목, 메뉴요소 등에 대한 마우스 감지기로 MouseOverHintManager를 추가한다.

mouseEntered 메소드는 마우스 포인터가 항목을 가리키면 맵에서 해당되는 설명을 찾아 JLabel에 뿌려준다. mouseExited 메소드는 JLabel을 비워 설명을 제거한다.

트릭
상태표시줄 설명과 관련된 몇가지 간단한 트릭이 있다.

우선 mouseEntered 메소드는 ‘이벤트를 생성하는 항목’의 설명을 받는다는 점을 이용한다. mouseEntered는 해당 항목에 대응하는 설명이 없으면 상위 항목을 확인한다. 이 프로세스는 최상위 항목에 도달하거나 설명이 발견될 때까지 계속된다. 이 특성을 이용해 패널과 같은 하나의 컨테이너에 대해 하위 모든 요소들이 같은 설명을 공유하도록 하거나, 또는 상위 항목과 하위 항목에 대해 서로 다른 설명을 설정할 수 있다.

또다른 트릭은 getHintFor 메소드다. mouseEntered는 항목에 대한 설명을 받기위해 getHintFor를 호출한다. 그런데 변수로 넘겨받은 항목에 설명이 존재하지 않는 경우, getHintFor는 몇가지 특별한 경우를 체크한다. 문제의 항목이 JLabel, 또는 JTableHeader일 경우, getHintFor 메소드는 각각 JLabel이 붙여진 설명과 Jtable 설명을 리턴한다. 즉 항목과 라벨에 대해 별도로 두 번 설명을 설정할 필요가 없는 것이다. 각자 서로 다른 항목으로 보여지는 경우라도 마찬가지다.

설명을 넣자
MouseOverHintDemo(소스보기2)는 MouseOverHintManager 클래스 사용에 대한 간단한 예로, 상태표시줄이 있는 JFrame을 생성한다. 이 JFrame은 가장 흔히 사용되는 스윙 요소 및 설명을 보여준다.

소스보기 2
import java.awt.*;
import java.util.*;
import javax.swing.*;
import javax.swing.table.*;


class MouseOverHintDemo {
  public static void main( String[] args ) {
    JLabel hintBar = new JLabel(" ");
   MouseOverHintManager hintManager = new MouseOverHintManager(hintBar);
    JFrame frame = new JFrame("MouseOverHintDemo");
    frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
  
    JMenuBar menuBar = new JMenuBar();
    JMenu menu = new JMenu("File");
    JMenuItem item1 = new JMenuItem("Load");
    JMenuItem item2 = new JMenuItem("Save");
    JMenuItem item3 = new JMenuItem("Exit");
  
    Box mainPanel = Box.createVerticalBox();
    JButton button = new JButton("Apply");
    JCheckBox checkBox = new JCheckBox("Disable hints");
    JLabel label = new JLabel("Backup strategy");
    JComboBox comboBox = new JComboBox(new String[] {"Always","Just the last","Never"});
    JFormattedTextField formattedText = new JFormattedTextField(new Date());
    Box radioPanel = Box.createVerticalBox();
    ButtonGroup radioGroup = new ButtonGroup();
    JRadioButton radio1 = new JRadioButton("left");
    JRadioButton radio2 = new JRadioButton("right");
    JTable table = new JTable(new String[][] {{"Copy","Ctrl+C"},{"Paste","Ctrl+V"},{"Cut","Ctrl+X"}}, new String[] {"Action","Shortcut"});
  
    hintManager.addHintFor( item1, "Loads a new file" );
    hintManager.addHintFor( item2, "Saves the current file" );
    hintManager.addHintFor( item3, "Exits the application" );
    hintManager.addHintFor( button, "Apply any changes made" );
    hintManager.addHintFor( checkBox, "Turns off the display of hints" );
    hintManager.addHintFor( comboBox, "Selects how many backups to make" );
    hintManager.addHintFor( formattedText, "Enters the date for next run" );
    hintManager.addHintFor( radioPanel, "Selects the position for application's toolbar" );
    hintManager.addHintFor( table, "Shortcuts for each application's action" );
  
    frame.setJMenuBar( menuBar );
    menuBar.add( menu );
    menu.add( item1 );
    menu.add( item2 );
    menu.add( item3 );
    frame.getContentPane().add( mainPanel, BorderLayout.CENTER );
    mainPanel.add( Box.createVerticalStrut(5) );
    mainPanel.add( button );
    mainPanel.add( Box.createVerticalStrut(5) );
    mainPanel.add( checkBox );
    mainPanel.add( Box.createVerticalStrut(5) );
    mainPanel.add( label );
    label.setLabelFor( comboBox );
    mainPanel.add( comboBox );
    mainPanel.add( Box.createVerticalStrut(5) );
   mainPanel.add( formattedText );
    mainPanel.add( Box.createVerticalStrut(5) );
    radioGroup.add( radio1 );
    radioGroup.add( radio2 );
    radioPanel.setBorder( BorderFactory.createTitledBorder(BorderFactory.createEtchedBorder(),"Toolbar") );
    radioPanel.add( radio1 );
    radioPanel.add( radio2 );
    mainPanel.add( radioPanel );
    mainPanel.add( Box.createVerticalStrut(5) );
    mainPanel.add( table.getTableHeader() );
    mainPanel.add( table );
    mainPanel.add( Box.createVerticalStrut(5) );
    hintBar.setBorder( BorderFactory.createLoweredBevelBorder() );
    frame.getContentPane().add( hintBar, BorderLayout.SOUTH );
    frame.pack();
  
    hintManager.enableHints( frame );
    frame.setVisible( true );
  }
}


이 예제는 단순명료하다. 사용자 인터페이스의 모든 항목을 생성하고 이들에 대해 설명을 설정한 후 애플리케이션 윈도우를 만들고 ouseOverHintManager.enableHints를 호출한다. 유의할 점은 유저 인터페이스를 구성한 후에 끝으로 enableHints를 호출해야 enableHints가 모든 항목에 대한 마우스 감지기로 등록된다는 것이다. 이렇게 해야 MouseOverHintManager가 모든 항목의 마우스 이벤트를 접수해 각각에 해당하는 설명을 선택할 수 있다.

소스코드2 예제를 실행하면 몇가지 조절기능을 갖춘 간단한 윈도우가 뜬다. 항목과 메뉴 위로 마우스를 움직이면서 상태표시줄에 각각에 해당되는 설명이 나타나는지 확인해 보자. 2개의 라디오 버튼에 대해서는 같은 설명이 표시되는데, 이는 상위 패널에서 설명을 설정했기 때문이다.

기능에 대한 설명은 애플리케이션 사용을 쉽게 한다는 점에서 중요한 부분이다. 스윙 툴팁의 대안인 상태표시줄 설명 방법은 적용하기도 쉽다. 사용자들은 결코 메뉴얼을 보지 않기 때문에 프로그램을 사용하기 쉽고 직관적으로 이용할 수 있도록 만들어야 하는 것이 개발자들의 임무지만, 자바 GUI를 사용하는 경우라면 상기한 상태표시줄 설명이라는 간단한 도움기능을 넣을 수 있을 것이다. @
반응형

쓰레딩과 동기화


쓰레딩과 동기화는 Java Language Specification (JLS)에 명시되어 있듯, 자바 프로그래밍 언어의 핵심 기능이다. RTSJ는 JLS의 핵심 기능들을 수 많은 방식으로 확장한다. (참고자료) 예를 들어, RTSJ는 일반 자바 쓰레드 보다 더욱 엄격한 스케줄링 정책을 따라야 하는 새로운 실시간(RT) 쓰레드 유형을 도입했다. 또 다른 예로, 우선 순위 상속(priority inheritance)이 있는데, 이는 잠금이 경쟁할 때 잠금 동기화가 관리되는 방법을 정의한 잠금 정책이다.

우선 순위와 우선 순위 큐의 관리를 이해하는 것이 RTSJ의 쓰레딩과 동기화 변화를 이해하는데 유용하다. 우선 순위 역시 RT 애플리케이션에는 중요한 장치이다. 이 글에서는 쓰레드 우선 순위와 우선 순위 큐가 관리되는 방법을 논하면서 RTSJ 쓰레딩과 동기화 측면을 설명하도록 하겠다. IBM WebSphere® Real Time을 사용하여 구현하는 것을 포함하여, RT 애플리케이션을 개발, 전개, 실행할 때 고려해야 하는 사항들도 설명한다. (참고자료).

일반 자바 쓰레드 이해하기

JLS에서 정의된 쓰레드는 일반 자바 쓰레드(regular Java threads)라고 한다. 일반 자바 쓰레드는 1부터 10까지 정수 우선 순위를 가진 java.lang.Thread 클래스의 인스턴스이다. 많은 실행 플랫폼을 수용하기 위해 JLS는 일반 자바 쓰레드의 우선 순위 구현, 스케줄링, 관리 방식에 많은 유연성을 가져왔다.

WebSphere Real Time을 포함하여, 리눅스 기반 WebSphere VM은 리눅스® OS가 제공하는 네이티브 쓰레딩 서비스를 사용한다. 리눅스 쓰레딩과 동기화를 이해함으로써 자바 쓰레딩과 동기화를 이해할 수 있다.

리눅스 쓰레딩과 동기화

리눅스 OS는 그 동안 다양한 사용자 레벨 쓰레딩 구현을 제공했다. Native POSIX Thread Library (NPTL)는 리눅스용 최신 전략적 쓰레딩 방향이고 WebSphere VM에서 사용된다. (참고자료) 전 구현에 비해 NPTL이 가진 장점은 POSIX 호환성과 더 나은 성능을 보인다는 점이다. POSIX 서버는 libpthread.so 동적 라이브러리와 기반 리눅스 커널 지원을 통해 런타임에서 사용할 수 있다. 리눅스 커널은 쓰레드 우선 순위 레벨 같은 정적 컨트롤과 시스템에서 실행되는 쓰레드의 동적인 조건을 기반으로 쓰레드 스케줄링을 수행한다.

POSIX에서는 다양한 스케줄링 정책과 우선 순위를 사용하여 POSIX 쓰레드(pthreads)를 만들어서 다양한 애플리케이션 필요를 채울 수 있다. 다음은 스케줄링 정책들이다.

  • SCHED_OTHER
  • SCHED_FIFO
  • SCHED_RR

SCHED_OTHER 정책은 프로그램 개발 툴, 오피스 애플리케이션, 웹 브라우저 같은 전통적인 사용자 태스크에 사용된다. SCHED_RRSCHED_FIFO는 보다 결정론적이며 시의성을 요구하는 애플리케이션에 사용된다. SCHED_RRSCHED_FIFO의 주요한 차이는 SCHED_RR 은 쓰레드의 실행에 타임 슬라이스 방식을 취하는 반면, SCHED_FIFO는 그렇지 않다는 점이다. SCHED_OTHERSCHED_FIFO 정책은 WebSphere Real Time에 의해 사용되며, 아래에 자세히 설명해 놓았다. (WebSphere Real Time이 사용하지 않는 SCHED_RR 정책에 대해서는 다루지 않겠다.

POSIX는 pthread_mutex 데이터 유형을 통해 잠금과 동기화 지원을 제공한다. pthread_mutex는 다양한 잠금 정책들로 만들어 질 수 있다. 잠금 정책은 한 개 이상의 쓰레드가 같은 잠금을 동시에 얻어야 할 때 실행 방식에 영향을 준다. 표준 리눅스 버전들은 하나의 기본 정책을 지원하지만, RT 리눅스 버전은 우선 순위 상속 잠금 정책도 지원한다. 동기화 개요 섹션에서 우선 순위 상속 정책에 대해 자세히 설명하겠다.

리눅스 스케줄링과 잠금에는 first in, first out (FIFO) 큐 관리가 포함된다.

일반 자바 쓰레드에서의 쓰레드 스케줄링

RTSJ에서는 일반 자바 쓰레드의 작동이 JLS에 정의된 것과 같다고 명시하고 있다. WebSphere Real Time에서, 일반 자바 쓰레드는 리눅스의 POSIX SCHED_OTHER 스케줄링 정책을 사용하여 구현된다. SCHED_OTHER 정책은 결정론을 필요로 하는 태스크 보다는 컴파일러와 워드 프로세서 같은 애플리케이션에 적용된다.

2.6 리눅스 커널에서, SCHED_OTHER 정책은 40 개의 우선 순위 레벨을 지원한다. 이러한 40 개의 우선 순위 레벨은 프로세서 기반으로 관리된다.

  • 리눅스는 캐시 성능 때문에 같은 프로세서에 하나의 쓰레드를 실행한다.
  • 쓰레드 스케줄링은 시스템 당 잠금 대신에 프로세서 당 잠금을 주로 사용한다.

필요할 경우, 리눅스는 하나의 프로세서에서 다른 프로세서로 쓰레드를 이동하여 워크로드의 균형을 맞춘다.

40개의 우선 순위 레벨에서, 리눅스는 활성 큐(active queue)와 종료 큐(expiration queue)를 관리한다. 각 큐에는 연결된 쓰레드 리스트가 포함되어 있거나 비어있다. 활성 큐와 종료 큐는 효율성, 로드 밸런싱, 기타 목적에 쓰인다. 이론상으로는, 시스템을 각 40 개의 우선 순위에 대해 실행 큐(run-queue)라고 하는 하나의 FIFO 큐를 관리하는 것으로 볼 수 있다. 쓰레드는 가장 높은 우선 순위를 가진 비어있지 않은 실행 큐부터 내보낸다. 이 쓰레드는 큐에서 제거되고 time quantum 또는 time slice라는 기간 동안 실행된다. 실행 쓰레드가 time quantum을 종료하면, 실행 큐의 끝에 배치되고 새로운 time quantum이 할당된다. 큐의 헤드에서 파견하고 종료된 쓰레드를 큐의 끝에 배치함으로써, 우선 순위 내에서 round-robin 방식으로 실행한다.

쓰레드에 주어진 time quantum은 쓰레드의 할당된 우선 순위에 기반한다. 높은 할당 우선 순위를 가진 쓰레드에게는 더 오랜 실행 time quantum이 주어진다. 쓰레드가 CPU를 차지하지 않도록 하기 위해, 리눅스는 쓰레드가 I/O 바운드 인지 또는 CPU 바운드인지에 따라서 쓰레드의 우선 순위를 높이거나 낮춘다. 쓰레드는 yielding (Thread.yield() 호출)에 의해 time slice를 자발적으로 포기하거나, 블로킹에 의해서 컨트롤을 포기한다. 이 시점에서 쓰레드는 이벤트 발생을 기다린다. 이 같은 이벤트는 잠금 해제를 통해 실행될 수 있다.

WebSphere Real Time의 VM은 40개의 SCHED_OTHER 리눅스 쓰레드 우선 순위 범위에서 10 개의 일반 자바 쓰레드 우선 순위는 명확히 할당하지 않는다. 모든 일반 자바 쓰레드는, 자바 우선 순위와 관계 없이, 기본 리눅스 우선 순위로 할당된다. 기본 리눅스 우선 순위는 40개의 SCHED_OTHER 우선 순위 범위의 중간에 있다. 기본을 사용함으로써, 일반 자바 쓰레드는 리눅스의 동적인 우선 순위 조정에 관계 없이, 실행 큐에 있는 모든 일반 자바 쓰레드가 실행되도록 한다. 이것은 RT 쓰레드를 실행하는 시스템을 실행하거나 실행하지 않는 일반 자바 쓰레드들만을 가진 시스템으로 가정한다.

WebSphere Real Time의 VM과 비 RT 버전의 WebSphere VM은 일반 자바 쓰레드에 SCHED_OTHER 정책과 기본 우선 순위 할당을 사용한다. 같은 정책을 사용함으로써, 두 개의 JVM들은 비슷하지만 동일하지는 않은 쓰레드 스케줄링과 동기화 특성을 갖추게 된다. RTSJ와 RT Metronome 가비지 컬렉터(참고자료 보기)의 도입으로 인한 WebSphere Real Time 클래스 라이브러리의 변경, JVM의 변경, JIT 컴파일러의 변경 때문에 두 JVM에서 동일한 타이밍과 성능 특성으로 애플리케이션이 실행될 수 없도록 한다. IBM WebSphere Real Time 테스팅 시, 타이밍 차이는 다른 JVM에서 수년 동안 잘 실행되었던 테스트 프로그램에서 경쟁 조건(다시 말해서, 버그)을 해결했다.




일반 자바 쓰레드를 사용한 코드 예제

Listing 1은 두 개의 쓰레드에 의해서 5초 간격으로 얼마나 많은 루프의 반복이 실행될 수 있는지를 보여주고 있다.


Listing 1. 일반 자바 쓰레드

                
class myThreadClass extends java.lang.Thread {
   volatile static boolean Stop = false;

   // Primordial thread executes main()
   public static void main(String args[]) throws InterruptedException {

      // Create and start 2 threads
      myThreadClass thread1 = new myThreadClass();
      thread1.setPriority(4);    // 1st thread at 4th non-RT priority
      myThreadClass thread2 = new myThreadClass();
      thread2.setPriority(6);    // 2nd thread at 6th non-RT priority
      thread1.start();           // start 1st thread to execute run()
      thread2.start();           // start 2nd thread to execute run()

      // Sleep for 5 seconds, then tell the threads to terminate
      Thread.sleep(5*1000);
      Stop = true;
   }

   public void run() { // Created threads execute this method
      System.out.println("Created thread");
      int count = 0;
      for (;Stop != true;) {    // continue until asked to stop
         count++;
         Thread.yield();   // yield to other thread
      }
      System.out.println("Thread terminates. Loop count is " + count);
   }
}

Listing 1의 프로그램에는 일반 자바 쓰레드들인 세 개의 사용자 쓰레드가 있다.

  • 기본 쓰레드
    • 프로세스 시작 시 생성된 메인 쓰레드이며, main() 메소드를 실행한다.
    • main()은 두 개의 일반 자바 쓰레드를 생성한다. 한 쓰레드는 우선 순위 4이고, 또 다른 쓰레드는 우선 순위 6이다.
    • 메인 쓰레드는 Thread.sleep()을 호출함으로써 5초 동안 수면(sleep)하는 것으로 차단을 수행한다.
    • 5초의 수면 후에, 이 쓰레드는 다른 두 개의 쓰레드 종료를 명령한다.
  • 우선 순위 4의 쓰레드
    • 이 쓰레드는 기본 쓰레드에 의해 생성되며, for 루프를 포함하고 있는 run() 메소드를 실행한다.
    • 이 쓰레드는,
      1. 각 루프 반복 시 카운트를 늘린다.
      2. Thread.yield()를 호출함으로써 타임 슬라이스를 자발적으로 포기한다.
      3. 메인 쓰레드가 요청될 때 종료한다. 종료 직전에, 쓰레드는 루프 카운트를 프린트 한다.
  • 우선 순위 6의 쓰레드: 이 쓰레드는 우선 순위 4 쓰레드와 같은 액션을 수행한다.

이 프로그램이 유니프로세서(uniprocessor) 또는 로딩되지 않은 멀티프로세서 시스템에서 실행된다면, 각 쓰레드는 for 루프 반복 카운트에 가까운 수를 프린트 한다. 프린트 내용은 다음과 같다.

Created thread
Created thread
Thread terminates. Loop count is 540084
Thread terminates. Loop count is 540083

Thread.yield()에 대한 호출이 제거되면, 두 개의 쓰레드에 대한 루프 카운트들은 거의 비슷하지만 동일하지는 않다. 두 쓰레드 모두 SCHED_OTHER 정책의 같은 기본 우선 순위가 할당되었기 때문에, 두 쓰레드에는 같은 타임 슬라이스가 주어진다. 이 쓰레드는 동일한 코드를 실행하기 때문에 비슷한 동적인 우선 순위 조정을 해야 하고 같은 실행 큐에서 round-robin 방식으로 실행되어야 한다. 하지만, 우선 순위 4의 쓰레드가 첫 번째로 실행되기 때문에, 5초 실행 간격에서 더 많은 부분을 차지하고 더 높은 루프 카운트로 출력되는 것이다.




RT 쓰레드 이해하기

RT 쓰레드는 javax.realtime.RealtimeThread의 인스턴스이다. RTSJ는 이 스팩이 RT 쓰레드에 최소한 28 개의 우선 순위를 제공해야 한다고 요구하고 있다. 이러한 우선 순위들을 실시간 우선 순위(real-time priorities)라고 한다. RT 우선 순위 범위의 시작 값에 대해서는 10보다 커야 한다고 스팩에서는 지정하고 있다. 이는 일반 자바 쓰레드에 주어진 가장 높은 우선 순위 값이다. 애플리케이션 코드는 새로운 PriorityScheduler 클래스의 getPriorityMin()getPriorityMax()를 사용하여 가용 RT 우선 순위 값의 범위를 정한다.

RT 쓰레드의 동기

JLS의 쓰레드 스케줄링은 정확하지 않고 단 10 개의 우선 순위 값을 제공한다. POSIX SCHED_OTHER는 다양한 애플리케이션의 필요를 채우고 있다. 하지만 SCHED_OTHER 정책에도 바람직하지 못한 특성들이 있다. 동적인 우선 순위 조정과 타임 슬라이스가 예견하지 못한 시간에 발생할 수 있다. 40개의 SCHED_OTHER 우선 순위도 충분하지 않으며, 이러한 우선 순위 범위는 이미 일반 자바 쓰레드와를 가진 애플리케이션과 동적 우선 순위 조정은 당연한 것이다. JVM 역시 가비지 컬렉션(GC) 같은 특별한 목적에 내부 쓰레드용 우선 순위를 필요로 한다.

결정론의 부족, 더 많은 우선 순위 레벨의 필요, 기존 애플리케이션과의 호환성 등이 새로운 스케줄링 기능을 제공하는 확장을 만드는 동기가 되었다. javax.realtime 패키지의 클래스들은 RTSJ에서 설명한 대로, 이러한 기능들을 제공한다. WebSphere Real Time에서, Linux SCHED_FIFO 스케줄링 정책은 RTSJ 스케줄링 필요를 다루고 있다.

RT 자바 쓰레드에 대한 쓰레드 스케줄링

WebSphere Real Time에서, 28개의 RT 자바 우선 순위들이 지원되고 11에서 38까지의 범위를 갖고 있다. PriorityScheduler 클래스의 API는 이러한 범위를 검색하는데 사용된다. 이 섹션에서는 RTSJ에서 설명하고 있는 쓰레드 스케줄링을 상세히 설명하고, RTSJ의 요구 사항들에 부합하는 Linux SCHED_FIFO 정책을 설명하도록 하겠다.

RTSJ는 RT 우선 순위들이 각 RT 우선 순위들을 위해 개별 큐를 관리하는 런타임 시스템에 의해 논리적으로 구현되고 있는 것으로 간주한다. 쓰레드 스케줄러는 비어있지 않은 가장 높은 우선 순위 큐의 헤드에서 파견된다. 어떤 쓰레드도 RT 우선 순위용 큐에 없다면, 일반 자바 쓰레드가 파견된다. (일반 자바 쓰레드에 대한 쓰레드 스케줄링).

RT 우선 순위로 파견된 쓰레드는 차단될 때까지 실행될 수 있고, yielding에 의해서 컨트롤을 자발적으로 포기하거나, 더 높은 RT 우선 순위를 가진 쓰레드로 선점된다. 자발적으로 양보하는 RT 우선 순위를 가진 쓰레드는 큐의 뒤쪽에 배치된다. RTSJ 역시 이 같은 스케줄링이 일정한 시간으로 수행되어야 함을 명시하고 있고 실행 중인 RT 쓰레드의 수 같은 요소에 기반하여 달라질 수 없다고 명시하고 있다. RTSJ의 1.02 버전은 이러한 규칙을 유니프로세서 시스템에 적용하고 있다. RTSJ에는 멀티프로세서 시스템에 스케줄링이 어떻게 발생하는지에 대해서는 언급되어 있지 않다.

리눅스는 모든 RTSJ 스케줄링 요구 사항에 SCHED_FIFO 정책을 제공한다. SCHED_FIFO 정책은 사용자 태스크가 아닌 RT용이다. SCHED_FIFOSCHED_OTHER 정책과 달리 99 개의 우선 순위 레벨을 제공한다. SCHED_FIFO는 쓰레드에 타임 슬라이스를 수행하지 않는다. 또한, SCHED_FIFO FIFO 정책은 우선 순위 상속 잠금 정책을 제외하고는 RT 쓰레드의 우선순위를 동적으로 조정하지 않는다. (동기화 개요 섹션) 우선 순위 상속 때문에 우선 순위 조정은 RTSJ에 필요한 것이다.

리눅스는 RT 쓰레드와 일반 자바 쓰레드에 일정한 시간 스케줄링을 제공한다. 멀티프로세서 시스템에서, 리눅스는 가용 프로세서로 파견된 RT 쓰레드의 하나의 글로벌 큐의 작동을 모방한다. 이는 RTSJ의 원리와 흡사하지만, 일반 자바 쓰레드에 사용되는 SCHED_OTHER 정책과는 다르다.

RT 쓰레드를 사용하는 문제가 많은 코드 예제

Listing 2는 Listing 1의 코드를 수정하여 일반 자바 쓰레드 대신 RT 쓰레드를 만든다. java.lang.Thread 대신 java.realtime.RealtimeThread를 사용한다는 것부터 다르다. 첫 번째 쓰레드는 4의 RT 우선 순위 레벨로 생성되고, 두 번째 쓰레드는 6의 RT 우선 순위 레벨로 생성된다. 이는 getPriorityMin() 메소드에 의해 결정된 것이다.


Listing 2. RT 쓰레드

                
import javax.realtime.*;
class myRealtimeThreadClass extends javax.realtime.RealtimeThread {
   volatile static boolean Stop = false;

   // Primordial thread executes main()
   public static void main(String args[]) throws InterruptedException {

      // Create and start 2 threads
      myRealtimeThreadClass thread1 = new myRealtimeThreadClass();
      // want 1st thread at 4th real-time priority
      thread1.setPriority(PriorityScheduler.getMinPriority(null)+ 4);
      myRealtimeThreadClass thread2 = new myRealtimeThreadClass();
      // want 2nd thread at 6th real-time priority
      thread2.setPriority(PriorityScheduler.getMinPriority(null)+ 6);
      thread1.start();           // start 1st thread to execute run()
      thread2.start();           // start 2nd thread to execute run()

      // Sleep for 5 seconds, then tell the threads to terminate
      Thread.sleep(5*1000);
      Stop = true;
   }

   public void run() { // Created threads execute this method
      System.out.println("Created thread");
      int count = 0;
      for (;Stop != true;) {    // continue until asked to stop
         count++;
         // Thread.yield();   // yield to other thread
      }
      System.out.println("Thread terminates. Loop count is " + count);
   }
}

Listing 2의 수정된 코드는 몇 가지 문제를 갖고 있다. 프로그램이 유니프로세서 환경에서 실행되면, 절대로 종료되지 않고 다음을 프린트 한다.

Created thread

이 결과는 RT 쓰레드 스케줄링 작동에 의해 설명될 수 있다. 기본 쓰레드는 일반 자바 쓰레드로 남아있고, 비 RT(SCHED_OTHER) 정책으로 실행된다. 기본 쓰레드가 첫 번째의 RT 쓰레드를 시작하자마자, RT 쓰레드는 기본 쓰레드를 선점하고, RT 쓰레드는 무한정 실행된다. Time quantum에 의해 한정되지 않았고 쓰레드 차단도 수행하지 않기 때문이다. 기본 쓰레드가 선점된 후에는 실행될 수 없기 때문에 두 번째 RT 쓰레드를 시작할 수 없다. Thread.yield()는 기본 쓰레드를 실행하는데 어떤 영향도 줄 수 없다. yielding은 논리적으로 RT 쓰레드를 실행 큐의 끝에 배치하지만, 이 쓰레드 스케줄러는 같은 쓰레드를 다시 파견한다. 이것이 가장 높은 우선 순위를 가지고 실행 큐의 앞에 있는 쓰레드이기 때문이다.

프로그램은 또한 Two-processor 시스템에서는 실패한다. 다음이 프린트 된다.

Created thread
Created thread

기본 쓰레드는 두 개의 RT 쓰레드를 만들 수 있다. 하지만, 두 번째 쓰레드를 만든 후에, 기본 쓰레드는 선점되고 쓰레드에게 종료를 명령할 수 없다. 두 개의 RT 쓰레드는 Two-processor 시스템에서 실행되고 절대로 차단하지 않기 때문이다.

프로그램은 끝까지 실행되고 세 개 이상의 프로세서를 가진 시스템에서 결과를 만들어 낸다.

단일 프로세서에서 실행되는 RT 코드 예제

Listing 3은 유니프로세서 시스템에서 올바르게 실행되도록 수정된 코드 모습이다. main() 메소드의 로직은 RT 우선 순위 8을 가진 "주" RT 쓰레드로 옮겨갔다. 이 우선 순위는 주 RT 쓰레드가 만든 다른 두 개의 RT 쓰레드의 우선 순위 보다 더 높다. 가장 높은 우선 순위를 지닌다는 것은 주 RT 쓰레드가 두 개의 RT 쓰레드를 성공적으로 생성할 수 있고, 5초 수면에서 깨어났을 때 주 RT 쓰레드가 현재 실행되는 쓰레드를 선점할 수 있다는 것을 의미한다.


Listing 3. 수정된 RT 쓰레드 예제

                
import javax.realtime.*;
class myRealtimeThreadClass extends javax.realtime.RealtimeThread {
   volatile static boolean Stop = false;

   static class myRealtimeStartup extends javax.realtime.RealtimeThread {

   public void run() {
      // Create and start 2 threads
      myRealtimeThreadClass thread1 = new myRealtimeThreadClass();
      // want 1st thread at 4th real-time priority
      thread1.setPriority(PriorityScheduler.getMinPriority(null)+ 4);
      myRealtimeThreadClass thread2 = new myRealtimeThreadClass();
      // want 1st thread at 6th real-time priority
      thread2.setPriority(PriorityScheduler.getMinPriority(null)+ 6);
      thread1.start();           // start 1st thread to execute run()
      thread2.start();           // start 2nd thread to execute run()

      // Sleep for 5 seconds, then tell the threads to terminate
      try {
                        Thread.sleep(5*1000);
      } catch (InterruptedException e) {
      }
      myRealtimeThreadClass.Stop = true;
      }
   }

   // Primordial thread creates real-time startup thread
   public static void main(String args[]) {
      myRealtimeStartup startThr = new myRealtimeStartup();
      startThr.setPriority(PriorityScheduler.getMinPriority(null)+ 8);
      startThr.start();
   }

   public void run() { // Created threads execute this method
      System.out.println("Created thread");
      int count = 0;
      for (;Stop != true;) {    // continue until asked to stop
         count++;
         // Thread.yield();   // yield to other thread
      }
      System.out.println("Thread terminates. Loop count is " + count);
   }
}

이 프로그램이 유니프로세서에서 실행되면, 다음과 같이 프린트 된다.

Created thread
Thread terminates. Loop count is 32767955
Created thread
Thread terminates. Loop count is 0

이 프로그램의 결과는 모든 쓰레드가 실행된 후 종료하지만, 두 쓰레드 중 단 하나만 for 루프를 반복한다는 것을 나타내고 있다. 이러한 결과는 RT 쓰레드의 우선 순위를 따져보면 이해될 수 있다. 주 RT 쓰레드는 Thread.sleep() 호출에 의해 차단될 때까지 실행된다. 주 RT 쓰레드는 두 개의 RT 쓰레드를 만들지만, RT 우선 순위 6의 두 번째 RT 쓰레드만이 주 RT 쓰레드가 수면하고 있는 동안 실행될 수 있다. 이 쓰레드는 주 RT 쓰레드가 종료되면, 우선 순위 6의 쓰레드는 실행 및 종료될 수 있다. 그리고 0외의 값으로 루프 카운트를 출력한다. 이 쓰레드가 종료된 후에, RT 우선 순위 4의 쓰레드가 실행될 수 있지만, 종료되어야 하는 명령을 받았기 때문에 for 루프를 우회한다. 이 쓰레드는 종료하기 전에 루프 카운트 값, 0을 출력한다.




위로


RT 애플리케이션을 위한 쓰레딩 고려 사항

이 섹션에서는 애플리케이션들을 포팅하여 RT 쓰레드를 사용하거나 RT 쓰레딩을 활용하는 새로운 애플리케이션을 작성할 때 고려해야 할 사항들을 설명한다.

RT 쓰레드의 새로운 확장

RTSJ는 RT 쓰레드를 만들어서 특정 시간 또는 적절한 시간에 시작하도록 하는 장치를 지정하고 있다. 지정된 시간 간격이나 주기로 특정 로직을 실행하는 쓰레드를 만들 수 있다. 로직이 지정된 기간 내에 완료되지 못할 때 AsynchronousEventHandler (AEH)를 실행하는 쓰레드를 정의할 수 있다. 또한, 쓰레드가 소비할 수 있는 메모리 유형과 양에 대한 한계도 정의할 수 있다. 쓰레드가 제한량 보다 더 많은 것을 소비할 때 OutOfMemoryError가 던져지는 방식이다. 이러한 장치들은 RT 쓰레드에서만 사용할 수 있고 일반 자바 쓰레드에서는 사용할 수 없다. RTSJ에는 이러한 장치들에 대한 자세한 정보가 수록되어 있다.

Thread.interrupt()과 보류(pending) 예외

Thread.interrupt() 작동은 RT 쓰레드를 위해 확장된다. 이 API는 JLS에서 기술된 것처럼 차단된 쓰레드를 인터럽트 한다. 이러한 예외는 사용자가 Throws AsynchronouslyInterruptedException문을 메소드 선언에 추가함으로써, 인터럽트 가능한 것으로 명확하게 표시했던 메소드에서도 발생할 수 있다. 이 예외는 사용자가 예외를 명확하게 처리해야 한다는 점에서, 이 쓰레드에는 성가신 존재이다. 그렇지 않으면, 쓰레드에 속한 채로 남아있는데, 이를 보류(pending)라고 한다. 사용자가 예외를 제거하지 않으면, 쓰레드는 골치 아픈 예외가 여전히 걸린 채로 종료될 수 있다. 이러한 에러는 RT 쓰레드 고유의 풀링 폼을 수행하는 애플리케이션이 아닌,"정상적인" 방식으로 종료한다면 비교적 양호한 것이다. 다시 말해서, 쓰레드는 InterruptedException 예외가 있는 상태로 풀로 리턴 될 수 있다. 이 경우, 쓰레드 풀링을 수행하는 코드는 예외를 명확히 제거한다. 그렇지 않으면, 예외는 풀링된 쓰레드가 다시 할당될 때 예외가 생길 것이다.

기본 쓰레드와 애플리케이션 디스패치 로직

기본 쓰레드는 언제나 RT 쓰레드가 아닌 일반 자바 쓰레드이다. 첫 번째 RT 쓰레드는 늘 일반 자바 쓰레드에 의해 생성된다. 이 RT 쓰레드는 RT 쓰레드와 일반 자바 쓰레드를 동시에 실행하는 프로세서가 불충분하다면 일반 자바 쓰레드를 즉각 선점한다. 선점으로 인해 일반 자바 쓰레드는 더 이상의 RT 쓰레드나 기타 로직을 만들 수 없어서 애플리케이션은 초기화된 상태가 된다.

높은 우선 순위 RT 쓰레드에서 애플리케이션 초기화를 수행함으로써 이런 문제를 해결할 수 있다. 이 기술은 고유한 쓰레드 풀링과 쓰레드 디스패치를 수행하는 애플리케이션이나 라이브러리에 필요하다. 쓰레드-디스패치 로직은 높은 우선 순위 또는 높은 우선 순위 쓰레드에서 실행되어야 한다. 쓰레드-풀링 로직을 실행하기에 적합한 우선 순위를 선택하면 쓰레드 인큐잉(enqueueing)과 디큐잉(dequeueing)에서 생기는 문제를 피할 수 있다.

폭주(Runaway) 쓰레드

일반 자바 쓰레드는 time quantum으로 실행되고, CPU 사용에 기반하여 스케줄러가 수행하는 동적 우선 순위 조정은 모든 일반 자바 쓰레드가 모두 실행될 수 있도록 한다. 반대로, RT 쓰레드는 time quantum에 의해 바운드 되지 않고, 쓰레드 스케줄러는 CPU 사용에 기반하여 어떤 형태의 동적 우선 순위 조정도 수행하지 않는다. 일반 자바 쓰레드와 RT 쓰레드간 스케줄링 정책에 있어서의 차이점은 폭주 RT 쓰레드가 발생할 수 있는 기회를 만들어 낸다. 폭주 RT 쓰레드는 시스템을 제어할 수 있고, 다른 애플리케이션들이 실행되지 못하게 하며, 사용자들이 시스템에 로그인 하지 못하게 한다.

개발과 테스팅 동안, 폭주 쓰레드의 영향력을 줄이는데 도움이 되었던 한 가지 기술은 프로세스가 사용할 수 있는 CPU 양에 한계를 설정하는 것이다. 리눅스에서, CPU 소비량을 제한하면 CPU 한계에 다다를 때 폭주 쓰레드는 죽는다. 또한, 시스템 상태를 감시하거나 시스템 로그인을 제공하는 프로그램들은 높은 RT 우선 순위로 실행되어 프로그램이 문제가 많은 쓰레드들을 선점할 수 있도록 한다.

자바 우선 순위와 OS 우선 순위 매핑하기

리눅스에서, POSIX SCHED_FIFO 정책은 1부터 99까지의 정수 범위에서 99의 RT 우선 순위를 제공한다. 이러한 시스템 범위에서, 우선 순위 11부터 89까지는 WebSphere VM 에 의해 사용되고, 이 범위의 일부는 28 RTSJ 우선 순위를 구현하는데 사용된다. 28 RT 자바 우선 순위를 POSIX 시스템 우선 순위로 매핑하는 것은 IBM WebSphere Real Time 문서에 설명되어 있다. 애플리케이션 코드는 이러한 매핑에 의존해서는 안되고, 자바 레벨에서 28 RT 우선 순위의 순서에 의존해야 한다. 이렇게 하면 JVM은 이 범위를 재 매핑하고, 앞으로의 WebSphere Real Time 릴리스에서는 이 부분이 향상될 것이다.

애플리케이션은 WebSphere Real Time에서 사용되었던 것 보다 높거나 낮은 RT 우선 순위를 요할 때 SCHED_FIFO 우선 순위 1 또는 우선 순위 90을 사용하여 데몬이나 기타 RT 프로세스를 구현할 수 있다.

JNI AttachThread()

Java Native Interface (JNI)에서는 JNI AttachThread() API를 사용하여 C 코드에서 생성된 쓰레드를 JVM에 붙일 수 있지만, RTSJ는 RT 쓰레드를 붙이기 위한 JNI 인터페이스를 변경하거나 제공하지 않는다. 따라서, 애플리케이션은 JVM에 붙일 POSIX RT 쓰레드를 C 코드로 만들지 않도록 해야 한다. 대신, 이 같은 RT 쓰레드는 자바 언어에서 생성되어야 한다.

분기된 프로세스와 RT 우선 순위

쓰레드는 또 다른 프로세스를 분기할 수 있다. 리눅스에서, 분기된 프로세스의 기본 쓰레드는 부모 쓰레드의 우선 순위를 상속 받는다. 분기된 프로세스가 JVM이라면, JVM의 기본 쓰레드는 RT 우선 순위로 생성된다. 이는 일반 자바 쓰레드(기본 쓰레드)의 순서를 위반한 것이다. 이러한 상황을 피하려면, JVM은 기본 쓰레드가 비 RT 우선 순위를 갖게끔 한다. (SCHED_OTHER 정책)

Thread.yield()

Thread.yield()는 같은 우선 순위에 있는 쓰레드에만 실행을 양보하고 더 높거나 낮은 우선 순위를 가진 쓰레드에는 절대로 양보하지 않는다. 같은 우선 순위를 가진 쓰레드에 대한 Yielding은 Thread.yield()가 한 개 이상의 RT 우선 순위를 사용하는 RT 애플리케이션에서 사용될 때 문제를 일으킨다는 것을 시사한다. 꼭 필요한 것이 아니라면 Thread.yield()를 사용하지 않는 것이 좋다.

NoHeapRealtimeThreads

javax.realtime.NoHeapRealtimeThread (NHRT)는 javax.realtime.RealtimeThread의 하위 클래스인 RTSJ의 새로운 쓰레드 유형이다. NHRT는 RT 쓰레드와 같은 스케줄링 특성을 갖고 있다. 단, NHRT는 GC에 의해 선점되지 않고, NHRT는 자바 힙을 읽고 쓸 수 없다. NHRT는 RTSJ의 중요한 측면이기 때문에 다음 기술자료에서 자세히 다루도록 하겠다.

AsynchronousEventHandlers

AsynchronousEventHandler (AEH) 역시 새로운 것이고 이벤트가 발생할 때 실행되는 RT 쓰레드이다. 예를 들어, AEH를 설정하여 지정된 시간 또는 알맞은 시간에 실행할 수 있다. AEH 역시 RT 쓰레드와 같은 스케줄링 특성을 갖고 있고 힙과 힙이 아닌 곳에 적용된다.




위로


동기화 개요

많은 자바 애플리케이션들은 자바 쓰레딩 기능을 직접 사용하거나, 개발 중인 애플리케이션들은 한 개 이상의 쓰레드를 포함하고 있는 라이브러리를 사용한다. 멀티쓰레디드 프로그래밍에서 중요한 것은 프로그램을 올바르게 실행하는 것이고, 여러 쓰레드들이 실행되는 시스템에서 쓰레드 보안을 확립하는 것이다. 안전한 프로그램 쓰레드를 만들기 위해서는 잠금 또는 최소한의 머신 연산 같은 동기화 명령들을 사용하여 한 개 이상의 쓰레드들 간 공유되는 데이터로의 액세스를 직렬화 해야 한다. RT 애플리케이션 프로그래머들은 특정 시간 제약 속에서 프로그램을 실행해야 하는 상황에 놓인다. 이를 위해서, 구현 상세, 함축적 의미, 컴포넌트의 성능 애트리뷰트를 알아야 한다.

이제부터는 자바 언어가 제공하는 기본적인 동기화 프리머티브의 측면, 이러한 프리머티브가 RTSJ에서는 어떻게 변화되는지, 이러한 프리머티브를 사용할 때 RT 프로그래머가 알아야 하는 것에 대해 설명하겠다.

자바 동기화 개요

자바는 세 가지 핵심적인 동기화 프리머티브를 제공한다.

  • 동기화 된 메소드와 블록은 쓰레드가 시작 시 객체를 잠그고 종료 시 잠금을 해제하도록 한다.
  • Object.wait()는 객체 잠금을 해제하고 쓰레드는 대기한다.
  • Object.notify()는 객체에 대해 wait()을 수행하는 쓰레드의 블록을 해제한다. notifyAll()은 모든 대기자들의 차단을 해제한다.

wait()notify()를 수행하는 쓰레드는 객체를 잠가야 한다.

잠금 경쟁(Lock contention)은 또 다른 쓰레드에 의해서 이미 잠긴 객체를 잠그려고 할 때 발생한다. 이러한 일이 발생하면, 잠금을 획득하는 것에 실패한 쓰레드는 객체에 대한 잠금 경쟁자들의 논리적 큐에 놓이게 된다. 이와 비슷하게, 여러 쓰레드들은 같은 객체에 대해 Object.wait()을 수행할 수 있기 때문에, 객체 대기자들의 논리적 큐가 있다. JLS는 큐가 관리되는 방식을 지정하지 않지만, RTSJ는 이러한 작동을 요구하고 있다.

우선 순위 기반 동기화 큐

RTSJ는 쓰레드의 모든 큐들이 FIFO 및 쓰레드 기반이다. 우선 순위 기반 FIFO 역시 잠금 경쟁자들 및 잠금 대기자들의 큐에 적용된다. 논리적 관점에서 볼 때, 실행을 기다리는 쓰레드의 실행 큐와 비슷한 잠금 경쟁자들의 FIFO 우선 순위 기반 큐들이 있다. 또한 잠금 대기자들의 비슷한 큐가 있다.

잠금이 해제될 때, 시스템은 가장 높은 우선 순위 큐의 경쟁자들의 앞에서 쓰레드를 선택하여 객체 잠금을 시도한다. notify()가 수행될 때, 가장 높은 우선 순위 큐의 대기자들의 앞에 있는 쓰레드는 블록이 해제된다. 잠금 해제 또는 잠금 notify() 연산은 가장 높은 우선 순위 큐의 힙에 있는 쓰레드가 실행된다는 점에서 스케줄링 디스패치 연산과 비슷하다.

우선 순위 기반 동기화를 지원하기 위해, RT 리눅스를 수정해야 한다. WebSphere Real Time의 VM이 notify() 연산이 수행될 때 어떤 쓰레드의 블록이 해제되어야 하는지를 선택하는 책임을 리눅스에 위임한다.

우선 순위 도치 및 우선 순위 상속

우선 순위 도치(Priority inversion)는 높은 우선 순위 쓰레드가 낮은 우선 순위 쓰레드가 갖고 있는 잠금에 대해 블록 되는 조건이다. 중간 우선 순위 쓰레드는 잠금을 갖고 있고 낮은 우선 순위 쓰레드에 기반하여 실행되는 동안 낮은 우선 순위 쓰레드를 선점한다. 우선 순위 도치는 낮은 우선 순위 쓰레드와 높은 우선 순위 쓰레드의 진행을 지연시킨다. 우선 순위 도치에 기인한 지연 때문에 중대한 데드라인을 맞출 수 다. 이러한 상황은 그림 1의 첫 번째 타임 라인에 나타나 있다.

우선 순위 상속(Priority inheritance)은 우선 순위 도치를 피하는 기술이다. 우선 순위 상속은 RTSJ에서 의무화 하고 있다. 우선 순위 상속의 개념은 잠금 경쟁의 상황에서, 잠금 보유자의 우선 순위가 그 잠금을 획득하고자 하는 쓰레드의 우선 순위로 올라가는 것이다. 잠금 보유자의 우선 순위가 잠금이 해제될 때 기본 우선 순위로 "강등"된다. 이러한 상황에서 낮은 우선 순위 쓰레드는 잠금 경쟁이 발생할 때 높은 우선 순위로 실행되다가 잠금이 해제될 때 끝난다. 잠금 해제 시, 높은 우선 순위 쓰레드는 객체를 잠그고 실행을 계속한다. 중간 우선 순위 쓰레드는 높은 우선 순위 쓰레드를 지연시키지 못하도록 되어있다. 그림 1의 두 번째 타임 라인은 우선 순위 상속이 실행될 때 첫 번째 타임 라인의 잠금 작동이 어떻게 변경되는지를 보여주고 있다.


그림 1. 우선 순위 도치와 우선 순위 상속
우선 순위 도치와 우선 순위 상속

높은 우선 순위 쓰레드가 낮은 우선 순위 쓰레드의 잠금을 획득하려고 할 때, 낮은 우선 순위 쓰레드는 또 다른 쓰레드가 보유한 잠금으로 차단될 수 있다. 이 경우, 낮은 우선 순위 쓰레드와 다른 쓰레드가 올라간다. 우선 순위 상속은 쓰레드 그룹의 우선 순위 상승과 강등을 필요로 할 수 있다.

우선 순위 상속

우선 순위 상속은 POSIX 잠금 서비스를 통해 사용자 공간으로 보내지는 리눅스 커널 기능을 통해 제공된다. 사용자 공간에서의 솔루션은 바람직하지 못하다. 이유는,

  • 리눅스 커널은 선점될 수 있고 우선 순위 도치가 될 가능성이 있다. 우선 순위 상속 역시 특정 시스템 잠금에 필요하다.
  • 사용자 공간에서 솔루션을 시도하면 풀기 어려운 경쟁 조건을 일으킨다.
  • 우선 순위 상승에는 커널 호출이 필요하다.

POSIX 잠금 유형은 pthread_mutex이다. pthread_mutex를 만드는 POSIX API는 mutex가 우선 순위 상속 프로토콜을 구현하도록 한다. pthread_mutex를 잠그고 pthread_mutex의 잠금을 해제하는 POSIX 서비스가 있다. 이것은 우선 순위 상속 지원이 되는 포인트이다. 리눅스는 경쟁 없이 사용자 공간에서 모든 잠금을 수행한다. 잠금 경쟁이 발생하면, 우선 순위 상승과 동기화 큐 관리가 커널 공간에서 수행된다.

WebSphere VM은 POSIX 잠금 API를 사용하여 우선 순위 상속 지원을 위해 자바 동기화 프리머티브를 구현한다. 사용자 레벨 C 코드 역시 POSIX 서비스를 사용한다. 자바 잠금 연산 시, 고유한 pthread_mutex가 할당되고 머신 연산을 사용하여 자바 객체로 바인딩 된다. 자바 레벨의 잠금 해제 연산을 할 때, 잠금 경쟁이 없을 경우, pthread_mutex는 원자 연산을 사용하여 객체로부터 바인딩을 해제한다. 경쟁이 있을 때, POSIX 잠금과 잠금 해제 연산은 리눅스 커널 우선 순위 상속을 지원한다.

mutex 할당과 잠금 시간을 최소화 하기 위해, JVM은 글로벌 잠금 캐시와 쓰레드당 잠금을 관리한다. 이곳에서 각 캐시는 할당되지 않은 pthread_mutex를 포함하고 있다. 쓰레드 캐시의 mutex는 글로벌 잠금 캐시로부터 획득된다. 이것이 쓰레드 잠금 캐시에 놓이기 전에, mutex는 쓰레드에 의해 미리 잠긴다. 경쟁하지 않은 잠금 해제 연산은 잠긴 mutex를 쓰레드 잠금 캐시로 리턴한다. 비경쟁 잠금이 기준이라면, 사전에 잠긴 mutex를 재사용 함으로써 POSIX 레벨 잠금은 줄어들거나 양도될 수 있다.

JVM은 쓰레드 리스트와 글로벌 잠금 캐시 같은 중요한 JVM 리소스로의 액세스를 직렬화 하는데 사용되는 내부 잠금을 갖고 있다. 이러한 잠금들은 우선 순위 상속에 기반하고 있고 단기간 보유된다.




위로


RT 애플리케이션을 위한 동기화 고려 사항

이 섹션에서는 애플리케이션을 포팅하여 RT 쓰레드를 사용하거나 새로운 애플리케이션을 작성하여 RT 쓰레딩을 활용하는 개발자를 위해 일부 RT 동기화 기능을 제공한다.

일반 자바 쓰레드들과 RT 쓰레드간 잠금 경쟁

RT 쓰레드는 일반 자바 쓰레드가 보유한 잠금으로 차단될 수 있다. 이러한 일이 발생하면, 잠금을 보유하고 있는 한 우선 순위 상속이 발생하여 일반 자바 쓰레드의 우선 순위가 RT 쓰레드의 우선 순위로 상승한다. 일반 자바 쓰레드는 RT 쓰레드의 모든 스케줄링 특성을 상속받는다.

  • 일반 자바 쓰레드는 SCHED_FIFO 정책을 실행하기 때문에, 쓰레드는 타임 슬라이스를 수행하지 않는다.
  • 디스패치와 yielding은 상승한 우선 순위의 RT 실행 큐에서 발생하지 않는다.

이러한 작동은 일반 자바 쓰레드가 잠금을 해제할 때 SCHED_OTHER로 복귀한다. Listing 1에서 만들어진 쓰레드들 중 어떤 것이라도 RT 쓰레드에서 요구하는 잠금을 보유하고 있는 동안 실행되었다면, 그 프로그램은 종료되지 않고, RT 쓰레드를 사용한 문제 코드 예제에 설명했던 것과 같은 문제를 나타냈을 것이다. 이러한 상황이 가능하기 때문에, SPIN 루프와 yielding을 수행하는 것은 실시간 JVM에서 실행되는 쓰레드에는 권장하지 않는다.

NHRT와 RT 쓰레드 간 잠금 경쟁

NHRT는 RT 쓰레드(또는 일반 자바 쓰레드)가 보유한 잠금으로 차단될 수 있다. RT 쓰레드가 잠금을 보유하고 있는 동안, GC는 RT를 선점하고 NHRT를 간접적으로 선점할 수 있다. NHRT는 RT가 GC에 의해서 더 이상 선점되지 않을 때까지 기다린 다음 NHRT가 실행할 기회를 갖기 전에 잠금을 해제해야 한다. GC에 의한 NHRT 선점은 NHRT가 시간에 민감한 기능을 수행할 경우 심각한 문제가 된다.

WebSphere Real Time의 결정론적 가비지 컬렉터는 1 밀리초 이하로 중지 시간을 유지하면서, NHRT 선점을 더욱 결정적으로 만든다. 이와 같은 중지 기간을 참을 수 없다면, NHRT와 RT 쓰레드 간 공유되는 잠금을 피하여 이러한 문제를 해결할 수 있다. 잠금이 의무적인 것이라면, RT와 NHRT 지정 리소스와 잠금을 갖는 것도 고려해 볼만하다. 예를 들어, 쓰레드 풀링을 실행하는 애플리케이션은 NHRT와 RT 쓰레드를 위한 개별 풀과 풀 잠금을 고려해 볼 수 있다.

또한, javax.realtime 패키지는 다음을 제공한다.

  • WaitFreeReadQueue 클래스는 RT 쓰레드에서 NHRT로 객체를 전달한다.
  • WaitFreeWriteQueue 클래스는 NHRT에서 RT 쓰레드로 객체를 전달한다.

이러한 클래스들은 비 대기(wait-free) 연산을 수행하는 동안 GC에 의해 차단될 수 있고, 비 대기 연산을 수행할 때 NHRT에서 요구하는 잠금을 보유하지 않는다.

javax.realtime 패키지의 동기화

특정 javax.realtime 메소드는 잠금이 경쟁하지 않을 때 조차 동기화의 오버헤드가 발생하기 때문에 의도적으로 동기화 되지 않는다. 동기화가 필요하다면, 콜러(caller)는 필요한 javax.realtime 메소드를 동기화 된 메소드 또는 블록으로 래핑한다. 프로그래머는 java.realtime 패키지의 메소드가 사용될 때 이와 같은 동기화를 추가하는 것을 고려해야 한다.

핵심 JLS 패키지에서의 동기화

반대로, java.util.Vector 같은 핵심 JLS 서비스들은 이미 동기화 된다. 또한, 특정 JLS 서비스들은 내부 잠금을 수행하여 특정 공유 리소스들을 직렬화 한다. 이러한 동기화 때문에, 핵심 JLS 서비스를 사용할 때, GC에 의한 NHRT 선점의 문제를 피해야 한다. (NHRT와 RT 쓰레드간 잠금 경쟁).

비경쟁 잠금 성능

비 RT 애플리케이션의 벤치마킹과 계측을 통해 잠금이 경쟁하지 않는다는 것을 알 수 있다. 비경쟁 잠금은 RT 애플리케이션의 뛰어난 부분이고, 특히 기존 컴포넌트나 라이브러리들이 재사용 될 때 그렇다. 비경쟁으로 알려진 잠금에 대한 결정적인 비용을 갖는 것이 좋지만, 동기화 지시문들은 피하거나 제거하기 어렵다.

앞서 설명했던 것처럼, 비경쟁 잠금 연산에는 설정과 원자 머신 명령이 포함된다. 비잠금 연산에는 원자 머신 연산이 포함된다. 잠금 연산용 설정에는 미리 잠긴 mutex의 할당이 포함된다. 이 할당은 비 경쟁 잠금 연산에서 가장 많은 변수 비용을 차지한다. RealtimeSystem.setMaximumConcurrentLocks()는 이러한 변수 비용을 제어할 수 있다.

RealtimeSystem.setMaximumConcurrentLocks(int numLocks)는 WebSphere Real Time의 VM이 numLocks mutex를 글로벌 잠금 캐시로 사전 할당하도록 한다. 글로벌 잠금 캐시는 쓰레드 당 잠금 캐시를 쓰레드 당 잠금 캐시들을 제공한다. RealTimeSystem API를 제공함으로써, 시간에 민감한 코드 영역 내에서 잠금이 발생할 기회를 줄일 수 있다. RealTimeSystem.getMaximumConcurrentLocks()setMaximumConcurentLocks() 호출에 어떤 숫자가 사용되어야 하는지를 결정하는데 사용되지만, getMaximumConcurrentLocks()는 최고 수위가 아닌 호출 시점에 잠금을 제공한다. 미래의 RTSJ 버전은 고수위 마크를 제공하는 API를 선보일 예정이다. numLocks 값에 터무니 없이 큰 값을 제공하지 않도록 하라. setMaximimConcurrentLocks()에 대한 호출은 많은 잠금을 만드는데 불규칙하고 정해지지 않은 시간과 메모리를 소비한다. 이러한 API는 JVM 스팩이 되도록 정의되기 때문에, 다른 JVM은 호출을 무시하거나 다른 작동을 제공한다.

경쟁 잠금 성능

쓰레드는 한 개 이상의 잠금을 동시에 가질 수 있고, 이러한 잠금은 특정 순서대로 획득된다. 이러한 잠금 패턴은 잠금 계층(lock hierarchy)을 형성한다. 우선 순위 상속은 쓰레드 그룹의 상승과 강등을 의미할 수 있다. 이 그룹의 쓰레드 수는 시스템에서 가능한 최대한의 잠금 계층보다 커서는 안된다. 잠금 계층을 좁게 유지함으로써, 가능한 적은 객체들을 잠글 수 있고, 이는 우선 순위 조정이 필요한 최대 쓰레드의 수에 영향을 미친다.

동기화 연산 시간

Object.wait(long timeout, int nanos)는 대기 연산에 나노초의 세분성을 제공한다. HighResolutionTime.waitForObject() API는 Object.wait()과 비슷하고 나노초 세분성으로 지정될 수 있는 상대 시간과 절대 시간을 제공한다. WebSphere Real Time에서, 두 API들은 기본적인 POSIX 잠금 대기 서비스들로 구현된다. 이러한 서비스들은 최대한 마이크로초 세분성을 제공한다. 이식성이 필요할 경우 javax.realtime 패키지의 Clock 클래스의 getResolution() 메소드는 실행 플랫폼을 위한 해상도를 검색하는데 사용된다.




위로


요약

RTSJ는 javax.realtime 패키지의 새로운 RT 클래스와 API를 통해 쓰레딩 및 동기화 기능을 확장 및 강화한다. WebSphere Real Time에서, 이러한 기능들은 RT 버전의 리눅스 커널, POSIX 쓰레딩 수정, JVM 수정을 통해 구현된다. RTSJ 쓰레딩과 동기화를 이해하면 RT 애플리케이션을 작성 및 전개할 때의 문제를 피하는데 도움이 된다.



 

참고자료

교육


제품 및 기술 얻기


토론



 

필자소개

Patrick Gallop은 1984년 워털루대학교(University of Waterloo)를 졸업했다. 졸업 직후 IBM Toronto Lab에 입사했으며, 다양한 컴파일러와 컴파일러 툴 프로젝트에 참여했다. IBM Java 프로젝트에서 일했으며, 최근에는 IBM Real-time Java 프로젝트의 리더로 활동했다.


Mark Stoodley

Mark Stoodley는 2001년 토론토대학교(University of Toronto)에서 컴퓨터 엔지니어링 박사 학위를 받았고, 2002년에 IBM Toronto Lab에 합류하여 Java JIT 컴파일 기술 분야에서 일했다. 2005년 초반부터 IBM WebSphere Real Time용 JIT 기술에 대해 작업했고, 기존 JIT 컴파일러를 실시간 환경에 적용하는 것과 관련한 작업을 했다. 현재 자바 컴파일 컨트롤 팀의 리더이며, 실행 환경의 코드 컴파일의 효과를 증진하기 위해 노력하고 있다.

반응형
JTextField에서 숫자만 입력하는지 검사하기
현재(역주:2001년 11월 20일) 베타판이 나와있는 Java 2 SDK 표준에디션에는 양식에 맞는 텍스트 입력을 위해 JFormattedTextField 라는 컴포넌트가 추가되었다. 이 컴포넌트는 텍스트 필드에 입력된 것을 검사할 수 있다. 그러나 지금 당장 그것이 필요한데 정식판이 나올 때까지 기다릴 수 없다면 어쩌겠나? 텍스트 필드에 입력된 것을 검사할 수 있는 적어도 세가지의 다른 방법이 있다. 그 세가지는 키를 누르는 수준에서, 포커스 수준에서 그리고 데이터 모델 수준에서 검사할 수 있다. 이 팁은 숫자 입력만 받아들이는 텍스트 필드를 만들기 위해 이 기술들을 어떻게 사용하는 지를 보여줄 것이다.
AWT의 TextField에도 있는 것처럼, 스윙의 JTextField 컴포넌트에 KeyListener를 등록할 수 있다. 리스너가 등록되면, 키가 눌려지는 것을 알아낼 수 있다. 틀린 것을 수정하기 위해 backspace 키나 delete키 빼고는 숫자 키가 아니면 이것을 거부할 수 있다. 거부하는 것은 KeyEvent의 consume() 메소드를 호출함으로 처리된다. 이 메소드는 컴포넌트에게 그 키보드 입력은 입력 리스너에 의해 처리되었으니 화면으로 출력하지마라고 지시한다.
리스너를 사용해서 입력 검사하는 코드는 아래와 비슷할 것이다.
keyText.addKeyListener(new KeyAdapter() {
public void keyTyped(KeyEvent e) {
char c = e.getKeyChar();
if (!((Character.isDigit(c) ||
(c == KeyEvent.VK_BACK_SPACE) || (c == KeyEvent.VK_DELETE))) {
getToolkit().beep();
e.consume();
}
}
});

입력기(역주:한글입력기 같이 시스템에 설치된 IME)가 설치해야 되는 환경에서 동작한다면, 키 리스너를 사용하는 데 특별히 고려해야 할 것이 있다. 이 경우 입력기가 키 리스너가 키를 누르는 것을 감지 못하게 할 것이다. 이것은 보통 문자가 키보드키와 1:1로 메핑되지 않는 경우 생긴다. 예를들면 한자나 일본어를 입력할 경우에 그렇다.
KeyListener 대신에 FocusListener를 사용하면 약간 다른 동작을 한다. KeyListener는 키보드 키가 눌러지는 매번 확인하지만, FocusListener는 입력필드가 포커스를 잃을 때 검사한다. 필드값 전체를 검사하기 때문에 이 방법은 간단히 Integer의 parseInt() 메소드를 사용한다. 사실 입력값은 중요하지 않다. 중요한 것은 입력된 것을 파싱할 수 있다는 것이다.
FocusListner를 이용한 입력값 검사코드는 다음과 비슷할 것이다.
focusText.addFocusListener(new FocusAdapter() {
public void focusLost(FocusEvent e) {
JTextField textField = (JTextField)e.getSource();
String content = textField.getText();
if (content.length() != 0) {
try {
Integer.parseInt(content);
} catch (NumberFormatException nfe) {
getToolkit().beep();
textField.requestFocus();
}
}
}
});

불행히도, 입력 화면에 메뉴가 있거나 팝업 대화상자가 있으면, 포커스 수준에서의 리스너를 사용하는 데는 문제가 있다. 메뉴를 사용할 때, 리스너는 top-level 메뉴가 입력 포커스를 받을 때, 실제 호출된다. 그리니까 선택하려는 메뉴를 찾아 이동할 때 실제 호출이 된다. 위의 리스너는 잘못된 입력에 대해 비프음을 낸다. 그러나 오류 메세지를 나타내기 위해 대화상자가 팝업되었다면 무슨 일이 일어날지를 상상해보라.
스윙 컴포넌트의 포커스 수준에서의 검사하는 두 번째 방법이 있다. InputVerifier를 컴포넌트에 붙일 수 있다. 이 추상 클래스는 boolean verify(JComponent) 메소드 하나를 가지고 있는데 입력값에 대해 검사를 수행하게 구현하면 된다. 이 메소드는 입력이 옳바르면 true를 반환하고 그렇지 않으면 false를 반환해야된다. FocusListener 기법에서처럼, true인지 false인지를 검사하기 위해 parseInt()를 사용해도 된다. 이 검사기를 붙이려면, setInputVerifier()를 호출하면된다. 사용자가 해당 필드를 벗어나 입력 포커스를 옮길 때 검사기는 입력값을 검사한다.
FocusListener의 경우에서처럼, InputVerifier는 각각의 입력이 유효한가를 결정하는 게 아니라 입력값 전체에 대해서 검사를 한다. 예를 들면, 이것은 입력값이 특정 범위 내에서 이뤄져야 하는 경우 중요하다. ??? [원문보기] As with the FocusListener, the InputVerifier permits validation on the whole field, versus trying to determine if part of the input is valid. This is important, for instance, if you want the input to be within a certain range.
InputVerifier에 잘못된 입력에 대해 어떻게 반응하는지를 기술하는 두번째 메소드 boolean shouldYieldFocus(JComponent)가 있다. 이 메소드는 기본적으로 verify()의 결과를 반환하게 구현되어 있다. 만약 잘못된 입력에 대해 비프음을 내고 싶다면, 그 값을 반환하기 전에 검사해야 된다.
InputVerifier를 사용해서 잘못된 입력에 대해 비프음을 내는 예제를 보자.
inputText.setInputVerifier(new InputVerifier() {
public boolean verify(JComponent comp) {
boolean returnValue = true;
JTextField textField = (JTextField)comp;
String content = textField.getText();
if (content.length() != 0) {
try {
Integer.parseInt(textField.getText());
} catch (NumberFormatException e) {
returnValue = false;
}
}
return returnValue;
}

public boolean shouldYieldFocus(JComponent input) {
boolean valid = super.shouldYieldFocus(input);
if (!valid) {
getToolkit().beep();
}
return valid;
}
});

이 세번째 방법은 입력 포커스를 다시 가져오기 위해 requestFocus()를 호출하지 않아도 되기 때문에 좀 깔끔하게 보이지만, 이것 역시 FocusListener와 같은 문제를 안고 있다.
이 팁에서 다루는 마지막 검증방법은 스윙의 모델-뷰-컨트롤러(MVC) 구조에 대한 이해를 수반한다. 모든 JTextComponent (JTextField 같은)은 이면에 데이터를 가진 텍스트 컴포넌트 모델이다. JTextField는 그 모델에 대해 단 한개의 뷰만 있다. 그 모델에 넣을 수 있는 것을 제한함으로써, JTextField에서 보여지는 것을 제한 할 수 있다.
데이터 모델에 입력 검사하는 것을 넣음으로써, 전에 언급된 메뉴를 선택되었을 때 무엇을 해야 하는 가하는 문제나 입력기(역주:IME) 리스너가 있을 때 입력을 검사하는 방법에 관한 문제는 피하게 된다. 이 마지막 검사 모델은 가장 복잡하지만 잘 동작한다.
JTextField의 기본 모델은 javax.swing.text.PlainDocument 클래스이다. 이 클래스는 사용자가 컴포넌트에 텍스트를 입력하거나 제거할 때 호출되는 insertString()와 remove() 메소드를 제공한다. 보통 한 번에 한 문자에 대해 행해진다. 그러나 오려내기나 복사하기가 수행될 때를 반드시 고려해야 된다. 각 메소드가 하는 것인 모델이 그 모델에 데이터가 추가되나 삭제되었을 때, 그것이 유효한가를 확인하는 것이다. 확인과정을 통과했다고 가정하면, super.insertString()이나 super.remove()를 호출해서 슈퍼클래스로 그 데이터를 전달한다.
insertString()의 핵심 부분은 대충 이렇하다. 입력된 것을 검정하기 위해서 어떤 것이 새 문자열이 될 것인가를 결정한다. 그 모델이 원래 비었다면 새 문자열은 입력된 값이다. 그렇지 않으면 새 값은 원래 있던 내용의 중간에 삽입된다. 새 값을 얻은 후, 그것을 Integer로 parseInt() 검사하고, 아무 문제 없으면, super.insertString()를 호출한다. 알아둘 것은 유효하지 않는 입력은 super.insertString()를 호출하지 않음으로써 그 입력이 거부되었음을 알린다. 문자열을 집어 넣지 않으면 아무것도 할 필요가 없다. 그러나 이 코드는 입력이 실패하면 비프음을 울린다.
String newValue;
int length = getLength();

if (length == 0) {
newValue = string;
} else {
String currentContent = getText(0, length);
StringBuffer currentBuffer = new StringBuffer(currentContent);
currentBuffer.insert(offset, string);
newValue = currentBuffer.toString();
}

try {
Integer.parseInt(newValue);
super.insertString(offset, string, attributes);
} catch (NumberFormatException exception) {
Toolkit.getDefaultToolkit().beep();
}

정수형 입력만 받아 들이는 모델의 경우, remove() 메소드의 기본 동작을 오버라이드할 필요는 없다. 정수형 텍스트 문자열에서 데이터(숫자)를 제거하고 숫자가 아닌 부분을 반환하는 것은 불가능하다. [원문보기] For the case of a model that only accepts integer input, it isn't necessary to override the default behavior of the remove() method. It is impossible to remove data from an integer text string and get back a non-integer.
완전한 모델을 정의한 후에, 그 모델을 텍스트 필드와 연결하기 위해 setDocument() 메소드를 사용해라.
modelText.setDocument(new IntegerDocument());

여기에 모든 네가지를 보여주는 완전한 예제가 있다. 이 안에 IntegerDocument 클래스의 정의도 볼 수 있을 것이다.
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.text.*;

public class TextInput extends JFrame {
JPanel contentPane;
JPanel jPanel1 = new JPanel();
FlowLayout flowLayout1 = new FlowLayout();
GridLayout gridLayout1 = new GridLayout();
JLabel keyLabel = new JLabel();
JTextField keyText = new JTextField();
JLabel focusLabel = new JLabel();
JTextField focusText = new JTextField();
JLabel inputLabel = new JLabel();
JTextField inputText = new JTextField();
JLabel modelLabel = new JLabel();
JTextField modelText = new JTextField();
IntegerDocument integerDocument1 =
new IntegerDocument();

public TextInput() {
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
contentPane = (JPanel)getContentPane();
contentPane.setLayout(flowLayout1);
this.setSize(new Dimension(400, 300));
this.setTitle("Input Validation");
jPanel1.setLayout(gridLayout1);
gridLayout1.setRows(4);
gridLayout1.setColumns(2);
gridLayout1.setHgap(20);
keyLabel.setText("Key Listener");
modelLabel.setText("Model");
focusLabel.setText("Focus Listener");
inputLabel.setText("Input Verifier");

keyText.addKeyListener(new KeyAdapter() {
public void keyTyped(KeyEvent e) {
char c = e.getKeyChar();
if (!(Character.isDigit(c) ||
(c == KeyEvent.VK_BACK_SPACE) ||
(c == KeyEvent.VK_DELETE))) {
getToolkit().beep();
e.consume();
}
}
});

focusText.addFocusListener(new FocusAdapter() {
public void focusLost(FocusEvent e) {
JTextField textField = (JTextField)e.getSource();
String content = textField.getText();
if (content.length() != 0) {
try {
Integer.parseInt(content);
} catch (NumberFormatException nfe) {
getToolkit().beep();
textField.requestFocus();
}
}
}
});

inputText.setInputVerifier(new InputVerifier() {
public boolean verify(JComponent comp) {
boolean returnValue = true;
JTextField textField = (JTextField)comp;
String content = textField.getText();
if (content.length() != 0) {
try {
Integer.parseInt(textField.getText());
} catch (NumberFormatException e) {
getToolkit().beep();
returnValue = false;
}
}
return returnValue;
}
});

modelText.setDocument(integerDocument1);

contentPane.add(jPanel1);
jPanel1.add(keyLabel);
jPanel1.add(keyText);
jPanel1.add(focusLabel);
jPanel1.add(focusText);
jPanel1.add(inputLabel);
jPanel1.add(inputText);
jPanel1.add(modelLabel);
jPanel1.add(modelText);
}

public static void main(String args[]) {
TextInput frame = new TextInput();
frame.pack();
frame.show();
}

static class IntegerDocument extends PlainDocument {

public void insertString(int offset, String string, AttributeSet attributes)
throws BadLocationException {

if (string == null) {
return;
} else {
String newValue;
int length = getLength();
if (length == 0) {
newValue = string;
} else {
String currentContent = getText(0, length);
StringBuffer currentBuffer =
new StringBuffer(currentContent);
currentBuffer.insert(offset, string);
newValue = currentBuffer.toString();
}
try {
Integer.parseInt(newValue);
super.insertString(offset, string, attributes);
} catch (NumberFormatException exception) {
Toolkit.getDefaultToolkit().beep();
}
}
}
}
}

네 개의 텍스트 필드 전부에 대해서 복사해서 붙이기를 해서 어떻게 되나 봐라. 예를 들면, 첫번째 KeyListener 기법을 사용해서 검사하는 텍스트 필드는 숫자가 아닌 것을 붙이기 하면 필드에 입력이 되어버린다. 이런 현상을 처리하려면 붙이기가 안되게 해야될 것이다. 반면에, "Model"이라고 되어 있는 IntegerDocument로 검사하는 텍스트 필드는 제대로 동작을 해서 붙이기가 되지 않는다.
프로그램을 AWT의 TextField로 바꾸려고 한다면, JTextField에서 지원되지 않지만 TextField에는 가능한 한가지는 컨트롤에 TextListener를 붙일 수 있다는 것이다. (역주: TextField의 슈퍼클래스인 java.awt.TextComponent에서 제공하는 메소드이다. javax.swing.text.JTextComponent에는 없다.) 그러나, 사용자 정의 Document를 붙이는 데이터 모델을 사용함으로써 이것을 쉽게 대체할 수 있다. Document는 값이 바뀌었을 때 통보되는 것에 (혹은, 적어도 바꾸고 깊어하는 것에) 직접적으로 연결해 사용한다. (역주:이 마지막 문장은 좀 잘 모르겠더라구요. 고민 끝에 이렇게 해석했는데, 이상하다고 생각되시면, 원문을 한 번 참조해 보세요..^^) [원문보기] If you are transitioning a program with an AWT TextField component to a Swing JTextField, note that one TextField behavior that is not supported on the JTextField is the ability to attach a TextListener to the control. However, you can you can easily replace this behavior by using the data model approach of attaching a custom Document. The Document usage directly maps to being notified when the value of the text has changed (or, at least, wants to change).
스윙 텍스트 컴포넌트에 대해 더 공부하고 싶으면 자바 튜토리얼의 Creating a GUI with JFC/Swing 뒷부분에 스윙 컴포넌트 사용하기 강좌를 참조해라.
반응형

HTML에 자바 스크립트 써보신 분이면 alert("알림메시지"); 기능을 많이 써보셨을 겁니다. VB(비주얼 베이직)에서도 MsgBox 함수로 알림창을 띄울 수 있지요. 자바라고 없겠습니까, 당연히 알림창 기능 있습니다.

그런데 저는 알림창을 띄울 수 있는 클래스가 뭔지 몰라서 한참동안 직접 JDialog로 알림창 비슷한 걸 만들어서 사용했답니다. 왠지 억울한 기분이 들더군요.

※ 자바 스윙(Java Swing, AWT 등)에서 알림창 띄우기
- JOptionPane 1. 종합편

(기타 관련 명칭 : 알림창, 확인창, 대화창, 경고창, 질문창, 메시지창, 메시지박스, MessageBox, MsgBox, Alert창, Confirm Dialog, Message Dialog 등...)

JOptionPane 클래스 하나만 알고 있으면 됩니다. 여기에 윈도우 기본 창들이 많이 들어있습니다.(물론, 파일 열기/닫기에 쓰이는 것은 FileDialog에 있지만요)

이번 강좌 아닌 강좌에 '넷빈즈 IDE(NetBeans IDE)로 하는 자바 프로그래밍'이라는 제목을 달지 않은 이유는, JOptionPane은 IDE 환경에서 사용하든, 메모장 텍스트 박치기로 프로그래밍하든 관계없이 똑같이 한 두 줄로 해결 가능한 것이니까요.

- JOptionPane 클래스 Java API
JOptionPane의 대표적인 정적 메소드 함수(static methods)들로 확인창, 알림창, 질문창 등을 띄울 수 있습니다.

showMessageDialog : 단순 알림창(경고창, 메시지 박스)을 띄웁니다.
showConfirmDialog : 확인창(예/아니오, 확인/취소 등)을 띄웁니다.
showInputDialog : 질문창을 띄웁니다. 간단한 입력 결과를 받습니다.
showOptionDialog : 확인창과 비슷한데, 예/아니오 대신 선택 버튼의 말을 직접 정해줄 수 있습니다.

위에 소개한 세가지 메소드이 자세한 사용법은 각각 따로 글을 작성하겠습니다.
- JOptionPane 2. 알림 메시지 창 띄우기 상세 설정 showMessageDialog
- JOptionPane 3. 확인창 띄우기 상세 설정 showConfirmDialog
- JOptionPane 4. 입력창 띄우기 상세 설정 showInputDialog
- JOptionPane 5. showOptionDialog 마음대로 주무르기

그 외 JOptionPane 클래스에 대한 내용은 JOptionPane Class 자바 API자바 튜토리얼(대화창 띄우는 법)을 참고하시고, 일단 여기서는 간단한 사용법과 예제만 보여드리겠습니다.

우선 JOptionPane 클래스 패키지를 import 해야합니다. 자바 소스 코드 최상단에 다음 import 문을 추가합니다.

import javax.swing.JOptionPane;

본문 중 창을 띄우고 싶은 부분에서 아래 예제를 따라 창을 띄웁니다.

사용자 삽입 이미지

JOptionPane.showMessageDialog(null, "기본 알림창입니다.");

사용자 삽입 이미지

int result = 0;
result = JOptionPane.showConfirmDialog(null, "기본 확인창입니다.");
// (result 리턴값)
// JOptionPane.YES_OPTION, JOptionPane.NO_OPTION,
// JOptionPane.CANCEL_OPTION 등

사용자 삽입 이미지

String resultStr = null;
resultStr = JOptionPane.showInputDialog("기본 입력창입니다.");
// (resultStr 리턴값으로 입력받은 메시지 문자열을 리턴해준다.)

<출처: http://hallang.tistory.com >
반응형

※ 알림 메시지 띄우기 - JOptionPane 2. showMessageDialog
(기타 관련 명칭 : 알림창, 확인창, 대화창, 경고창, 질문창, 메시지창, 메시지박스, MessageBox, MsgBox, Alert창, Confirm Dialog, Message Dialog 등...)

메시지 다이얼로그는 리턴값이 없습니다(void 형). 역시 JOptionPane 클래스 패키지를 import 해야합니다. 자바 소스 코드 최상단에 다음 import 문을 추가합니다.

import javax.swing.JOptionPane;


※ showMessageDialog 메소드 함수 형태

- 리턴 함수 타입 : public static void (되돌려 주는 값이 없습니다)
showMessageDialog( Component parentComponent, Object message )
showMessageDialog( Component parentComponent, Object message,
           String title, int messageType )
showMessageDialog( Component parentComponent, Object message,
           String title, int messageType, Icon icon )



※ 매개변수(파라메터, Parameter) 설명

- Component parentComponent
메시지창 다이얼로그가 어떤 Frame에서 나타나게 될 것인지를 정해주는 변수. null 값이거나 설정해준 값에 Frame이 없다면 기본값 Frame(default Frame)이 지정됩니다. 쉽게 null 로 둬도 될 것 같네요.

- Object message
출력할 Object 클래스형 개체. 주로 문자열 메시지를 출력할 것이므로, 간단하게 그냥 출력할 문자열 자체를 써주면 됩니다. 문자열을 여러줄에 걸쳐서 표현하고 싶다면 newline에 해당하는 escape 문자열(" \n ")을 써주면됩니다.

- String title
메시지창 상단 제목표시줄에 나타낼 알림창 제목입니다.

- int messageType
알림창의 메시지 종류를 정해줍니다. 여기서 정해주는 메시지 종류에 따라 자동으로 지정된 아이콘이 표시됩니다. 자세한 설정 방법은 아래와 같습니다.

JOptionPane.showMessageDialog(null, "기본 알림창입니다.");

사용자 삽입 이미지
아무런 메시지 타입도 지정해주지 않은 경우 디폴트 값입니다.

JOptionPane.showMessageDialog(null, "경고 메시지 내용", "경고 메시지 제목", JOptionPane.WARNING_MESSAGE);
JOptionPane.WARNING_MESSAGE

JOptionPane.WARNING_MESSAGE

JOptionPane.showMessageDialog(null, "오류 메시지 내용", "오류 메시지 제목", JOptionPane.ERROR_MESSAGE);
JOptionPane.ERROR_MESSAGE

JOptionPane.ERROR_MESSAGE

JOptionPane.showMessageDialog(null, "아이콘 없는 메시지 내용", "아이콘 없는 메시지 제목", JOptionPane.PLAIN_MESSAGE);
JOptionPane.PLAIN_MESSAGE

JOptionPane.PLAIN_MESSAGE

JOptionPane.showMessageDialog(null, "정보 메시지 내용", "정보 메시지 제목", JOptionPane.INFORMATION_MESSAGE);
JOptionPane.INFORMATION_MESSAGE

JOptionPane.INFORMATION_MESSAGE

JOptionPane.showMessageDialog(null, "질문 메시지 내용", "질문 메시지 제목", JOptionPane.QUESTION_MESSAGE);
JOptionPane.QUESTION_MESSAGE

JOptionPane.QUESTION_MESSAGE

 

- Icon icon
Icon 클래스를 통해서 직접 정해줄 수도 있습니다. 아이콘 클래스는 제가 잘 모르겠네요 죄송^^ 웬만하면 위 메시지 종류 선택으로 적당히 커버는 될 겁니다. 직접 사용자 아이콘을 정해주려면 Icon 클래스 관련 내용을 공부하셔야 할 거 같네요. null 로 해두면 기본값이 출력되는 것 같습니다.

<출처: http://hallang.tistory.com >

반응형

※ 확인창 메시지 띄우기 - JOptionPane 3. showConfirmDialog

JOptionPane 클래스 패키지를 import 해야합니다. 자바 소스 코드 최상단에 다음 import 문을 추가합니다.

import javax.swing.JOptionPane;


※ showConfirmDialog 메소드 함수 형태

- 리턴값(static int) : 사용자의 확인 버튼 선택값을 되돌려줍니다.
CLOSED_OPTION - 종료(X) 버튼으로 창을 닫은 경우. CANCEL / NO와 비슷하게 처리해줘야합니다.
YES_OPTION - 예(YES) 버튼 선택시 리턴값
NO_OPTION - 아니오(NO) 버튼 선택시
CANCEL_OPTION - 취소(CANCEL) 버튼 선택시
OK_OPTION - 확인(OK) 버튼 선택시

- 메소드 함수 형태 종류
showConfirmDialog( Component parentComponent, Object message )
showConfirmDialog( Component parentComponent, Object message,
         String title, int optionType)
showConfirmDialog( Component parentComponent, Object message,
         String title, int optionType,
         int messageType )
showConfirmDialog( Component parentComponent, Object message,
         String title, int optionType,
         int messageType, Icon icon )



※ 매개변수(파라메터, Parameter) 설명

- Component parentComponent
메시지창 다이얼로그가 어떤 Frame에서 나타나게 될 것인지를 정해주는 변수. null 값이거나 설정해준 값에 Frame이 없다면 기본값 Frame(default Frame)이 지정됩니다. 쉽게 null 로 둬도 될 것 같네요.

- Object message
출력할 Object 클래스형 개체. 주로 문자열 메시지를 출력할 것이므로, 간단하게 그냥 출력할 문자열 자체를 써주면 됩니다. 문자열을 여러줄에 걸쳐서 표현하고 싶다면 newline에 해당하는 escape 문자열(" \n ")을 써주면됩니다.

- String title
메시지창 상단 제목표시줄에 나타낼 알림창 제목입니다.

- int optionType
사용자의 대답 선택 버튼을 어떻게 표현할 것인가 결정합니다. "예", "아니오", "취소" 버튼을 어떤 식으로 배치할 지 결정합니다.
YES_NO_OPTION
YES_NO_CANCEL_OPTION
OK_CANCEL_OPTION
위 버튼의 텍스트(예, 아니오)를 다른 글자로 바꿀 수 있습니다. showOptionDialog를 통해서 간으한 작업으로, 자세한 건 showOptionDialog 설명글을 참고하세요!

- int messageType
알림창의 메시지 종류를 정해줍니다. 여기서 정해주는 메시지 종류에 따라 자동으로 지정된 아이콘이 표시됩니다.
ERROR_MESSAGE
INFORMATION_MESSAGE
WARNING_MESSAGE
QUESTION_MESSAGE

아이콘 없음 :
PLAIN_MESSAGE

- Icon icon
Icon 클래스를 통해서 직접 정해줄 수도 있습니다. 아이콘 클래스는 제가 잘 모르겠네요 죄송^^ 웬만하면 위 메시지 종류 선택으로 적당히 커버는 될 겁니다. 직접 사용자 아이콘을 정해주려면 Icon 클래스 관련 내용을 공부하셔야 할 거 같네요. null 로 해두면 기본값이 출력되는 것 같습니다.


※ 사용 예제
JOptionPane.showConfirmDialog(null, "기본 확인창입니다.");

사용자 삽입 이미지
아무런 타입도 지정해주지 않은 경우 디폴트 값

JOptionPane.showConfirmDialog(null, "경고 메시지 + 예/아니오 옵션", "제목표시줄", JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE);
example

JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE

 <출처: http://hallang.tistory.com >

+ Recent posts