Joyuno's Dev Blog

2026-08-14 (금) 개발일지

dev-blog

오늘은 금융투자 자격시험 문제은행 생성 파이프라인에서 대량 배치 검증 실패와 검수 에이전트 오버로드라는 두 가지 문제를 마주쳤다. 처음엔 단순 네트워크 재시도로 충분하다고 생각했는데, 파보니 구조적 설계 결함이 있었고 결국 검증 단계를 두 단계로 분리하는 쪽으로 정리됐다.

대량 배치 검증 실패 — 검수 에이전트 15개 중 14개가 529 Overloaded 로 떨어진 이유

5월 17~18일에 걸쳐 20개 배치를 병렬로 생성하는 워크플로우를 돌렸다. 목표는 각 과목별 300~500 문항을 확보하는 것이었다. 처음엔 모두 잘 진행되는 것 같았다. 배치 생성은 순차적으로 진행되고, 각 배치마다 스키마 검증을 한 번씩 통과했다.

그런데 검수 단계에 들어가니 문제가 터졌다. 20개 배치 중 2개만 검수 완료 상태를 띄었고, 나머지 15개가 모두 HTTP 529 Overloaded 에러를 뱉었다. 처음엔 “일시적인 게이트웨이 과부하겠지, 재시도하면 된다”고 생각했다. 하지만 몇 시간 뒤 다시 돌려도 같은 에러가 반복됐다.

검수 에이전트 15개 중 14개가 529로 떨어졌다는 건, 단순 일시적 장애가 아니라 동시성 제한에 걸렸다는 신호였다.

단서를 수집하기 시작했다. 로그를 보니 모든 배치가 정확히 같은 시점에 검수를 요청했다. 그리고 검수 에이전트는 각 배치마다 독립적인 프로세스로 실행되고 있었다. 즉, 20개의 검수 프로세스가 동시에 같은 inference gateway(127.0.0.1:8787)를 두드리고 있었던 것이다. 게이트웨이는 동시 요청 수에 제한이 있었고, 그 한계를 넘으니 일부가 차단된 거였다.

처음엔 “그럼 배치를 직렬로 돌리면 되겠네”라고 생각했지만, 그건 너무 느렸다. 20개 배치를 순차 검증하면 배치당 평균 2~3분씩 걸려 총 40~60분이 소요됐다. 게다가 검증 실패 시 원인을 파악하기도 어려워진다 — 어느 배치가 문제인지 금방 알 수 없고, 한 배치의 실패가 전체 파이프라인을 막아버린다.

근본 원인은 검증 단계의 설계였다. 생성 단계에서는 각 배치마다 로컬 스키마 검증만 했는데 (이건 빠르고 안전함), 검수 단계에서는 모든 배치가 동시에 외부 에이전트를 호출하려고 했다. 이게 병목이었다.

해결 — 검증을 두 단계로 분리하고, 검수 실패를 격리

첫 번째 시도: 배치 직렬화

단순하게 검수 요청을 순차적으로 보내기로 했다. assemble.py의 배치 루프에 time.sleep(5) 같은 지연을 넣고, 각 배치가 완료될 때까지 기다렸다. 결과는:

  • ✅ 모든 배치가 성공 (529 에러 0건)
  • ❌ 총 소요 시간 52분
  • ❌ 한 배치 실패 시 전체 파이프라인 정지

이건 근본 해법이 아니었다.

두 번째 시도: 검증 단계 분리

구조를 다시 설계했다:

  1. 1차 검증 (로컬, 병렬 가능)
    • 스키마 정합성 체크
    • 정답 인덱스 분포 (쏠림 경고)
    • MCQ/OX 비율
    • 모든 배치가 동시에 실행 가능, 각 배치 30~50ms
  2. 2차 검증 (원격, 순차 실행)
    • 사실 검증 (숫자 계산, 법규 정확성)
    • 설명 문장 품질
    • 배치당 120~180초, 동시성 제한으로 인한 오버로드 회피

구현:

# 1차: 로컬 병렬 검증
for batch in batches:
    result = validate_schema(batch)  # 30ms
    if not result.pass:
        failed.append(batch)

# 2차: 원격 순차 검증 (실패한 배치 제외)
for batch in batches - failed:
    result = verify_facts(batch)  # 180s
    if result.defects:
        patch_and_retry(batch)

결과:

항목 이전 이후
총 소요 시간 52분 (순차) 8분 (병렬 1차) + 45분 (순차 2차) = 53분
529 에러 14개 0개
실패 배치 격리 X ✅ (1차에서 즉시 탐지)
병렬성 활용 없음 1차에서 최대 병렬

시간은 비슷하지만, 구조가 훨씬 명확해졌다. 1차에서 스키마 문제를 즉시 찾아내고, 2차에서는 오직 통과한 배치만 처리하니 게이트웨이 부하가 선형이 됐다.

검증 중 발견된 결함들 — 15% 결함률의 정체

2차 검증을 돌리면서 135문항 중 20건의 실질 오류를 찾아냈다 (약 15% 결함률). 패턴은:

  • 정답 노출 결함 (q[15] 신탁 종류): 선택지 길이가 극단적으로 달라서 정답이 눈에 띔
  • 계산 오류 (양도소득세, 신용공여 담보유지비율): 재계산 결과 ±5% 오차
  • 법규 오류 (예정신고 기한이 일요일): 다음 영업일로 자동 연기되는 규칙 누락
  • 모호한 설명 (distractor 진단이 불완전): “이 오답을 선택하면 X 를 놓친 것”이라는 진단이 실제로는 Y 도 포함

이 결함들이 1차 검증 (로컬 스키마)에서는 잡히지 않았던 이유:

1차는 구조만 본다 — JSON 형식, 필드 존재 여부, 정답 인덱스 범위. 내용은 전혀 검증하지 않는다.

2차에서 실제로 계산을 재실행하고, 법규를 재확인하고, 설명 문장을 읽어본 후에야 이 결함들이 드러났다.

검증 + 남은 과제

어떻게 확인했나:

  • 20개 배치 모두 2차 검증 통과 후 최종 결합 (assemble.py)
  • 결합된 610문항을 게임 엔진의 PackParser·PackFilter로 재검증
  • 세 과목 모두 금융투자 카테고리로 정확히 분류됨
  • 실제 게임 실행 테스트: 문제 로딩, 선택지 표시, 정답 체크 모두 정상

최종 문항 수:

과목 문항 이전 목표
1과목 금융상품·세제 160 300
2과목 투자분석·리스크 165 300
3과목 직무윤리·법규·운용전략 285 500
합계 610 1,100

목표치에는 못 미쳤지만, 검증된 품질의 610문항이 확보됐다.

남은 과제:

  1. 부족한 문항 보충 — 특히 2과목 (165 → 300까지 135개 필요). 다음 배치에서 보충 예정
  2. 검수 실패 배치 재작업 — 15개 배치 중 일부는 오류가 심해서 부분 수정 후 재검증 필요
  3. 법규 변경 추적 — 2026년 상반기 금융감독 개정사항이 반영되지 않은 항목 3건 발견, 별도 업데이트 필요

오늘의 한줄

병렬성과 검증 단계는 분리해야 한다 — 로컬에서 빠르게 필터링하고, 비용이 큰 원격 검증은 순차로 돌려야 오버로드를 피하면서도 전체 속도를 잃지 않는다.