동시 요청 테스트하기

N’oublie pas de t’aimer·2025년 7월 10일

DIVE

목록 보기
10/10

영상 처리 요청이 동시에 여러 개가 들어오면 어떻게 될까?

이전에 사용자가 영상을 녹화한 후 대기시간을 줄이고자 영상 처리 작업을 비동기 처리한 바 있다.
이 비동기 작업은 크기가 4인, 즉 최대 4개의 작업을 동시에 병렬적으로 할 수 있는 별개의 스레드 풀에서 실행된다.

@Bean(name = "videoProcessingExecutor")
    public Executor videoProcessingExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(2);
        executor.setMaxPoolSize(4);
        executor.setQueueCapacity(50);
        executor.setThreadNamePrefix("VideoProcessing-");
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.initialize();
        return executor;
    }

CPU bound 작업의 경우에는 CPU 코어 개수와 동일하게 스레드 풀의 크기를 정하는 게 좋다.

IO Bound 작업의 경우에는 코어의 개수 뿐만 아니라 대기 시간을 고려해서 스레드 풀의 크기를 결정해야 한다.

그렇다면 내 프로젝트의 영상처리는 둘 중 어떤 작업에 해당하는가?

1. processVideoAsync

  • 특성: 주로 I/O 바운드 (일부 CPU 바운드 작업 포함)
  • 이유:
    • I/O 바운드 작업:
      • S3에서 파일 메타데이터 조회, 썸네일 업로드, 오디오 파일 업로드(getVideoDurationFromS3, uploadThumbnailToS3, processShortVideoWithPresignedUrl, processLongVideoWithPresignedUrl)는 네트워크 I/O에 의존.
      • AWS Transcribe 호출(음성-to-텍스트 변환)은 외부 API 호출로, 네트워크 대기 시간이 큼.
      • 데이터베이스 조회 및 저장(videoRepository.findById, videoRepository.save)은 데이터베이스 I/O 작업.
      • 알림 전송(notificationService.send)은 외부 시스템과의 통신.
    • CPU 바운드 작업:
      • FFmpeg를 사용한 썸네일 생성 및 오디오 추출은 비디오/오디오 디코딩 및 인코딩으로 CPU 집약적.
      • 특히 processLongVideoWithPresignedUrl에서 병렬 처리로 CPU 사용량 증가.
    • 결론: I/O 작업(네트워크, 데이터베이스, 외부 API 호출)의 비중이 더 크며, FFmpeg 작업은 일부 단계에 국한되므로 I/O 바운드가 주도적.

2. updateVideoThumbnailAndStatus

  • 특성: I/O 바운드
  • 이유:
    • 데이터베이스 조회(videoRepository.findById)와 저장(videoRepository.save)은 데이터베이스 I/O 작업.
    • 로컬 CPU 연산(객체 업데이트)은 매우 가볍음.
    • 결론: 데이터베이스 I/O가 주된 작업이므로 I/O 바운드.

3. handleInvalidAnswer

  • 특성: I/O 바운드
  • 이유:
    • 데이터베이스 조회 및 저장(videoRepository.findById, videoRepository.save)은 I/O 작업.
    • 알림 전송(notificationService.send)은 외부 시스템과의 통신으로 네트워크 I/O.
    • 로컬 CPU 연산(객체 생성, 로그 출력)은 미미함.
    • 결론: 데이터베이스와 알림 서비스 I/O가 주요 작업이므로 I/O 바운드.

4. handleValidAnswer

  • 특성: I/O 바운드
  • 이유:
    • 데이터베이스 조회 및 저장(videoRepository.findById, videoRepository.save)은 I/O 작업.
    • 피드백 서비스 호출(feedbackService.getFeedback, feedbackService.findFeedback)은 외부 시스템 또는 데이터베이스 I/O 포함 가능.
    • 알림 전송(notificationService.send)은 네트워크 I/O.
    • 로컬 CPU 연산(객체 생성, 로그 출력)은 가볍음.
    • 결론: 데이터베이스와 외부 서비스 호출이 주된 작업이므로 I/O 바운드.

5. handleError

  • 특성: I/O 바운드
  • 이유:
    • 데이터베이스 조회 및 저장(videoRepository.findById, videoRepository.save)은 I/O 작업.
    • 알림 전송(notificationService.send)은 네트워크 I/O.
    • 로컬 CPU 연산은 거의 없음.
    • 결론: 데이터베이스와 알림 서비스 I/O가 전부이므로 I/O 바운드.

6. isValidAnswer

  • 특성: CPU 바운드
  • 이유:
    • 문자열 처리(널 체크, 공백 제거, 길이 확인, 정규 표현식 매칭)는 모두 로컬 CPU 연산.
    • 외부 I/O 작업(네트워크, 데이터베이스 등)은 전혀 없음.
    • 결론: 문자열 처리에만 의존하므로 CPU 바운드 (다만, 작업 자체가 가벼워 영향은 미미함).

7. getVideoDurationFromS3

  • 특성: I/O 바운드
  • 이유:
    • S3에서 파일 메타데이터 조회(headObject)는 네트워크 I/O 작업.
    • 비트레이트 추정 및 duration 계산(getEstimatedBitrate)은 가벼운 CPU 연산.
    • 결론: S3 API 호출이 주요 시간이므로 I/O 바운드.

8. getEstimatedBitrate

  • 특성: CPU 바운드
  • 이유:
    • 파일 확장자와 Content-Type을 기반으로 비트레이트를 결정하는 작업은 문자열 비교 및 조건문으로 이루어진 로컬 CPU 연산.
    • I/O 작업은 전혀 없음.
    • 결론: 순수 로컬 연산이므로 CPU 바운드 (작업이 매우 가벼움).

9. processShortVideoWithPresignedUrl

  • 특성: I/O 바운드 (일부 CPU 바운드 작업 포함)
  • 이유:
    • I/O 바운드 작업:
      • S3 Presigned URL 생성 및 오디오 파일 업로드(s3Client.putObject)는 네트워크 I/O.
      • AWS Transcribe 호출(startTranscriptionJob, getTranscriptionResult)은 네트워크 I/O.
    • CPU 바운드 작업:
      • FFmpeg로 오디오 추출(ProcessBuilder로 FFmpeg 실행)은 비디오 디코딩 및 오디오 인코딩으로 CPU 집약적.
    • 결론: AWS Transcribe 호출과 S3 작업의 네트워크 대기 시간이 더 큰 비중을 차지하므로 I/O 바운드가 주도적.

10. processLongVideoWithPresignedUrl

  • 특성: I/O 바운드 (CPU 바운드 작업 비중 큼)
  • 이유:
    • I/O 바운드 작업:
      • S3 Presigned URL 생성, 오디오 파일 업로드, AWS Transcribe 호출은 네트워크 I/O.
      • 각 청크의 결과를 S3에 업로드하고 Transcribe로 처리하는 과정은 I/O 대기 시간이 큼.
    • CPU 바운드 작업:
      • FFmpeg로 비디오를 4개 청크로 분할하고 오디오를 추출하는 작업은 CPU 집약적이며, 병렬 처리(ExecutorService)로 CPU 사용량 증가.
    • 결론: AWS Transcribe와 S3 작업의 I/O 대기 시간이 병목 지점이므로 I/O 바운드가 주도적이지만, FFmpeg 병렬 처리로 CPU 바운드 비중도 상당함.

11. uploadThumbnailToS3

  • 특성: I/O 바운드 (일부 CPU 바운드 작업 포함)
  • 이유:
    • I/O 바운드 작업:
      • S3 Presigned URL 생성 및 썸네일 업로드(s3Client.putObject)는 네트워크 I/O.
    • CPU 바운드 작업:
      • FFmpeg로 썸네일 추출(ProcessBuilder로 FFmpeg 실행)은 비디오 디코딩 및 이미지 변환으로 CPU 집약적.
    • 결론: S3 업로드와 네트워크 호출이 주요 시간이므로 I/O 바운드가 주도적.

12. startTranscriptionJob

  • 특성: I/O 바운드
  • 이유:
    • AWS Transcribe API 호출(transcribeClient.startTranscriptionJob)은 네트워크 요청.
    • 로컬 CPU 연산(클라이언트 초기화, 요청 객체 생성)은 매우 가벼움.
    • 결론: 네트워크 I/O가 전부이므로 I/O 바운드.

13. getTranscriptionResult

  • 특성: I/O 바운드
  • 이유:
    • AWS Transcribe API를 주기적으로 호출(transcribeClient.getTranscriptionJob)하며, 네트워크 요청과 응답 대기가 주요 시간.
    • 폴링(Thread.sleep)은 CPU가 아닌 스레드 대기 상태.
    • 로컬 CPU 연산(응답 파싱)은 가벼움.
    • 결론: 네트워크 호출과 폴링 대기 시간이 지배적이므로 I/O 바운드.
메서드주요 특성이유
processVideoAsyncI/O 바운드S3, Transcribe, DB, 알림 I/O가 주도적, FFmpeg는 일부 CPU 바운드
updateVideoThumbnailAndStatusI/O 바운드DB 조회/저장 I/O
handleInvalidAnswerI/O 바운드DB 조회/저장, 알림 전송 I/O
handleValidAnswerI/O 바운드DB 조회/저장, 피드백 서비스, 알림 전송 I/O
handleErrorI/O 바운드DB 조회/저장, 알림 전송 I/O
isValidAnswerCPU 바운드문자열 처리(가벼운 CPU 연산)
getVideoDurationFromS3I/O 바운드S3 메타데이터 조회 I/O, 가벼운 비트레이트 계산
getEstimatedBitrateCPU 바운드문자열 비교 및 조건문(가벼운 CPU 연산)
processShortVideoWithPresignedUrlI/O 바운드S3, Transcribe I/O가 주도적, FFmpeg는 일부 CPU 바운드
processLongVideoWithPresignedUrlI/O 바운드S3, Transcribe I/O가 주도적, 병렬 FFmpeg로 CPU 바운드 비중 큼
uploadThumbnailToS3I/O 바운드S3 업로드 I/O가 주도적, FFmpeg는 일부 CPU 바운드
startTranscriptionJobI/O 바운드AWS Transcribe API 호출 I/O
getTranscriptionResultI/O 바운드AWS Transcribe API 폴링, 네트워크 대기 I/O

실제로는 HTTP 커넥션 풀 뿐만 아니라 JDBC 커넥션 풀, JMS로 부터의 요청 등 더 많은 요소들을 고려해야 한다.

따라서 여러 클래스에서 각자의 스레드 풀, 즉 여러 개의 스레드 풀이 존재한다면 각자의 워크로드에 따라 이 수치를 조정해야 한다. 이 경우 CPU 목표 사용률을 공식에 추가해 줄 수 있다.

적정 스레드 개수

리틀의 법칙 (Little`s Law)

적정 스레드 개수를 구하는 공식은 대략적으로라도 알았다. 그렇다면 이러한 쓰레드의 개수가 지연시간이나

처리량(시스템이 처리 가능한 처리량)에 미치는 영향을 계산할 수 있는 방법은 없을까?

리틀의 법칙을 이용한다면 계산할 수 있다.

리틀의 법칙은 MIT 교수 리틀이 제시한 재고 산정 법칙으로 많은 분야에서 사용된다. IT 분야에선 이를 응용해 성능 평가에 많이 사용된다.

IT 분야에서 사용되는 리틀의 법칙을 설명하자면 다음과 같다.

L = λ * W

예를 들어 평균 응답시간이 55ms이고 스레드 풀의 크기가 22라고 하자. 리틀의 법칙을 적용해 시스템이 처리 가능한 평균 처리량을 구하면 다음과 같다.

22/0.055 = 400 -> 시스템이 1초당 처리할 수 있는 요청의 개수는 400개이다.

이처럼 공식을 이용하면 이상적인 스레드 풀의 적정 개수를 구할 수 있지만, 이것은 실제로 모든 프로젝트에 적용시키는 것에 대해서는 무리가 있다. 시스템에 요청이 일정하게 들어오면 좋겠지만, 트래픽이 폭증하는 등 평균 요청 개수는 들쑥날쑥할 수 있다. 따라서 위 공식은 참고용으로 사용하고 많은 테스트를 거쳐 적절한 스레드 풀의 크기를 정하는 것이 중요하다고 하겠다.

그러므로 일단은 스레드 풀의 크기를 4개로 설정한 후, 처리량 및 CPU 사용률을 측정해보자.

만약 10개의 요청이 동시에 들어온다면 어떻게 될까?
먼저 들어온 4개의 요청이 병렬적으로 처리되고 나머지 6개의 요청은 대기 큐에 들어갈 것이다.

앞서 스프링부트에서 동시요청을 어떻게 하는지 알아본 바 있다.

스프링부트가 아닌 내장 서블릿 컨테이너인 톰캣에서 다중 요청을 처리한다.

톰캣 설정 디폴트값:

public Tomcat() {
    this.uriEncoding = StandardCharsets.UTF_8; // 클라이언트 요청 URI를 해석할 때 사용할 문자 인코딩 (기본: UTF-8)
    this.maxConnections = 8192; // 동시에 수립 가능한 최대 커넥션 수
    this.acceptCount = 100; // 요청 대기 큐의 최대 크기 (커넥션이 가득 찬 상태에서 추가 요청이 대기할 수 있는 개수)
    this.processorCache = 200; // 요청을 처리할 프로세서 객체를 캐시할 수 있는 최대 개수
    this.maxKeepAliveRequests = 100; // Keep-Alive 상태에서 하나의 커넥션이 처리할 수 있는 최대 요청 수
    /.../
}

public static class Threads {
    private int max = 200; // 최대 스레드 수 (이 수 이상은 생성되지 않음)
    private int minSpare = 10; // 항상 유지할 여유 스레드 수 (즉시 처리 가능한 상태로 대기)
    private int maxQueueCapacity = Integer.MAX_VALUE; // 요청 처리 대기 큐의 최대 용량 (Integer.MAX_VALUE는 사실상 무제한)
    /.../ 
}

  1. 첫 작업이 들어오면, core size만큼의 스레드를 생성합니다.
  2. 유저 요청(Connection, Server socket에서 accept한 소캣 객체)이 들어올 때마다 작업 큐(queue)에 담아둡니다.
  3. core size의 스레드 중, 유휴상태(idle)인 스레드가 있다면 작업 큐에서 작업을 꺼내 스레드에 작업을 할당하여 작업을 처리합니다.3-1. 만약 유휴상태인 스레드가 없다면, 작업은 작업 큐에서 대기합니다.3-2. 그 상태가 지속되어 작업 큐가 꽉 찬다면, 스레드를 새로 생성합니다.3-3. 3번과정을 반복하다 스레드 최대 사이즈 에 도달하고 작업큐도 꽉 차게 되면, 추가 요청에 대해선 connection-refused 오류를 반환합니다.
  4. 태스크가 완료되면 스레드는 다시 유휴상태로 돌아갑니다.4-1. 작업큐가 비어있고 core size이상의 스레드가 생성되어있다면 스레드를 destory합니다.

현재 설정해둔 값은 다음과 같다.

요청 처리 스레드의 크기가 10이므로 동시 요청 10개를 받을 수 있고 즉시 응답한다. 영상 처리 작업 스레드로 작업을 넘긴 후 완료되면 결과를 전달하고 요청 스레드를 스레드풀에 반환할 것이다.

이제 영상 처리 요청 10개를 동시에 생성하고

  1. 실제 동작 검증
  • 10개 요청이 동시에 들어왔을 때 정말로 4개가 병렬 처리되고 6개가 큐에서 대기하는지
  • 요청 처리 스레드(톰캣)와 영상 처리 스레드 풀이 독립적으로 동작하는지
  1. 성능 및 리소스 모니터링
  • CPU 사용률
  • 각 요청별 처리 시간 측정
  • 대기 중인 작업들의 응답 시간
  1. 실제 사용자 경험
  • 응답 시간이 사용자에게 어떻게 체감되는지

확인해보자.

참고자료:
이상적인 스레드 풀의 적정 크기에 대하여, 스레드 풀 크기 공식, 리틀의 법칙

profile
매일 1퍼센트씩 나아지기 ୧(﹒︠ ̫ ̫̊ ̫﹒︡)୨

0개의 댓글