프로그래밍/디자인패턴

콜백 패턴(Callback Pattern) 총정리: 동기(Blocking) vs 비동기(Non-blocking)

Jinwookoh 2025. 12. 7. 19:35

"헐리우드 원칙을 아시나요? '우리에게 전화하지 마세요. 우리가 당신에게 전화할게요(Don't call us, we'll call you).'"

보통은 우리가 함수를 호출하고 결과값을 받습니다(return). 하지만 콜백 패턴에서는 우리가 함수에게 **"일이 끝나면 이 코드를 실행해줘"**라고 함수 자체를 파라미터로 넘깁니다.

하지만 이 콜백이 "기다렸다가 실행되는지(동기)" 아니면 **"나를 놔두고 따로 실행되는지(비동기)"**에 따라 프로그램의 흐름이 완전히 달라집니다.

Shutterstock
 

 


1. 방식 1: 동기 콜백 (Synchronous Callback) - "기다림의 미학"

자바의 Stream이나 Template Callback 패턴에서 주로 쓰이는 방식입니다. 호출한 함수가 콜백을 모두 실행하고 끝날 때까지 제어권을 돌려주지 않고 기다립니다(Blocking).

특징

  • 순차 실행: 코드가 작성된 순서대로 정확하게 실행됩니다.
  • 단일 스레드: 특별한 설정이 없으면 메인 스레드에서 쭉 실행됩니다.

코드 예시

Java
 
// 1. 콜백 인터페이스
interface MyCallback {
    void call();
}

// 2. 작업을 수행하는 클래스
class TaskExecutor {
    public void execute(MyCallback callback) {
        System.out.println("1. 작업 시작");
        callback.call(); // 여기서 콜백 실행 (끝날 때까지 다음 줄로 안 넘어감)
        System.out.println("3. 작업 종료");
    }
}

// 3. 사용 (Client)
public class Main {
    public static void main(String[] args) {
        TaskExecutor executor = new TaskExecutor();
        
        // 람다로 콜백 전달
        executor.execute(() -> System.out.println("2. >> 콜백 로직 수행 중..."));
    }
}

실행 결과

Plaintext
 
1. 작업 시작
2. >> 콜백 로직 수행 중...
3. 작업 종료

(1 -> 2 -> 3 순서가 무조건 보장됨)

장단점

  • 장점: 로직의 흐름을 예측하기 쉽고 디버깅이 편합니다.
  • 단점: 콜백 로직이 오래 걸리면 전체 시스템이 멈춘 것처럼 보입니다(Blocking).

2. 방식 2: 비동기 콜백 (Asynchronous Callback) - "따로 또 같이"

자바스크립트(Node.js)나 자바의 CompletableFuture, GUI 이벤트 처리에서 주로 쓰입니다. 작업을 별도의 스레드에 던져놓고, 메인 스레드는 기다리지 않고 제 갈 길을 갑니다(Non-blocking).

특징

  • 병렬 실행: 메인 로직과 콜백 로직이 동시에(혹은 시차를 두고) 실행됩니다.
  • 실행 순서 불확실: 코드는 위에 있지만, 실행은 나중에 될 수 있습니다.

코드 예시

Java
 
class AsyncTaskExecutor {
    public void executeAsync(MyCallback callback) {
        System.out.println("1. 작업 요청");
        
        // 새로운 스레드에서 실행 (Non-blocking)
        new Thread(() -> {
            try { Thread.sleep(2000); } catch (InterruptedException e) {} // 2초 대기
            callback.call(); // 2초 뒤에 나중에 실행됨
        }).start();
        
        System.out.println("3. 다음 작업 바로 진행");
    }
}

// 사용
public class Main {
    public static void main(String[] args) {
        AsyncTaskExecutor executor = new AsyncTaskExecutor();
        
        executor.executeAsync(() -> System.out.println("2. >> (2초 뒤) 콜백 실행!"));
        
        System.out.println("4. 메인 메서드 끝");
    }
}

실행 결과

Plaintext
 
1. 작업 요청
3. 다음 작업 바로 진행
4. 메인 메서드 끝
2. >> (2초 뒤) 콜백 실행!  <-- 나중에 찍힘

(1 -> 3 -> 4 -> 2 순서로 실행됨)

장단점

  • 장점: I/O 작업(DB 조회, 파일 읽기)이 오래 걸려도 메인 스레드가 멈추지 않아 성능과 반응성이 좋습니다.
  • 단점:
    • 콜백 지옥(Callback Hell): 콜백 안에 콜백, 그 안에 콜백... 코드가 들여쓰기 지옥에 빠집니다.
    • 디버깅 어려움: 언제 실행될지 예측하기 힘들고 예외 처리가 까다롭습니다.

3. 실무 예시: 자바에서의 진화

1) Java 8 Stream (동기)

Java
 
List<String> list = Arrays.asList("a", "b", "c");
// forEach 안에 넘기는 람다는 '동기 콜백'입니다.
list.forEach(s -> System.out.println(s)); 
// 리스트 순회가 끝나야 다음 코드로 넘어갑니다.

2) CompletableFuture (비동기)

자바 8부터는 비동기 콜백 지옥을 해결하기 위해 체이닝 방식을 지원합니다.

Java
 
CompletableFuture.supplyAsync(() -> {
    return "DB 데이터 조회"; // 비동기 실행
}).thenAccept(result -> {
    System.out.println(result + " 처리 완료"); // 콜백 (성공 시 실행)
});

요약: 기다릴까, 먼저 갈까?

구분 Synchronous Callback (동기) Asynchronous Callback (비동기)
제어 흐름 호출자(Caller)가 대기함 (Blocking) 호출자는 즉시 리턴함 (Non-blocking)
실행 스레드 주로 Main 스레드 그대로 사용 별도의 Worker 스레드 사용
순서 보장 코드 작성 순서 = 실행 순서 순서 보장 안 됨
사용 사례 리스트 순회, 전략 패턴, 템플릿 콜백 AJAX, 파일 I/O, 타이머, 이벤트 리스너

결론

"로직의 재사용성을 높이고 싶다(Template Callback)"면 동기 콜백을 사용하세요. 코드가 깔끔해집니다.

하지만 "서버의 응답을 기다리거나, 무거운 작업을 백그라운드에서 처리해야 한다"면 반드시 비동기 콜백을 사용해야 합니다. 단, 비동기 콜백을 쓸 때는 **"콜백 지옥"**을 피하기 위해 CompletableFuture나 RxJava 같은 리액티브 라이브러리의 도움을 받는 것이 정신 건강에 좋습니다.