← 개발 로그 목록

LearningCollector: 초안 생성 실패 버그 수정

/ 7분 분량 / 개발 로그

초안 생성 과정에서 실패한 JSON 파일이 완료 처리되는 치명적인 버그를 수정했습니다. 이제 성공한 항목만 상태를 업데이트해 재시도 로직이 제대로 작동합니다.

LearningCollector: 초안 생성 실패 버그 수정

초안 생성 과정에서 실패한 JSON 파일이 완료 처리되는 치명적인 버그를 수정했습니다. 이제 성공한 항목만 상태를 업데이트해 재시도 로직이 제대로 작동합니다.

요약

주요 변경사항: generate_drafts() 함수가 성공한 JSON 파일명 리스트를 추가 반환하도록 변경하고, orchestrator에서 이를 활용해 성공 항목만 draft_created/posted=True로 업데이트. 실패 항목은 상태 변경 없이 다음 실행에서 재시도 대상으로 남김.

작업 날짜: 2026년 2월 6일

전체 맥락: LearningCollector 프로젝트에서 자동화된 블로그 초안 생성 파이프라인을 안정화하는 작업. 백준 풀이, 개발 진척, AI 대화 초안을 Gemini로 생성하다가 일부 실패 시 전체 프로세스가 꼬이는 문제를 해결[1].

배경 및 목적

LearningCollector는 백준 문제 풀이 JSON, GitHub 커밋 JSON, AI 채팅 JSON을 입력으로 받아 자동 블로그 초안을 생성하는 도구입니다. 그런데 초안 생성 중 Gemini API 호출 실패나 중복 체크 등으로 일부 JSON이 처리되지 못할 때, 기존 코드에서는 실패한 항목까지 무조건 draft_created=True, posted=True로 상태 업데이트를 했어요.

이로 인해 실패한 JSON이 "완료"로 잘못 표시되어 다음 실행에서 스킵되면서, 블로그 포스팅이 누락되는 문제가 발생했습니다. 실제로 Claude와의 코드 세션에서 이 버그를 발견하고 수정하게 됐죠.

목표:

  • 실패 항목을 정확히 식별하고 상태 변경 생략
  • 재시도 가능한 안정적인 파이프라인 구축
  • 로그를 통해 실패 건수 명확히 표시

구현 내용

총 61라인 추가, 30라인 삭제로 비교적 규모 있는 리팩토링이었습니다. 변경된 파일은 두 개뿐이에요:

  • core/gemini_draft_generator.py (43 추가, 22 삭제)
  • core/orchestrator.py (18 추가, 8 삭제)

gemini_draft_generator.py 주요 변경

기존 generate_drafts()는 List[str] (생성된 draft 경로만) 반환했는데, 이제 Tuple[List[str], List[str]]로 변경: 첫 번째는 draft 경로 리스트, 두 번째는 성공한 JSON 파일명 리스트입니다.

각 하위 함수(_generate_baekjoon_drafts, _generate_dev_drafts, _generate_study_drafts)도 동일하게 수정됐어요. 핵심은 중복 처리 시에도 성공으로 간주하고 succeeded_jsons에 추가하는 점입니다.

# 기존
def generate_drafts(...) -> List[str]:

# 변경 후
def generate_drafts(...) -> Tuple[List[str], List[str]]:
    all_drafts = []
    all_succeeded_jsons = []  # 신규 추가
    # 각 생성 함수 호출 시 succeeded 리스트 수집
    baekjoon_drafts, succeeded = self._generate_baekjoon_drafts(baekjoon_jsons)
    all_succeeded_jsons.extend(succeeded)
    return all_drafts, all_succeeded_jsons

중복 체크 로직도 개선: processed 리스트를 제거하고, 중복/성공 모두 succeeded_jsons에 넣어 "처리된" 것으로 취급.

orchestrator.py 주요 변경

_auto_process와 _interactive_process에서 반환된 succeeded_jsons를 받아 성공한 JSON만 상태 업데이트:

drafts, succeeded_jsons = self.draft_generator.generate_drafts(...)
succeeded_set = set(succeeded_jsons)
for json_file in all_new:
    if json_file in succeeded_set:
        self.json_saver.update_status(json_file, "draft_created", True)
        self.json_saver.update_status(json_file, "posted", True)

failed_count = len(all_new) - len(succeeded_set)
if failed_count > 0:
    print(f"  → {failed_count}개 항목 초안 생성 실패 (다음 실행에서 재시도)")

이제 실패 시 로그가 명확히 출력돼 디버깅이 쉬워졌습니다.

기술적 의사결정

반환 타입 변경 (List → Tuple): 단순 리스트 대신 Tuple을 선택한 이유는 명시적 의미 전달. 첫 번째는 "생성된 drafts", 두 번째는 "성공 JSON"으로 역할이 다르기 때문. List[Union] 같은 대안은 타입 안전성이 떨어져 피함.

중복을 성공으로 간주: 중복은 "이미 초안 존재"하므로 재처리 불필요. 실패 케이스(예: Gemini API 에러)에만 재시도가 필요하지만, 중복도 "의도된 성공"으로 취급해 로직 단순화.

대안 비교:

접근법 장점 단점
Tuple 반환 (선택) 타입 안전, 명확성 ↑ 약간의 리팩토링 비용
예외 발생으로 실패 전달 에러 핸들링 직관적 중복 케이스 처리 복잡
모든 성공/실패 플래그 JSON에 저장 상세 추적 가능 파일 I/O 오버헤드 ↑

기존 코드와 호환성을 위해 destructuring unpack (drafts, succeeded_jsons = ...) 사용. Python 3.9+ 타입 힌트로 안전성 확보.

어려웠던 점: 각 하위 함수 3개를 일관되게 수정하다 보니 실수 위험이 컸음. 테스트 없이 Claude 프롬프트로 검증 후 커밋.

배운 점 및 개선점

배운 점:

  • 상태 관리의 함정: "처리했다" ≠ "성공했다". 실패 재시도 로직은 세밀한 성공 판정이 핵심.
  • 중복 체크를 성공으로 보는 관점이 실용적이었음. 불필요한 재작업 방지.
  • 로그 메시지(failed_count 출력)가 디버깅 시간을 단축시켜줌.

개선점:

  • 단위 테스트 추가: generate_drafts의 성공 리스트 검증.
  • 실패 원인 상세 로깅 (Gemini 에러 메시지 저장).
  • 다음 단계: 실제 JSON 100개로 end-to-end 테스트 후 메인 브랜치 머지. cronjob으로 매일 자동 실행 설정.

참고 자료