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

한빛출판네트워크

한빛랩스 - 지식에 가능성을 머지하다 / 강의 콘텐츠 무료로 수강하시고 피드백을 남겨주세요. ▶︎

IT/모바일

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

한빛미디어

|

2007-04-17

|

by HANBIT

14,687

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

[이전 기사 보기] 예제로 살표보는 쓰레드 제어하기 - (1)

쓰레드 진행 상황 알려주기

이번에는 검색 과정을 보여줄 수 있는 기능을 추가할 것입니다. 화면은 아래와 크게 다르지 않을 것입니다([그림 6]을 보세요).


[그림 6] 작업 모니터링을 위한 화면

검색 버튼을 클릭하면 취소 버튼이 활성화 될 것입니다. 물론 결과물들은 화면에 출력이 될 것이고(검색이 끝날 때가 아니라 새로운 검색 결과물을 찾았을 때마다) 상태 필드에는 퍼센티지가 보이게 됩니다. 위 기능을 구현하려면 검색 쓰레드가 작업의 완료 정도를 알 수 있도록 FIleFinder 코드를 리팩토링 해야 합니다. 지금(FileFinder)의 구현에서는 완료하는데 얼마나 많은 작업을 필요로 할지 미리 계산하지 않았습니다. 따라서 다음의 두 개의 메소드를 가진 TokenSearchWork 클래스를 만들겠습니다.
  • getAllDirectories ():어떤 디렉토리들을 대상으로 검색하는지 확인합니다. 검색 쓰레드에게 수행 할 작업의 전체 범위를 알려줍니다. 효율적으로 작업을 여러 하위 작업들로 나눕니다.
  • findFilesInDirectory(): 특정 디렉토리에서 해당 키워드로 검색을 합니다.
TokenSearchWork 클래스를 살펴 보겠습니다.
public class TokenSearchWork {

    private File rootDir;

    public TokenSearchWork(File rootDir) {
        this.rootDir = rootDir;
    }

    public List getAllDirectories() 
        throws InterruptedException {
        
        return findDirs(rootDir);
    }

    private List findDirs(File directory) 
        throws InterruptedException {

        checkForInterrupt();

        ....

        return foundDirs;
    }

    ....

    private void checkForInterrupt() 
        throws InterruptedException {
        
        if (Thread.currentThread()
                        .isInterrupted()) {
            throw new InterruptedException(
                "Interrupted !!!");
        }
    }

}
InterruptedException 사용을 주의 싶게 보시기 바랍니다: 두 메소드에서 모두 던지고 있으며 쓰레드를 멈출 때 유용합니다. 중단 요청이 발생할 때 InterruptedException 을 던지는 private 메소드 checkForInterrupt()를 만들었습니다. 이것으로 인해 구현이 다소 단순해 졌습니다. 이 예외는 검색 쓰레드에서 잡힐 것이며 적절하게 처리 될 것입니다. SearchThread의 run 메소드는 이제 TokenSearchWork 클래스를 사용하도록 수정 하겠습니다.
public void run() {

    List allFiles = new ArrayList();
    TokenSearchWork work 
        = new TokenSearchWork(rootDir);
    int percentDone = 0;
    try {
        List allDirs 
                = work.getAllDirectories();
        int sizeWork = allDirs.size();
        for (int j = 0; 
                  j < allDirs.size(); j++) {

            File directory 
                = (File) allDirs.get(j);
            allFiles.addAll(
                work.findFilesInDirectory(
                        directory, token));

            percentDone 
                = 100 * (j + 1) / sizeWork;
            form.setTextArea(allFiles, 
                percentDone, false);
        }
    } catch (InterruptedException intExp) {
        // The Task was interrupted
    }

    form.setTextArea(allFiles, 
        percentDone, true);

}
run() 메소드는 먼저 TokenSearchWork 객체를 생성합니다. 다음으로 getAllDirectories() 메소드를 호출하여 디렉터리의 리스트를 받아 옵니다. 이것으로 쓰레드에게 전체 수행해야 할 작업이 얼만큼인지를 알려줍니다. 그 후, 각각의 디렉터리에서 findFilesInDirectory(..)를 호출하여 파일 내부에 키워드가 존재하는지 검색합니다. 이렇게 함으로써, 검색 쓰레드는 항상 현재까지 수행한 작업의 퍼센티지를 알 수 있습니다. 또한, InterruptedException은 for(..) 문을 빠져나가도록 도와줍니다. SearchForm.setTextArea()의 인자가 완료 한 퍼센티지와 취소한 것을 알기 위해서 변경된 것을 살펴봅시다. 작업이 완료 됐는지 알 수 있도록 percentDone과 boolean 두 개의 새로운 파라미터가 추가 되었습니다. SearchForm은 이런 변경을 다음과 같이 처리합니다:
public void setTextArea(List javaFiles, 
        int percentDone, boolean done) {

    ....

    SwingUtilities.invokeLater(
        new SetAreaRunner(area, areaBuffer
          .toString(), percentDone, done));
}

private class SetAreaRunner 
        implements Runnable {
    
    private JTextArea area;
    private String text;
    private int percent;
    private boolean done;

    public SetAreaRunner(JTextArea area, 
        String text, int percent, 
        boolean done) {
        
        this.area = area;
        this.text = text;
        this.percent = percent;
        this.done = done;
    }

    public void run() {
       // Set the UI fields correctly
    }
}
이번 단계에서 개선한 기능성으로 대부분의 상황에서 유용하게 사용 수 있습니다. 하지만 가끔 두 개의 기능이 필요할 수 있습니다: pause()와 resume()입니다. stop()과 유사하게 Java는 Thread 클래스에서 이런 메소드들을 제공합니다. 하지만 역시 deprecate 됐기 때문에 사용하기엔 위험 합니다. 따라서 만약에 저런 기능이 필요하다면 pause()와 resume()을 만들어야만 할 것입니다.

쓰레드 일시 정지와 재 시작

이번에는 일시 정지와 재 시작 기능을 구현하겠습니다. 두 개의 새로운 버튼을 다음과 같이 추가합니다. 화면에서 일시 정지 버튼을 클릭하면 [그림 7]처럼 보일 것입니다.


[그림 7] 일시 정지/재 시작을 위한 화면

Pause와 resume 기능을 구현하기 위해서, 어떻게 해서든 일지 정지 버튼을 클릭하면 쓰레드를 “sleep” 상태로 만들어야 합니다. 또한, 재 시작 버튼을 클릭하면 쓰레드를 “wake up” 상태로 만들 수 있어야 합니다. 이것을 Java에서 구현하기 위한 최선의 방법은 wait(..)와 notify(...) 메소드를 사용하는 것입니다. SearchThread 에는 request라는 새로운 멤버변수를 추가합니다. 이 변수는 다음의 세 개의 값 중 하나를 가지고 있게 됩니다.
private static final int NORMAL   = 0;
private static final int PAUSE    = 1;
private static final int RESUME   = 2;
일시 정지와 재 시작을 위한 두 개의 새로운 메소드를 SearchThread에 다음과 같이 추가합니다(synchronized 키워드를 사용한 것을 주의 깊게 보시기 바랍니다.):
public synchronized void pauseWork() {
    request = PAUSE;
    notify();
}

public synchronized void resumeWork() {
    if (request == PAUSE) {
        request = RESUME;
        notify();
    }
}
위에 있는 두 개의 메소드는 request 변수를 적절한 값으로 설정하고 있습니다. 게다가 resumeWork()는 대기 상태에 있는 SearchThread를 깨우기 위해 notify() 메소드를 사용합니다. 쓰레드를 sleep 상태로 만들기 위해서 새로운 메소드인 waitIfPauseRequest를 추가합니다.
private void waitIfPauseRequest() 
        throws InterruptedException {
    
    synchronized (this) {
        if (request == PAUSE) {
            while (request != RESUME) {
                wait();
            }

            request = NORMAL;
        }
    }
}
위에 보이듯이, 쓰레드는 request 변수가 PAUSE값으로 설정되어 있다면 wait(..) 메소드를 호출할 것입니다. Request가 RESUME으로 설정 될 때까지 쓰레드는 sleep 상태가 됩니다. 유일한 예외 상황은 예외(Exception)가 발생할 때입니다. 그런 경우로는, wait(..) 메소드에서 InterruptedException이 전달 됐을 때가 있습니다. 사용자가 취소 버튼을 클릭했을 때 발생하게 되고 그 결과 쓰레드는 종료하게 됩니다. 이게 바로 저희가 원하던 기능입니다. 화면 쪽에서는 간단하게 사용자가 일시 정지나 재 시작 버튼을 클릭했을 때 이 메소드들을 호출 할 것입니다(버튼들은 적절하게 사용가능/불가능 상태로 변하게 됩니다.)
....
} else if (source == pauseButton) {
    if (sThread != null) {
        sThread.pauseWork();
        pauseButton.setEnabled(false);
        resumeButton.setEnabled(true);
    }
} else if (source == resumeButton) {
    if (sThread != null) {
        sThread.resumeWork();
        pauseButton.setEnabled(true);
        resumeButton.setEnabled(false);
    }
}
....
시작, 모니터링, 일시 정지, 재 시작 그리고 멈춤 기능을 갖춘 사용자 중심의 검색 기능을 구현했습니다. 일시 정지와 재 시작 기능에서 주의해야 할 사실은 데이터베이스 트랜잭션 내부에서 동작하는 작업을 위해 구현해서는 안 된다는 것입니다. 일시 정지 같은 기능은 트랜잭션 시간을 증가 시키고 다른 쓰레드들이 지연됩니다. 트랜잭션 타임아웃과 같은 눈에 띄는 결점들은 제쳐 놓는다 하더라도, 성능이 현저히 저하될 것입니다.

결론

비록 stop(), pause() 그리고 resume() 메소드들이 deprecate 됐지만 충분히 신경을 쓰면 구현할 수 있습니다. 이 글에서는 Java 프로그램에서 이런 기능들을 간단하게 다루는 방법을 소개했습니다. 하지만, SearchThread는 화면과 실제 검색 작업에 종속성이 매우 높습니다. 이 부분을 유심히 살펴보면, 진행 상황을 측정할 수 있는(검색이 하나의 예로 사용됐습니다.) 모든 시간을 소비 작업들은 SearchThread에 있는 쓰레드 기능들을 동일하게 필요로 하는 것을 알 수 있습니다. 더 나은 재사용성을 고려하여 멈춤, 모니터링, 일시 정지 와 재 시작을 다룰 수 있는 제네릭 프레임워크(generic framework)를 만들 수 있습니다. 만약 여러분들께 특별한 동기가 주어진다면, 이런 기능들을 멀티 유저를 위한 엔터프라이즈 환경에 제공 할 수 있도록 시도 해볼 수도 있겠습니다(Ajax와 servlet을 사용해서).
저자 Viraj Shetty 는 Johns Hopkins University에서 석사 학위를 받았으며 J2EE 아키텍처로 Enterprise J2EE Technology에서 8년째 일하고 있습니다.
역자 백기선님은 AJN(http://agilejava.net)에서 자바 관련 스터디를 하고 있는 착하고 조용하며 점잖은 대학생입니다. 요즘은 특히 Spring과 Hibernate 같은 오픈소스 프레임워크를 공부하고 있습니다. 공부한 내용들은 블로그(http://whiteship.tistory.com)에 간단하게 정리하고 있으며 장래 희망은 행복한 개발자입니다.
TAG :
댓글 입력
자료실

최근 본 상품0