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

한빛출판네트워크

IT/모바일

예제로 설명하는 쓰레드 제어하기(1)

한빛미디어

|

2007-05-10

|

by HANBIT

11,053

제공 : 한빛 네트워크
저자 : Viraj Shetty
역자 : 조춘식
원문 : Controlling Threads by Example

자바 언어에서 기본적으로 사용 할 수 있는 유용한 특징 중 하나로 멀티 쓰레드 어플리케이션 작성이 있다. 쓰레드란, 실행중인 프로그램의 실행경로인데, 쓰레드 자신의 지역변수, 프로그램 카운터, 일생을 가진다. 만약 특정 작업이 어떤 쓰레드 안에서 긴 시간 동안 실행해야 한다면, 그 작업을 stop, monitor, pause, 그리고 resume의 메커니즘이 필요하다.

이 글에서는 쓰레드를 사용하는 간단치 않은 예제를 만든 후에 이런 메커니즘들을 포함하도록 리팩토링 하겠다. 이 글에서 독자가 SWING프로그래밍 약간의 지식과 쓰레드에 대한 기본 이해를 필요로 한다. 그리고 글의 마지막에서 예제 압축 파일을 다운로드 할 수 있다.

간단치 않은 예제

특정 디렉토리와 그 하위 디렉토리에 있는 모든 자바 소스들에 대해 특정 문자열을 포함하는지를 검색하는 어플리케이션을 개발한다. 사용자는 SWING 인터페이스를 이용하여 디렉토리를 선택할 수 있고 찾고자 하는 문자열을 입력할 수 있다. 사용자는 검색 작업을 start, stop, monitor, pause 또는 resume 할 수 있게 된다. 최종 사용자 인터페이스는 [그림 1]과 같을 것이다.


[그림 1] 예제를 위한 최종 사용자 인터페이스

[그림 1]의 아래에 Search, Cancel, Pause, Resume 작업을 위한 4개의 버튼이 있다. Resume 버튼 오른편에는 레이블이 작업 진행 정도를 표시한다. 검색결과 파일들은 중앙에 표시된다. 모든 입력 필드는 필요한 경우에만 활성화되어야 한다. 이 요구사항들은 아래 기술한 세가지 iteration을 통해 해결하게 된다:
  • Stop 기능이 있는 쓰레드 사용하기
  • 쓰레드 진행사항 결정하기
  • pause와 resume 기능 추가
사용자 인터페이스와 쓰레드

최소 구현은 물론 쓰레드를 전혀 사용하지 않는 것이다. 쓰레드를 사용하지 않은 경우를 한번 상상해보자. 아마 화면에는 Search 버튼 하나만 존재할 것이다. 버튼을 클릭하고 나면 사용자는 문자열 포함한 모든 파일이 다 검색될 때까지 기다려야만 될 것이다. 이것은 분명 사용자에게는 끔직한 성능문제이지만, 쓰레드를 사용하지 않는 다는 가정이 가지는 한계를 넘는 성능 개선은 있을 수 없다. 즉 이 문제에 대한 해결책은 검색 도중에도 어플리케이션을 사용할 수 있도록 실용적으로 검색을 수행하는 것이다. 예를 들면 이 검색이 진행중임에도 불구하고 파일들을 편집 가능한 기능이 입력 창에 내장되어야 한다. 물론 이렇게 하려면 쓰레드를 사용해야 한다. 그러나 여전히 응답시간이 짧고 사용자가 기다릴 의지가 있는 경우에만 이런 구현은 받아들여진다.

SWING에서는, 화면에 표시되는 사용자 인터페이스의 전부는 Event Dispatching Thread에 의해 그려진다. 버튼 클릭은 이벤트로 인식되고 우리는 이런 클릭들을 “listen”하는 클래스를 작성할 수 있다. 특별히 버튼 클릭 이벤트를 “listen”하길 원하는 클래스는 ActionListener 인터페이스의 actionPerformed 함수를 구현해야 한다. 이 함수들은 Event Dispatching 쓰레드에 의해 호출되어짐을 주의하라. 그러므로 이 함수 안에서 시간이 많이 요구되는 작업을 할 경우 어플리케이션 전체에 하나밖에 없는 Event Dispatching 쓰레드가 묶여있게 되어 화면에 관한 사용자의 다른 요구들은 제시간에 응답하지 않을 수 있게 된다.

Stop 기능이 있는 쓰레드 사용하기

SWING의 Event Dispatching 쓰레드가 다른 작업을 처리할 수 있도록, 검색 기능을 별도의 쓰레드에서 실행하자. 하지만 쓰레드 사용은 몇 가지 궁금 점을 유발한다.
  • 내가 만든 쓰레드에서 생성된 결과를 어떻게SWING 화면에 표시하나?
  • 사용자가 버튼을 두 번 클릭하는 것을 어떻게 금지하나?
  • 쓰레드를 어떻게 멈추나?
  • 쓰레드가 계속 동작 중이라는 것을 어떻게 알 수 있나?
다음 사용자 인터페이스가 사용되어 질것이다 (그림 2를 보라)


[그림 2] Search/Cancel 기능을 가진 사용자 인터페이스

기본적으로 Search 버튼, Directory 선택 버튼, 그리고 문자열 입력 창이 사용가능 하다. 사용자가 Search버튼을 클릭하면, 이 두 버튼들과 입력 창은 disable된다. (이것은 버튼을 다시 클릭하는 것을 방지한다). 이 시점에서 취소 버튼이 enable 해지고 상태 레이블은 “Working…”이 표시된다. 이 상태 레이블은 사용자가 검색이 여전히 수행중임을 알 수 있도록 도입했다. 이러한 작업에 필요한 클래스들을 먼저 생각해보자. 분명히 3개의 클래스들을 필요로 한다.
  • FileFinder: 실제 검색 작업을 수행
  • SearchForm: SWING을 사용하여 결과를 화면 처리하는 VIEW로 동작
  • SearchThread: 쓰레드의 기능을 구현
[그림 3]의 클래스 모델을 통해 디자인을 설명한다.


[그림 3] Search/Stop 기능을 위한 클래스 다이어그램

FileFinder.java 클래스는 하위 디렉토리 포함, 특정 디렉토리의 특정 문자열을 포함하는 모든 파일을 찾아내는 책임을 가진다. 생성자는 디렉토리를 지정한 File Object를 인자로 가진다. findFiles(..) 함수는 검색 결과로서 File 객체들을 리턴 한다. FileFinder 클래스를 사용하는 법은 아래와 같다:
FileFinder finder = new FileFinder(dirFile);
List javaFiles = finder.findFiles(token);
여기서 알고리즘을 살펴보는데 시간을 보내지는 않을 것이다. 여러분이 다음 iteration을 진행하기 앞서 이번 iteration의 전체 소스를 직접 살펴보기를 바란다. SearchForm.java의 SearchForm 클래스는 SWING 인터페이스를 통해 화면을 만들고 검색 버튼이 눌리면 결과를 화면에 나타낼 것이다. 생성자에서 windows의 크기를 설정한다. JFrame contentPane에 모든 SWING 컴포넌트를 추가한다. 버튼 클릭을 처리하기 위해 SearchForm은 ActionListener 인터페이스를 구현한다. SWING에서는 일반적으로 Listener 메커니즘을 사용해 화면에 대한 사용자 작업을 처리한다. SearchForm.actionPerformed(..) 함수는 Directory, Search, Cancel 버튼이 클릭된 모든 경우의 기능을 구현한다. Directory 버튼이 클릭되면 JFileChooser를 통해 디렉토리를 선택 할 수 있다. 소스 압축파일을 다운 받아 실제 코드를 확인해라. 검색 버튼이 클릭되면, SearchThread 클래스를 이용하여 별도의 쓰레드 검색이 호출되어 검색 프로세스를 시작한다. 아래에 SearchForm 클래스 코드의 일부가 있다.
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);
    }

}
생성자는 디렉토리와 검색 문자열, SearchForm(화면 결과 출력을 위해)을 인자로 가진다. SearchThread가 FileFinder를 사용한다는 것을 주의하고, 쓰레드 자신은 사용자가 Search버튼이 누른 경우 호출된다:
// Invoke the search on a different Thread
File dirFile = new File(dirName);
String token = tokenField.getText();
sThread 
        = new SearchThread(dirFile,token,this);
sThread.start();
sThread 변수는 SearchForm 멤버 변수란 걸 주의해라. 이 쓰레드가 시작되면, SWING의 Event Dispatching 쓰레드는 다른 작업을 자유롭게 수행할 수 있다. 검색 시작 되면 필요한 작업 중 중요한 한가지는 stop 작업이다. 검색 쓰레드가 화면 TextArea에 파일을 보여주기 아래 함수를 호출한다는 것을 주의해라:
form.setTextArea(javaFiles);
SWING에서 사용되는 한가지 황금률은 모든(거의 모든) 화면 수정은 SWING Event Dispatcing 쓰레드 안에서 수행되어지는 것을 바란다는 것이다. 하지만 우리는 SearchThread안에서 화면을 수정할 필요가 있는데 이 경우는 소스를 통해 설명하겠다. Form.sextTextArea(..)의 구현을 보도록 하자:
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를 사용하면 SWING은 그 즉시 화면 갱신 필요성을 알아차린다. setTextArea(..) 함수는 먼저 파일 리스트 문자열을 만들고나서 invokeLater(..)를 사용해 text area 필드를 화면에 설정한다. SwingUtilities.invokeLater(..)의 코드에 의해 우리는 효과적으로 Event Dispatching 쓰레드가 화면 갱신코드를 호출하도록 만든다. invokeLater 함수는 단순히 Runaable 객체를 단순히 Event Dispatcher 큐에 삽입한다. SWING Event Dispatcher 쓰레드는 변화를 감지하고 우리 코드의 run() 메써드를 호출한다. [그림 4]는 사용자가 Search 버튼을 누른 경우 생기는 일에 대한 시퀀스 다이어그램이다.


[그림 4] 사용자가 Search 버튼을 클릭했을 때의 이벤트 시퀀스

이제 쓰레드를 시작하는 코드를 작성했는데, 어떻게 하면 멈출 수 있을까? 자바에서는 쓰레드를 인터럽트 하기 위해 중요한 특성을 제공한다. 문법은 아래와 같다.
thread.interrupt();
인터럽트된 쓰레드는 자신이 인터럽트가 되었는지 주기적으로 확인해야 한다. 만약 인터럽트 되었다면, 쓰레드는 for(..) 나 while(..) 같은 loop를 빠져 나와 작업을 종료할 것이다. 이런 코드는 개발자에 의해 추가되는 것이지 자동으로 사용 가능한 것은 아니란 것을 주의해라. JDK는 stop() 함수를 쓰레드 클래스에서 제공한다. 그러나 이 함수는 deprecated되었고 사용하는 것은 권장되지 않는다. 우리 코드에서의 구현은 매우 간단하다. 사용자가 Cancel버튼을 누른 경우 아래와 같다.
if (sThread != null) {
    sThread.interrupt();
}
SearchThread 클래스는 간단히 검색에 대한 책임을 FileFinder 클래스에 모두 위임하고 있음을 주의해라. 그러므로 FileFinder 클래스는 인터럽트가 발생된 경우 반드시 이를 알릴 필요가 있다. 어떤 쓰레드가 인터럽트 되었는지 여부는 아래처럼 확인 할 수 있다.
Thread.currentThread().isInterrupted()
만약 리턴 값이 참이라면 해당 thread는 인터럽트 된 것이다. 그러므로 SearchThread는 주기적으로 인터럽트 되었는지 여부를 확인하고, 그렇다면 종료해야 한다. 쓰레드의 생존여부와 성능 감소 둘 사이의 균형을 위해 너무 잦은 인터럽트 체크를 하지 않도록 주의해라; 잦은 체크는 쓰레드의 생존여부를 자주 확인하여 인터럽트 처리를 빠르게 할 수 있지만 체크하는 만큼의 성능 감소가 된다. 우리 구현에서는 파일 하나가 검색될 때 마다 체크를 수행한다. [그림 5]는 사용자가 Stop 버튼을 클릭한 경우 발생 하는 일에 대한 시퀀스 다이어그램이다.


[그림 5] 사용자가 Stop 버튼을 클릭했을 때의 이벤트 시퀀스

이 구현은 여전히 몇 가지 눈에 띄는 문제를 가진다.
  • 작업이 마칠 때까지 결과는 전혀 나타나지 않는다.
  • 작업 진행 사항을 알려주지 않는다. 사용자는 그냥 “Working” 만 볼 수 있고 진행 정도를 알 수 없다.

본 기사는 담당자의 착오로 기존에 게재된 기사와 같은 원문을 번역한 것입니다.
TAG :
댓글 입력
자료실