메뉴 바로가기 검색 및 카테고리 바로가기 본문 바로가기

한빛출판네트워크

IT/모바일

예제로 살펴보는 쓰레드 제어하기 - (1)

한빛미디어

|

2007-04-13

|

by HANBIT

20,406

제공 : 한빛 네트워크
저자 : Viraj Shetty
역자 : 백기선
원문 : Controlling Threads by Example

Java에 내장된 유용한 기능 중에 하나가 바로 멀티 쓰레드 어플리케이션을 작성할 수 있다는 것입니다. 쓰레드는 자신만의 지역 변수, 프로그램 카운터, 생명주기를 가진 프로그램 내의 실행 경로입니다. 만약에 쓰레드 위에서 어떤 작업이 매우 오랫동안 수행된다면 종료, 모니터링, 일시 정지, 다시 계속하기 등의 작업이 필요합니다.

이 글에서는 쓰레드와 이런 기능을 포함하도록 리팩토링에 관한 중요한 예제를 다룰 것입니다. 쓰레드와 SWING 프로그래밍에 관한 기본 지식을 이해하고 있다는 가정하에 작성합니다. 글 마지막 부분에 있는 zip파일을 통해 예제 코드를 다운로드 할 수 있습니다.

흔치 않은 예제

특정 디렉터리로부터 시작해서 하위에 있는 모든 폴더들을 뒤지면서 그 안에 있는 자바 소스 파일 중에서 특정 문자열을 포함하는 파일을 검색하는 어플리케이션을 만들어 봅시다. 사용자가 자신의 컴퓨터에서 검색하기 위한 루트 폴더를 찾을 수 있도록 SWING을 사용합니다. 사용자는 검색 작업을 시작, 멈춤, 모니터링, 일지정지, 재 시작 할 수 있어야 합니다. UI(User Interface)는 Figure 1처럼 생겼습니다.


[그림 1] 예제의 최종 화면

검색 시작, 멈춤, 일시 정지, 재 시작에 대응하는 네 개의 버튼이 있습니다. 현재 상태를 나타내는 label이 재 시작 버튼 오른쪽에 있으며 현재 작업의 진행 정도를 퍼센티지로 나타냅니다. 결과물인 파일들은 가운데에 나타냅니다. 화면에 있는 모든 요소들은 적절할 때에만 사용이 가능해야 합니다. 이 예제를 다음의 세 가지 과정을 통해 구현할 것입니다.
  • 멈춤 기능을 위해서 별도의 쓰레드 사용하기
  • 쓰레드 진행 상황 알려주기
  • 일시 정지와 재 시작 기능 추가하기
UI와 쓰레드

가장 간단한 해결책은 쓰레드를 전혀 사용하지 않고 구현 하는 것 입니다. 그럼 만약 쓰레드를 사용하지 않고 위의 기능들을 구현했을 때 사용자가 겪게 될 불편을 생각해 봅시다. 검색이라는 버튼 하나 만 있으면 됩니다. 버튼을 클릭하면 모든 파일들에서 검색할 키워드가 존재하는지 찾을 때까지 기다려야 합니다. 이 것은 유저에게 심각한 성능 문제로 보일 수 있겠지만 문제의 특성상 어떤 한계 이상 성능이 좋아질 수 없습니다. 따라서 다른 해결책은 사용자가 검색을 하면서 이 어플리케이션에서 다른 작업도 할 수 있도록 구현하는 것 입니다. 예를 들어, 이 기능이 편집기에 추가 된다면, 사용자는 뒤에서 검색 기능이 동작 되는 중에도 계속해서 편집을 할 수 있습니다. 분명 이 해결책은 쓰레드를 사용할 것이다. 하지만 이것 역시 응답 시간이 짧고 사용자가 기다릴 수 있을 만한 경우에 채택 될 수 있습니다.

SWING에서 모든 UI 요소들은 Event Dispatching Thread를 사용하여 그렸습니다. 버튼 클릭은 이벤트를 발생시키고 이런 클릭들을 “처리(listen)” 하는 클래스를 작성할 수 있습니다. 일부 버튼 클릭 이벤트를 처리하는 클래스는 ActionListener 인터페이스의 actionPerformed 메소드를 구현합니다. 이 메서드를 Event Dispatching Thread가 호출한다는 것을 알아 두시길 바랍니다. 따라서 시간이 오래 걸리는 작업의 경우에 Event Dispatching Thread가 묶여있게 되며 다른 작업 요청을 처리하기 위해 사용할 수 없게 됩니다.

쓰레드를 사용하여 멈춤 기능 구현하기

검색 기능을 별도의 쓰레드에서 실행함으로써 SWING의 Event Dispatching Thread가 자유롭게 다른 작업을 할 수 있습니다. 하지만 쓰레드 사용은 몇 가지 의문점을 안고 있습니다:
  • 어떻게 분리된 쓰레드의 결과를 SWING 화면에 나타낼 것인가?
  • 어떻게 버튼을 두 번 클릭하는 것을 막을 것인가?
  • 어떻게 쓰레드를 멈출 것인가?
  • 어떻게 쓰레드가 계속 동작 중이라는 것을 알 수 있을까?
다음의 UI처럼 동작할 것입니다([그림 2]를 보시죠).


[그림 2] 검색/멈춤 기능을 위한 화면

처음에는 검색 버튼, 디렉터리 버튼 그리고 Token 필드가 사용 가능한 상태여야 합니다. 사용자가 검색 버튼을 클릭하면 이 세 개의 필드들이 사용 불가능한 상태가 됩니다(이것으로 사용자가 연속하여 버튼을 클릭하는 것을 방지한다.). 이 순간 취소 버튼이 사용 가능한 상태가 되고 현재 상태 필드의 문구가 “Working ...”으로 바뀝니다. 상태 필드를 통해서 사용자에게 검색이 여전히 진행 중이라는 것을 알려줍니다. 이 작업을 위해 필요한 클래스들을 살펴봅시다. 정확히 세 개의 클래스가 필요합니다.
  • FileFinder: 검색 작업을 합니다.
  • SearchForm: SWING을 사용하여 결과를 보여주고 뷰(View)역할을 합니다.
  • SearchThread: 쓰레드를 위한 기능을 구현합니다.
[그림 3]에서 클래스간의 관계를 볼 수 있습니다.


[그림 3] 검색/멈춤 기능을 위한 클래스 다이어그램

FileFinder.java 클래스의 책임은 루트 디렉터리 밑에 있는 모든 파일에서 특정 문자열을 포함하고 있는 모든 파일을 찾는 것입니다. 생성자는 루트 디렉터리 정보를 가지기 위해서 File 객체를 받습니다. findFiles(..) 메소드는 File 객체의 리스트를 반환합니다. FileFinder 클래스는 아래처럼 사용할 것입니다.
FileFinder finder = new FileFinder(dirFile);
List javaFiles = finder.findFiles(token);
전에 완전한 소스코드를 살펴 보시기를 권장합니다. SearchForm.java의 SearchForm 클래스는 사용자를 위한 swing 인터페이스(화면)와 사용자가 검색 버튼을 클릭했을 때 결과를 보여주는 일을 합니다. 생성자는 화면 크기를 설정하는 일부터 시작해서 모든 SWING 컴포넌트들을 JFrame contentPane에 추가합니다. 버튼 클릭을 다루기 위해서 SearchForm은 ActionListener 인터페이스를 구현했습니다. 리스너는 SWING에서 사용자가 화면을 통해 발생시키는 행위를 처리하기 위해 사용하는 일반적인 메커니즘입니다. SearchForm.actionPerformed(..) 메소드는 디렉터리, 검색, 취소 버튼들이 클릭 했을 때 필요한 기능을 구현했습니다. 디렉터리 버튼이 클릭하면 JFileChooser를 사용하여 선택할 수 있는 디렉터리를 보여줍니다. zip파일에서 실제 코드를 확인하세요. 검색 버튼을 클릭하면 별도의 쓰레드가 SearchThread 클래스를 사용하여 검색 프로세스를 시작하기 위해 호출되어야 합니다.
public class SearchForm extends JFrame 
        implements ActionListener {

    private JButton dirButton 
        = new JButton("Choose Directory");
    private JButton searchButton 
        = new JButton("Search");
    private JTextArea area 
        = new JTextArea();
    private JTextField tokenField 
        = new JTextField("");

    public SearchForm() {

        // set the initial size
        setSize(600, 300);

        // Exit the application on window close
        this.setDefaultCloseOperation(
                JFrame.EXIT_ON_CLOSE);

        ((JPanel) getContentPane()).setBorder(
          BorderFactory.createEmptyBorder(
                5, 5, 5, 5));

         ....

        // add the panels to the frame
        getContentPane().add("North", 
                northPanel);
        getContentPane().add("Center", 
                new JScrollPane(area));
        getContentPane().add("South", 
                southPanel);
    }

    public static void main(String[] args) {
        SearchForm form = new SearchForm();
        form.setVisible(true);
    }

    /**
     * Act on the button click from the user
     */
    public void actionPerformed(ActionEvent e){
        // open a file dialog and let the 
        // user choose a file
        Object source = e.getSource();

        if (source == dirButton) {
             ....

        } else if (source == searchButton) {
             ....
        }

    }

    public void setTextArea(List javaFiles) {
        ....
    }
}
SearchThread.java: 검색을 위한 새로운 쓰레드를 만들기 위해서 Thread 클래스를 상속합니다. SearchThread 클래스는 아래처럼 만들었습니다.
public class SearchThread extends Thread {

    private File rootDir;
    private String token;
    private SearchForm form;

    public SearchThread(File rootDir, 
        String token, SearchForm form) {
        
        this.rootDir = rootDir;
        this.token = token;
        this.form = form;
    }

    public void run() {
        FileFinder finder 
                = new FileFinder(rootDir);
        List javaFiles 
                = finder.findFiles(token);
        form.setTextArea(javaFiles);
    }

}
생성자에 루트 디렉토리와 검색할 키워드(token) 그리고 SearchForm 자신을 인자로 넘겨 줍니다(이렇게 하는 이유는 화면에 결과를 출력하기 위해서 입니다.). FIleFinder 클래스가 SearchTread에서 사용된다는 것을 확인하시기 바랍니다. 사용자가 검색 버튼을 클릭하면 아래의 코드처럼 쓰레드가 호출됩니다.
// Invoke the search on a different Thread
File dirFile = new File(dirName);
String token = tokenField.getText();
sThread 
        = new SearchThread(dirFile,token,this);
sThread.start();
sThead는 SearchForm 클래스의 멤버 변수라는 것에 유의하시기 바랍니다. 한 번 쓰레드가 시작하면 SWING의 Event Dispatching Thread는 다른 기능들을 자유롭게 할 수 있습니다. 검색이 시작된 후에 사용자가 할 수 있는 가장 중요한 기능은 검색을 멈추는 것입니다. 검색 쓰레드가 호출하는 다음의 메소드에 주목하시기 바랍니다.
form.setTextArea(javaFiles);
화면 가운데 있는 text area에 파일들을 보여 주기 위한 것입니다. SWING의 철칙 중 하나로 (거의)모든 화면을 변경하는 작업들은 SWING의 Event Dispatching Thread에 의해 수행 될 필요가 있습니다. 하지만 지금은 검색 쓰레드에서 그 작업을 하고 있는 것처럼 보입니다. form.setTextArea(..)를 어떻게 구현했는지 살펴봅시다.
public void setTextArea(List javaFiles) {
    StringBuffer areaBuffer     
        = new StringBuffer();
    Iterator fileIter 
        = javaFiles.iterator();
        
    while (fileIter.hasNext()) {
        File file 
                = (File) fileIter.next();
        areaBuffer.append(
                file.getAbsolutePath())
                .append("n");
    }

    if ("".equals(areaBuffer.toString())){
        areaBuffer.append(
                "No Files Found !!!");
    }

    SwingUtilities.invokeLater(
        new SetAreaRunner(area, 
                areaBuffer.toString()));
}
SWING 지지자들 이라면 SwingUtilities.invokeLater가 문제를 해결했다는 것을 즉시 알아차렸을 것입니다. setTextArea(..) 메소드는 먼저 파일 리스트로부터 문자열을 만들고 invokerLater(..)를 호출하여 화면의 text area 필드에 설정합니다. 코드를 SwingUtilities.invokeLater(..)로 감싸서 효율적으로 Event Dispatcher Thread에서 호출할 수 있도록 합니다. invokeLater 메소드는 단순히 Event Dispatcher 큐에 Runnable 객체를 차례대로 관리합니다. SWING의 Event Dispatcher Thread는 메시지를 읽고 run() 메소드 안에 있는 코드를 실행합니다. Figure 4는 사용자가 검색 버튼을 클릭했을 때 어떤 일이 일어나는지 시퀀스 다이어그램으로 보여줍니다.


[그림 4] 사용자가 검색 버튼을 클릭했을 때를 나타낸 시퀀스 다이어그램

자 이제 시작 한 쓰레드를 어떻게 멈추게 할까요? Java는 쓰레드를 중단시키기 위한 중요한 기능을 제공합니다. 문법은 다음과 같습니다:

중단된 쓰레드는 주기적으로 쓰레드가 중단 된 상태인지 확인할 필요가 있습니다. 만약 그렇다면, 쓰레드는 for(..)나 while(..) 반복문과 시간을 많이 소비하는 작업에서 빠져나올 필요가 있습니다. 이 코드는 개발자가 직접 입력해야 하며 자동적으로 사용가능 한 것이 아니라는 것에 유의하시기 바랍니다. JDK는Thread클래스의stop() 메소드를 제공합니다. 하지만 이 메소드는 deprecate 됐으며 사용시 위험할 수 있습니다. 지금 만들고 있는 코드에서는 간단하게 구현했습니다. 사용자가 취소 버튼을 클릭하면 다음의 코드를 실행합니다.
if (sThread != null) {
    sThread.interrupt();
}
SearchThread 클래스는 검색에 관한 모든 기능을 FileFinder 클래스에게 위임하고 있습니다. 따라서 어떻게 해서든지 FileFinder 클래스는 검색 쓰레드가 중단됐다는 것을 알 수 있어야 합니다. 어떤 쓰레드가 중단 됐는지는 다음의 코드로 확인할 수 있습니다:

만약 true를 반환하면 쓰레드는 중단된 것입니다. 따라서 SearchThread는 주지적으로 중단 됐는지 확인을 해야 하고 만약에 중단 됐다면 종료합니다. 쓰레드의 활동과 (중단 요청에 빠르게 응답할 수 있도록) 쓰레드가 중단 됐는지 확인을 너무 자주 할 때 발생할 수 있는 성능 저하 사이에 균형이 필요합니다. 여기서는 각각의 파일들을 검색하기 전에 확인하도록 구현했습니다. [그림 5]는 사용자가 멈춤 버튼을 클릭했을 때의 과정을 시퀀스 다이어그램으로 보여줍니다.


[그림 5] 사용자가 멈춤 버튼을 클릭했을 때를 나타낸 시퀀스 다이어그램

지금까지 구현한 코드는 여전히 눈에 띄는 몇 가지 문제들을 가지고 있습니다.
  • 사용자는 여전히 결과를 보기 위해서는 검색이 끝날 때 까지 기다려야 합니다.
  • 작업이 얼마나 진행됐는지 알 수 없습니다. 사용자는 그저 “Working...” 이라는 메시지만 볼 수 있고 얼마나 완료 됐는지 알 수 없습니다.

역자 백기선님은 AJN(http://agilejava.net)에서 자바 관련 스터디를 하고 있는 착하고 조용하며 점잖은 대학생입니다. 요즘은 특히 Spring과 Hibernate 같은 오픈소스 프레임워크를 공부하고 있습니다. 공부한 내용들은 블로그(http://whiteship.tistory.com)에 간단하게 정리하고 있으며 장래 희망은 행복한 개발자입니다.
TAG :
댓글 입력
자료실

최근 본 책0