본문 바로가기
Language/Java

[JAVA] Java 쓰레드 생성의 양대 산맥 : Thread 클래스 vs Runnable 인터페이스 완벽 가이드

by Papa Martino V 2026. 1. 21.
728x90

Thread 클래스 vs Runnable 인터페이스
Thread 클래스 vs Runnable 인터페이스

 

자바(Java)는 탄생부터 멀티쓰레딩을 고려한 강력한 언어입니다. 복잡한 연산을 백그라운드에서 처리하거나, 서버에서 동시에 여러 요청을 처리할 때 쓰레드는 필수적인 요소입니다. 자바에서 쓰레드를 실행하는 방법은 크게 두 가지로 나뉩니다. Thread 클래스를 상속받는 방법Runnable 인터페이스를 구현하는 방법입니다. 얼핏 보면 결과는 같아 보이지만, 자바의 설계 철학인 '객체 지향' 관점에서 보면 두 방식은 큰 차이를 가집니다. 본 포스팅에서는 각 방식의 기술적 차이점과 실무에서 Runnable 인터페이스가 더 선호되는 근본적인 이유를 심도 있게 다룹니다.

1. Thread 클래스 상속 vs Runnable 인터페이스 구현

자바는 단일 상속만을 지원하는 언어입니다. 이 사실 하나가 두 방식의 운명을 결정짓는 가장 큰 요인이 됩니다. Thread 클래스를 상속받으면 다른 클래스를 상속받을 수 없지만, Runnable 인터페이스를 사용하면 다중 인터페이스 구현과 동시에 특정 클래스 상속이 가능해집니다.

구분 Thread 클래스 상속 Runnable 인터페이스 구현
구현 방식 extends Thread implements Runnable
상속 가능 여부 다른 클래스 상속 불가능 (단일 상속 제한) 다른 클래스 상속 가능 (유연함)
자원 공유 각 객체가 독립적인 인스턴스를 가짐 동일한 Runnable 객체를 여러 쓰레드가 공유 가능
객체 지향 관점 쓰레드 그 자체를 확장하는 개념 실행할 '작업 내용'만 정의하는 개념
권장 사항 쓰레드의 동작을 오버라이딩할 때만 사용 일반적인 병렬 작업 수행 시 강력 권장

2. 방식별 실전 예제 (Sample Example)

2.1 Thread 클래스 상속 방식

가장 직관적인 방법으로, Thread 클래스를 직접 상속받아 run() 메서드를 오버라이딩합니다.

class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("Thread 클래스 상속: " + Thread.currentThread().getName());
    }
}

public class ThreadExample {
    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        t1.start(); // 반드시 start()를 호출해야 새로운 호출 스택이 생성됨
    }
}
        

2.2 Runnable 인터페이스 구현 방식

실행할 로직만 인터페이스로 정의한 뒤, 실제 Thread 객체의 생성자에 전달하는 방식입니다. 실무에서 가장 많이 쓰이는 패턴입니다.

class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("Runnable 구현: " + Thread.currentThread().getName());
    }
}

public class RunnableExample {
    public static void main(String[] args) {
        // 작업 내용(Runnable) 정의
        Runnable task = new MyRunnable();
        
        // 실제 쓰레드(일꾼)에게 작업을 맡김
        Thread t1 = new Thread(task);
        t1.start();
        
        // 익명 객체나 람다식으로도 간결하게 표현 가능 (Java 8+)
        new Thread(() -> System.out.println("람다 활용 쓰레드")).start();
    }
}
        

3. 왜 Runnable 인터페이스가 '정답'에 가까운가?

현대 자바 개발에서 Runnable이 압승을 거둔 이유는 단순히 상속의 유연성 때문만은 아닙니다. 바로 작업(Task)과 수행자(Worker)의 분리라는 설계 철학 때문입니다.

  • 재사용성: 동일한 Runnable 작업을 여러 쓰레드나 ExecutorService(쓰레드 풀)에 던져줄 수 있습니다.
  • 메모리 효율: Thread 클래스를 매번 생성하는 것보다 작업 단위인 Runnable만 관리하는 것이 훨씬 가볍습니다.
  • 결합도 낮추기: 로직이 Thread 클래스에 종속되지 않으므로 코드의 유지보수가 쉬워집니다.

4. 독창적인 인사이트: Thread.start()와 Thread.run()의 차이

쓰레드를 생성할 때 초보자가 가장 많이 하는 실수가 start() 대신 run()을 직접 호출하는 것입니다. run()을 직접 호출하면 새로운 쓰레드가 생성되지 않고 현재 실행 중인 쓰레드(예: 메인 쓰레드)에서 단순히 메서드를 실행할 뿐입니다. start()는 JVM에게 "새로운 스택 영역을 확보하고 별도의 쓰레드로 이 작업을 돌려라"라고 명령하는 신호임을 반드시 기억해야 합니다.

5. 마무리 및 요약

Java에서 쓰레드를 다루는 것은 시스템의 성능을 극대화하는 강력한 무기를 얻는 것과 같습니다. 단순한 상속보다는 인터페이스 구현(Runnable)을 통해 확장성을 확보하고, 작업 로직과 실행 로직을 분리하는 설계를 지향하세요. 이는 차후 java.util.concurrent 패키지의 고급 기술들을 습득하는 데 아주 중요한 밑거름이 됩니다.

내용 출처: Java Language Specification (SE 21), Modern Java in Action (Raoul-Gabriel Urma), Oracle Java Tutorials

728x90