maxDuration 300초 안에 AI를 네 번 부르기 — 앞 단계로 뒤 단계를 예측하려다 틀린 이야기

미리 3줄로 보면
  • 증상: 서버리스 함수 안에서 AI를 여러 번 부르는데, 천장(300초)을 넘으면 이미 완성된 결과물까지 통째로 사라집니다
  • 원인: *"뒤 단계는 앞 단계에 비례할 것"*이라 보고 예산을 잡았는데, 비례한다는 근거가 없었습니다(표본 13에서 상관 −0.30). 재보지도 않고 상수로 박았던 거예요
  • 해결: 비례를 버리고 절대값으로 예약. 안전은 그대로인데 보정 승인이 10건 중 6건 → 9건으로 늘었습니다
딱따구리 선생님이 모래시계 두 개를 양손에 들고 견주어 보는데, 남은 모래의 양이 서로 딴판이라 갸웃하는 모습
딱따구리 선생님이 모래시계 두 개를 양손에 들고 견주어 보는데, 남은 모래의 양이 서로 딴판이라 갸웃하는 모습 (AI 생성 삽화)

혹시 이런 상황인가요

웹사이트에서 버튼 하나만 누르면 AI가 뭔가 만들어주는 기능을 떠올려 보세요. 그 버튼 뒤에서 도는 게 오늘의 무대입니다.

서버리스 함수가 뭔가요

서버를 24시간 켜두는 대신 요청이 올 때만 잠깐 깨어나 일하고 꺼지는 방식입니다. 대신 조건이 하나 붙습니다 — 정해진 시간 안에 끝내야 합니다.

코인 노래방을 생각하시면 됩니다. 동전을 넣으면 정해진 시간만 돌고, 시간이 다 되면 노래 중간이어도 꺼져요. 부르던 노래가 어디 저장되지도 않고요.

제 경우 그 함수 안에서 AI를 네 번 부르고, 상한은 제가 300초로 설정해 뒀습니다. 처음엔 한 번만 불렀는데 검토하고 고치는 단계를 붙였거든요.

사용자가 버튼을 누르면 서버리스 함수가 깨어나 AI를 네 번 부르고, 300초를 넘기면 함수가 통째로 꺼지면서 만든 결과물까지 사라지는 흐름
사용자가 버튼을 누르면 서버리스 함수가 깨어나 AI를 네 번 부르고, 300초를 넘기면 함수가 통째로 꺼지면서 만든 결과물까지 사라지는 흐름

네 번을 다 합쳐서 300초 안에 끝나야 합니다. 그런데 다른 경로의 초기 관측(표본 2)에서 총 소요 상단이 332초로 천장을 넘긴 적이 있어요.

넘기면 함수가 강제 종료됩니다. 여기서 진짜 문제는 느린 게 아니에요. 이미 다 만들어 놓은 결과물까지 같이 사라진다는 것입니다.

저도 똑같이 당했습니다

제 코드에는 이런 안전장치가 있었습니다.

보정 루프가 실패하면 → 원본 결과를 그대로 유지 → 결과는 항상 돌려준다

잘 짠 것 같죠. 그런데 함수 자체가 강제 종료되면 이 안전장치도 같이 죽습니다. catch로 잡히는 실패가 아니에요 — 플랫폼이 프로세스를 내려버려서 그 아래 코드는 실행될 기회가 없습니다.

그러니까 시간 예산 관리가 곧 안전장치였습니다.

다만 순서를 미리 말해두면, 소실을 실제로 막아준 건 호출마다 건 하드 타임아웃입니다(뒤에 나옵니다). 지금부터 이야기할 예산 모델은 안 막아도 될 작업을 막고 있었습니다.

예산을 잘못 모델링했습니다 — 앞 단계로 뒤 단계를 예측하려 했어요

처음엔 비례로 잡았습니다

네 번의 호출은 이렇게 이어집니다.

1차 생성  →  평가  →  보정  →  재평가

예산을 짜려면 "보정이 얼마나 걸릴지"를 미리 알아야 합니다. 아직 시작도 안 한 작업인데요. 그래서 저는 이렇게 가정했습니다.

보정은 1차 생성과 비슷한 일이니까, 1차 생성 시간에 비례하겠지

그리고 관측 두 건에서 나온 최악 비율 1.42배를 상수로 박았습니다. *"1차 생성이 100초 걸렸으면 보정은 최대 142초로 잡는다"*는 식이죠.

표본을 열 건으로 늘렸더니 뒤집혔습니다

근거가 두 건뿐인 게 마음에 걸려서 "표본을 열 건으로 늘려 다시 재보자"고 시켰습니다. 결과가 이랬어요.

1차 생성 ↔ 보정 상관계수   r = −0.44  (표본 10)
                           r = −0.30  (표본 13, 더 넓힌 뒤)

음수입니다. 다만 표본이 열셋이라 *"반대로 움직인다"*고까지 말할 수는 없어요 — 비례한다는 근거가 없다까지가 이 숫자가 말하는 전부입니다. 극단만 뽑아 보면 이렇습니다.

런 1차 생성 보정 보정/생성
C 182.2초(최장) 47.1초 0.26
K 86.2초 33.8초(최단) 0.39
G 63.7초 88.4초 1.39
H 66.7초 100.6초 1.51
I 92.2초 106.8초 1.16

가장 오래 걸린 1차 생성이 짧은 축의 보정으로 이어졌고, 가장 짧은 보정은 중간 길이의 생성에서 나왔습니다. 어느 방향으로도 규칙이 없어요.

관측 최악을 상수로 쓰는 관례대로 1.42를 1.51로 올렸습니다. 그런데 상수 하나만 올리니 앞 가드가 통과시킨 요청을 뒤 가드가 상시 차단하게 됐어요. 그래서 진입 데드라인도 95초에서 91초로 같이 내려야 했습니다.

하나를 만지면 다른 하나가 따라오는 이 결합이 뒤에 나올 ②절의 복선입니다.

왜 비례하지 않나요

이 표본으로는 원인을 특정하지 못했습니다. 확인된 건 둘입니다 — 같은 입력으로 돌려도 1.7배까지 벌어졌고(그래서 입력 크기로는 설명이 안 됩니다), 시간대에 따라 뚜렷이 갈렸습니다(오전 6371초 · 심야 86115초 · 새벽 78~182초). 오전과 새벽은 구간이 겹치지도 않았어요.

비례 모델은 양방향으로 틀렸습니다

모델 자체가 틀렸다는 건 제가 아니라 검수를 맡긴 AI가 짚었습니다. 제가 한 건 그 지적을 받고 "그럼 재보자"고 결정한 것뿐이에요. 그리고 이 모델이 나쁜 건 *"가끔 틀린다"*가 아니었습니다 — 양쪽 끝에서 다 틀립니다.

1차 생성이 느리면 과하게 예약해 보정을 전부 막고, 빠르면 적게 예약해 실측 최대에 못 미치는 비례 모델의 양방향 오류
1차 생성이 느리면 과하게 예약해 보정을 전부 막고, 빠르면 적게 예약해 실측 최대에 못 미치는 비례 모델의 양방향 오류
  • 1차 생성이 느릴 때 — 비례로 계산하니 예약이 커집니다. 그래서 *"시간 없다"*며 보정을 통째로 차단해요. 그런데 실제 보정은 47초면 끝났을 일이었습니다
  • 1차 생성이 빠를 때 — 예약이 작아집니다. 그런데 실측 최대 보정은 111초예요. 예약이 실제를 못 덮습니다

여기에 더 고약한 게 있습니다. *"관측된 최악 비율"*이라는 숫자 자체가 1차 생성이 빠를수록 커지는 불안정한 값입니다. 분모가 작아지니까요. 그리고 그렇게 부풀려진 비율이 1차 생성이 느린 요청의 예산을 깎습니다. 인과가 뒤집혀 있었어요.

이렇게 고쳤습니다

① 비례를 버리고 절대값으로

"1차 생성의 몇 배" 대신 관측 최악 보정 소요를 그대로 예약했습니다.

예약 = 111초 (실측 최악 보정 소요)

승인 조건:  현재까지 경과 + 111초 + 재평가·마무리 30초 ≤ 285초
        →  현재 경과가 144초 이하면 보정을 넣는다

결과가 이랬습니다.

예산 모델 10건 중 승인
비례 1.51배 + 진입 가드 6건
절대 111초 🟢 9건

네 건이 차단되고 있었는데 그중 세 건이 풀렸습니다. 그 세 건의 실제 총 소요는 223.5 · 213.3 · 240.1초로 전부 285초 안이었어요. 완주할 수 있는 작업을 막고 있었던 겁니다.

표본을 13건으로 넓힌 뒤에도 12건이 승인됐습니다. 차단된 하나는 새벽에 나온 극단값(1차 생성 182.2초)이었어요.

안전은 그대로입니다 — 최악 경로가 여전히 정확히 285초예요(경과 144 + 보정 111 + 재평가 25 + 마무리 5).

② 가드를 둘에서 하나로

앞에서 본 그 결합입니다. 판정하는 곳이 두 군데였어요 — 루프에 들어갈 때 한 번, 보정 직전에 한 번.

판정 지점이 둘이면 상수끼리 결합이 생기고, 하나로 줄이면 그 결합이 사라진다
판정 지점이 둘이면 상수끼리 결합이 생기고, 하나로 줄이면 그 결합이 사라진다

들여다보니 진입 판정은 아무것도 절약하지 않고 있었습니다. 보정만 막을 뿐 평가 호출은 어차피 나갔고, 그 판정은 명목 25초라는 가정 위에 서 있었어요. 뒤쪽 판정은 실제 경과 시간으로 같은 결정을 더 정확하게 하고 있었고요.

더 이른 시점에, 더 나쁜 정보로, 같은 판정을 하고 있었습니다. 그래서 지웠습니다.

③ 호출마다 하드 타임아웃 — 잔여 예산 기준으로

각 호출에 타임아웃을 걸되 고정 상수가 아니라 남은 예산에서 역산합니다.

천장            300초
안전 마진        15초
────────────────────
쓸 수 있는 예산  285초

평가 호출 : 남은 예산에서 마무리 몫을 뺀 만큼, 최대 60초
보정 호출 : 남은 예산에서 재평가 몫과 마무리 몫을 뺀 만큼, 최대 150초

어느 호출이 느려지면 그 뒤 호출의 타임아웃이 자동으로 줄어듭니다. 천장을 보장하는 건 예산 예측이 아니라 이 클램프예요 — 예약이 빗나가도 보정만 중단되고 원본은 남습니다.

🔴 그런데 결과물을 낳는 호출에는 타임아웃을 안 걸었습니다

1차 생성에는 타임아웃이 없습니다. 일부러요.

이유는 목적을 되짚으면 나옵니다.

함수가 강제 종료돼서 결과물이 통째로 사라지는 것을 막으려고 건다

그런데 1차 생성을 중간에 끊으면 결과물이 통째로 없어집니다. 막으려던 그 결과와 똑같아요.

호출을 끊었을 때 덜 좋은 결과라도 남으면 타임아웃을 걸고, 아무것도 안 남으면 걸지 않는다는 갈림
호출을 끊었을 때 덜 좋은 결과라도 남으면 타임아웃을 걸고, 아무것도 안 남으면 걸지 않는다는 갈림
판별 기준은 길이가 아닙니다

끊었을 때 "덜 좋은 결과"가 남으면 걸고, "아무 결과도 없으면" 걸지 않습니다.

  • 평가·보정·재평가 → 품질을 올리는 단계입니다. 끊겨도 그 전 결과물이 남아요 → 건다
  • 1차 생성 → 이게 결과물 자체입니다. 끊기면 남는 게 없어요 → 걸지 않는다

🔴 *"가장 오래 걸리는 호출"*이 기준이 아닙니다. 제 실측에서도 보정이 1차 생성보다 긴 런이 표에 실린 것만 세 건 있었어요.

같은 이유로 실패 처리도 갈랐습니다. 보정 실패는 삼키고 최선본을 돌려주지만, 1차 평가 실패는 거부하고 원본으로 되돌립니다. 평가 결과가 없으면 *"둘 중 나은 것"*을 고를 수가 없거든요.

그래도 남는 구멍 하나

1차 생성이 혼자 천장을 다 먹으면 어떻게 되나요? 막을 방법이 없습니다. 뒤 단계를 다 건너뛰어도 1차 생성 하나로 300초를 넘기면 결과물은 사라집니다.

바깥에 데드라인을 하나 더 두긴 합니다. 경과가 255초를 넘으면 평가부터 생략하고 원본을 돌려줘요. 다만 이건 1차 생성이 그 안에 끝났을 때 뒤를 잘라주는 장치지, 1차 생성 자체가 천장을 먹는 경우는 못 막습니다. 그 구간은 지금도 열려 있습니다.

슬랙이 넉넉하지 않습니다

제 표본에서 1차 생성 최댓값은 182.2초였습니다. 그런데 이건 한쪽 경로 얘기고, 다른 경로에서는 45~261초가 관측됐어요.
261초는 방금 말한 255초 가드를 이미 넘는 값입니다. 그 구간에서는 보정은커녕 평가도 생략됩니다.

보정 예약(111초)도 빠듯합니다. 2위 관측치가 106.8초로 예약치의 96%까지 왔고, 평균 74.1초 · 표준편차 23.7초라 평균+2σ가 예약을 넘습니다.

테스트가 지키지 않던 상수

고친 뒤 단위 테스트가 전부 통과했습니다. 그런데 코드 검수를 맡긴 AI가 이걸 짚었어요.

안전 마진 15초를 아예 없애봤는데도 테스트가 그대로 통과한다.

테스트가 그 상수를 지키고 있지 않았다는 뜻입니다. 코드에 적혀 있을 뿐 아무도 감시하지 않는 숫자였습니다.

이렇게 값을 일부러 바꿔보고 테스트가 잡아내는지 확인하는 것을 뮤테이션 테스트라고 합니다. 이런 식으로 살아남은 게 3건이었어요. 통과한 테스트가 그 값을 지켜준다는 보장은 없습니다.

같은 실수를 막는 체크리스트

  • 천장을 상수로 적어뒀나. 그리고 마진을 뒀나 — 내 타이머가 언제부터 도는지 확인한다. 요청이 도착하는 시간은 그 앞에 있다
  • 뒤 단계를 앞 단계로 예측하고 있지 않나. 예측한다면 상관을 실제로 재봤나(내 경우 근거가 없었다). 차라리 관측 최악값을 그대로 예약하는 쪽이 더 안전하고 덜 막는다
  • 판정하는 곳이 몇 군데인가. 둘이면 상수끼리 결합이 생긴다. 더 정확한 정보를 쥔 쪽 하나만 남긴다
  • 끊으면 안 되는 호출을 끊고 있지 않나. 길이가 아니라 끊었을 때 뭐가 남는지로 가른다

자주 묻는 질문

Q. 백그라운드 작업으로 빼면요?
그게 정석입니다. 천장 문제 자체가 사라져요. 다만 큐·상태 저장·알림이 새로 필요해 비용이 큽니다. 저는 먼저 예산을 닫아 사고를 막고 구조 변경은 다음 문제로 미뤘습니다.

Q. 절대값 예약이면 작업이 짧은 경우엔 과하게 잡는 것 아닌가요?
맞습니다. 실제로 제 상수는 두 경로가 공용인데, 한쪽 실측은 26~41초라 과잉 예약이에요. 다만 과잉은 안전한 방향입니다 — 모자라면 승인해놓고 잘리거든요.

Q. 뮤테이션 테스트를 도구로 돌려야 하나요?
손으로 한 번 해보는 것만으로 충분히 얻습니다. 중요한 상수 하나를 바꿔보고 테스트를 돌려보세요. 초록불이면 그 자리에 단언이 없는 겁니다.

한 줄 교훈

모르는 값을 아는 값으로 추정하기 전에, 둘이 실제로 관계가 있는지부터 재야 합니다.

저는 *"보정은 생성에 비례하겠지"*를 재보지도 않고 상수로 박았습니다. 재봤더니 비례한다는 근거가 없었어요. 관계를 확인한 적 없는 두 값을 묶어 놓고 예산이라 부르고 있었던 겁니다.

그리고 예측이 안 되는 값은 예측을 더 정교하게 만드는 게 아니라, 예측을 그만두면 됩니다. 관측된 최악값을 그대로 예약하는 쪽이 더 안전하면서 덜 막았습니다.

댓글 남기기