LearningCollector: 초안 생성 실패 버그 수정
초안 생성 과정에서 실패한 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으로 매일 자동 실행 설정.
참고 자료
- Claude 코드 세션: 버그 발견 및 초기 수정 아이디어 제공.