Handler, Looper, and Thread

프로그램(program)을 실행하게 되면 프로그램의 인스턴스(instance)가 생성되는데 이를 프로세스(process)라고 한다. 프로세스는 운영체제(OS)로부터 필요한 메모리 등의 리소스(resource)를 할당받고 이를 이용하여 특정 기능을 수행한다. 현존하는 대부분의 운영체제들은 동시에 여러 개의 프로로세스를 실행할 수 있는 멀티 프로세싱(multi-processing) 운영체제들이다. 하지만, 동시에 실행중인 프로세스들은 서로 리소스를 공유하지 않는다. 각각의 프로세스는 자신만의 고유한 리소스를 운영체제로부터 할당받아 독립적으로 실행되며, 프로세스 종료시 할당받은 리소스를 다시 운영체제에 반환한다.

스레드(thread)는 하나의 프로세스 내에서 실질적으로 수행되는 세부 실행 단위를 말한다. (시작점과 종료점을 가지는 하나의 작업 흐름이라고 정의하기도 한다.) 프로세스가 시작되면 일단 하나의 메인 스레드(main thread)가 생성되고 프로그램의 모든 코드는 메인 스레드에서 실행된다. 하지만, 이렇게 단일 스레드를 사용하게 되면 실행코드가 순차적으로 처리되기 때문에 하나의 프로세스 내에서 동시에 여러 작업을 수행할 수가 없게 된다. 만약 계산 시간이 오래 걸리는 작업을 하나의 스레드로만 실행하게 된다면 이 작업이 끝날 때까지 해당 프로세스는 멈추어 있는 것처럼 보이게 된다.  따라서, 많은 경우에 서브 스레드(sub thread)들을 생성하여 사용하게 되는데, 이를 멀티 스레딩(multi-threading)이라고 한다. 이 과정에서 각 스레드들은 다른 스레드들과 프로세스의 리소스를 공유하여 사용하지만, 하나의 스레드가 예외 상황이 발생하여 종료된다고 하더라도 이것이 다른 스레드들의 실행에 영향을 미치지는 않는다.

안드로이드(Android)에서도 역시 앱(app.)이 실행되면 메인 액티비티(main activity)가 메모리로 올려지면서 메인 스레드가 생성되는데 (main activity = main thread 라고 봐도 무방함), 안드로이드는 시스템 안정성 등의 이유로 메인 스레드만 사용자 인터페이스(UI)를 변경할 수 있도록 하는 정책을 취하고 있다. (동시에 두 개 이상의 스레드가 하나의 뷰(view)를 업데이트 하는 것을 방지하기 위함.) (메인 스레드는 UI를 그리거나 갱신하고 이벤트를 처리할 수 있는 유일한 스레드이기 때문에 UI 스레드라고 불리기도 함.) 따라서, 메인 스레드를 제외한 다른 모든 서브 스레드들은 화면상에 표시되는 UI를 직접적으로 변경할 수 없다. 하지만, 안드로이드는 서브 스레드들이 간접적으로 UI를 변경할 수 있는 수단을 제공하는데, 여기서 나오는 개념이 바로 핸들러(handler)이다. 다수의 서브 스레드들이 동시다발적으로 발생시키는 메시지(message)들은 메인 스레드 내의 메시지 큐(message queue)로 전달되며, 핸들러는 메시지 큐를 이용하여 선입선출 방식으로 메시지를 차례대로 처리한다. 조금 더 정확하게 말하면 메인 스레드는 내부적으로 루퍼(looper)를 가지고 있으며, 루퍼가 무한 루프(loop)를 돌며 메시지 큐에 들어온 메시지를 차례대로 꺼내서 핸들러에 전달하게 된다. 이러한 방식으로 모든 서브 스레드들이 요청하는 뷰 변경 정보를 메인 스레드에서 취합하고 최종적인 단계에서만 한 차례 뷰가 업데이트 된다.

한편, 안드로이드 앱 개발의 중요한 규칙 중 하나는 메인 스레드에 계산 시간이 오래 걸리는 작업을 절대로 수행하지 않는 것이다. 계산 시간이 오래 걸리는 작업을 메인 스레드 내에서 수행한다면 마치 앱이 정지해 있는 것과 같은 문제가 나타날 수 있다. 따라서, 메인 스레드는 UI 처리를 수행하고 기타 다른 계산 시간이 오래 걸리는 작업은 별도의 스레드를 이용해서 처리해야 한다.  안드로이드의 스레드는 기본적으로 자바의 스레드를 사용하며, 다음과 같이 3가지 종류의 스레드 생성 방법을 제공한다.

1) java.lang.Thread 클래스를 상속받아서 run() 함수를 오버라이딩(overriding)하는 방법

class MyThread extends Thread {
   public void run() {
      ...
   }
}

MyThread thread = new MyThread();
thread.start();

다음과 같이 해도 된다.

class MyThread extends Thread {
   public void run() {
      ...
   }
}

new MyThread().start();

하지만, JAVA는 다중 상속을 지원하지 않기 때문에 이러한 방식에서는 MyThread 클래스는 Thread 클래스 이외의 클래스로 부터 상속을 받을 수가 없게 된다. 따라서, 다음과 같이 Runnable 인터페이스를 구현하여 스레드를 사용하는 방법이 많이 이용된다.

2) java.lang.Runnable 인터페이스를 상속받아서 run() 함수를 구현하는 방법

class MyRunnable implements Runnable {
   public void run() {
      ...
   }
}

MyRunnable runnable = new MyRunnable();
Thread thread = new Thread( runnable );
thread.start();

다음과 같이 해도 된다.

class MyRunnable implements Runnable {
   public void run() {
      ...
   }
}

new Thread( new MyRunnable() ).start();

방법 1)처럼 스레드 생성시 생성자에 Runnable 객체를 인자로 넘겨주지 않으면 스레드 자신 스스로가 Runnable 객체로 인식되기 때문에 MyThread의 run() 함수가 실행된다. 하지만, 여기서는 별도의 Runnable 객체를 생성하고, 이를 스레드 생성자의 인자로 넘겨주어 스레드의 run() 함수 대신 MyRunnable의 run() 함수가 실행되도록 하였다.

3) 필요한 곳에서 Thread를 생성하여 사용하는 방법

한편, 1), 2)에서 처럼 별도의 클래스를 선언하여 사용하지 않고 필요한 곳에서 간단하게 스레드를 생성하여 사용하는 것 또한 가능한데, 다음과 같이 하면 된다.

Thread thread = new Thread() {
   public void run() {
      ...
   }
};
thread.start();

다음과 같이 해도 된다.

new Thread() {
   public void run() {
      ...
   }
}.start();

답글 남기기