Java에서 Runnable과 Thread의 진정한 차이점

많은 개발자들이 멀티스레딩을 구현할 때 RunnableThread를 선택할 때 직관적으로 사용하지만, 이 둘 사이의 본질적 차이에 대해 명확히 이해하지 못한 채 지나가곤 한다. 특히 인터뷰에서는 종종 "두 가지의 차이점을 설명해 주세요"라는 질문이 등장하며, 잘못된 정보로 인해 혼동을 겪는 경우가 많다.

결론부터 말하면, RunnableThread는 본질적인 차이가 없으며, 단지 프로그래밍 패턴의 차이일 뿐이다. Runnable은 인터페이스이고, Thread는 클래스이므로 상속과 구현의 방식이 다르다는 점만 다를 뿐이다. 하지만 중요한 것은, 스레드 간 자원 공유 여부는 사용 방식에 따라 결정되며, Runnable이나 Thread 자체의 성격 때문이 아니다.

1. 구현 방식의 차이

  • Runnable: 인터페이스를 구현하여 run() 메서드를 정의한다.
  • Thread: 클래스를 상속하여 run() 메서드를 오버라이드한다.

2. 다중 상속 문제

Runnable은 인터페이스이므로 다른 클래스와 함께 사용 가능하나, 실제로는 Thread를 상속하는 경우가 거의 없다. 대부분의 경우 Runnable을 사용해 여러 스레드가 동일한 작업 로직을 공유하도록 설계한다.

3. 실습: 티켓 예매 시뮬레이션

10장의 티켓을 3개의 스레드가 동시에 구매하는 시나리오를 통해 두 방식의 결과를 비교해 보자.

① Runnable을 구현한 경우

public class TicketSeller implements Runnable {
    private int tickets = 10;

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            synchronized (this) {
                if (tickets > 0) {
                    System.out.println(Thread.currentThread().getName() + " 예매 전 남은 티켓: " + tickets);
                    tickets--;
                    System.out.println(Thread.currentThread().getName() + " 예매 후 남은 티켓: " + tickets);
                    System.out.println("----------------");
                    try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); }
                } else {
                    break;
                }
            }
        }
    }

    public static void main(String[] args) {
        TicketSeller seller = new TicketSeller();
        new Thread(seller, "스레드-1").start();
        new Thread(seller, "스레드-2").start();
        new Thread(seller, "스레드-3").start();
    }
}

출력 결과: 모든 스레드가 동일한 tickets 변수를 공유하며, 총 10장의 티켓이 순차적으로 소진된다.

② Thread를 상속한 경우

public class TicketThread extends Thread {
    private int tickets = 10;

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            synchronized (this) {
                if (tickets > 0) {
                    System.out.println(Thread.currentThread().getName() + " 예매 전 남은 티켓: " + tickets);
                    tickets--;
                    System.out.println(Thread.currentThread().getName() + " 예매 후 남은 티켓: " + tickets);
                    System.out.println("----------------");
                    try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); }
                } else {
                    break;
                }
            }
        }
    }

    public static void main(String[] args) {
        // ❌ 잘못된 방법: 각각 별도 인스턴스 생성 → 각각 독립된 티켓 카운트
        new TicketThread().start();
        new TicketThread().start();
        new TicketThread().start();

        // ✅ 올바른 방법: 하나의 인스턴스를 여러 스레드가 공유
        TicketThread shared = new TicketThread();
        new Thread(shared, "공유-1").start();
        new Thread(shared, "공유-2").start();
        new Thread(shared, "공유-3").start();
    }
}

결과 분석:

  • 별도 인스턴스 생성 시: 각 스레드가 독립된 tickets를 가짐 → 총 30장 판매됨 (원하지 않는 결과).
  • 공유 인스턴스 사용 시: 동일한 객체를 공유 → 총 10장만 판매됨 (예상된 결과).

따라서 Thread가 자원 공유를 불가능하게 만든다는 주장은 완전히 틀린 것이다. 문제는 인스턴스 공유 여부에 있으며, Runnable이나 Thread의 선택과는 무관하다.

4. 핵심 정리

  • Runnable은 인터페이스이며, Thread는 클래스다.
  • 스레드 간 자원 공유 여부는 공유되는 인스턴스의 존재 여부에 달려 있다.
  • 모든 스레드가 동일한 Runnable 인스턴스를 참조하면 공유 가능하고, Thread를 상속한 경우에도 동일한 인스턴스를 전달하면 마찬가지다.
  • 즉, Runnable이 더 나은 이유는 클래스 상속의 제약을 피할 수 있기 때문이며, 자원 공유 능력 때문이 아니다.

결론적으로, RunnableThread는 기술적 차이가 아닌 설계 패턴의 차이일 뿐이다. 올바른 선택은 코드 재사용성과 유연성을 고려한 것이어야 하며, 자원 공유 여부를 판단하는 기준은 인스턴스 공유 여부다.

태그: java Multithreading Runnable Thread concurrency

7월 7일 05:22에 게시됨