IBM Korea Skip to main content
       IBM 홈    |  제품 & 서비스  |  고객지원 & 다운로드  |  회원가입  

Java theory and practice : 내 쓰레드는 어디에?
서버 애플리케이션에서 쓰레드 유출 피하기

Level: 중급

Brian Goetz
Principal Consultant, Quiotix Corp
2002년 9월

Column icon주의를 기울이지 않으면 쓰레드는 아무런 흔적도 없이 서버 애플리케이션에서 사라질 수 있다. 쓰레딩 전문가 Brian Goetz는 쓰레드 이탈 방지 및 감시 기술을 제공한다.

싱글 쓰레드 애플리케이션의 주 쓰레드가 잡히지 않은 예외를 던질 때 스택 트레이스가 콘솔상에 프린트 되는 것을 통해 알게 된다. 하지만 멀티쓰레드 애플리케이션에서 특히 서버로서 실행되고 콘솔에 어태치 되지 않은 애플리케이션의 경우 쓰레드의 소실은 알려질 수 없는 이벤트가 되어 애플리케이션 작동을 혼돈시키킬 수 있는 부분적인 시스템 고장을 일으킨다.

7월 Java theory and practice 에서, 쓰레드 풀(thread pool)을 다뤘다. 부적절하게 작성된 쓰레드 풀이 쓰레드를 어떻게 유출시키는지를 검토했다. 대부분의 쓰레드 풀 구현은 던져진 예외를 잡고 죽은 쓰레드를 재시작하는 것으로 유출을 보호하려 한다. 하지만 쓰레드 유출의 문제는 쓰레드 풀에 제한되지 않는다. 서비스 작업 큐에 쓰레드를 사용하는 서버 애플리케이션도 이러한 문제를 피할 수 없다. 서버 애플리케이션이 워커(worker) 쓰레드를 잃을 때 애플리케이션은 잠시동안 작업이 잘 진행되는 것 처럼 보이게 하면서 문제의 진짜 원인은 확인하기 힘들게 만든다.

많은 애플리케이션들은 백그라운드 서비스를 제공하는 데에 쓰레드를 사용하여 백그라운드 서비스를 제공한다. 이벤트 큐에서 태스크를 프로세싱하고 소켓의 명령어를 읽거나 UI 쓰레드 밖에서 장기 실행 태스크를 수행한다.

가끔씩 사용자들은 어떤 일도 발생하지 않는 것 처럼 느낀다. 예를 들어 스펠 체킹 같이 사용자에 의한 장기 쓰레드 실행 태스크를 수행하거나 오퍼레이션 또는 프로그램을 멈출 경우이다. 하지만 대부분은 백그라운드 쓰레드는 정리작업을 수행하고 그들의 의존성 때문에 오랫동안 알 수가 없다.

예제 서버 애플리케이션
가상의 미들웨어 서버 애플리케이션을 생각해보자. 다양한 인풋 소스로부터 메시지들을 모으고 그들을 외부 서버 애플리케이션으로 보내고 외부 서버에서 응답을 받아 그것을 다시 적당한 인풋 소스로 라우팅한다. 각 인풋 소스의 경우 고유의 방식으로 인풋 메시지를 받는 플러그인이 있다. 파일의 디렉토리 스캐닝, 소켓 연결을 기다리기, 데이터 베이스 등록 작업을 한다. 이 플러그인은 JVM 서버에서 실행된다 하더라도 제 3자에 의해 작성된다. 이 애플리케이션은 적어도 두 개의 내부 작업 큐를 가지고 있다 (플러그인에서 받은 메시지는 서버로 전송되기를 기다리고 있고 서버에서 받은 응답들은 적절한 플러그인으로 전송되기를 기다린다). 메시지들은 서비스 루틴인 incomingResponse()를 플러그인 객체상에서 호출함으로서 원래 플러그인으로 라우팅된다.

플러그인에서 메시지를 받은 후에 이 메시지들은 아웃고잉(outgoing) 메시지 큐로 대기한다. 아웃고잉 메시지 큐에서 받은 메시지는 하나 이상의 쓰레드로 처리된다. 이 쓰레드는 큐로 부터 온 메시지를 읽고 소스를 기록하고 이것을 원격 서버 애플리케이션으로 보낸다. 원격 애플리케이션은 웹 서비스 인터페이스를 통해 응답하고 서버는 받은 응답들을 인커밍 응답 큐로 대기시킨다. 하나 이상의 응답 쓰레드는 인커밍 응답 큐에서 메시지를 읽고 그들을 적절한 플러그인으로 라우팅하면서 모든 프로세스를 끝낸다.

이 애플리케이션에서 우리는 두 개의 메시지 큐(아웃고잉 요청과 인커밍 응답)를 가지게 된다. 그리고 아마도 다양한 플러그인 안에는 추가 큐 들도 있을 것이다. 우리는 또한 여러가지의 서비스 쓰레드를 가지고 있는데 아웃고잉 메시지 큐에서 요청을 읽어 그들을 외부 서버로 보내는 쓰레드와 인커밍 응답 큐에서 응답을 읽어 그들을 플러그인으로 라우팅하는 쓰레드가 있다.

쓰레드 소실이 명백하지 않다!
이들 쓰레드 중 응답을 보내는 쓰레드가 사라진다면? 플러그인이 새로운 메시지를 보낼 수 있기 때문에 잘못되고 있다는 사실을 즉시 인식하지 못한다. 메시지들은 다양한 인풋 소스를 통해 도착하고 또한 애플리케이션을 통해 외부 서버로 보내진다. 플러그인이 응답을 즉시 기대하고 있지 않기 때문에 문제가 있다는 것 역시 알 수 없다. 결국 받은 응답은 대기가 길어지게 된다. 그들이 메모리에 저장되어 있다면 메모리는 소진될 것이다. 그렇지 않더라도 어느 시점에서 누군가가 응답이 전송되지 않았다는 것을 인식하겠지만 오랜 시간이 지난 다음이다. 왜냐하면 시스템의 다른 측면들은 정상적으로 작동하기 때문이다.

주 태스크 핸들링 측면이 싱글 쓰레드가 아닌 쓰레드 풀로 핸들 될 때 갑작스러운 쓰레드 유출 결과에 대해 어느 정도 확실한 보장이 있다. 여덟 개의 쓰레드로 잘 수행되는 쓰레드 풀은 일곱 개로도 작업을 수행할 것이기 때문이다. 우선 인식할 수 있는 차이점이 없다. 결국 미묘한 방식으로 시스템 퍼포먼스는 떨어진다.

서버 애플리케이션에서 쓰레드 유출 문제는 외부에서는 쉽게 감지될 수 있는 것이 아니다. 대부분의 쓰레드는 서버 워크로드의 어느 한 부분만을 핸들하거나 특정 유형의 백그라운드 태스크를 핸들하기 때문에 심각한 오류에 처해 있더라도 프로그램은 사용자에게 기능적으로 이상이 없는 것으로 보일 수 있다. 이러한 문제는 쓰레드 유출이 항상 증거를 남기지 않는 다는 사실과 더불어 애플리케이션 작동을 어렵게 만드는 요인이 된다.

RuntimeException은쓰레드 소실의 원인이 된다!
쓰레드는 잡히지 않는 예외나 에러를 줄 때 사리질 수 있다. 또는 전혀 완료되지 않을 I/O 작동을 기다리거나 어떤 누구도 notify()를 호출하지 않는 다는 것을 모니터링 할 때 작동을 멈춘다. 예측하지 못한 쓰레드 소실의 가장 일반적인 요인은 RuntimeException 을 던질 때이다. 예제 애플리케이션에서 RuntimeException 이 주어질 한 장소는 플러그인 객체상에서 incomingResponse() 를 호출하여 플러그인으로 다시 응답이 넘겨지게 된다. 플러그인 코드는 제 삼자에 의해 작성되었거나 애플리케이션이 작성된 후에 작성되었다. 따라서 애플리케이션 작성자가 잘못을 검사하는 것은 불가능하다. 플러그인이 RuntimeException를 던질 때 응답 서비스 쓰레드가 종료한다면 이것은 오류가 농후한 플러그인이 전체 시스템을 다운시킬 수 있다는 것을 의미한다. 이러한 취약점이 매우 일반적이라는 것은 불행한 일이다.

체크된 예외에 대해 적극적으로 코딩하겠지만 체크되지 않은 예외는 자바 개발자들에 의해 무시당할 것이다. 싱글 쓰레드 애플리케이션에서 핸들되지 않은 RuntimeException의 결과는 명확하다. 그리고 이것이 발생하는 곳에 명확한 스택 트레이스가 있다. 이는 문제를 알려줄 뿐더러 이를 해결할 유용한 정보도 제공한다. 하지만 멀티쓰레드 애플리케이션에서 쓰레드는 체크되지 않은 예외 때문에 조용히 없어진다. 사용자들과 개발자들은 그 원인부터 찾아야 한다.

우리의 예제 애플리케이션의 요청/응답 핸들러 쓰레드 같은 태스크 프로세싱 쓰레드는 기본적으로 거의 모든 시간을 Runnable 같은 추상 배리어를 통해 서비스 메소드를 호출한다. 추상 배리어의 다른 부분에 어떤 것이 있는지 알 수 없기 때문에 서비스 루틴이 RuntimeException을 던지면 호출 쓰레드는 이것을 찾아 기록하고 큐의 다음 아이템으로 옮기거나 쓰레드를 다운시키고 이를 재시작한다.

Listing 1의 코드는 작업 큐에서 Runnable 태스크를 처리하는 쓰레드의 전형이다. 체크되지 않은 예외를 던지는 플러그인을 감시하지 않는다.

Listing 1. RuntimeException를 감시하지 않는 worker 쓰레드

private class TrustingPoolWorker extends Thread {
    public void run() {
        IncomingResponse ir;

        while (true) {
            ir = (IncomingResponse) queue.getNext();
            PlugIn plugIn = findPlugIn(ir.getResponseId());
            if (plugIn != null)
                plugIn.handleMessage(ir.getResponse());
            else
                log("Unknown plug-in for response " + ir.getResponseId());
        }
    }
}

이 worker 쓰레드가 실패에 대해 더욱 강해지도록 많은 코드를 추가할 필요가 없다. RuntimeException을 잡고 그런다음 정정 작업을 함으로서 전체 서버에 해를 끼치는 어설프게 작성된 플러그인으로 부터 보호할 수 있는 것이다. 적절한 정정 작업은 에러를 기록하고 다음 메시지로 이동시키고 현재 쓰레드를 종료하고 이를 재시작하거나 문제를 일으키는 플러그인을 언로딩하는 것이다. (Listing 2):

Listing 2. RuntimeException을 감시하는 worker 쓰레드

private class SaferPoolWorker extends Thread {
    public void run() {
        IncomingResponse ir;

        while (true) {
            ir = (IncomingResponse) queue.getNext();
            PlugIn plugIn = findPlugIn(ir.getResponseId());
            if (plugIn != null) {
                try {
                    plugIn.handleMessage(ir.getResponse());
                }
                catch (RuntimeException e) {
                    // Take some sort of action; 
                    // - log the exception and move on
                    // - log the exception and restart the worker thread
                    // - log the exception and unload the offending plug-in
                }
            }
            else
                log("Unknown plug-in for response " + ir.getResponseId());
        }
    }
}

ThreadGroup이 제공한 잡히지 않은 예외 핸들러 사용하기
ThreadGroup 클래스의 uncaughtException을 사용하는 방법도 있다. ThreadGroup은 그다지 많이 유용하지 않지만 uncaughtException 기능이 반드시 필요할 때가 있다. Listing 3은 쓰레드가 잡히지 않은 예외로 없어질 때 ThreadGroup을 사용하여 이를 감지하는 예제이다:

Listing 3. uncaughtException을 사용하여 쓰레드 소실 알아내기

public class ThreadGroupExample {

    public static class MyThreadGroup extends ThreadGroup {

        public MyThreadGroup(String s) {
            super(s);
        }

        public void uncaughtException(Thread thread, Throwable throwable) {
            System.out.println("Thread " + thread.getName() 
              + " died, exception was: ");
            throwable.printStackTrace();
        }
    }

    public static ThreadGroup workerThreads = 
      new MyThreadGroup("Worker Threads");

    public static class WorkerThread extends Thread {
        public WorkerThread(String s) {
            super(workerThreads, s);
        }

        public void run() {
            throw new RuntimeException();
        }

    }

    public static void main(String[] args) {
        Thread t = new WorkerThread("Worker Thread");
        t.start();
    }
}

잡히지 않은 예외를 주었기 때문에 thread group의 쓰레드가 없어진다면 thread group의 uncaughtException() 메소드가 호출된다. 이것은 로그에 엔트리를 작성하고 쓰레드와 시스템을 재시작하고 필요할 경우 정정 및 진단 작동을 한다. 쓰레드가 없어질 때 모든 쓰레드가 로그 메시지를 작성한다면 무엇이 잘못되었는지에 대한 기록을 갖게된다.

요약
애플리케이션에서 쓰레드가 사라지고 쓰레드가 흔적없이 사라질 때 혼란스럽다. 쓰레드 유출을 방지하는 최상의 방법은 방지와 감시의 협동작용이다.

참고자료

목 차:
예제
쓰레드 소실이 명백하지 않다!
쓰레드 소실의 원인
잡히지 않은 예외 핸들러 사용하기
요약
참고 자료
필자 소개
기사에 대한 평가
관련 dW 링크:
Multithreaded Java programming discussion forum
Thread pools and work queues
The Orphaned Thread bug pattern
Intro to Java threads
Java theory and practice columns
Subscribe to the developerWorks newsletter
US 원문 읽기
Also in the Java zone:
Tutorials
Tools and products
Code and components
Articles
필자소개
Brian Goetz는 소프트웨어 컨설턴트이며 지난 15년간 전문적인 소프트웨어 개발자로 일해왔다. 그는 캘리포니아주 Los Altos 소재 소프트웨어 개발 및 컨설팅 업체인 Quiotix사의 수석 컨설턴트이다.
이 기사에 대하여 어떻게 생각하십니까?

정말 좋다 (5) 좋다 (4) 그저그렇다 (3) 수정보완이 필요하다(2) 형편없다 (1)

  회사소개  |  개인정보 보호정책  |  법률  |  문의