본문 바로가기
프로그래밍 놀이터/안드로이드, Java

[Java Concurrency] 중단 및 종료 #2

by 돼지왕 왕돼지 2017. 4. 26.
반응형


 [Java Concurrency] 중단 및 종료 #2


afterExecute, callable, CLOSE, daemon, default, excutionexception, execute, executor, Finalize, finalize 메소드, Finally, Future, futuretask, Get, halt, java concurrency, jvm, jvm 강제종료, JVM 종료, jvm 종료 원인, run, runFinalizersOnExit, Runnable, runtime.addshutdownhook, RuntimeException, setdefaultuncaughtexceptionhandler, setuncaughtexceptionhandler, shutdown hook, stack trace, SUBMIT, thread api, threadfactory, threadgroup, try catch, try finally, uncaughtexceptionhandler, [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 등의 프레임웍을 사용하면 작업이나 서비스를 실행 도중에 중단할 수 있는 기능을 쉽게 구현할 수 있다는 점을 알아두자.






반응형

댓글