본문 바로가기

프로그래밍(TA, AA)/JVM 언어

[자바] Event-Dispatching Thread

이벤트-디스패칭 쓰레드란?


GUI 응용 프로그램을 작성할 때에는 이벤트-디스패칭 쓰레드(the event-dispatching thread)를 의식할 필요가 있습니다.


GUI 응용 프로그램을 사용할 때 우리들은 버튼을 누른다든지 마우스를 움직인다든지 합니다. 우리들의 그러한 동작에 대응해서 GUI 응용 프로그램 처리가 수행됩니다. "버튼을 누른다" 혹은 "마우스를 움직인다"라고 하는 것은 이벤트(event)라고 부르고 Swing 클래스로서 표현되고 있습니다. 버튼을 누르면 java.awt.event.ActionEvent라는 클래스의 인스턴스가 생성되고 마우스를 움직이면 java.awt.event.MouseEvent라는 클래스의 인스턴스가 생성됩니다. 그리고 각각의 인스턴스는 Swing이 내부에 가지고 있는 이벤트 큐(event queue)에 쌓이게 됩니다. 


여기까지 이야기하면 관찰력 좋은 독자는 "이벤트"가 Request에 대응하고 "이벤트 큐"가 Channel에 대응하고 있는 것은 아닐까하고 생각할 수도 있겠습니다. 버튼을 누른다든지 마우스를 움직인다든지 하는 것은 Swing 내부에 이벤트라고 하는 Request로 변화되어 이벤트 큐라는 Channel에 건네집니다. Client에 대응하는 것은 마우스 등을 관리하고 있는 부분이므로 이것은 Swing의 내부에 감추어져 있어 밖에서는 보이지 않습니다.


그것까지 이해했으면 이벤트-디스패칭 쓰레드를 이해하는 것은 쉽습니다. 이벤트-디스패칭 쓰레드는 워커 쓰레드(Worker)이기 때문입니다.


이벤트-디스패칭 쓰레드는 이벤트 큐에서 이벤트를 하나 꺼내어 그것을 실행합니다. 실행이 끝나면 다시 이벤트 큐로 돌아옵니다. 그리고 다음의 이벤트를 집어서 실행합니다. 이 것을 계속합니다. 만일 이벤트 큐에 이벤트가 하나도 없다면 이벤트-디스패칭 쓰레드는 이벤트가 오는것을 기다립니다. (Worker Thread 패턴)




이벤트-디스패칭 쓰레드는 딱 한 명


이벤트-디스패칭 쓰레드에는 여러 가지 이름이 있습니다. 이벤트-디스패칭 쓰레드(event dispatch thread)라고 부르기도 하고, 더욱 단순하게 이벤트-쓰레드(the event thread)라고 부르기도 합니다. 그러나 어느 경우라도 영어로는 "the가 붙은 단수형"이 됩니다. 이것은 이벤트-디스패칭 쓰레드가 그 시스템 속에 "유일"하기 때문입니다.


Worker Thread 패턴의 Worker는 Producer-Consumer 패턴의 Consumer에 해당합니다. 이벤트-디스패칭 쓰레드가 한사람이라는 것은 워커 쓰레드가 한 사람이라는 것입니다.


Worker가 한 사람이면 멀티 쓰레드의 장점이 아닌것 같지만 배타 제어가 불필요하게 되고 있다는 설계 덕분에 이벤트-디스패칭 쓰레드가 실행하기 전의 메소드는 워커 쓰레드끼리의 배타 제어가 불필요하게 되는 것입니다.




이벤트-디스패칭 쓰레드는 리스너를 호출


이벤트-디스패칭 쓰레드가 이벤트를 실행한다는 것을 이해했다면 그 "실행"의 내용을 구체적으로 생각해보겠습니다.


이벤트-디스패칭 쓰레드가 하는 처리 중 하나는 Listener의 메소드 호출입니다.


예를 들면 버튼이 눌려졌을 때 java.awt.event.ActionEvent 클래스의 인스턴스가 이벤트 큐에 가득찹니다. 이벤트-디스패칭 쓰레드가 이 인스턴스를 취해서 실행할 때에는 이 "ActionEvent를 받아들이게 되어 있는 인스턴스"(리스너)의 actionPerformed 메소드를 호출합니다. 이벤트-디스패칭 쓰레드는 실제로 그 메소드 속에서 무엇이 일어나고 있는지 알 수가 없습니다. 이벤트-디스패칭 쓰레드는 단순히 actionPerformed를 호출할 뿐입니다.


마우스가 움직일때는 어떨가요. 이 때에는 java.awt.event.MouseEvent 클래스의 인스턴스가 이벤트 큐에 꽉 차게 됩니다. 이벤트-디스패칭 쓰레드가 이 인스턴스를 실행할 때에는 MouseEvent를 받아들이게 되어 있는 인스턴스(리스너)의 mouseMoved 메소드를 호출합니다. actionPerformed의 경우도 마찬가지로 이벤트-디스패칭 쓰레드는 mouseMoved의 구체적인 처리를 알지 못합니다.






리스너를 등록하는 것의 의미


Swing으로 프로그래밍을 한 적이 있는 사람은 버튼(JButton) 등의 컴포넌트(JComponent)에 "리스너를 등록한다"라는 처리를 쓴 적이 분명 있을 것입니다. 구체적으로 addActionListener 메소드나 addMouseMotionListener 메소드를 호출하는 처리입니다.


이벤트-디스패칭 쓰레드를 이해했다면 "리스너를 등록한다"라는 것의 의미를 확실히 알게됩니다. 컴포넌트에 리스너를 등록한다는 것은 그 컴포넌트 상에서 이벤트가 발생한 때에 "이벤트-디스패칭 쓰레드가 메소드를 호출하기 전의 인스턴스를 지정했다"라는 것이 되는 것입니다.




이벤트-디스패칭 쓰레드는 화면 그리기도 한다


이벤트-디스패칭 쓰레드는 리스너의 메소드 호출하는 것 이외에도 화면에 그리는 것과 관계된 메소드(update나 paint) 호출도 합니다. 화면을 다시 그리고 싶을때 우리들은 repaint 메소드를 호출합니다. 그러나 repaint를 불러 "바로" 다시 그리게 되는 것은 아닙니다. repaint 메소드는 "다시 그려야 할 영역"을 내부적으로 기록해 두는 것뿐입니다. 실제의 그리기 처리는 이벤트-디스패칭 쓰레드에 의해서 별도 처리됩니다.




javax.swing.SwingUtilities 클래스


javax.swing.SwingUtilities 클래스에는 Swing에 관한 편리한 클래스 메소드가 모여있습니다. 그 중 이벤트-디스패칭 쓰레드와 관련된 메소드는 아래와 같습니다.



* static void invokeAndWait() throws InteruptedException, InvocationTargetException

이벤트-디스패칭 쓰레드에 의해서 runnable.run()을 실행한다. invokeAndWait 메소드에서 제어가 돌아오는 것은 지금까지의 이벤트가 모두 처리되어 runnable.run()의 실행이 종료되고 부터이다. 결국 runnable.run()은 동기적으로 실행되는 것이 된다. runnable.run()이 예외를 던진 경우 invokeAndWait 메소드는 InvocationTargetException을 던진다. runnable.run()이 던진 예외는 InvocationTargetException의 getTargetException 메소드를 사용해서 얻을 수 있다.


invokeAndWait 메소드는 인수로 주어진 Runnable 객체를 실행시키는 메소드입니다. 단지 그 실행을 행하는 쓰레드는 이벤트-디스패칭 쓰레드입니다. 결국 invokeAndWait 메소드를 사용하면 임의의 처리를 Swing의 이벤트 큐에 넣을 수 있다는 것입니다.


invokeAndWait라는 것은 "기동하고 대기한다"라는 의미입니다. 그 이름 그대로 인수로 주어진 Runnable 객체가 실행되는 것을 대기합니다. 그래서 invokeAndWait 메소드를 호출한 시점에서 이벤트 큐에 머물고 있던 이벤트가 모두 실행되고 결국 인수로 주어진 Runnable 객체의 run 메소드가 실행된 다음 invokeAndWait 메소드에 되돌아옵니다.


어째서 이러한 메소드가 필요한 것일까요. "이벤트-디스패칭 쓰레드가 하나라면 베타 제어가 필요없다"라는 이야기를 생각해보면, Swing 컴포넌트 메소드의 대부분은 이벤트-디스패칭 쓰레드라는 딱 하나의 쓰레드에서만 실행되도록 되어 있어서 Thread Safe가 아닙니다. 그러므로 이벤트-디스패칭 쓰레드 이외의 쓰레드가 컴포넌트 메소드를 호출하는 것은 위험한 것입니다. 그래도 컴포넌트 메소드를 호출하고 싶은 경우에는 일단 그 처리 내용을 기술한 Runnable 객체를 만들고 invokeAndWait 메소드(또는 invokeLater 메소드)에 넘겨 두는 것입니다.



* static void invokeLater(Runnable runnable)

이벤트-디스패칭 쓰레드에 의해 runnable.run()을 실행한다. 이 메소드를 호출하면 바로 제어가 돌아온다. 결국 runnable.run()은 비동적으로 실행되는 것이 된다. 정말로 실행되는 것은 지금까지의 이벤트가 모두 처리된 후이다. 이 메소드는 이벤트-디스패칭 쓰레드 이외의 쓰레드에서 GUI의 컴포넌트에 접근하고 싶을 때에 이용되는 일이 많다.


invokeLater 메소드는 invokeAndWait 메소드와 거의 비슷한 처리를 합니다. 단지 invokeLater는 Runnable 객체의 실행 완료를 기다리지 않습니다. 이벤트 큐에 이벤트를 넣은 후에는 바로 되돌아옵니다.



* static boolean isEventDispatchThread()

현재의 쓰레드가 이벤트-디스패칭 쓰레드라면 true, 그렇지 않으면 false를 보낸다.


javax.swing.SwingUtilites 클래스의 isEventDispatchThread 메소드를 사용하면 현재의 쓰레드(isEventDispatchThread를 호출한 쓰레드)가 이벤트-디스패칭 쓰레드인지 아닌지를 판정할 수 있습니다.




Swing의 싱글 쓰레드(The Single-Thread Rule) 법칙


Swing의 컴포넌트가 일단 실현되면 컴포넌트의 상태를 변화시키는 코드, 상태에 의존하는 코드는 이벤트-디스패칭 쓰레드가 실행되어야 합니다.


컴포넌트가 실현된 상태(realized)라는 것은 컴포넌트의 paint 메소드를 호출시킨 상태를 말합니다. 구체적으로는 그 컴포넌트의 setVisible(true) 메소드, show() 메소드, pack() 메소드가 호출되어 이미 실행된 컴포넌트의 자식 컴포넌트라는 것을 의미합니다.


간단히 말하자면 다음과 같습니다. 컴포넌트를 준비하고 있는 사이에는 다른 쓰레드에서 컴포넌트를 호출해도 상관없습니다. 그러나 일단 표시한(표시 가능하게 된) 컴포넌트의 메소드는 이벤트-디스패칭 쓰레드에서만 부를 수 있습니다.


Swing 프로그래밍을 할 때에는 이 싱글 쓰레드 법칙을 지킬 필요가 있습니다. 이 법칙은 이벤트-디스패칭 쓰레드가 한 사람이라는 사실에서부터 생겨난 것입니다. 배타 제어를 없애고 수행 능력을 높이는 대가로서 프로그래머는 이 법칙을 지켜야 하는 것입니다.


또한 실행된 후에도 임의의 쓰레드에서 호출될 수 있는 메소드도 있습니다.


 - repaint

 - revalidate

 - addActionListener

 - removeActionListener

 - addMouseMotionListener

 - removeMouseMotionListener

 - 이외에 Listener의 추가/소거 메소드 모두