ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 착한 가로채기, InterruptedException
    Java & Kotlin 2022. 2. 21. 03:16
    반응형

    1) 서론

    JVM은 Java 언어가 어떠한 운영체제와도 동작할 수 있도록 도와주는 가상 머신입니다. 기계어로 바로 변환되는 C언어와 다르게, byte code로 변환 후 필요시 native code로 변환하고 읽는데요.

     

    "Write once, Run anywhere"라는 자바 언어를 만들던 당시의 철학을 떠올려보면, 당연한 동작 방식 입니다.

     

    JVM은 단순히 코드를 변환해주는 역할만 하지 않습니다. 운영체제 위해서 애플리케이션과 메모리를 관리하는 역할도 하는데요. 특히 프로세스, 스레드 관리를 매우 철저하게 해 줍니다. GC와 같이 사용되지 않지만, 메모리에 올라와있는 코드를 정리해주기도 합니다.

     

    특히 스레드가 deadlock에 빠졌을 때 JVM은 그냥 두고만 보지 않는데요. 이때 발생할 수 있는 InterruptedException에 대해서 알아보도록 하겠습니다.

     


    2) InterruptedException이란?

    InterruptedException은 스레드가 waiting, sleeping, interrupted 된 상태에서 발생할 수 있습니다. 자바의 sleep() 메서드를 보게 되면, InterruptedException을 상위 메서드로 던지는 것을 알 수 있는데요.

     

    프로그래머가 임의로 스레드를 멈추는것이 아닌, 애플리케이션 구동 중에 위와 같은 상황에 빠진다는 것은 어떤 의미일까요?

     

    멀티 스레드(multi thread)라고 하면, 왠지 여러 개의 스레드가 동시에 병렬적으로 동작할것 같습니다. 하지만 멀티 스레드는 병렬적으로 동시에 수행되는 것처럼 보일 뿐, 실제로는 CPU의 문맥 교환(context switch)에 의해서 빠르게 각각의 스레드를 바꿔서 실행합니다. 현대의 CPU의 성능은 고성능(core, clock)입니다. 스레드 단위의 문맥 교환은 매우 빠르게 수행되며, 동시에 수행되는 것처럼 착각을 일으키기에 충분한데요.

     

    개인적으로는 "멀티'처럼' 보이는 스레드"라고 이름을 지어야 하지 않나 생각합니다.

     

    즉 하나의 스레드가 동작하고 있을 때 나머지는 waiting 상태에서 대기하게 되는데요. 이때 JVM은 sleep(), wait() 메서드를 활용해서 스레드를 wait, sleep 상태로 만들 수 있습니다.

     

    하지만 어떤 스레드가 sleep 혹은 wait 상태에서 영원히 block이 된다면, 어떻게 될까요? Block된 쓰레드가 내부에 존재하는 메서드는 정상적인 수행을 하지 못 하고, runnable 상태가 되기를 끝나기를 기다릴 것입니다.

     

    이때 JVM은 영원히 기다리지 않고, block 된 스레드를 interrupt 후 InterruptedException을 발생시킵니다.


    3) Interrupt란?

    그래서 interrupt는 뭘까요?

     

    앞서 언급했듯이, 현대의 CPU는 매우 고성능입니다. 특히 하이퍼쓰레딩이라는 기술로 인해 '코어 수 * 2배'의 스레드를 처리하기도 하는데요. 이런 고성능 CPU가 단순 입출력을 기다리느라, 다른 작업을 못 한다면 낭비일 것입니다.

     

    예를 들어 키보드 사용 중 마우스를 사용하려고 할 때 여전히 키보드의 입출력을 기다리느라, 마우스를 사용하지 못 한다면 매우 비효율적일것입니다. 이때 운영체제는 키보드 입출력에 대한 스레드를 interrupt 해서 buffer에 저장합니다. 그리고 마우스 동작에 대한 스레드를 run 시키고, 사용하는데요. 

     

    JVM도 마찬가지로, 애플리케이션 구동 중 스레드를 강제로 중단시킬 때 interrupt 합니다. 

     

    아래처럼 한 메서드 안에서 특정 스레드가 영원히 sleep 상태에 빠지는 경우, 다른 스레드는 영원히 기다려야합니다.

    method() {
    	절대 끝나지 않는 쓰레드;
    	다음에 수행되어야 할 쓰레드; // 계속 기다려야 합니다.
    }

     

    이때 다른 스레드는 영원히 끝나지 않는 스레드를 기다리지 않습니다.

     

    영원히 sleep 혹은 wait 상태에 빠진 쓰레드를 Thread.interrupt()를 활용해서 interrupt 합니다. 이때 interrupt 받은 스레드는 동작을 중지하고, InterruptedException을 발생시킵니다.


    4) Interrupt된 쓰레드는 어떻게 판단할까요?

    스레드에는 interrupt status라는 것이 존재하는데요. interrupt() 메서드를 활용해서, block 된 target 스레드의 interrupt status에 flag를 추가합니다.

     

    그리고 다른 스레드는 Thread.interrupted()를 사용해서, target 스레드가 interrupt 됐는지 확인합니다. 이때 interrupt status에 flag가 있다면, interrupt로 판단합니다.

     

    • 자바의 interrupt() 메소드입니다.
    • intterup status를 추가하는 로직이 포함되어있습니다.

    5) InterruptException을 다루는 방법

    InterruptException을 유발할 수 있는 메서드를 사용하면, 아래와 같은 메시지를 볼 수 있습니다.

    Either re-interrupt this method or rethrow the "InterruptedException" that can be caught here

     

    이미 interrupt에 의해서 스레드의 동작이 중단되었고, catch로 잘 다루고 있는데 왜 re-interrupt 해야 할까요?

    사실은 interrupt()를 사용하게 되면 아래와 같은 흐름이 발생합니다.

    • interrupt status clear: interrupt status 초기화
    • throw InterruptedException: exception 발생

     

    즉 interrupt status가 clear되기 때문에 JVM의 stack 중 상위 메소드 혹은 thread pool에서 해당 내용을 알 수 없습니다. 그래서 interrupt 되었음을 알려줄 필요가 있습니다.

     

    아래의 코드들은 Thread가 interrupt 될 때까지 무한반복하는 코드입니다.

    public class ExceptionTest {
    
        public static void main(String[] args) {
    
            Thread targetThread = new targetThread();
            targetThread.start();
    
            try {
                Thread.sleep(1000);
                // 1초 뒤 targetThread interrupt 합니다.
                targetThread.interrupt();
            } catch (InterruptedException e) {
                System.out.println("finished!");
            }
        }
    
        static class targetThread extends Thread {
            @Override
            public void run() {
                // Thread interrupt 될 때까지 반복합니다.
                while (!Thread.currentThread().isInterrupted()) {
                    try {
                        System.out.println("sleeping...");
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        System.out.println("It is interrupted!");
                        // Thread.currentThread().interrupt();
                    }
                }
            }
        }
    }
    • Thread가 interrupt 될 때까지 Thread.sleep(1000) 반복합니다.
    • main 메서드에서 1초 후 interrupt() 합니다.

    • 1초 뒤 Interrupt()에 의해 InterruptedException이 발생했습니다.
    • 하지만 while문은 멈추지 않고, 계속해서 반복합니다.
    • 왜냐하면 Interrupt 후 interrupt status는 clear 되고, InterruptException만 발생합니다.
    • 즉 stack의 상위 메서드 혹은 thread pool이 이를 알 수 없습니다.

     

    그렇다면 어떻게 sleep 상태의 스레드를 종료할 수 있을까요?

                    // 생략 ...
                    
                    catch (InterruptedException e) {
                        System.out.println("It is interrupted!");
                        
                        // interrupt status flag 추가
                        Thread.currentThread().interrupt();
                    }
                    
                    // 생략 ...
    • Thread.currentThread().interrupt()를 추가했습니다.
    • re-interrupt를 해서, interrupt status의 flag를 추가합니다.
    • 즉 stack의 상위 메서드에 interrupt 되었음을 알려줍니다.

    • 정상적으로 잘 종료됩니다.

     

    아마도 실제 서비스에서는 위와 같은 코드를 작성하는 경우는 없을것입니다. InterruptedException을 catch로 잡은 후 custom exception을 던지는것이 방법이라고 생각합니다.

                    catch (InterruptedException e) {
                        System.out.println("It is interrupted!");
                        Thread.currentThread().interrupt();
                        throw new CustomException();
                    }

    5) 결론

    사실 실제 서비스에서 Thread.sleep()과 같이 임의로 스레드의 동작을 일시정지 시키는 경우는 잘 없을것 같습니다.

     

    개인적으로, 자바를 다루는 개발자가 스레드 단위까지 조작을 하는 것은 좋지 않다고 생각합니다. 왜냐하면 스레드를 잘 못 조작한다면, 전체 프로세스가 멈춰 버릴 수 있는 위험이 있기 때문입니다. 위와 같이 영원히 block 될 수도 있고, 회수가 제대로 되지 않아 자원을 소모할 수도 있습니다.

     

    이미 성능적으로 뛰어난 JVM에게 스레드와 프로세스의 흐름을 맡기고, 오히려 JVM의 성능 튜닝을 하는 방법이 더 좋다고 생각합니다.

     

    혹은 하나의 서버에서 다수의 스레드 동작으로 인해 성능상 불리한 점이 존재한다면 하드웨어 스펙 증가, 서버 분리를 고려하는것이 더 좋다고 생각합니다. 특히 요즘처럼 클라우드와 쿠버네티스의 조합으로, scale out과 scale up이 쉬운 시대에는 더욱더 불필요하다고 생각합니다.

     

    하지만 단순히 sleep() 외에도 InterruptedException을 발생시킬 수 있는 메서드는 존재합니다. 특히 입출력을 위한 메서드에는 throws InterruptedException이 기본 설정인 것이 많은데요. 이때는 interrupt status를 생각하여, 예외를 잘 다루는 것이 중요할 것 같습니다.


    6) 참고 문헌

    https://docs.oracle.com/javase/tutorial/essential/concurrency/interrupt.html

    https://www.baeldung.com/java-interrupted-exception

    반응형

    댓글

Designed by Tistory.