Huny's Dev Blog
huny.dev

일괄 처리 API의 부분 성공 설계: 결과 미확인과 안전한 재시도 구분하기

일괄 승인·변경 API에서 항목별 성공, 실패, 결과 미확인을 나누는 방법을 정리한다. Promise.allSettled 실행 예제와 조건부 갱신, 멱등 키, 선택적 재시도 및 검증 항목을 살펴본다.

Hun Jang
Hun Jang Nov 13, 2026

독립된 작업 흐름과 한 항목의 재처리를 표현한 생성형 에디토리얼 이미지다. 실제 서비스 화면은 아니다.

관리 화면에서 여러 요청을 한꺼번에 처리할 때, 한 건이 유효하지 않다는 이유로 나머지까지 실패하면 사용자는 같은 선택을 반복해야 한다. 반대로 화면에 “완료”만 표시하면 어떤 항목이 반영됐는지 알 수 없다.

해결책은 배치 요청의 성공과 항목별 처리 결과를 분리하는 것이다. 독립적인 작업이라면 항목마다 처리하고, 성공·실패·결과 미확인을 나눠 반환한다. 재시도도 전체 목록이 아니라 확인된 재시도 대상에만 적용한다.

이 글은 공식 문서를 바탕으로 한 일반적인 API 설계안이다. 특정 서비스의 내부 구현이나 운영 결과를 설명하지 않는다. 아래 JavaScript 예제만 로컬에서 실행했으며, 데이터베이스 동시성이나 외부 API 호출은 실행하지 않았다.

1. HTTP 200은 전체 항목의 성공을 뜻하지 않을 수 있다

Microsoft Graph의 JSON batching은 바깥 응답과 개별 응답의 상태 코드를 구분한다. 배치 응답이 200이어도 내부 요청은 실패할 수 있고, 응답 순서도 요청 순서와 다를 수 있어 id로 연결해야 한다. 이는 Graph의 계약이며 모든 API가 자동으로 같은 규칙을 갖는 것은 아니다. 공식 JSON batching 문서

자체 API에서도 이 구분을 명시하면 된다. 먼저 JSON 형식, 인증, 허용된 배치 크기처럼 요청 전체에 적용할 검증을 한다. 이를 통과한 뒤 항목별 권한과 현재 상태를 확인한다.

항목별 결과는 다음처럼 설계할 수 있다. 아래 값과 필드는 이 글의 예제 계약이며 표준이나 실제 호출 결과가 아니다.

여기서 unknown은 중요한 별도 상태다. 응답을 받지 못했다고 서버가 아무것도 하지 않았다는 뜻은 아니다. 커밋 후 응답 전달이 끊겼을 수도 있다. 실패로 단정해 다시 실행하지 말고, 작업 식별자로 결과를 조회하거나 같은 작업 키를 사용하는 멱등 처리를 거쳐야 한다.

2. 배치가 아니라 항목을 원자적 처리 단위로 잡는다

이 방식은 서로 독립적인 요청에 적합하다. 송금처럼 두 변경이 반드시 함께 성공해야 하는 업무를 부분 성공으로 바꾸면 안 된다. 먼저 업무의 불변 조건을 확인한다.

독립적인 승인 업무라면 한 항목 안에서 현재 상태 확인, 권한 변경, 처리 결과와 감사 기록 저장을 하나의 트랜잭션으로 묶는다. 한 항목의 실패가 다른 항목의 커밋을 되돌리지 않도록 트랜잭션 범위를 정한다. 배치 전체가 끝나기 전에 일부 항목은 이미 반영될 수 있다는 점도 API 계약에 적는다.

조회한 뒤 무조건 UPDATE하면 두 관리자의 동시 요청을 놓칠 수 있다. 아래는 상태와 버전 조건을 함께 검사하는 예시다.

반환 행이 없으면 성공으로 세지 않는다. 테넌트 범위, 권한, 상태 변경을 구분하는 서버 로직이 필요하며, 접근 권한이 없는 사용자에게 자원 존재 여부나 내부 상태를 노출하지 않는다. 이 SQL 하나만으로 인가·실제 권한 부여·감사 기록이 완성되는 것은 아니다.

PostgreSQL의 Read Committed에서는 동시 UPDATE가 먼저 끝난 뒤 갱신된 행에 WHERE 조건을 다시 평가한다. 위 조건부 갱신을 이해하는 근거가 되지만, 여러 행에 걸친 복잡한 규칙까지 이 예제만으로 보호한다고 보면 안 된다. PostgreSQL 18 트랜잭션 격리 문서

3. Promise.allSettled는 결과 수집 도구일 뿐이다

JavaScript의 Promise.allSettled는 모든 입력 Promise가 정착한 뒤 결과를 모은다. 반환 배열은 완료 순서가 아니라 입력 순서에 대응한다. 한 작업의 rejection 때문에 다른 항목의 결과 수집을 포기하지 않을 때 유용하다. MDN 문서

다만 fulfilled는 업무 성공과 다르다. 함수가 실패 응답 객체를 정상 반환해도 Promise는 fulfilled다. HTTP 요청을 쓰는 코드라면 HTTP 상태와 업무 응답 본문을 별도로 검사해야 한다.

다음 코드를 batch-demo.mjs로 저장한 뒤 node batch-demo.mjs로 실행할 수 있다. 외부 요청이나 저장소 변경 없이 결과 분류만 확인한다.

Node.js v24.22.1에서 실행한 결과 succeeded=1, failed=1, unknown=1이었고 두 assertion이 통과했다. 이는 분류 코드의 실행 결과일 뿐, 서버의 원자성·멱등성·경쟁 상태를 검증한 결과는 아니다.

실서비스에서 수천 개의 작업을 한 번에 map으로 시작하면 연결 풀과 외부 API 한도를 소모할 수 있다. 배치 크기와 동시 실행 수를 별도로 제한한다. allSettled 자체에는 동시성 제한이나 타임아웃 기능이 없다. 작업 하나가 끝나지 않으면 전체 수집도 끝나지 않으므로 작업별 제한 시간과 결과 미확인 정책이 필요하다.

4. 실패 사유에 따라 재시도를 다르게 한다

권한 없음, 이미 변경된 상태, 잘못된 입력은 같은 요청을 그대로 보내도 해결되지 않는다. 화면에서 원인을 보여주고 수정·새로고침을 유도한다.

반면 일시적인 요청 제한은 기다렸다가 재시도할 수 있다. Microsoft Graph는 배치 내부의 제한된 요청을 각각 429로 반환하며, 개별 응답의 retry-after를 바탕으로 실패한 요청을 다시 보내도록 안내한다. 배치에 포함된 제한 오류가 SDK에서 자동 재시도된다고 가정하면 안 된다. 공식 throttling 안내

자체 쓰기 API라면 재시도 전에 중복 실행 방지도 설계해야 한다.

  • 배치 ID와 항목 작업 키를 구분한다. 새 배치로 묶어도 같은 작업의 키는 유지한다.
  • 작업 키를 사용자·테넌트·동작 및 요청 내용과 묶어 저장한다. 다른 내용으로 키를 재사용하면 거부한다.
  • 결과 저장과 실제 상태 변경의 원자성을 확보한다. 동시 중복 요청도 유일성 제약 등으로 막는다.
  • 키 보존 기간과 만료 뒤 동작을 문서화한다. 메모리 캐시만으로 영구 중복 방지를 주장하지 않는다.
  • 알림 같은 외부 부수 효과는 DB 커밋과 별도로 실패할 수 있다. 필요하면 outbox와 소비자 측 중복 방지를 함께 설계한다.

이는 이 글의 설계 권고다. allSettled나 배치 형식을 쓴다고 자동으로 제공되는 기능이 아니다.

5. 완료 화면과 테스트도 부분 성공을 기준으로 만든다

완료 화면에는 “성공 8건, 실패 1건, 결과 확인 중 1건”처럼 표시한다. 성공 항목은 선택에서 빼고, 실패 사유는 해당 행에 붙인다. 결과 미확인은 재조회 대상으로 남긴다. 단순한 “실패 항목 전체 재실행” 버튼보다, 실제로 안전한 재시도 대상만 선택하는 동작이 낫다.

배포 전에는 다음 조건을 검증한다.

  1. 유효한 항목 사이에 잘못된 항목을 끼워도 나머지가 처리되는가.
  1. 같은 항목을 동시에 승인·거절하면 한 번만 상태가 바뀌는가.
  1. 커밋 직후 응답 연결을 끊어도 재조회와 같은 작업 키의 재요청이 중복 효과를 만들지 않는가.
  1. 일부 결과가 누락되거나 순서가 바뀌어도 id로 정확히 연결하는가.
  1. 권한 없는 항목의 오류가 자원 정보나 다른 사용자의 정보를 드러내지 않는가.
  1. 제한 오류, 영구 실패, 결과 미확인을 화면과 재시도 정책에서 구분하는가.

일괄 처리는 버튼을 한 번 누르는 편의를 제공하지만, 처리 결과까지 하나의 불리언으로 줄일 필요는 없다. 항목별 결과와 재시도 경계를 명확히 해야 사용자는 이미 성공한 작업을 반복하지 않고 남은 문제만 해결할 수 있다.

추천 글