프로그래밍/디자인패턴

비동기 요청-응답 패턴(Asynchronous Request-Reply) 완벽 정리

Jinwookoh 2025. 12. 7. 21:00

"사용자가 '월간 리포트 생성' 버튼을 눌렀습니다. 이 작업은 3분이 걸립니다. HTTP 연결을 3분 동안 붙들고 있으면 브라우저나 로드 밸런서가 연결을 끊어버립니다(Time-out)."

이런 상황에서는 일단 **"알겠어, 접수했어(202 Accepted)"**라고 응답하고 연결을 끊어야 합니다. 문제는 **"그 작업이 다 끝났는지 클라이언트가 어떻게 아는가?"**입니다.


1. 방식 1: 폴링 (Polling) - "다 됐어요?"

클라이언트가 서버에게 주기적으로 상태를 물어보는 방식입니다. 가장 기본적이고 구현하기 쉽습니다.

동작 순서

  1. Client: POST /report 요청.
  2. Server: 작업을 큐에 넣고, 즉시 202 Accepted와 함께 상태 조회 URL(Location: /report/123/status)을 반환.
  3. Client: 1초마다 GET /report/123/status 호출.
  4. Server: "진행 중(Running)" 응답.
  5. Server: (작업 완료 후) "완료(Completed)" 및 결과 다운로드 링크 응답.

코드 예시 (Spring Boot)

Java
 
@RestController
public class ReportController {

    // 1. 작업 요청 접수
    @PostMapping("/reports")
    public ResponseEntity<?> createReport() {
        String jobId = jobService.submit(); // 비동기 처리 시작
        // "접수됨(202)" + "상태 확인 URL" 반환
        return ResponseEntity.accepted()
                .header("Location", "/reports/" + jobId + "/status")
                .build();
    }

    // 2. 상태 조회 (Polling 대상)
    @GetMapping("/reports/{jobId}/status")
    public ResponseEntity<?> getStatus(@PathVariable String jobId) {
        JobStatus status = jobService.getStatus(jobId);
        
        if (status == COMPLETED) {
            // 303 See Other: 결과물이 있는 곳으로 리다이렉트
            return ResponseEntity.status(HttpStatus.SEE_OTHER)
                    .header("Location", "/reports/" + jobId + "/download")
                    .build();
        }
        
        // 아직 처리 중
        return ResponseEntity.ok("PROCESSING");
    }
}

장단점

  • 장점:
    • 클라이언트 제어: 클라이언트가 원하는 주기로 확인할 수 있습니다.
    • 방화벽 친화적: 클라이언트가 서버로 요청을 보내는 구조라, 클라이언트가 사설망(Private Network)에 있어도 문제없습니다.
  • 단점:
    • 리소스 낭비: 작업이 10분 걸리는데 1초마다 물어보면, 600번의 요청 중 599번은 쓸모없는 트래픽입니다. (Chatty Protocol)
    • 지연(Latency): 작업이 끝난 직후가 아니라, 다음 폴링 주기 때 완료 사실을 알게 됩니다.

2. 방식 2: 웹훅 (Webhook) - "다 되면 연락드릴게요"

서버가 작업이 완료되면 클라이언트가 미리 등록해둔 URL(Callback URL)로 결과를 쏴주는 방식입니다. 슬랙(Slack), 스트라이프(Stripe), 깃허브(GitHub) 등 최신 API의 표준입니다.

동작 순서

  1. Client: POST /report 요청 시, 본문에 callbackUrl: https://client.com/hooks를 실어서 보냄.
  2. Server: 202 Accepted 응답 후 연결 종료.
  3. Server: (작업 수행) ... (완료)
  4. Server: 클라이언트의 callbackUrl로 POST 요청을 보냄 (Push).

코드 예시

Java
 
// Server Side (작업 처리기)
public void processReport(ReportRequest req) {
    // 1. 무거운 작업 수행
    Result result = heavyJob.execute();
    
    // 2. 작업 완료 후, 클라이언트에게 역으로 요청을 보냄 (Webhook)
    RestTemplate restTemplate = new RestTemplate();
    restTemplate.postForObject(req.getCallbackUrl(), result, String.class);
}

// Client Side (웹훅 수신용 API를 열어둬야 함)
@PostMapping("/hooks/report-finished")
public void onReportFinished(@RequestBody Result result) {
    System.out.println("리포트 생성 완료 알림 도착: " + result.getUrl());
}

장단점

  • 장점:
    • 효율성: 불필요한 폴링 트래픽이 "0"입니다. 리소스 낭비가 없습니다.
    • 실시간성: 작업이 끝나자마자 즉시 알림을 받을 수 있습니다.
  • 단점:
    • 구현 복잡도: 클라이언트도 서버(수신용 API) 역할을 해야 하므로 공인 IP나 도메인이 필요합니다.
    • 보안 이슈: "이 요청이 진짜 서버가 보낸 건지 해커가 보낸 건지" 검증하는 로직(HMAC 서명 등)이 필요합니다.

3. 실무 비교: 언제 무엇을 쓰는가?

구분 Polling (능동 확인) Webhook (수동 대기)
통신 방향 Client -> Server (반복) Server -> Client (1회)
네트워크 효율 낮음 (쓸모없는 요청 많음) 높음 (필요할 때만 통신)
클라이언트 요건 단순함 (HTTP Client만 있으면 됨) 복잡함 (Public IP 및 수신 서버 필요)
실시간성 낮음 (Polling 주기에 의존) 높음 (즉시 전송)
추천 상황 프론트엔드(JS) 연동, 클라이언트가 방화벽 뒤에 있을 때 B2B API 연동, 서버 간 통신, 긴 작업 시간

결론

클라이언트가 **브라우저(SPA)**이거나, 모바일 앱이라면 폴링(Polling) 방식을 쓰세요. (단, 부하를 줄이기 위해 폴링 주기를 점점 늘리는 전략을 쓰기도 합니다.) 브라우저가 공인 IP를 가질 순 없으니까요.

하지만 서버 대 서버(Server-to-Server) 통신이거나, 결제 완료 알림처럼 실시간성이 중요한 B2B 서비스를 만든다면 **웹훅(Webhook)**을 지원하는 것이 현대적인 API 설계의 정석입니다.