Runnable이란? 비동기 워커 설계에서 마주친 첫 스레드 인터페이스

2025. 3. 23. 21:21Back-End/JAVA

반응형

 

업무 중 스케줄 기반으로 콜을 발신해야 하는 시스템을 설계하게 됐다.
단일 스케줄을 처리하는 로직이 꽤 간단해서 한 개의 쓰레드로만 처리할 수도 있었지만,
여러 건을 동시에 병렬로 처리하고 싶어서 멀티스레드 구조를 도입하기로 했다.

 

그 과정에서 처음 접하게 된 것이 바로 Runnable 이라는 인터페이스였다.
오늘은 그걸 처음 만났던 경험과, 어떻게 적용했는지를 남겨본다.

 

Runnable이란?

Java에서 멀티스레드를 사용할 때 “이 스레드가 어떤 작업을 할 건지”를 정의할 수 있어야 한다.
그걸 정의해주는 게 바로 Runnable 인터페이스다.

public interface Runnable {
    void run();
}

 

딱 하나의 메서드 run()만 가지고 있고,
이 안에 “스레드가 해야 할 작업”을 작성하면 된다.

 

스레드에 Runnable을 넘긴다는 건?

Runnable task = () -> {
    System.out.println("별도의 스레드에서 실행됩니다.");
};

new Thread(task).start();

또는

ExecutorService executor = Executors.newFixedThreadPool(5);
executor.submit(task); // 쓰레드풀에 비동기 작업 제출

 

 

실전에서의 활용: 콜 발신 워커 설계

나의 경우는 스케줄러가 주기적으로 DB를 조회하고,
콜 발신이 필요한 행을 워커 스레드에 넘겨 처리하는 구조였다.

그래서 다음과 같이 Runnable을 구현한 워커를 만들었다.

public class CallWorker implements Runnable {

    private final Schedule schedule;

    public CallWorker(Schedule schedule) {
        this.schedule = schedule;
    }

    @Override
    public void run() {
        for (int i = 0; i < schedule.getMaxRetry(); i++) {
            boolean success = call(schedule);

            if (success) {
                schedule.setResult("SUCC");
                schedule.setStatus("DONE");
                break;
            }

            if (i == schedule.getMaxRetry() - 1) {
                schedule.setResult("FAIL");
                schedule.setStatus("WAIT");
                schedule.setTarget("MANAGER");
            }

            try {
                Thread.sleep(schedule.getRetryInterval() * 1000L); // 재시도 대기
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    }

    private boolean call(Schedule schedule) {
        // 실제 콜 발신 로직
        return false;
    }
}

그리고 스케줄러에서는 이렇게 쓰레드풀에 워커를 넘겨 처리했다.

ExecutorService executor = Executors.newFixedThreadPool(5);
executor.submit(new CallWorker(schedule));

 

 

사용하면서 느낀 점

  • Runnable은 매우 단순하다. 구현도 쉽다.
  • 굳이 추상 클래스나 복잡한 객체 없이 딱 하나의 목적에 집중된 로직을 넘기기에 좋았다.
  • 여러 Row를 병렬로 처리하되, Row 하나는 워커 하나가 끝까지 책임지는 구조를 만들기 좋았다.
  • 스레드풀과 함께 쓰면 병렬성도 확보된다.

 

+) Callable?

이건 Runnable을 쓰면서 나중에 알게 된 내용인데,

Runnable은 반환값이 없고, 예외도 throws 할 수 없다.

Runnable run = () -> {
    // 결과 반환 불가
};

반면 Callable은 결과를 반환할 수 있다.

Callable<String> task = () -> {
    return "작업 결과";
};

이럴 땐 Future로 결과를 받을 수도 있고,
예외가 발생하면 try-catch로 처리도 가능하다.

 

📌 즉,

  • 결과값이 필요 없다면 → Runnable
  • 결과값이 필요하거나 예외를 던져야 한다면 → Callable

처음엔 인터페이스 하나라고 쉽게 봤는데,
직접 워커 스레드를 설계해보면서 이 Runnable이 어떤 의미인지 제대로 와닿았다.
비동기 시스템에서는 작업 단위 자체를 객체처럼 다뤄야 한다는 점, 그게 정말 중요한 경험이었다.

나처럼 스레드 설계가 처음이라면 Runnable부터 천천히 직접 써보는 걸 추천한다.

반응형