공공 포털 크롤링 자동 적재 봇 초기 구조 설계
목차
자동 적재 봇 프로젝트의 첫 커밋을 올렸다. 파일 다섯 개, 그리고 .env.example과 .gitignore까지 — 전형적인 "0에서 시작하는 날"이었다.
왜 이걸 만들기로 했냐면
공공 정보 포털에서 데이터를 주기적으로 긁어와서 내부 시스템에 적재해야 하는 수요가 생겼다. 담당자가 수동으로 들어가서 다운로드하고 업로드하는 루틴이 반복되고 있었는데, 그걸 자동화하지 않은 채 운영하는 건 명백한 낭비였다. 팀 입장에서도 이런 반복 작업은 사람이 해야 하는 일이 아니다.
초기 커밋이지만 파일 구성을 보면 설계 방향이 어느 정도 잡혀 있다.
| 파일 | 역할 |
|---|---|
bizinfo_crawler.py |
대상 포털 크롤링 진입점 |
classifier.py |
수집 데이터 분류/파싱 로직 |
crawler_common.py |
공통 유틸 (세션, 재시도, 로깅 등) |
.env.example |
환경변수 템플릿 (실 값 노출 방지) |
.gitignore |
민감 파일 커밋 제외 |
README.md |
실행 방법, 구조 설명 |
처음부터 단일 파일로 때려 박는 대신 크롤링 / 분류 / 공통 레이어를 분리한 건 의식적인 결정이었다. 나중에 다른 포털이 추가되거나, 분류 로직만 교체해야 할 때 건드릴 범위를 최소화하기 위해서다.
초기 커밋에서 챙겨야 할 것들
크롤러 프로젝트는 첫 커밋 시점에 이미 몇 가지를 결정해둬야 한다. 나중에 고치려면 히스토리가 지저분해지거나 팀원이 실수할 여지가 생긴다.
.env.example먼저: 실제.env는 절대 커밋하지 않는다. 그 규칙을 첫 커밋부터 못 박아두는 것..gitignore에.env추가하고.env.example로 어떤 환경변수가 필요한지 명시해두면 온보딩 시 설명할 내용이 줄어든다.- 공통 모듈 분리: 크롤러는 재시도, 타임아웃, 헤더 관리, 로깅 패턴이 반복된다.
crawler_common.py하나에 몰아두면 각 크롤러 파일은 "어디서 뭘 가져오냐"에만 집중할 수 있다. README.md초안: 아무것도 없는 상태에서도 실행 방법 한 섹션은 써두는 게 맞다. 나 혼자 쓰는 프로젝트라도 두 달 뒤 내가 낯선 사람이 된다.
# crawler_common.py 에서 보통 이런 패턴으로 시작
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
def build_session(retries=3, backoff=0.5) -> requests.Session:
session = requests.Session()
retry = Retry(
total=retries,
backoff_factor=backoff,
status_forcelist=[429, 500, 502, 503, 504],
)
adapter = HTTPAdapter(max_retries=retry)
session.mount("https://", adapter)
session.mount("http://", adapter)
return session
공공 포털 크롤링은 응답이 불안정한 경우가 종종 있어서, 재시도 로직을 공통 레이어에 미리 박아두는 게 거의 필수다. 각 크롤러 파일에서 매번 구현하면 놓치는 케이스가 생긴다.
classifier.py를 분리한 이유
이 부분을 조금 더 얘기하고 싶다. 크롤러와 분류 로직을 붙여두면 테스트가 어려워진다. 크롤러는 네트워크가 필요하지만, 분류 로직은 그냥 문자열/HTML 파싱이라 mock 없이도 단위 테스트가 가능하다. 처음부터 분리해두면 CI에서 네트워크 없이 분류 로직만 검증하는 게 쉬워진다.
# classifier.py 역할 — 수집된 raw 데이터를 구조화된 dict 로 변환
def classify_entry(raw: dict) -> dict | None:
# 필요한 키가 없으면 None 반환 (적재 스킵)
required = ["title", "category", "date"]
if not all(raw.get(k) for k in required):
return None
return {
"title": raw["title"].strip(),
"category": raw["category"],
"date": raw["date"],
# ... 나머지 필드 정제
}
분류 함수가 None을 반환하면 적재 단계에서 스킵하는 방식. 예외를 던지는 것보다 None 리턴으로 처리하는 게 배치 작업에서는 더 자연스럽다. 한 건 실패로 전체 파이프라인이 죽는 건 원하지 않으니까.
팀에 이 봇을 공유할 때 제일 먼저 물어볼 것 같은 질문이 "이거 언제 어떻게 실행되냐"였다. README에 그걸 먼저 써두고 커밋한 게 결과적으로 맞는 순서였다. 코드보다 README가 먼저인 날도 있다.
끝.
🛒 이 글과 어울리는 추천 상품
*위 링크는 쿠팡파트너스 활동의 일환이며, 일정액의 수수료를 제공받을 수 있습니다.
댓글 0
첫 댓글 달아줘.