8.2.4 Runnable을 사용하는 이유
Thread 클래스를 상속받는 것과 Runnable 인터페이스를 구현하는 것은 별다른 차이가 없습니다. 알고 보면 Thread를 상속하는 것이 휠씬 편한 방법입니다. 상속의 장점을 최대한 살릴 수 있는 것이죠. 부모 클래스인 Thread 클래스의 메서드를 마음대로 이용할 수 있다는 것은 객체지향 자체의 장점이니까요! 하지만 굳이 Runnable 인터페이스를 사용하는 이유는 여러분의 클래스가 다른 클래스를 미리 상속했기 때문입니다. 자바는 단일 상속을 기본으로 하기 때문에 다른 클래스를 상속했다면 Thread를 상속할 수 없습니다. 다음의 경우를 한번 보시죠.
▣ extends Frame을 상속했기 때문에 Thread를 상속할 수 없는 경우
◈ class RunFrame extends Frame{
◈ //...작업
◈ }
자바의 기본 법칙상 다중 상속은 금지하고 있습니다. 이러한 이유 때문에 Runnable 인터페이스를 구현하는 방식으로 단일 상속의 한계를 피해 나가는 것입니다. 다음과 같이 수정한다면 Frame도 상속할 수 있으며, 스레드를 위한 Runnable도 구현할 수 있습니다.
▣ Frame은 상속을 하고 Runnable은 구현을 한 상태
◈ class RunFrame extends Frame implements Runnable{
◈ public void run(){
◈ //...스레드 내의 작업
◈ }
◈ }
▣ Runnable을 사용하는 이유
◈ Thread를 상속하기 이전에 다른 클래스를 상속한 경우 Runnable을 이용해서 스레드를 만든다.
위와 같은 방법으로 클래스를 만들었다면 다음과 같이 창을 띄우난 뒤 스레드를 동작시켜야 합니다.
▣ 창 띄우기와 스레드 동작 시키기
◈ RunFrame r = new RunFrame();
◈ r.setSize(300, 100);
◈ r.show();
◈ Thread t = new Thread(r);
◈ t.start();
다음은 Frame의 상속으로 인해 Thread를 상속할 수 없기 때문에 Runnable 인터페이스를 구현하는 예를 보여주고 있습니다.
『chap08\RunFrameMain.java』
ⓙ───────────────────────────────────────
/**
Frame의 상속과 Runnable의 구현
Thread를 상속하지 못하는 경우 Runnable로 구현
**/
import java.awt.*;
class RunFrame extends Frame implements Runnable {
public void run() {
int i = 0;
System.out.println("스레드 시작!");
while(i<20) {
System.out.print(i + "\t");
this.setTitle("스레드 동작중" + i++);
try{
Thread.sleep(300);
}catch(InterruptedException e){System.out.println(e);}
}
System.out.println("스레드 종료!");
}
} //end of RunFrame class
public class RunFrameMain{
public static void main(String args[]){
RunFrame r = new RunFrame();
r.setSize(300, 100);
r.show();
Thread t = new Thread(r);
t.start();
} //end of main
} //end of RunFrameMain class
//㉶--------------------------------------------㉳
/***
C:\javasrc\chap08>javac RunFrameMain.java
C:\javasrc\chap08>java RunFrameMain
【chap08\runnableFrame.bmp】



스레드 시작!
0 1 2 3 4 5 6 7 8 9
10 11 12 13 14 15 16 17 18 19
스레드 종료!
***/
───────────────────────────────────────ⓑ
이 예제에서 Frame을 먼저 상속하고 있기 때문에 Runnable을 이용해서 스레드를 구현하고 있습니다.
▣ Frame은 상속하고 Runnable은 구현한 경우
◈ class RunFrame extends Frame implements Runnable{
◈ public void run(){
◈ //...스레드 내의 작업
◈ }
◈ }
▣ 제어권
◈ 만약 위의 예제를 스레드로 구현하지 않고 일반 메서드를 호출하는 방식으로 구현한다면 while문이 끝날 때까지 Frame의 다른 작업을 할 수가 없습니다. 스레드로 구현하지 않으면 while문이 끝나야만 다른 작업을 할 수 있습니다.
자바는 단일 상속을 기본으로 하기 때문에 Frame이 먼저 상속된 상태라면 Thread 대신 Runnable을 이용하면 됩니다.
RunFrame 클래스는 2개의 상위 클래스를 보유하고 있습니다. Frame과 Runnable이 그것이죠. 5장의 다형성 부분에서 인터페이스가 클래스의 한 종류라는 것을 학습했습니다. 인터페이스도 상위 클래스로서의 역할을 할 수 있으며 업캐스팅 또한 가능합니다. 현재 Frame과 Runnable은 동등한 부모 클래스의 자격이 부여되어 있는 것입니다. 그렇기 때문에 다음과 같은 구문이 가능합니다.
▣ RunFrame으로 가능한 업캐스팅
◈ Frame f = new RunFrame(); //Frame으로의 캐스팅
◈ Runnable r = new RunFrame(); //Runnable로의 캐스팅
RunFrame 클래스는 Frame으로 업캐스팅이 가능하며, Runnable로도 업캐스팅이 가능합니다. 위의 RunFrame의 경우 다음과 같이 2가지 작업을 해주어야 합니다.
◈ 창만들고 띄우기
◈ 스레드 동작시키기
먼저 다음과 같이 RunFrame을 생성한 후 화면에 디스플레이합니다.
▣ 창 띄우기
◈ RunFrame r = new RunFrame();
◈ r.setSize(300, 100);
◈ r.show();
그리고 앞에서 배운 Runnable의 실행 방법을 이용해서 다음과 같이 스레드를 start() 시키면 됩니다.
▣ 스레드 동작시키기
◈ Thread t = new Thread(r);
◈ t.start();
RunFrame r을 Thread의 매개변수로 줄 수 있는 이유는 업캐스팅 때문입니다. Runnable을 필요로 하는 Thread의 생성자는 다음과 같습니다.
▣ Thread 클래스의 생성자
◈ public Thread(Runnable target)
여기서 Runnable을 매개변수로 주어야 하지만 RunFrame을 매개변수로 주어도 상관없습니다. 그것은 매개변수로 삽입될 때 다음과 같은 업캐스팅 현상이 일어나기 때문입니다.
▣ Thread 객체를 생성할 때 매개변수의 업캐스팅
◈ Runnable target = r; //r은 RunFrame의 객체
이와 비슷한 구문을 자주 만나게 될 것입니다. Vector의 데이터 삽입에서도 이와 비슷한 구문을 사용했으며, Runnable을 사용할 때에도 이러한 업캐스팅이 사용되는 것입니다. 그리고 스트림, 직렬화, 이벤트 처리 등 계속해서 이러한 업캐스팅의 구문을 만나게 될 것입니다. 업캐스팅의 현상은 아무리 강조해도 지나치지 않습니다. 반드시 숙지하시기 바랍니다.
위의 예에서 창을 만들고 스레드를 동작시키는 방법은 프로그램적으로 그렇게 세련되지 못한 기법입니다. 프로그램은 동작합니다만 그렇게 매끄러운 구문은 아닙니다. 여러분들의 이해를 돕기 위해서 main()에서 스레드를 동작시켰지만, 실제로는 Frame 내부에서 스레드를 생성시키고 자동으로 스레드가 동작하는 방식을 더 많이 사용합니다. 다음은 Frame 내부에서 스레드를 동작시키는 방법을 보여주고 있습니다.
▣ 클래스 내부에서 스레드 구동 시키기
◈ class RunnableFrame extends Frame implements Runnable {
◈ public RunnableFrame() {
◈ new Thread(this).start(); //Runnable을 이용한 스레드의 구동
◈ }
◈ //생략...
◈ }
다음은 일반적인 스레드 생성 및 실행 방법을 테스트하는 예입니다.
『chap08\RunnableFrameMain.java』
ⓙ───────────────────────────────────────
/**
Frame 내부에서 스레드 동작 시키기
**/
import java.awt.*;
class RunnableFrame extends Frame implements Runnable {
public RunnableFrame() {
new Thread(this).start();
}
public void run() {
int i = 0;
System.out.println("스레드 시작!");
while(i<20) {
System.out.print(i + "\t");
this.setTitle("스레드 동작중" + i++);
try{
Thread.sleep(300);
}catch(InterruptedException e){System.out.println(e);}
}
System.out.println("스레드 종료!");
}
} //end of RunnableFrame class
public class RunnableFrameMain{
public static void main(String args[]){
RunnableFrame r = new RunnableFrame();
r.setSize(300, 100);
r.show();
} //end of main
} //end of RunnableFrameMain class
//㉶--------------------------------------------㉳
/***
C:\javasrc\chap08>javac RunnableFrameMain.java
C:\javasrc\chap08>java RunnableFrameMain
【chap08\runnableFrame.bmp】



스레드 시작!
0 1 2 3 4 5 6 7 8 9
10 11 12 13 14 15 16 17 18 19
스레드 종료!
***/
───────────────────────────────────────ⓑ
앞의 RunFrame과 RunnableFrame을 잘 비교해 보시기 바랍니다. RunFrame의 경우 main()에서 스레드를 생성한 후 실행했지만, RunnableFrame은 RunnableFrame 객체를 생성할 때 생성자에서 스레드를 만든 후 실행하고 있습니다. 다음은 생성자에서 스레드를 생성하고 실행하는 구문입니다.
▣ RunnableFrame 클래스 내에서 스레드 동작 시키기
◈ new Thread(this).start(); //this는 RunnableFrame형이다.
이와 같은 구문이 가능한 이유 또한 업캐스팅 때문입니다. this는 언젠가 생성될 메모리를 의미하며 어차피 this 자체는 Runnable로 캐스팅될 수 있기 때문에 위와 같은 구문이 가능한 것입니다. 프로그램적인 기법이기 때문에 어느 것을 사용하셔도 상관없지만 RunFrame보다 RunnableFrame 방식이 더 일반적인 기법입니다.
만약 위의 프로그램에서 Runnable을 사용하지 않고 Thread로 구현하겠다면, Thread를 상속할 수 없기 때문에 Frame과 Thread를 분리시켜야 합니다. 프로그램이 복잡하거나 독립적으로 스레드를 관리하고 싶다면, 다음과 같이 스레드를 독립적으로 구현해서 사용할 수도 있습니다.
『chap08\SoloFrameMain.java』
ⓙ───────────────────────────────────────
/**
Frame과 Thread 분리시켜서 구현
**/
import java.awt.*;
class SoloFrame extends Frame{
public SoloFrame(){
SoloThread t = new SoloThread(this);
t.start();
}
} //end of SoloFrame class
class SoloThread extends Thread{
private Frame f = null;
public SoloThread(Frame f){
this.f = f;
}
public void run() {
int i = 0;
System.out.println("스레드 시작!");
while(i<20) {
System.out.print(i + "\t");
f.setTitle("스레드 동작중" + i++);
try{
this.sleep(300);
}catch(InterruptedException e){System.out.println(e);}
}
System.out.println("스레드 종료!");
}
} //end of SoloThread class
public class SoloFrameMain{
public static void main(String args[]){
SoloFrame s = new SoloFrame();
s.setSize(300, 100);
s.show();
} //end of main
} //end of SoloFrameMain class
//㉶--------------------------------------------㉳
/***
C:\javasrc\chap08>javac SoloFrameMain.java
C:\javasrc\chap08>java SoloFrameMain
【chap08\runnableFrame.bmp】



스레드 시작!
0 1 2 3 4 5 6 7 8 9
10 11 12 13 14 15 16 17 18 19
스레드 종료!
***/
───────────────────────────────────────ⓑ
위의 예제에서는 다음과 같이 프레임과 스레드를 분리시켜서 구현하고 있습니다.
▣ 프레임과 스레드를 분리시켜서 구현
◈ class SoloFrame extends Frame{...}
◈ class SoloThread extends Thread{...}
그런데 SoloThread에서 타이틀바에서 제목을 출력하기 위해서는 반드시 SoloFrame의 참조값이 필요합니다. SoloThread에서 참조값을 넘겨받기 위해서 다음과 같이 생성자에서 SoloFrame의 참조값을 받은 뒤 멤버 변수에 저정해 두고 있습니다.
▣ 스레드에 프레임의 참조값 셋팅
◈ private Frame f = null;
◈ public SoloThread(Frame f){
◈ this.f = f; //참조값복사
◈}
run() 메서드에서는 이 참조값 f를 이용해서 setTitle()을 호출하고 있습니다. 생성자에 매개변수로 들어온 참조값은 참조값복사에 의해서 참조값만 넘겨받게 됩니다. 그리고 SoloThread 내부에서 이를 이용하는 것입니다. 또 한가지 짚고 넘어가야 하는 것이 바로 this.sleep(300)입니다. sleep()이라는 메서드는 스레드를 일정시간 동안 대기시키는 역할을 하게 됩니다. sleep(300)은 1/1000초 단위로 사용하며, 0.3초동안 현재의 스레드를 대기상태로 만들기 때문에 다른 스레드들이 활동할 시간이 많아지게 되는 것입니다.
▣ sleep() 메서드
◈ sleep() 메서드는 현재의 스레드를 일정시간 동안 대기상태로 만든다.
실제 스레드를 생성하는 것은 SoloFrame의 생성자에서 하고 있습니다. 다음과 같이 스레드를 생성할 때 SoloFrame의 참조값만 넘겨준 후 일반 스레드처럼 start()만 호출해 주면 됩니다.
▣ SoloThread 시작
◈ SoloThread t = new SoloThread(this); //this는 SoloFrame형
◈ t.start();
위의 3가지 방법중 첫 번째 방법을 제외한 두 번째와 세 번째는 많이 사용하는 기법입니다. 간단하게 스레드를 구현하고자 한다면 Runnable을 구현하면 될 것입니다. 그리고 독립적으로 스레드를 관리하고자 한다면, Thread를 상속받는 클래스를 따로 만들어 사용하면 됩니다. 실제 프로그램을 작성할 때 스레드 내의 프로그램이 상당히 길어질 수도 있기 때문에 스레드를 독립적으로 관리하는 경우도 많이 사용합니다. 세 가지를 비교해 가면서 스레드의 사용법을 익히시기 바랍니다.