4주차 Unit 3.3 — Runnable 인터페이스 (왜 더 좋은가)

Psj·2026년 5월 21일

F-lab

목록 보기
129/240

Unit 3.3 — Runnable 인터페이스 (왜 더 좋은가)

F-LAB JAVA · 4주차 · Phase 3 · 스레드 만들고 다루기


📌 학습 목표

이 Unit을 끝내면 다음을 답할 수 있어야 한다.

  • Runnable 인터페이스 의 정의와 구조는?
  • Thread 상속의 한계 (단일 상속, 결합) 는?
  • Runnable 의 장점 3가지 는?
  • 람다로 Runnable 을 만들 수 있는 이유는?
  • 같은 Runnable 을 여러 스레드 가 공유하면?
  • 작업과 스레드의 분리 의 의미는?
  • Spring @Async 는 내부적으로 무엇을 쓰나?
  • Runnable vs Callable 의 차이는?
  • Thread 상속 vs Runnable 구현 의 선택은?

🎯 핵심 한 문장

Runnable 은 단 하나의 추상 메서드 run() 을 가진 함수형 인터페이스로, Thread 에 작업을 전달하는 방식이며, Thread 상속보다 권장된다.
Thread 상속의 한계 — 자바는 단일 상속만 가능하여 다른 클래스를 상속할 수 없고, 작업과 스레드 제어가 결합된다.
Runnable 의 장점 3가지 — (1) 다른 클래스 상속 가능 (인터페이스라 다중 구현), (2) 작업과 스레드 분리 (객체지향적 설계), (3) 메모리 효율 (여러 스레드가 같은 Runnable 공유 가능).
Runnable 은 함수형 인터페이스 (SAM) 이므로 람다로 간결하게 작성할 수 있다 (() -> {...}).
Spring 의 @Async 는 내부적으로 작업을 Runnable (또는 Callable) 로 감싸 ThreadPoolTaskExecutor 에 제출한다 — Runnable 기반.

비유 — 일(task)과 일꾼(worker) 분리

Thread 상속 = 일꾼이 일을 내장:
  class 청소일꾼 extends 일꾼 {
    void 일() { 청소하기 }
  }
  - 일꾼 = 청소만 하는 일꾼
  - 다른 일 시키려면 새 클래스

Runnable = 일(작업서)과 일꾼 분리:
  Runnable 청소 = () -> 청소하기;
  Runnable 요리 = () -> 요리하기;
  
  일꾼(Thread)에게 작업서 전달:
  new 일꾼(청소).start();
  new 일꾼(요리).start();
  - 일꾼은 범용
  - 작업서만 바꿈
  - 같은 작업서를 여러 일꾼이 (공유)

장점:
  - 일꾼 재사용
  - 작업 교체 쉬움
  - 일꾼이 다른 역할도 (상속)

→ Runnable = 작업서, Thread = 일꾼, 분리가 핵심.


🧭 9개 섹션 로드맵

1. Runnable 인터페이스의 정의
2. Thread 상속의 한계
3. 장점 1 — 다른 클래스 상속 가능
4. 장점 2 — 작업과 스레드 분리
5. 장점 3 — 메모리 효율 (공유)
6. 람다로 Runnable
7. Spring @Async의 내부
8. Runnable vs Callable, 선택 가이드
9. 면접 + 자기 점검

1️⃣ Runnable 인터페이스의 정의

1.1 Runnable 인터페이스

@FunctionalInterface
public interface Runnable {
    void run();   // 단 하나의 추상 메서드 (SAM)
}

핵심:

  • 함수형 인터페이스 (SAM)
  • run() 메서드 하나
  • 스레드가 실행할 작업

1.2 사용 방법

// 1. 클래스로 구현
class MyTask implements Runnable {
    @Override
    public void run() {
        System.out.println("Task running");
    }
}

Thread t = new Thread(new MyTask());
t.start();

// 2. 람다 (함수형 인터페이스)
Runnable task = () -> System.out.println("Lambda task");
Thread t2 = new Thread(task);
t2.start();

// 3. 익명 클래스
Thread t3 = new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("Anonymous task");
    }
});
t3.start();

1.3 Thread 와 Runnable 의 관계

// Thread 도 Runnable 구현
public class Thread implements Runnable {
    
    private Runnable target;   // 전달받은 Runnable
    
    public Thread(Runnable target) {
        this.target = target;
    }
    
    @Override
    public void run() {
        if (target != null) {
            target.run();   // Runnable 실행
        }
    }
}

1.4 두 가지 방식 비교

// 방식 1: Thread 상속
class MyThread extends Thread {
    @Override
    public void run() { doWork(); }
}
new MyThread().start();

// 방식 2: Runnable 구현 (권장)
class MyTask implements Runnable {
    @Override
    public void run() { doWork(); }
}
new Thread(new MyTask()).start();

1.5 ILIC 의 맥락

// Runnable 구현
public class ShipmentTask implements Runnable {
    
    private final Shipment shipment;
    private final ShipmentService service;
    
    public ShipmentTask(Shipment shipment, ShipmentService service) {
        this.shipment = shipment;
        this.service = service;
    }
    
    @Override
    public void run() {
        service.process(shipment);
    }
}

// 사용
Runnable task = new ShipmentTask(shipment, service);
new Thread(task).start();

// 또는 람다
new Thread(() -> service.process(shipment)).start();

1.6 자기 점검 답변

Runnable 인터페이스의 정의는?

:
1. 정의:

  • 함수형 인터페이스 (SAM)
  • run() 메서드 하나
  1. 사용:

    • 클래스 구현
    • 람다
    • 익명 클래스
  2. Thread 와 관계:

    • Thread 가 Runnable 구현
    • target 으로 받음
  3. 방식:

    • new Thread(runnable).start()

2️⃣ Thread 상속의 한계

2.1 두 가지 한계

Thread 상속의 한계:

1. 단일 상속 제약
   - 자바는 단일 상속
   - Thread 상속 시 다른 클래스 X

2. 작업과 제어 결합
   - 작업 (run) 과 스레드 제어 섞임
   - 객체지향적 X
   - 재사용 어려움

2.2 단일 상속 제약

// ❌ Thread 상속 + 다른 클래스 상속 불가
class BaseProcessor {
    protected void log(String msg) { }
}

class ShipmentProcessor extends BaseProcessor {  // 이미 상속
    // extends Thread 불가!
    // 자바는 단일 상속
}

// 해결: Runnable 구현
class ShipmentProcessor extends BaseProcessor 
        implements Runnable {   // 인터페이스 추가 (다중 가능)
    @Override
    public void run() {
        log("Processing");   // BaseProcessor 메서드 활용
    }
}

2.3 작업과 제어의 결합

// Thread 상속 — 작업 + 제어 결합
class ShipmentThread extends Thread {
    // 스레드 제어 (Thread 의 것)
    // + 작업 (run)
    // 섞여 있음
    
    @Override
    public void run() {
        // 작업
    }
}

// 문제:
// - 작업만 따로 쓰기 어려움
// - 스레드 없이 작업 실행 X
// - 테스트 어려움

2.4 재사용의 어려움

// Thread 상속 — 작업 재사용 어려움
class MyThread extends Thread {
    @Override
    public void run() { doWork(); }
}

// 같은 작업을 여러 스레드가?
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
// 각각 별도 인스턴스 (작업 정의 중복)

// Runnable — 작업 공유
Runnable task = () -> doWork();
new Thread(task).start();
new Thread(task).start();   // 같은 작업 공유

2.5 시각화

Thread 상속:
  ┌─────────────────┐
  │ MyThread        │
  │  - 스레드 제어   │  ← Thread 의 기능
  │  - run() 작업    │  ← 작업
  │  (결합)          │
  └─────────────────┘
  단일 상속 (다른 클래스 X)

Runnable 구현:
  ┌──────────┐    ┌──────────┐
  │ Thread   │ ── │ Runnable │
  │ (제어)    │    │ (작업)    │
  └──────────┘    └──────────┘
  분리 (다른 클래스 상속 가능)

2.6 ILIC 의 맥락

// ❌ Thread 상속의 한계
public class ShipmentThreadBad extends Thread {
    // BaseService 상속 불가 (이미 Thread)
    // 작업 + 제어 결합
    @Override
    public void run() {
        // process()...
    }
}

// ✓ Runnable — 한계 극복
public class ShipmentTaskGood extends BaseService 
        implements Runnable {   // 다른 클래스 + Runnable
    
    @Override
    public void run() {
        // BaseService 의 메서드 활용
        process();   // 상속 메서드
    }
}

2.7 자기 점검 답변

Thread 상속의 한계는?

:
1. 단일 상속:

  • 자바 단일 상속
  • Thread 상속 시 다른 클래스 X
  1. 작업 + 제어 결합:

    • run 과 스레드 제어 섞임
    • 객체지향적 X
  2. 재사용 어려움:

    • 작업 분리 X
    • 같은 작업 공유 어려움
  3. 해결:

    • Runnable 구현

3️⃣ 장점 1 — 다른 클래스 상속 가능

3.1 인터페이스의 다중 구현

Runnable 의 장점 1:

  Runnable 은 인터페이스.
  - 클래스는 다른 클래스 상속 가능
  - + Runnable 인터페이스 구현
  - 다중 인터페이스도 가능

자바:
  - 클래스 단일 상속
  - 인터페이스 다중 구현

3.2 예시

// 다른 클래스 상속 + Runnable
class ShipmentProcessor extends BaseProcessor   // 클래스 상속
        implements Runnable, Comparable<ShipmentProcessor> {  // 다중 인터페이스
    
    @Override
    public void run() {
        // 작업
    }
    
    @Override
    public int compareTo(ShipmentProcessor o) {
        return 0;
    }
}

// Thread 상속으로는 불가능

3.3 기존 클래스에 Runnable 추가

// 기존 클래스
class ExistingService {
    public void doBusinessLogic() { }
}

// Runnable 추가 (상속 유지)
class ExistingServiceTask extends ExistingService 
        implements Runnable {
    @Override
    public void run() {
        doBusinessLogic();   // 기존 메서드 활용
    }
}

// 기존 코드 변경 없이 스레드화

3.4 프레임워크 클래스 상속

// Spring 의 클래스 상속 + Runnable
class ScheduledTask extends AbstractTask   // 프레임워크 클래스
        implements Runnable {
    @Override
    public void run() {
        // 스케줄 작업
    }
}

// AbstractTask 상속 유지
// + 스레드로 실행 가능

3.5 ILIC 의 맥락

// 기존 서비스 + Runnable
@Service
public class ShipmentService extends AbstractAuditableService {
    // AbstractAuditableService 상속 (감사 로그 등)
    
    public void process(Shipment s) {
        audit("Processing " + s.getId());   // 상속 메서드
        // 처리
    }
}

// 스레드화 — Runnable 구현 별도 클래스
public class ShipmentProcessTask implements Runnable {
    private final ShipmentService service;
    private final Shipment shipment;
    
    @Override
    public void run() {
        service.process(shipment);
    }
}

// 또는 람다
Runnable task = () -> service.process(shipment);

3.6 자기 점검 답변

장점 1 — 다른 클래스 상속 가능은?

:
1. 인터페이스 다중:

  • 클래스 상속 + Runnable
  • 다중 인터페이스
  1. 예시:

    • extends Base implements Runnable
    • Thread 로는 불가
  2. 활용:

    • 기존 클래스에 Runnable 추가
    • 프레임워크 클래스 상속 유지
  3. 이유:

    • 자바 단일 상속 회피

4️⃣ 장점 2 — 작업과 스레드 분리

4.1 관심사 분리

Runnable 의 장점 2:

  작업 (Runnable) 과 스레드 제어 (Thread) 를
  분리.

효과:
  - 객체지향적 설계
  - 작업 단독 사용 가능
  - 테스트 쉬움
  - 유연성

4.2 작업 단독 사용

// Runnable — 작업 단독 사용 가능
Runnable task = () -> processShipment();

// 1. 스레드로 실행
new Thread(task).start();

// 2. 직접 실행 (테스트 등)
task.run();   // 현재 스레드

// 3. Executor 에 제출
executor.submit(task);

// 4. 다른 곳에 전달
scheduleService.schedule(task);

// 같은 작업, 여러 실행 방식

4.3 테스트 용이성

// Runnable — 테스트 쉬움
public class ShipmentTask implements Runnable {
    @Override
    public void run() {
        process();
    }
    
    void process() {
        // 작업
    }
}

// 테스트 — 스레드 없이
@Test
void testProcess() {
    ShipmentTask task = new ShipmentTask();
    task.run();   // 직접 호출 (스레드 X)
    // 결과 검증
}

// Thread 상속이면 스레드 띄워야 (테스트 복잡)

4.4 실행 방식 교체

// 작업 정의 (Runnable)
Runnable task = () -> processData();

// 실행 방식 자유롭게 교체
// 1. 단순 스레드
new Thread(task).start();

// 2. 스레드 풀
executor.submit(task);

// 3. 스케줄러
scheduledExecutor.scheduleAtFixedRate(task, 0, 1, SECONDS);

// 4. 비동기
CompletableFuture.runAsync(task);

// 작업은 그대로, 실행만 변경

4.5 시각화

작업과 스레드 분리:

작업 (Runnable):
  ┌──────────────┐
  │ run() { ... } │
  └──────┬───────┘
         │ 다양한 실행
    ┌────┼────┬────────┐
    ↓    ↓    ↓        ↓
  Thread Executor 스케줄러 CompletableFuture

  하나의 작업, 여러 실행

4.6 ILIC 의 맥락

@Service
public class ShipmentExecutionService {
    
    private final ExecutorService executor;
    private final ScheduledExecutorService scheduler;
    
    // 작업 정의 (Runnable)
    private Runnable createTask(Shipment shipment) {
        return () -> shipmentService.process(shipment);
    }
    
    // 1. 즉시 실행 (스레드 풀)
    public void processNow(Shipment shipment) {
        executor.submit(createTask(shipment));
    }
    
    // 2. 지연 실행 (스케줄러)
    public void processLater(Shipment shipment, long delaySeconds) {
        scheduler.schedule(createTask(shipment), delaySeconds, TimeUnit.SECONDS);
    }
    
    // 3. 비동기
    public CompletableFuture<Void> processAsync(Shipment shipment) {
        return CompletableFuture.runAsync(createTask(shipment), executor);
    }
    
    // 같은 작업 정의, 다양한 실행
}

4.7 자기 점검 답변

장점 2 — 작업과 스레드 분리는?

:
1. 관심사 분리:

  • 작업 (Runnable)
  • 제어 (Thread)
  1. 효과:

    • 작업 단독 사용
    • 테스트 쉬움
    • 유연성
  2. 실행 방식:

    • Thread, Executor
    • 스케줄러, CompletableFuture
    • 교체 자유
  3. 객체지향:

    • 책임 분리

5️⃣ 장점 3 — 메모리 효율 (공유)

5.1 같은 Runnable 공유

Runnable 의 장점 3:

  하나의 Runnable 인스턴스를
  여러 스레드가 공유 가능.

효과:
  - 메모리 효율
  - 작업 정의 한 번
  - 여러 스레드 같은 작업

5.2 공유 예시

// 하나의 Runnable
Runnable task = () -> processShipments();

// 여러 스레드가 공유
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
Thread t3 = new Thread(task);
// 같은 task 인스턴스 (메모리 1개)

t1.start();
t2.start();
t3.start();
// 3 스레드가 같은 작업 실행

5.3 Thread 상속과 비교

// Thread 상속 — 각각 인스턴스
class MyThread extends Thread {
    @Override
    public void run() { doWork(); }
}

MyThread t1 = new MyThread();   // 인스턴스 1
MyThread t2 = new MyThread();   // 인스턴스 2
MyThread t3 = new MyThread();   // 인스턴스 3
// 작업 정의가 각 인스턴스에

// Runnable — 작업 공유
Runnable task = () -> doWork();   // 작업 1개
new Thread(task);   // 공유
new Thread(task);   // 공유
new Thread(task);   // 공유

5.4 공유 시 주의 — 상태

// 공유 Runnable 의 상태 주의
class CounterTask implements Runnable {
    private int count = 0;   // ★ 공유 상태!
    
    @Override
    public void run() {
        count++;   // 여러 스레드 동시 → 위험
    }
}

// 같은 인스턴스 공유
CounterTask task = new CounterTask();
new Thread(task).start();
new Thread(task).start();
// count 동시 수정 → 데이터 손실

// 무상태 Runnable 권장
Runnable stateless = () -> {
    int local = 0;   // 지역 변수 (안전)
    local++;
};

5.5 시각화

Runnable 공유:

   Runnable task (메모리 1개)
        ↑    ↑    ↑
        │    │    │
   Thread1 Thread2 Thread3
   (각자 실행)

   작업 정의 1개, 스레드 여러 개

Thread 상속:

   MyThread1  MyThread2  MyThread3
   (각자 작업 내장)
   메모리 3개

5.6 ILIC 의 맥락

public class SharedTaskExample {
    
    // 무상태 작업 (공유 안전)
    public void processWithSharedTask(List<Shipment> shipments) {
        // 작업 정의 (무상태)
        Runnable healthCheck = () -> {
            log.info("Health check on {}", Thread.currentThread().getName());
            checkSystemHealth();
        };
        
        // 여러 스레드가 같은 작업
        for (int i = 0; i < 5; i++) {
            new Thread(healthCheck).start();   // 공유
        }
        // healthCheck 인스턴스 1개
        // 5 스레드가 공유 (안전 — 무상태)
    }
    
    // 상태 있는 작업 (스레드별 인스턴스 또는 동기화)
    public void processWithState(List<Shipment> shipments) {
        for (Shipment s : shipments) {
            // 각 스레드마다 별도 (상태 격리)
            Runnable task = () -> process(s);   // s 캡처
            new Thread(task).start();
        }
    }
    
    private void checkSystemHealth() { }
    private void process(Shipment s) { }
}

5.7 자기 점검 답변

장점 3 — 메모리 효율은?

:
1. 공유:

  • 하나의 Runnable
  • 여러 스레드
  1. 효과:

    • 메모리 효율
    • 작업 정의 한 번
  2. vs Thread 상속:

    • Thread: 각 인스턴스
    • Runnable: 공유
  3. 주의:

    • 공유 상태 위험
    • 무상태 권장

6️⃣ 람다로 Runnable

6.1 함수형 인터페이스

Runnable 은 함수형 인터페이스:

  @FunctionalInterface
  interface Runnable {
      void run();   // SAM
  }

  → 람다로 작성 가능

6.2 람다 표현

// 익명 클래스 (옛)
Runnable task1 = new Runnable() {
    @Override
    public void run() {
        System.out.println("Hello");
    }
};

// 람다 (Java 8+)
Runnable task2 = () -> System.out.println("Hello");
// 훨씬 간결

// 여러 줄
Runnable task3 = () -> {
    System.out.println("Line 1");
    System.out.println("Line 2");
};

6.3 Thread 에 람다

// Thread 에 직접 람다
new Thread(() -> {
    processShipment();
}).start();

// 메서드 참조
new Thread(this::processShipment).start();

// 간결, 가독성

6.4 왜 람다 가능한가

람다가 가능한 이유:

  Runnable 이 SAM (Single Abstract Method).
  - 추상 메서드 1개 (run)
  - 컴파일러가 람다 → run() 매핑

  () -> {...}
  → run() 의 구현으로 해석

6.5 람다 vs 익명 클래스

// 익명 클래스
Runnable r1 = new Runnable() {
    @Override
    public void run() { doWork(); }
};

// 람다 (더 간결)
Runnable r2 = () -> doWork();

// 차이:
// - 람다: 간결, this 는 외부
// - 익명: 길음, this 는 자신
// (3주차 Phase 10 람다 참고)

6.6 ILIC 의 맥락

public class LambdaRunnableExample {
    
    private final ShipmentService service;
    
    public void variousLambdas(Shipment shipment) {
        // 1. 간단한 람다
        new Thread(() -> service.process(shipment)).start();
        
        // 2. 메서드 참조
        Runnable healthCheck = this::checkHealth;
        new Thread(healthCheck).start();
        
        // 3. 여러 줄
        new Thread(() -> {
            log.info("Start processing {}", shipment.getId());
            service.process(shipment);
            log.info("Done");
        }).start();
        
        // 4. Executor 와 함께
        executor.submit(() -> service.process(shipment));
        
        // 5. CompletableFuture
        CompletableFuture.runAsync(() -> service.process(shipment));
    }
    
    private void checkHealth() { }
}

6.7 자기 점검 답변

람다로 Runnable을 만들 수 있는 이유는?

:
1. 함수형 인터페이스:

  • SAM (run 하나)
  • 람다 가능
  1. 표현:

    • () -> {...}
    • 메서드 참조
  2. 이유:

    • 추상 메서드 1개
    • 컴파일러 매핑
  3. 장점:

    • 간결
    • 가독성

7️⃣ Spring @Async의 내부

7.1 @Async 의 동작

Spring @Async:

  메서드를 비동기로 실행.
  내부적으로 작업을 TaskExecutor 에 제출.

동작:
  1. @Async 메서드 호출
  2. Spring 프록시가 가로챔
  3. 작업을 Runnable/Callable 로 감쌈
  4. TaskExecutor 에 제출
  5. 별도 스레드에서 실행

7.2 @Async 예시

@Service
public class ShipmentAsyncService {
    
    @Async
    public void processAsync(Shipment shipment) {
        // 별도 스레드에서 실행
        service.process(shipment);
    }
    
    @Async
    public CompletableFuture<Result> processWithResult(Shipment shipment) {
        Result result = service.process(shipment);
        return CompletableFuture.completedFuture(result);
    }
}

// 호출
shipmentAsyncService.processAsync(shipment);
// 즉시 반환 (별도 스레드에서 처리)

7.3 내부 — Runnable/Callable

@Async 의 내부:

void 반환:
  - 작업을 Runnable 로 감쌈
  - taskExecutor.execute(runnable)

CompletableFuture/Future 반환:
  - Callable 로 감쌈
  - taskExecutor.submit(callable)

핵심:
  - Runnable/Callable 기반
  - ThreadPoolTaskExecutor 사용

7.4 TaskExecutor 설정

@Configuration
@EnableAsync
public class AsyncConfig {
    
    @Bean
    public Executor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(4);
        executor.setMaxPoolSize(8);
        executor.setQueueCapacity(100);
        executor.setThreadNamePrefix("async-");
        executor.initialize();
        return executor;
    }
}

// @Async 가 이 executor 사용
// 내부적으로 Runnable/Callable 제출

7.5 @Async 의 제약

@Async 의 주의:

1. public 메서드만
   - 프록시 기반

2. self-invocation X
   - 같은 클래스 내부 호출 시 비동기 X
   - 프록시 우회

3. 반환 타입
   - void
   - Future / CompletableFuture

4. 예외 처리
   - void: AsyncUncaughtExceptionHandler
   - Future: get() 시 예외

7.6 ILIC 의 맥락

@Configuration
@EnableAsync
public class ShipmentAsyncConfig {
    
    @Bean("shipmentExecutor")
    public Executor shipmentExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        int cores = Runtime.getRuntime().availableProcessors();
        executor.setCorePoolSize(cores);
        executor.setMaxPoolSize(cores * 2);
        executor.setThreadNamePrefix("shipment-async-");
        executor.initialize();
        return executor;
    }
}

@Service
public class ShipmentNotificationService {
    
    // 비동기 알림 (Runnable 기반)
    @Async("shipmentExecutor")
    public void sendNotificationAsync(Shipment shipment) {
        // 별도 스레드 (executor 의)
        // 내부적으로 Runnable 로 감싸짐
        emailService.send(shipment.getConsignee(), buildMessage(shipment));
    }
    
    // 비동기 + 결과 (Callable 기반)
    @Async("shipmentExecutor")
    public CompletableFuture<TrackingInfo> fetchTrackingAsync(String blNo) {
        TrackingInfo info = trackingApi.fetch(blNo);
        return CompletableFuture.completedFuture(info);
    }
}

7.7 자기 점검 답변

Spring @Async는 내부적으로 무엇을 쓰나?

:
1. 동작:

  • 프록시가 가로챔
  • TaskExecutor 에 제출
  1. 내부:

    • void: Runnable
    • Future: Callable
  2. TaskExecutor:

    • ThreadPoolTaskExecutor
    • 스레드 풀
  3. 제약:

    • public 메서드
    • self-invocation X
    • 반환 타입 (void/Future)

8️⃣ Runnable vs Callable, 선택 가이드

8.1 Runnable vs Callable

항목RunnableCallable
메서드void run()V call() throws Exception
반환값✅ (제네릭)
체크 예외
도입Java 1.0Java 5
사용Thread, ExecutorExecutor (submit)

8.2 Callable 예시

// Callable — 반환값 + 예외
Callable<Shipment> task = () -> {
    Shipment s = repository.findById(1L);   // 예외 가능
    return s;   // 반환값
};

// Executor 에 제출
Future<Shipment> future = executor.submit(task);
Shipment result = future.get();   // 결과 회수

// Runnable 은 반환값 X
Runnable r = () -> {
    repository.findById(1L);   // 반환 안 됨
    // throws 불가
};

8.3 언제 Runnable, 언제 Callable

Runnable 사용:
  - 반환값 불필요
  - 단순 작업
  - 부수 효과 (로그, 알림)

Callable 사용:
  - 반환값 필요
  - 체크 예외 처리
  - 결과 회수 (Future)

예:
  - 알림 발송: Runnable
  - 계산 + 결과: Callable

8.4 Thread 상속 vs Runnable 구현

선택 가이드:

Runnable 구현 (권장):
  ✓ 다른 클래스 상속 필요
  ✓ 작업과 스레드 분리
  ✓ 작업 공유
  ✓ 대부분의 경우

Thread 상속 (드물게):
  ✓ Thread 기능 확장 필요
  ✓ 매우 단순한 경우

실무:
  - Runnable/Callable + Executor
  - Thread 직접 거의 안 씀

8.5 현대적 권장

// ❌ 옛 방식 — Thread 직접
new Thread(() -> doWork()).start();

// ✓ 권장 — Executor + Runnable/Callable
ExecutorService executor = Executors.newFixedThreadPool(4);
executor.submit(() -> doWork());           // Runnable
Future<Result> f = executor.submit(() -> compute());  // Callable

// ✓✓ 현대 — Virtual Thread (Java 21+)
try (var ex = Executors.newVirtualThreadPerTaskExecutor()) {
    ex.submit(() -> doWork());
}

8.6 ILIC 의 맥락

@Service
public class ShipmentTaskService {
    
    private final ExecutorService executor;
    
    // Runnable — 반환값 불필요 (알림)
    public void sendNotification(Shipment shipment) {
        executor.submit(() -> {   // Runnable
            emailService.send(shipment);
        });
    }
    
    // Callable — 반환값 필요 (계산)
    public Future<BigDecimal> calculateFreight(Shipment shipment) {
        return executor.submit(() -> {   // Callable
            return freightCalculator.calculate(shipment);   // 반환 + 예외
        });
    }
    
    // CompletableFuture (권장 — Phase 8)
    public CompletableFuture<BigDecimal> calculateFreightAsync(Shipment shipment) {
        return CompletableFuture.supplyAsync(
            () -> freightCalculator.calculate(shipment),
            executor);
    }
}

8.7 자기 점검 답변

Runnable vs Callable, 선택은?

:
1. Runnable:

  • void run()
  • 반환값 X
  • 체크 예외 X
  1. Callable:

    • V call() throws Exception
    • 반환값 O
    • 체크 예외 O
  2. 선택:

    • 반환 불필요: Runnable
    • 반환/예외: Callable
  3. Thread vs Runnable:

    • Runnable 권장
    • Executor 와 함께

9️⃣ 면접 + 자기 점검

9.1 면접 단골 질문 매핑

Q핵심 답변
Runnable?run() 하나 (함수형 인터페이스)
Thread 상속 한계?단일 상속, 결합
Runnable 장점 3가지?상속 가능, 분리, 공유
람다 가능 이유?SAM (함수형)
작업 공유?하나의 Runnable 여러 스레드
작업/스레드 분리?객체지향, 유연
@Async 내부?Runnable/Callable + TaskExecutor
Runnable vs Callable?반환값/예외 유무
Thread vs Runnable 선택?Runnable 권장
공유 시 주의?상태 (무상태 권장)

9.2 자기 점검 체크리스트

Runnable

  • 정의 (SAM)
  • 사용 방법
  • Thread 와 관계

Thread 한계

  • 단일 상속
  • 결합
  • 재사용

장점 3가지

  • 다른 클래스 상속
  • 작업/스레드 분리
  • 메모리 효율 (공유)

람다

  • 함수형 인터페이스
  • 표현

@Async

  • 내부 (Runnable/Callable)
  • TaskExecutor
  • 제약

Callable

  • vs Runnable
  • 선택

9.3 추가 심화 질문

Q1: Runnable 의 run 이 예외 못 던지는 이유?

답:

  • 인터페이스 시그니처: void run() (throws 없음)
  • checked exception 못 던짐
  • 반환값도 없음
  • → Callable 이 대안 (throws Exception, 반환값)

Q2: Thread 도 Runnable?

답:

  • Thread implements Runnable
  • Thread 도 run() 보유
  • Thread(Runnable) 생성자
  • 둘 다 가능

Q3: 람다 vs 메서드 참조?

답:

  • 람다: () -> doWork()
  • 메서드 참조: this::doWork
  • 둘 다 Runnable
  • 메서드 참조가 더 간결 (단순 호출 시)

Q4: @Async self-invocation 문제?

답:

  • 같은 클래스 내부 호출 시 비동기 X
  • 프록시 우회 (this 직접 호출)
  • 해결: 별도 빈, 또는 self-injection
  • AOP 프록시 한계

Q5: ThreadPoolTaskExecutor vs ExecutorService?

답:

  • ThreadPoolTaskExecutor: Spring 추상화
  • ExecutorService: 자바 표준
  • Spring 은 TaskExecutor 인터페이스
  • 내부적으로 ThreadPoolExecutor

🎯 핵심 요약 — 3줄 정리

1. Runnable

  • 함수형 인터페이스 (run 하나)
  • Thread 에 작업 전달
  • 람다 가능

2. 장점 3가지

  • 다른 클래스 상속 가능
  • 작업과 스레드 분리
  • 메모리 효율 (공유)

3. 실무

  • Runnable 권장 (Thread 상속보다)
  • @Async 도 Runnable/Callable 기반
  • Executor 와 함께

📚 다음으로...

Unit 3.4 — 데몬 스레드 (Daemon Thread)

이번 Unit에서 Runnable 을 봤다면, 다음은 데몬 스레드.

  • 데몬 스레드의 정의
  • 일반 스레드 vs 데몬
  • setDaemon(true)
  • 용도와 주의

Phase 3 진행 상황

🚀 Phase 3 — 스레드 만들고 다루기
  ✅ Unit 3.1 스레드 상태 다이어그램
  ✅ Unit 3.2 Thread 클래스 상속
  ✅ Unit 3.3 Runnable 인터페이스 ← 여기
  ⏭ Unit 3.4 데몬 스레드
  ⏭ Unit 3.5 join()

4주차 누적 진행

✅ Phase 1 — 동시성의 기초 (4 Unit)
✅ Phase 2 — 4분면 매트릭스 (3 Unit)
🚀 Phase 3 — 스레드 다루기 (3/5 진행)

총: 10/35 Unit
profile
Software Developer

0개의 댓글