Language/JS(Node.js)

[Node.js] 비동기 프로그래밍 깊게 파기: Promise부터 동시성 제어까지

Joonfluence 2026. 3. 2.

[Node.js] 비동기 프로그래밍 깊게 파기: Promise부터 동시성 제어까지

1. Promise의 내부 상태와 에러 전파의 원리

Promise는 단순한 콜백 헬 해결사가 아닙니다. 이는 비동기 작업의 상태를 관리하는 객체입니다.

  • 3가지 상태: Pending, Fulfilled, Rejected. 한 번 결정된 상태는 바뀌지 않습니다(Immutability).
  • 에러 버블링: .then() 체인 내부에서 발생한 에러는 중간에 .catch()가 없다면 계속 뒤로 전파됩니다.
  • Unhandled Rejection: 최신 Node.js 환경에서는 .catch()하지 않은 에러가 발생하면 프로세스가 종료될 수 있습니다. 반드시 최종 단계에서 예외 처리가 필요합니다.

2. async/await: 문법적 설탕 그 이상의 가치

async/await는 비동기 코드를 동기 코드처럼 읽게 해주지만, 내부적으로는 Generator와 Promise의 조합으로 동작합니다.

  • Pause & Resume: await 키워드를 만나면 해당 함수의 실행 컨텍스트는 일시 중단(Suspend)되고, 제어권이 이벤트 루프로 돌아갑니다. 비동기 작업이 완료되면 다시 스택에 올라와 재개(Resume)됩니다.
  • 병렬 처리의 중요성: 무분별한 await 사용은 성능 저하를 일으킵니다. 서로 의존성이 없는 작업은 Promise.all을 통해 병렬로 처리해야 합니다.
패턴 실행 방식 소요 시간 (각 3초 작업 시)
Sequential await A; await B; 6초 (직렬)
Parallel Promise.all([A, B]) 3초 (병렬)

3. 실무형 에러 핸들링: 타임아웃 패턴

비동기 작업(API 호출, DB 쿼리)은 무한정 기다릴 수 없습니다. Promise.race를 활용하면 자원을 효율적으로 관리할 수 있습니다.

// Promise.race를 이용한 타임아웃 유틸리티
function withTimeout(promise, ms) {
  const timeout = new Promise((_, reject) =>
    setTimeout(() => reject(new Error('Timeout')), ms)
  );
  return Promise.race([promise, timeout]);
}

Why? 외부 시스템의 장애가 내 서버의 이벤트 루프 지연으로 이어지는 것을 방지하는 최소한의 안전장치입니다.


4. Node.js에서의 동시성(Concurrency)과 Race Condition

많은 개발자가 "Node.js는 싱글 스레드니까 Race Condition이 없다"고 오해합니다. 하지만 논리적 시점에 의한 문제는 발생합니다.

  • 문제 상황: 데이터 조회(await)와 수정(await) 사이에 다른 요청이 들어와 데이터를 변경하는 경우.
  • 해결 방안:
  1. Application Level: p-limit 같은 라이브러리로 동시 실행 수 제한.
  2. Database Level: 비관적 락(Pessimistic Lock) 또는 낙관적 락(Optimistic Lock) 사용.

5. Summary: 주간 회고 (시나리오 문제)

Q: Promise.all 중 일부가 실패해도 나머지를 살리려면?

  • 방법 1: Promise.allSettled를 사용하여 모든 결과를 수집 후 필터링. (권장)
  • 방법 2: 개별 Promise에 .catch()를 붙여 에러를 null이나 특정 값으로 치환.

다음 단계 (Week 3 예고)

다음 주에는 이러한 비동기 객체들이 메모리 상에서 어떻게 관리되는지, V8 엔진의 Garbage CollectionTypeScript의 고급 타입 시스템을 연결하여 학습할 예정입니다.

반응형

댓글