JAVA - 멀티쓰레딩 : Runnable 과 Thread
Runnable vs Thread: 차이점 및 실무에서의 선택
Java에서 멀티쓰레딩을 구현하는 방법에는 Thread 클래스 상속과 Runnable 인터페이스 구현 두 가지 방법이 있습니다. 두 방식은 각각 장단점이 있으며, 실무에서는 일반적으로 Runnable을 더 선호하는 경향이 있습니다. 그 이유를 포함해 깊이 있게 살펴보겠습니다.
1. Thread 클래스와 Runnable 인터페이스의 차이점
비교 항목 Thread 클래스 상속 Runnable 인터페이스 구현
상속 가능 여부 | Thread를 상속하므로 다른 클래스를 상속할 수 없음 | 인터페이스이므로 다른 클래스를 상속 가능 |
코드 재사용성 | 코드 재사용성이 낮음 (쓰레드 실행 외 다른 기능을 추가하기 어려움) | 코드 재사용성이 높음 (비즈니스 로직과 쓰레드 로직을 분리 가능) |
객체 공유 | 새로운 쓰레드 객체가 필요 (공유 불가능) | 같은 Runnable 객체를 여러 쓰레드에서 실행 가능 (객체 공유 가능) |
메모리 소비 | 더 많은 메모리를 사용 (각각의 Thread 객체 생성) | 상대적으로 적은 메모리 사용 (하나의 Runnable 객체를 여러 쓰레드에서 실행) |
2. Thread 클래스 상속 방식
2.1 기본 개념
Thread 클래스를 상속받아 run() 메서드를 오버라이딩하여 쓰레드를 실행하는 방식입니다.
2.2 예제 코드
class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + " 실행 중...");
}
}
}
public class ThreadExample {
public static void main(String[] args) {
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
t1.start();
t2.start();
}
}
2.3 장점
✅ Thread 클래스를 직접 상속받기 때문에 코드가 단순함.
✅ 쓰레드 관련 코드가 독립적으로 동작하므로 Runnable보다 직관적임.
2.4 단점
❌ Java에서는 다중 상속을 지원하지 않으므로 다른 클래스를 상속할 수 없음.
❌ 새로운 Thread 객체를 생성해야 하므로 객체 공유가 불가능하고 메모리 소비가 큼.
3. Runnable 인터페이스 구현 방식
3.1 기본 개념
Runnable 인터페이스를 구현하고 Thread 객체에서 실행하는 방식입니다.
3.2 예제 코드
class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + " 실행 중...");
}
}
}
public class RunnableExample {
public static void main(String[] args) {
MyRunnable task = new MyRunnable();
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
t1.start();
t2.start();
}
}
3.3 장점
✅ Runnable은 인터페이스이므로 다른 클래스를 상속할 수 있음.
✅ 하나의 Runnable 객체를 여러 쓰레드에서 실행할 수 있어 객체 공유가 가능함.
✅ ThreadPool과 같은 고급 멀티쓰레딩 기법에서 활용 가능함.
3.4 단점
❌ Thread 클래스에 비해 상대적으로 코드가 길어질 수 있음.
❌ 별도의 Thread 객체를 생성해야 실행할 수 있음.
4. 실무에서 선호되는 방식과 이유
실무에서는 Runnable 인터페이스 구현 방식이 더 선호됩니다.
그 이유는 다음과 같습니다.
✅ 1) 객체 공유 가능 (메모리 절약)
- Thread 클래스를 상속하면 각 쓰레드마다 개별 객체를 생성해야 하지만,Runnable은 하나의 객체를 여러 쓰레드에서 실행할 수 있음.
- 따라서 객체 생성 비용이 줄어들고 메모리를 절약할 수 있음.
✅ 2) 다중 상속 문제 해결
- Java에서는 단일 상속만 지원하므로,Thread 클래스를 상속받으면 다른 클래스를 상속할 수 없음.
- 반면, Runnable은 인터페이스이므로 다른 클래스를 상속하면서 멀티쓰레딩을 구현 가능.
✅ 3) ExecutorService와의 호환성
- 실무에서는 ThreadPool (스레드 풀) 을 사용하여 성능 최적화를 많이 함.
- ExecutorService와 같은 API는 Runnable을 요구하기 때문에 ThreadPool을 활용하려면 Runnable이 유리.
✅ 4) 유지보수성 및 코드 재사용성 향상
- Runnable을 사용하면 비즈니스 로직과 쓰레드 실행 로직을 분리할 수 있음.
- 코드 재사용성과 유지보수성이 높아져 확장성과 유지보수가 용이함.
5. ThreadPool (ExecutorService) 활용
실무에서는 Thread 객체를 직접 생성하는 것이 아니라 ThreadPool (쓰레드 풀) 을 사용하여 쓰레드를 관리합니다.
5.1 ThreadPool 사용 예제
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
class Task implements Runnable {
private final int taskId;
public Task(int taskId) {
this.taskId = taskId;
}
@Override
public void run() {
System.out.println("Task " + taskId + " 실행 중... (쓰레드: " + Thread.currentThread().getName() + ")");
try { Thread.sleep(1000); } catch (InterruptedException e) {}
}
}
public class ThreadPoolExample {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(3); // 3개의 쓰레드를 가진 풀 생성
for (int i = 1; i <= 10; i++) {
executor.execute(new Task(i)); // 쓰레드 풀에서 실행
}
executor.shutdown(); // 모든 작업이 끝나면 종료
}
}
5.2 ThreadPool을 사용하는 이유
✅ 성능 최적화: 쓰레드 생성/제거 비용 절감.
✅ 효율적 자원 관리: 제한된 쓰레드 개수로 많은 작업 처리.
✅ 자동 쓰레드 관리: 필요 시 자동으로 쓰레드 생성/종료.
6. 결론
방식 장점 단점 실무 적용
Thread 상속 | 코드가 직관적이고 간단 | 다중 상속 불가능, 객체 공유 불가 | 거의 사용되지 않음 |
Runnable 구현 | 다중 상속 가능, 객체 공유 가능 | 코드가 다소 길어질 수 있음 | 실무에서 선호됨 |
ExecutorService | 쓰레드 풀 관리, 성능 최적화 | 초기 설정 필요 | 실무에서 가장 많이 사용됨 |
👉 실무에서는 Runnable을 활용한 ExecutorService (ThreadPool)이 가장 선호되는 방식입니다.
- 단순한 멀티쓰레딩 → Runnable 사용
- 고성능 최적화 및 자원 관리 필요 → ThreadPool 사용