반응형
[Java Concurrency] 중단 및 종료 #2
7.3. 비정상적인 스레드 종료 상황 처리
-
스레드를 예상치 못하게 종료시키는 가장 큰 원인은 바로 RuntimeException 이다.
RuntimeException 은 대부분 프로그램이 잘못 짜여져서 발생하거나 기타 회복 불가능의 문제점을 나타내는 경우가 많기 때문에
try_catch 구문으로 잡지 못하는 경우가 많다.
RuntimeException 은 호출 스택을 따라 상위로 전달되기보다는 현재 실행되는 시점에서 콘솔에 스택 호출 추적 내용을 출력하고 해당 스레드를 종료시키도록 되어 있다.
-
스레드 풀에서 사용하는 작업용 스레드나 스윙의 이벤트 처리 스레드와 같은 작업 처리용 스레드는 항상 Runnable 등의 인터페이스를 통해 남이 정의하고, 그래서 그 내용을 알 수 없는 작업을 실행하느라 온 시간을 보낸다.
이런 작업 처리 스레드는 자신이 실행하는 남의 작업이 제대로 동작하지 않을 수 있다고 가정하고 조심스럽게 실행해야 한다.
작업처리 스레드는 실행할 작업을 try-catch 구문 내부에서 실행해 예상치 못한 예외 상황에 대응할 수 있도록 준비하거나, try-finally 구문을 사용해 스레드가 피치 못할 사정으로 종료되는 경우에도 외부에 종료된다는 사실을 알려 프로그램의 다른 부분에서라도 대응할 수 있도록 해야 한다.
RuntimeException 을 catch 구문에서 잡아 처리해야 할 상황은 그다지 많지 않은데,
몇 안 되는 상황 가운데 하나가 바로 남이 Runnable 등으로 정의해 둔 작업을 실행하는 프로그램을 작성하는 경우이다.
-
Thread API 중 UncaughtExceptionHandler 라는 기능이 있다.
이 기능을 사용하면 처리하지 못한 예외 상황으로 인해 특정 스레드가 종료되는 시점을 정확히 알 수 있다.
처리하지 못한 예외 상황 떄문에 스레드가 종료되는 경우에 JVM 이 앱에서 정의한 UncaughtExceptionHandler 를 호출하도록 할 수 있다.
만약 핸들러가 하나도 정의되어 있지 않다면 기본 동작으로 스택 트레이스를 출력한다.
자바 6부터 Thread.setUncaughtExceptionHandler 메소드를 사용해 스레드별로 UncaughtExceptionHandler 를 지정할 수 있고,
setDefualtUncaughtExceptionHandler 메소드를 사용해 기본적으로 사용할 UncaughtExceptionHandler 를 지정할 수도 있게 됐다.
여러 단계로 UncaughtExceptionHandler 를 지정할 수 있지만 실제로 이 중 하나만 불린다.
스레드 지정 핸들러 > ThreadGroup 설정 > DefaultUncaughtException Handler 의 우선순위를 갖는다.
-
잠깐 실행하고 마는 앱이 아닌 이상, 예외가 발생했을 때 로그 파일에 오류를 출력하는 간단한 기능만이라도 확보할 수 있도록 모든 스레드를 대상으로 UncaughtExceptionHandler 를 활용해야 한다.
-
스레드 풀의 작업 스레드를 대상으로 UncaughtExceptionHandler 를 설정하려면
ThreadPoolExecutor 를 생성할 때 작업용 스레드 생성을 담당하는 ThreadFactory 클래스를 별도로 넘겨주면 된다.
자바에서 기본적으로 제공하는 스레드 풀에서는 작업에서 예상치 못한 예외가 발생하면 해당 스레드가 종료되면서, try-finally 구문으로 스레드가 종료되기 전 스레드 풀에 종료 사실을 알려 다른 스레드를 대체해 실행할 수 있도록 하고 있다.
이런 곳에 UncaughtExceptionHandler 를 지정하지 않거나 기타 다른 오류 확인 방법을 전혀 사용하지 않는다면
오류가 생긴 작업이 아무 소리 없이 조용히 종료되어 개발자나 운영자를 혼란스럽게 할 수도 있다.
작업을 실행하는 도중에 예외가 발생해 작업이 중단되는 경우가 생길 때 오류가 발생했다는 사실을 즉시 알고자 한다면, Runnable 이나 Callable 인터페이스를 구현하면서 run 메소드에서 try-catch 구문으로 오류를 처리하도록 되어 있는 클래스를 거쳐 실제 작업을 실행하도록 하거나,
ThreadPoolExecutor 클래스에 마련되어 있는 afterExecute 메소드를 오버라이드 하는 방법으로 오류 상황을 알리도록 하자.
-
ThreadPoolExecutor 에 예외상황이 발생했을 때 UncaughtExceptionHandler 가 호출되도록 하려면
반드시 execute 를 통해 작업을 실행해야 한다.
만약 submit 메소드로 작업을 등록했다면, 그 작업에서 발생하는 모든 예외 상황은
모두 해당 작업의 리턴 상태로 처리해야 한다.
다시 말해 submit 메소드로 등록된 작업에서 예외가 발생하면 Future.get 메소드에서 해당 예외가 ExcutionException 에 감싸진 상태로 넘어온다.
7.4. JVM 종료
-
JVM 이 종료되는 두 가지 경우를 생각할 수 있는데,
하나는 예정된 절차대로 종료되는 경우이고,
또 하나는 예기치 못하게 임의로 종료되는 경우이다.
-
예정된 절차대로 종료되는 경우에 JVM 은 가장 먼저 등록되어 있는 모든 종료 훅(shutdown hook)을 실행시킨다.
종료 훅은 Runtime.addShutdownHook 메소드를 사용해 등록된 아직 시작되지 않은 스레드를 의미한다.
하나의 JVM 에 여러개의 종료 훅을 등록할 수도 있으며, 두 개 이상의 종료 훅이 등록되어 있는 경우에 어떤 순서로 훅을 실행하는지에 대해서는 아무런 규칙이 없다.
-
종료 훅이 모두 작업을 마치고 나면 JVM 은 runFinalizersOnExit 값을 확인해 true 라고 설정되어 있으면
클래스의 finalize 메소드를 모두 호출하고 종료한다.
JVM 은 종료 과정에서 계속해서 실행되고 있는 앱 내부의 스레드에 대해 중단 절차를 진행하거나 인터럽트를 걸지 않는다.
-
만약 종료 훅이나 finalize 메소드가 작업을 마치지 못하고 계속해서 실행된다면
종료 절차가 멈추는 셈이며, JVM 은 계속해서 대기 상태로 머무르기 때문에 결국 JVM 을 강제로 종료하는 수밖에 없다.
JVM 을 강제로 종료시킬 때는 JVM 이 스스로 종료되는 것 이외에 종료 훅을 실행하는 등의 어떤 작업도 하지 않는다.
-
종료 훅은 스레드 안전하게 만들어야만 한다.
공유된 자료를 사용해야 하는 경우에는 반드시 적절한 동기화 기법을 적용해야 한다.
또한 앱의 상태에 대해 어떤 가정도 해서는 안 되며, JVM 이 종료되는 원인에 대해서도 생각해서는 안 되는 등 어떤 상황에서도 아무런 가정 없이 올바로 동작할 수 있도록 굉장히 방어적인 형태로 만들어야 한다.
마지막으로 JVM 이 종료될 때 종료 훅의 작업이 끝나기를 기다리기 때문에
마무리 작업을 최대한 빨리 끝내고 바로 종료해야 한다.
-
종료 훅은 어떤 서비스나 앱 자체의 여러 부분을 정리하는 목적으로 사용하기 좋다.
예를 들어 임시로 만들어 사용했던 파일을 삭제하거나,
운영체제에서 알아서 정리해주지 않는 모든 자원을 종료 훅에서 정리해야 한다.
-
종료 훅에서는 종료될 수 있는 다른 앱이나, 다른 종료 훅이 종료시킬 수 있는 서비스를 사용하지 말아야 한다.
이런 문제를 쉽게 해결하려면 서비스별로 각자 종료 훅을 만들어 등록하기보다는 모든 서비스를 정리할 수 있는 하나의 종료 훅을 사용해 각 서비스를 의존성에 맞춰 순서대로 정리하는 것도 방법이다.
-
스레드를 하나 만들어 부수적인 기능을 처리하도록 하고 싶은데,
해당 스레드가 떠 있다는 이유로 JVM 이 종료되지 않게 하고 싶지 않다면
데몬 스레드(daemon) 을 사용하면 된다.
-
스레드는 두 가지 종류로 볼 수 있다.
하나는 일반 스레드이고 다른 하나는 데몬 스레드이다.
JVM 이 처음 시작할 때 main 스레드를 제외하고 JVM 내부적으로 사용하기 위해 실행하는 스레드(가비지 컬렉터 스레드나 기타 여러 부수적인 스레드)는 모두 데몬 스레드이다.
새로운 스레드가 생성되면 자신을 생성해 준 부모 스레드의 데몬 설정 상태를 확인해 그 값을 그대로 사용하며,
따라서 main 스레드에서 생성한 모든 스레드는 기본적으로 데몬 스레드가 아닌 일반 스레드이다.
-
일반 스레드와 데몬 스레드는 종료될 때 처리 방법이 약간 다를 뿐 그 외에는 모든 것이 완전히 동일하다.
스레드 하나가 종료되면 JVM 은 남아있는 모든 스레드 가운데 일반 스레드가 있는지를 확인하고,
일반 스레드는 모두 종료되고 남아있는 스레드가 모두 데몬 스레드라면 즉시 JVM 종료 절차를 진행한다.
JVM 이 중단(halt)될 때는 모든 데몬 스레드가 버려지는 셈이다.
finally 블록의 코드도 실행되지 않으며, 호출 스택도 원상 복구되지 않는다.
따라서 데몬 스레드는 보통 부수적인 용도로 사용하는 경우가 많다.
데몬 스레드에 사용했던 자원을 꼭 정리해야 하는 일을 시킨다면,
JVM이 종료될 때 자원을 정리하지 못할 수 있기 때문에 적절하지 않다.
-
데몬 스레드는 예고 없이 종료될 수 있기 때문에 앱 내부에서 시작시키고 종료하기에는 그다지 좋은 방법이 아니다.
-
finalize 메소드는 과연 실행이 될 것인지 그리고 언제 실행될지에 대해서 아무런 보장이 없고,
finalize 메소드를 정의한 클래스를 처리하는 데 상당한 성능상의 문제점이 생길 수 있다.
게다가 finalize 메소드를 올바른 방법으로 구현하기도 쉬운 일이 아니다.
대부분의 경우에는 finalize 메소드를 사용하는 대신 try-finally 구문에서 각종 close 메소드를 적절하게 호출하는 것만으로도 finalize 메소드에서 해야 할 일을 훨씬 잘 처리할 수 있다.
finalize 메소드가 더 나을 수 있는 유일한 예는 바로 네이티브 메소드에서 확보했던 자원을 사용하는 객체 정도밖에 없다.
Summary
작업, 스레드, 서비스, 앱 등이 할 일을 모두 마치고 종료되는 시점을 적절하게 관리하려면 프로그램이 훨씬 복잡해질 수 있다.
자바에서는 선점적으로 작업을 중단하거나 스레드를 종료시킬 수 있는 방법을 제공하지 않는다.
그 대신 인터럽트라는 방법을 사용해 스레드 간의 협력 과정을 거쳐 작업 중단 기능을 구현하도록 하고 있으며,
작업 중단 기능을 구현하고 전체 프로그램에 일괄적으로 적용하는 일은 모두 개발자의 몫이다.
FutureTask 나 Executor 등의 프레임웍을 사용하면 작업이나 서비스를 실행 도중에 중단할 수 있는 기능을 쉽게 구현할 수 있다는 점을 알아두자.
반응형
'프로그래밍 놀이터 > 안드로이드, Java' 카테고리의 다른 글
[Java Concurrency] GUI 앱 (0) | 2017.04.28 |
---|---|
[Java Concurrency] 스레드 풀 활용 (0) | 2017.04.27 |
[Java Concurrency] 중단 및 종료 #1 (0) | 2017.04.25 |
[Java Concurrency] 작업 실행 (0) | 2017.04.24 |
[Java Concurrency] 구성 단위 #2 (0) | 2017.04.21 |
댓글