28조각 중 하나만 실패해도 전부 버리던 전사 작업, 부분 성공과 구간 재시도로 바꿨습니다
사내 문서 지식그래프 검색 도구에 회의 녹음 전사를 붙였습니다. 회의록 화면에서 녹음 파일을 올리면 뒤에서 전사가 진행되고, 요약이 회의록 본문이 되고, 전사문은 색인돼 검색과 채팅의 근거가 됩니다.
긴 회의는 한 번에 보낼 수 없어서 5분 단위로 잘라 병렬로 전사합니다. 131분짜리 회의면 **28조각**입니다. 문제는 이 28조각을 하나의 트랜잭션처럼 묶어 뒀다는 점이었습니다.
조각 하나가 시간초과(504)로 실패하면 문서 전체가 실패로 끝났습니다. 이미 성공한 나머지 27조각의 전사 비용도 같이 잃었습니다. 전날 그렇게 잃은 비용은 131분 회의를 처음부터 한 번 전사하는 비용의 두 배 가까이 됐습니다.
조각당 3%가 전체 57%가 되는 산수
조각 하나가 실패할 확률을 3%로 잡아 보겠습니다. 28조각이 전부 성공할 확률은 0.97을 28번 곱한 값입니다. 그러면 '하나라도 실패'할 확률은 절반을 넘습니다.
전부 성공 = (1 − p)^n = 0.97^28 ≈ 0.43
하나라도 실패 = 1 − 0.43 ≈ 57%조각당 실패율은 낮아 보였습니다. 그런데 조각 수가 늘면 전체 성공 확률은 지수로 떨어집니다. 회의가 길어질수록 '전부 성공'은 사실상 기대할 수 없는 결과가 됩니다.
그래서 분할 단위는 성능 설정이 아니라 **성공률 설정**이라고 보게 됐습니다. 조각 수를 늘려 빠르게 처리하기로 했다면, 한 번에 전부 성공하는 결과는 포기한 것과 같습니다.

전부 아니면 전무, 아니면 부분 성공
선택지는 둘이었습니다.
전부 아니면 전무 | 부분 성공 | |
|---|---|---|
조각 하나 실패 시 | 문서 전체 실패 | 실패 구간에 표시를 남기고 나머지 저장 |
재시도 범위 | 전체를 다시 전사 | 실패한 구간만 |
색인·검색이 보는 것 | 완성된 문서만 | 구멍이 표시된 문서 |
성공률 | 조각 수에 지수로 묶임 | 조각 단위로 따로 정해짐 |
첫째 방식은 구현이 단순하고, 색인과 검색이 반쪽짜리 문서를 볼 일이 없습니다. 대신 재시도할 때마다 전체 비용이 다시 나갑니다.
둘째 방식은 받는 쪽이 구멍을 견뎌야 합니다. 그래도 사람이 읽는 전사문은 5분이 비어도 나머지 126분이 그대로 쓸모 있습니다. 버리는 것과 살리는 것이 **27:1**이라 오래 고민할 비교가 아니었습니다.
함께 넣은 네 가지
**부분 성공** — 실패한 조각 자리에 구간이 적힌 표시를 남기고 나머지를 살린다
**동시 실행 5 → 3, 재시도 간격에 무작위 편차(지터)** — 한꺼번에 실패하고 한꺼번에 재시도하는 모양을 흩는다
**원본을 전사 전에 저장** — 문서 id 로 전사해 큰 파일을 한 번만 보낸다
**구간 재시도** — 본문의 실패 표시를 읽어 그 구간만 다시 전사한다
첫째는 실패 자리에 `[전사 실패: 15:00~20:00]` 같은 표시를 남기는 것입니다. 사람이 읽을 때도 어디가 비었는지 바로 보입니다.
둘째는 실패의 실제 모양을 보고 정했습니다. 실패는 흩어져 나지 않았습니다. 동시 실행이 높으면 상대 서비스에 같은 순간 요청이 몰렸고, 실패한 요청들이 또 같은 순간에 다시 몰려갔습니다. 그래서 동시 실행을 3으로 낮추고, 재시도 간격에 무작위 편차(지터)를 넣었습니다.
셋째는 재시도 비용을 줄이려는 순서 조정입니다. 원래는 녹음 파일을 들고 바로 전사에 들어갔습니다. 원본을 먼저 저장하고 그 문서를 가리키는 id 로 전사하도록 바꾸니, 64MB 파일은 한 번만 가고 그 뒤로는 id 만 오갑니다.
넷째가 이 설계의 마무리입니다. 재시도할 구간을 별도 작업 큐에 적지 않고, 본문에 이미 남긴 실패 표시를 읽어 그 구간만 다시 전사합니다. 결과물이 곧 할 일 목록이 됩니다. 큐를 따로 두면 본문과 큐가 서로 다른 말을 할 자리가 생기는데, 그 자리가 처음부터 없습니다.
비용 차이도 큽니다. 구간 하나만 다시 전사하는 비용은 회의 전체를 다시 전사하는 비용의 **25분의 1도 안 됩니다.**

이번에 난 실패는 시간초과(504)였습니다. 시간초과나 한도 초과(429)는 상대 서비스 쪽 사정이라 우리 코드로 없앨 수 없습니다. 그래서 원인을 없애는 대신 실패가 났을 때 잃는 범위를 줄이는 쪽을 골랐습니다.
33초에 끝났지만, 실패 경로는 아직 실행되지 않았습니다
22분 12초짜리 녹음(10.9MB)으로 처음부터 끝까지 돌려 봤습니다.
업로드 0.1초
전사 24초
요약 9초
합계 **33초**
전사문 8,801자 → 요약 본문 724자
다만 정직하게 적어 둡니다. 이 실행에서는 실패한 조각이 0개였습니다. 그래서 부분 성공과 구간 재시도는 **실제로 한 번도 실행되지 않았습니다.** 코드에 넣은 것과 실제로 도는 것은 다른 이야기입니다.
실패 처리 코드는 실패가 나야 실행됩니다. 잘 도는 날에는 영영 실행되지 않습니다. 실패를 일부러 주입해 한 번은 그 경로를 지나가게 하는 일이 아직 남아 있습니다.
비용 표에는 단위를 같이 적습니다
같이 정리한 것이 둘 있습니다. 하나는 과금이 한 회의 안에서 갈린다는 점입니다. 오디오를 받아 주는 제공자가 하나뿐이라 전사는 API 크레딧이 나갑니다. 요약은 텍스트라 구독 요금제 안에서 처리해 추가 비용이 없습니다. 관리 화면의 단가표에 이 차이가 보이도록 항목을 하나 더 뒀습니다.
다른 하나는 단가표의 단위입니다. 전사 단가를 '1건에 1회'로 적지 않았습니다. 131분 회의 한 건은 실제로 28번 호출되기 때문입니다. 그렇게 적었다면 보는 사람이 비용을 한 자릿수 이상 작게 읽었을 겁니다.

부분 성공이 맞지 않는 자리
**반쪽 결과가 틀린 답인 업무** — 정산·이체·재고는 일부만 반영되면 불완전한 답이 아니라 틀린 답이 됩니다. 이런 곳은 전부 아니면 전무가 맞고, 조각 수를 줄이거나 조각마다 중복 실행에 안전한 커밋을 설계하는 쪽으로 갑니다
**구멍이 사람 눈에만 보일 때** — 실패 표시가 색인과 검색 근거에 담기지 않으면 '그 회의에 그 이야기가 없었다'는 조용한 오답이 나옵니다. 받는 쪽까지 '여기 구멍이 있다'를 전달해야 부분 성공이 완성됩니다
**관측 없이 동시 실행을 낮출 때** — 5에서 3으로 낮춘 것은 처리량을 깎은 결정입니다. 실패가 몰린다는 관측이 있을 때만 맞고, 상대 서비스가 여유로운 구간에서는 그냥 느려질 뿐입니다
쪼개서 병렬로 돌리기 전에 볼 것
조각 수와 조각당 실패율로 '전부 성공' 확률을 한 번 계산해 봤는가
실패한 조각 자리에 구간을 적어, 결과물만 보고도 다시 할 일을 알 수 있는가
재시도할 때 큰 입력을 다시 보내지 않도록 원본 저장이 앞에 와 있는가
실패 표시가 사람 화면뿐 아니라 색인·검색까지 전달되는가
실패 경로를 일부러 한 번 실행해 봤는가
이번 변경은 큰 기술이 아니었습니다. 0.97을 28번 곱한 한 줄이 설계를 바꿨습니다. 쪼개는 순간 전부 성공을 목표로 둘 수 없으니, 실패한 자리를 결과물에 남기고 그 자리만 다시 하는 구조로 옮겼습니다. 남은 일은 그 실패 경로를 일부러 한 번 지나가게 하는 것입니다.