기본으로 돌아가자_동시실행제한큐

안승찬·2026년 6월 1일

이번에는 시스템 모듈에 많이 사용되는 큐 방식에 대해서 구현을 해볼려고 합니다.

────
결제 시스템, 대량 알림 발송, 이미지 업로드 등에서
"동시에 N개만 실행하고 나머지는 순서대로 대기"가 필요합니다.
Promise.all은 모두 동시에 터뜨리기 때문에 서버 부하·Rate Limit에
걸릴 수 있습니다.

목표
────
ConcurrentQueue 클래스를 완성하세요.

스펙
────
constructor({ concurrency })
	- concurrency: 동시에 실행할 최대 작업 수 (기본값 1)

add(task) → Promise
  - task: () => Promise 형태의 함수
  - 실행 슬롯이 남아 있으면 즉시 실행, 아니면 대기열에 추가
  - task가 resolve/reject 되면 그 값을 그대로 전파
  - 한 task가 reject 되어도 다른 task 실행에 영향을 주지 않음

pause()
  - 대기 중인 작업의 시작을 멈춤
  - 이미 실행 중인 작업은 끝까지 실행됨

resume()
  - pause 해제, 대기 중인 작업을 concurrency에 맞게 재개

clear()
  - 대기 중인 작업을 모두 제거
  - 이미 실행 중인 작업은 영향 없음
  - 제거된 작업의 Promise는 reject({ reason: 'cleared' }) 됨

 get metrics()
  - { running, queued, completed, failed } 반환

먼저 구조를 잡아 봅시다.
생성자로는 작업을 담을 queue와 동시에 작업할 수 있는 concurrency가 먼저 생각났습니다.

class ConcurrentQueue {
    constructor({concurrency = 1}={}){
      this.queue:[];
      this.concurrency=concurrency;
    }
  
  	add(task){
      
    }
  
  	pause() {

	}

	resume() {

    }

    clear() {
      
    }

	get metrics() {
    
	}
  
  
}

그 다음 요구사항 위에서 부터 구현해 보도록 하겠습니다.

...
constructor({concurrency = 1}={}){
      this.queue = [];
      this.concurrency=concurrency;
	  this.runningCount=0;//실제 실행되고 있는 작업 수
    }


add(task){
	if (typeof task !== "function")  throw new TypeError("task must be a function");
  //task: () => Promise 형태
  return new Promise((resolve,reject)=>{
    //task와 실행결과를 큐에 담는다
    this.queue.push({task,resolve,reject});
     nextTask();
  })
}
  
nextTask(){
    // 실행 가능한 작업 수를 넘겼거나 실제 작업 큐가 비었을 경우
    if(this.runningCount >= this.concurrency || this.queue.length===0) return;
    
     // 맨 첫번째 대기열 작업 
	 const next = this.queue.shift();
     this.runningCount++;
     
     next.task().then((result)=>{
       //작업 성공
       next.resolve(result)
     }).catch((error)=>{
       //작업 실패
       next.reject(error)
     }).finally(()=>{
       //작업 테스크 카운트 감소
       this.runningCount--;
       //다음 작업 진행
       this.nextTask();
     })
}

순환 구조를 잡는 부분 제외하고는 나머지부분은 어렵지 않게 구현 했던 것 같습니다.

그 다음으로 나머지 함수들도 만들어 봅시다.

 constructor({concurrency = 1}={}){
      this.queue = [];
      this.concurrency=concurrency;
	  this.runningCount=0;
  	  this.isPaused=false; // 일시정지 상태 여부
    }

 	...

      nextTask(){
      // 중지 상태 이면 다음 작업 진행 X
      if(this.isPaused) return;
      if(this.runningCount >= this.concurrency || this.queue.length===0) return;

       const next = this.queue.shift();
       this.runningCount++;

       next.task().then((result)=>{
         next.resolve(result)
       }).catch((error)=>{
         next.reject(error)
       }).finally(()=>{
         this.runningCount--;
         this.nextTask();
       })
  }

	pause() {
	   this.isPaused =true;
	}

	resume() {
	   this.isPaused =false;
	   this.nextTask();
    }

    clear() {
       while(this.queue.length>0){
         const item = this.queue.shift();
         // 모두 실패 처리
         item.rejected({reason : "cleared"})
       }
      
       // 큐 초기화
       this.queue=[];      
    }

검증

 const queue = new ConcurrentQueue({
    concurrency: 2
  });
  
  const task = (name, delay) => () =>
    new Promise((resolve) => {
      console.log("start", name);
  
      setTimeout(() => {
        console.log("end", name);
        resolve(name);
      }, delay);
    });
  
  queue.add(task("A", 1000));
  queue.add(task("B", 1000));
  queue.add(task("C", 1000));
  queue.add(task("D", 1000));


---console---
start A
start B
end A
start C
end B
start D
end C
end D

로그까지 잘 찍히는 걸 볼수있다!

✍️ 구현하면서 배운 점

이번 ConcurrentQueue를 구현하면서 단순히 “비동기 작업을 순서대로 실행하는 구조”가 아니라, 동시성 제어(concurrency control)가 어떻게 시스템 안정성과 직결되는지를 체감할 수 있었습니다.

1. “동시성 제한”은 큐 + 카운터로 해결된다

처음에는 복잡한 스케줄러가 필요할 것 같았지만, 핵심은 단순했습니다.

queue: 대기 중인 작업 저장
runningCount: 현재 실행 중인 작업 수
concurrency: 동시에 실행 가능한 최대 개수

이 3가지 조합만으로도 충분히 “동시 실행 제한”이 가능했습니다.

결국 중요한 건 복잡한 로직이 아니라,현재 상태를 정확히 추적하는 변수 설계였습니다.

2. 핵심은 “재귀적인 다음 실행 구조”

가장 중요한 설계 포인트는 nextTask()였습니다.

작업 종료 → runningCount 감소 → 다음 작업 실행

이 구조는 단순한 반복문이 아니라,비동기 기반의 자기 호출 루프 구조입니다.

이 덕분에 작업이 끝날 때마다 자연스럽게 다음 작업이 이어집니다.

0개의 댓글