🔒 내부용 · 비배포 출처:
ai/wiki/concepts/crawler-collection-count-log.md,ai/worklog/2026/2026-W24.md(2026-06-10)
5개 라이브러리 크롤러에 "이번 배치에 뭘 얼마나 수집했나"를 남기는 로그를 붙인 작업의 온보딩 상세.
0. 한눈에
- 대상:
crawler-lib-golang / php / ruby / swift / dotnet(Java·Rust 제외). 디렉터리 이름은 생태계 이름이고 5개 모두 Python,baseCralwer.py(swift만baseCrawler.py) 구조를 공유한다. - 한 일: 배치가 끝날 때마다 그 배치에서 product / version 각각 신규(insert)·갱신(update) 건수를 로그로 남기고, 런 종료 시 누적 총합도 남긴다.
- 핵심 난점: 모든 쓰기가
INSERT ... ON DUPLICATE KEY UPDATE(upsert)라 DB가 행별로 insert인지 update인지 알려주지 않는다.
1. 왜 했나
기존 로그는 "이번 배치에서 패키지 N개 처리"라는 누적 카운트뿐이라, 그 배치가 실제로 새로 들어온 데이터를 모은 건지 기존 데이터를 다시 덮어쓴 건지 구분되지 않았다. 백필·재수집·증분 수집을 함께 돌리는 운영 환경에서 "오늘 신규 유입이 얼마였나"를 로그만 보고 알 수 없었던 것이 출발점이다. 그래서 배치 단위로 insert/update를 분리해 보이게 만드는 게 목표였다.
2. 핵심 제약: upsert는 insert/update를 안 알려준다
배치는 1,000행(golang V2는 이벤트 단위)을 한 방의 멀티로우 INSERT로 보낸다. MySQL이 ON DUPLICATE KEY UPDATE에서 돌려주는 affected_rows는 행마다 신규=1, 변경 update=2, 변화 없음=0인데, 배치 전체가 한 statement라 돌아오는 건 합쳐진 숫자 하나뿐이다. 거기서 insert가 몇·update가 몇인지는 역산되지 않는다(무변화 update가 0으로 잡혀 산수마저 깨진다). 게다가 현재 실행 계층은 rowcount를 읽지도 않는다.
그래서 선택지를 셋으로 정리하고 가장 가벼운 정확 방식을 골랐다:
| 방식 | 정확도 | 비용 | 채택 |
|---|---|---|---|
| A. 배치 전 PK 조회 — upsert 직전 그 배치 PK를 SELECT, DB에 있으면 update·없으면 insert | 정확 | 배치당 SELECT 1회(가벼움) | 채택 |
| B. 한 행씩 실행 + rowcount | 가장 정확(무변화까지 구분) | 배치당 수백~천 번 왕복 → 배치 INSERT 장점 상실 | 기각 |
| C. affected_rows 산수 | 부정확(무변화 update에서 깨짐) | 공짜 | 기각 |
의사결정 기록: 사용자가 "쿼리 날릴 때 insert/update를 알 수 있지 않냐"고 짚었고, "한 행씩이면 가능하지만 지금처럼 배치로 보내면 합계 하나뿐이라 불가"임을 확인한 뒤 가벼운 A로 합의했다.
3. 어떻게 구현했나
- 공통 베이스 클래스(
baseCralwer.py)에 집계 헬퍼 6개 추가:_resetCollectStats/_entityForTable/_selectExistingKeys/_recordCollectStats/_collectBatchSummary/_collectTotalSummary. setInsertIntoDB진입부(upsert 직전)에서_recordCollectStats(table_name, datas)호출. 대상이 PRODUCT_TABLE / VERSION_TABLE일 때만 집계(status·list·license 테이블은 무시).- 분류: product는
PRODUCT_KEY, version은(PRODUCT_KEY, VERSION)기준으로 500개씩 묶어WHERE PRODUCT_KEY IN (...)(파라미터 바인딩) 조회 → 있으면 update, 없으면 insert. - 배치 진입 시
_resetCollectStats()로 배치 카운터·중복제거 set 리셋. 배치 종료 시[batch summary], 런 종료 STOP 로그 직전[total summary].
중복제거 포인트: golang/dotnet/ruby는 SORT_ORDER를 채우려 같은 version을 한 번 더 upsert한다. 배치 단위
_collectSeenset으로 이미 센 키를 건너뛰어, 이 재삽입이 update로 이중 집계되는 것을 막았다.
크롤러별 주입 위치 — php/ruby/dotnet은 dataCrawling() 상단 리셋 + 말미 배치요약, run() STOP 직전 총합. swift(cocoapods.py)는 반복 배치 루프가 없는 단일 패스라 crawl 메서드 전체를 1배치로 보고 finally:에서 둘 다 출력(중간 예외에도 보장). golang은 쓰기 경로가 setInsertIntoDB(V1 정렬)와 _setInsertIntoDBWithConnection(V1-core·V2) 둘로 갈려 양쪽 다 훅했고(인스턴스 단위 dedup이라 겹쳐도 1회), V1·V2 각 루프와 STOP 3곳을 모두 처리한다.
4. 로그 포맷 (5개 크롤러 동일)
메시지 문자열은 5개 크롤러에서 완전히 동일하다(공통 헬퍼의 f-string을 그대로 복사). 로그 라인의 파일명:라인 prefix만 호출 위치에 따라 다르다.
[batch summary] product insert=12 update=3, version insert=45 update=8
[total summary] product insert=1340 update=210, version insert=5021 update=880전체 라인 포맷도 5개 공통: %(asctime)s %(levelname)s %(process)s %(filename)s:%(lineno)d %(message)s.
파싱 정규식: \[(batch|total) summary\] product insert=(\d+) update=(\d+), version insert=(\d+) update=(\d+) 하나로 5개 모두 처리.
5. 검증 / 안전장치
- 집계 실패가 수집 본류를 막지 않도록
_recordCollectStats를 try/except로 격리 — 실패 시[collect stats] skippedwarning만 남기고 진행. - 5개 repo 전부
python3 -m py_compile통과(base + 크롤러 파일). - 분류 로직은 헬퍼 본문을 떼어내 fake DB로 단위 테스트 — product insert/update, version insert/update, sorting 재삽입 dedup, 비대상 테이블 무시, 배치 reset 후 누적 total까지 기대값 일치(전부 PASS).
⚠️ 아직 실컨테이너 스모크는 미실시(실DB·네트워크 의존). 운영 반영 전 1 윈도우 실행 권장. golang V2는 이벤트 단위라 집계 SELECT가 배치당 1회가 아니라 이벤트당 소량 여러 번 나간다(각각 가벼움) — 더 줄이려면 기존
_batchSkipExistingEvents존재체크 결과 재사용이 후보.
6. 온보딩 포인트
- upsert 환경에서 "신규 vs 갱신"을 세려면 쓰기 직전 키 존재 조회가 사실상 유일하게 가볍고 정확한 길이다.
affected_rows로 분리하려는 시도는 멀티로우·무변화 update 때문에 막힌다. - 5개 크롤러는 디렉터리 이름과 무관하게 같은 Python 베이스를 공유하므로, 공통 동작은 베이스 클래스 한 곳에 넣고 크롤러별로는 리셋/요약 호출만 꽂는 게 정석.
- 같은 키를 두 번 쓰는 경로(version sorting)가 있으면 카운트는 배치 단위 dedup 필수. 안 하면 update가 부풀려진다.
- 코드 상세·메서드 시그니처·크롤러별 위치표는
ai/wiki/concepts/crawler-collection-count-log.md.
7. 공개(포트폴리오)와의 차이
이 문서는 내부 식별자(repo id crawler-lib-*, 내부 테이블 TB_*·gatheringdb, 메서드명)를 그대로 쓴 본인 참고/온보딩용이다. 포트폴리오 공개가 필요하면 식별자를 일반화하고 "upsert에서 insert/update 가시화"라는 문제·접근·결과 중심으로 다시 쓴다. 일반 기술 명칭(MySQL, ON DUPLICATE KEY UPDATE 등)은 공개에도 유지 가능.
🕷 라이브러리 크롤러 시리즈 ④ — ① 멀티 언어 크롤러 구축 · ② 인기도 차등 수집 · ③ 크롤러 현대화 · ④ 배치 수집량 로그