스레드 생명 주기와 Runnable의 예외 처리 제약 이해하기

송현진·2025년 5월 24일
0

CS공부

목록 보기
8/17

스레드 생명 주기

스레드는 생성되고 실행되며 종료되는 일련의 생명주기를 갖는다. 자바에서는 Thread.State 열거형으로 상태를 구분한다.

주요 상태들

상태설명
NEW스레드 객체가 생성되었으나 start()가 호출되지 않은 상태
RUNNABLE실행 준비 완료 상태. 실제로 CPU에 의해 실행될 수도 있음
BLOCKED동기화 락을 얻기 위해 대기 중인 상태
WAITING무기한 대기 상태 (다른 스레드가 notify해야 깨어남)
TIMED_WAITING일정 시간 동안만 대기 (sleep, join(timeout) 등)
TERMINATED스레드 실행 종료 상태

스레드의 상태 전이 과정

  1. NEW -> RUNNABLE: start() 호출
  2. RUNNABLE -> TIMED_WAITING: sleep(), join(timeout) 등 호출
  3. TIMED_WAITING -> RUNNABLE: 시간이 지나거나 notify 등으로 깨어남
  4. RUNNABLE -> TERMINATED: run() 메서드 종료

예시 코드

public class ThreadStateMain {
   public static void main(String[] args) throws InterruptedException {
         Thread thread = new Thread(new MyRunnable(), "myThread");
         log.info("myThread.state1 = " + thread.getState()); // NEW
         log.info("myThread.start()");
         thread.start();
         Thread.sleep(1000);
         log.info("myThread.state3 = " + thread.getState()); // TIMED_WAITING
         Thread.sleep(4000);
         log.info("myThread.state5 = " + thread.getState()); // TERMINATED
         log.info("end");
     }
     static class MyRunnable implements Runnable {
       @Override
       public void run() {
       try {
           log.info("start");
           log.info("myThread.state2 = " + Thread.currentThread().getState()); // RUNNABLE
           log.info("sleep() start");
           Thread.sleep(3000);
           log.info("sleep() end");
           log.info("myThread.state4 = " + Thread.currentThread().getState()); // RUNNABLE
           log.info("end");
       } catch (InterruptedException e) {
       		throw new RuntimeException(e);
       }
     }
   }
 }
  • Thread.currentThread() 를 호출하면 해당 코드를 실행하는 스레드 객체를 조회할 수 있다.
  • Thread.sleep() : 해당 코드를 호출한 스레드는 TIMED_WAITING 상태가 되면서 특정 시간 만큼 대기한다. 시간은 밀리초(ms) 단위이다. 1밀리초 = 1/1000 초, 1000밀리초 = 1초이다.
    -Thread.sleep()InterruptedException 이라는 체크 예외를 던진다. 따라서 체크 예외를 잡아서 처
    리하거나 던져야 한다.
    • run() 메서드 안에서는 체크 예외를 반드시 잡아야 한다.
  • InterruptedException 은 인터럽트가 걸릴 때 발생하는데, 인터럽트는 뒤에서 알아본다. 지금은 체크 예외가 발생한다 정도만 이해하면 충분하다.

결과

11:40:31.503 [ main] myThread.state1 = NEW
11:40:31.505 [ main] myThread.start()
11:40:31.505 [ myThread] start
11:40:31.505 [ myThread] myThread.state2 = RUNNABLE
11:40:31.505 [ myThread] sleep() start
11:40:32.507 [ main] myThread.state3 = TIMED_WAITING
11:40:34.510 [ myThread] sleep() end
11:40:34.512 [ myThread] myThread.state4 = RUNNABLE
11:40:34.512 [ myThread] end
11:40:36.511 [ main] myThread.state5 = TERMINATED
11:40:36.512 [ main] end

  • state1 = NEW
    • main 스레드를 통해 myThread 객체를 생성한다. 스레드 객체만 생성하고 아직 start() 를 호출하지 않았기 때문에 NEW 상태이다.
  • state2 = RUNNABLE
    • myThread.start() 를 호출해서 myThread 를 실행 상태로 만든다. 따라서 RUNNABLE 상태가 된다. 참고로 실행 상태가 너무 빨리 지나가기 때문에 main 스레드에서 myThread 의 상태를 확인하기는 어렵다. 대신에 자기 자신인 myThread 에서 실행 중인 자신의 상태를 확인했다.
  • state3 = TIMED_WAITING
    • Thread.sleep(3000) : 해당 코드를 호출한 스레드는 3000ms (3초)간 대기한다. myThread 가 해당코드를 호출했으므로 3초간 대기하면서 TIMED_WAITING 상태로 변한다.
    • 이때 main 스레드가 myThreadTIMED_WAITING 상태를 확인하기 위해 1초간 대기하고 상태를 확인했다.
  • state4 = RUNNABLE
    • myThread 는 3초의 시간 대기 후 TIMED_WAITING 상태에서 빠져나와 다시 실행될 수 있는
      RUNNABLE 상태로 바뀐다.
  • state5 = TERMINATED
    • myThreadrun() 메서드를 실행 종료하고 나면 TERMINATED 상태가 된다.
    • myThread 입장에서 run() 이 스택에 남은 마지막 메서드인데 run() 까지 실행되고 나면 스택이 완전히 비워진다. 이렇게 스택이 비워지면 해당 스택을 사용하는 스레드도 종료된다.

체크 예외 재정의

Runnable 인터페이스의 run() 메서드를 구현할 때 InterruptedException 체크 예외를 throws로 밖에 선언할 수 없다

Runnable 인터페이스

public interface Runnable {
	void run();
}

자바에서 메서드를 재정의 할 때 재정의 메서드가 지켜야할 예외와 관련된 규칙이 있다.

  • 체크 예외
    • 부모 메서드가 체크 예외를 던지지 않는 경우 재정의된 자식 메서드도 체크 예외를 던질 수 없다.
    • 자식 메서드는 부모 메서드가 던질 수 있는 체크 예외의 하위 타입만 던질 수 있다.
  • 언체크(런타임) 예외
    • 예외 처리를 강제하지 않으므로 상관없이 던질 수 있다.

Runnable 인터페이스의 run() 메서드는 아무런 체크 예외를 던지지 않는다. 따라서 Runnable 인터페이스의 run() 메서드를 재정의 하는 곳에서는 체크 예외를 밖으로 던질 수 없다. 다음 코드를 실행하면 컴파일 오류가 발생한다.

static class MyRunnable implements Runnable {
    public void run() throws InterruptedException { // 컴파일 오류
        Thread.sleep(3000);
    }
}

왜냐하면 InterruptedException은 체크 예외이기 때문에 반드시 try-catch 블록으로 감싸거나 던져야 한다. 하지만 run() 메서드는 예외를 throws로 던질 수 없기 때문에 반드시 try-catch로 내부에서 처리해야 한다

자바는 왜 이런 제약을 두는 것일까?

부모 클래스의 메서드를 호출하는 클라이언트 코드는 부모 메서드가 던지는 특정 예외만을 처리하도록 작성된다. 자식 클래스가 상위 타입의 예외를 던지면 해당 코드는 모든 예외를 제대로 처리하지 못할 수 있다. 이는 예외 처리의 일관성을 해치고 예상하지 못한 런타임 오류를 초래할 수 있다.

체크 예외 재정의 규칙

  • 자식 클래스에 재정의된 메서드는 부모 메서드가 던질 수 있는 체크 예외의 하위 타입만을 던질 수 있다.
  • 원래 메서드가 체크 예외를 던지지 않는 경우 재정의된 메서드도 체크 예외를 던질 수 없다.

안전한 예외 처리

체크 예외를 run() 메서드에서 던질 수 없도록 강제함으로써 개발자는 반드시 체크 예외를 try-catch 블록 내에서 처리하게 된다. 이는 예외 발생 시 예외가 적절히 처리되지 않아서 프로그램이 비정상 종료되는 상황을 방지할 수 있다. 특히 멀티스레딩 환경에서는 예외 처리를 강제함으로써 스레드의 안정성과 일관성을 유지할 수 있다.

📝 배운점

이번 학습을 통해 자바 스레드의 생명 주기가 어떻게 전이되는지를 코드와 로그를 통해 명확히 이해할 수 있었다. 특히 NEW, RUNNABLE, TIMED_WAITING, TERMINATED 등 상태들이 어떻게 전이되며 그 시점을 정확히 포착하는 것이 멀티스레드 디버깅에 중요한 역할을 한다는 걸 실감했다. sleep() 메서드로 인해 TIMED_WAITING 상태에 들어가는 흐름을 직접 확인하면서 상태 변화에 대한 공부를 할 수 있었다.

또한 Runnable 인터페이스의 run() 메서드에서 InterruptedException 같은 체크 예외를 밖으로 던질 수 없다는 자바의 예외 처리 규칙은 멀티스레드 환경에서의 안정성을 위한 설계임을 이해하게 되었다. 덕분에 자바가 왜 run() 메서드 내에서 반드시 try-catch 처리를 강제하는지 그 이유에 대해 구조적 측면에서 접근할 수 있었다. 이런 규칙을 알고 쓰는 것과 모르고 쓰는 것은 큰 차이를 만든다는 걸 다시 한 번 느꼈다.

참고

profile
개발자가 되고 싶은 취준생

0개의 댓글