학습 기록

Python 공부 기록 #25 - 200줄 넘은 크롤러를 함수와 파일로 쪼개기

0. 목차

  • 인트로: 내가 짠 코드인데 내가 못 찾는다
  • 한 파일에 다 넣으면 왜 힘들어지는지
  • 먼저 역할부터 종이에 적어봤습니다
  • 1단계: 긴 함수를 작은 함수로 쪼개기
  • 2단계: 파일을 역할별로 나누기
  • 3단계: main.py는 순서만 남기기
  • 삽질했던 부분들
  • 아웃트로: 고칠 자리를 바로 찾는다는 것

1. 인트로: 내가 짠 코드인데 내가 못 찾는다

지난 글에서는 Discord Webhook으로 크롤링 결과를 폰으로 받아봤습니다.

이제 스크립트가 알아서 돌고, 로그도 쌓이고, 결과 알림까지 옵니다. 그런데 8월 초에 사이트 구조가 살짝 바뀌면서 선택자를 하나 고쳐야 할 일이 생겼습니다.

파일을 열었는데 어디를 고쳐야 할지 한참을 스크롤했습니다.

크롤링 코드, Excel 저장 코드, 알림 코드, 설정 읽는 코드가 한 파일에 220줄 정도 뒤엉켜 있었습니다. 처음엔 30줄짜리였는데 회차를 거치면서 계속 위아래로 붙인 결과였습니다.

찾아보니까 이걸 리팩터링이라고 부르더군요. 기능은 그대로 두고 코드 생김새만 정리하는 작업입니다.


2. 한 파일에 다 넣으면 왜 힘들어지는지

처음엔 파일 하나가 편하다고 생각했습니다. 실행할 때 그거 하나만 돌리면 되니까요.

문제는 고칠 때 생깁니다. 선택자 한 줄을 바꾸려고 열었는데 저장 코드와 알림 코드까지 눈에 들어옵니다. 잘못 건드릴까 봐 조심스러워집니다.

고칠 곳을 찾는 시간이 실제로 고치는 시간보다 길어지면 정리할 때가 된 겁니다.


3. 먼저 역할부터 종이에 적어봤습니다

코드를 만지기 전에 지금 스크립트가 하는 일을 순서대로 적었습니다.

  1. 설정 파일과 .env를 읽는다
  2. 웹페이지를 가져와서 필요한 값을 뽑는다
  3. 뽑은 값을 Excel로 저장한다
  4. 결과를 Discord로 보낸다
  5. 위 순서를 차례로 실행한다

적고 나니까 파일을 몇 개로 나눠야 할지가 그냥 보였습니다. 다섯 줄이 그대로 다섯 덩어리였습니다.


4. 1단계: 긴 함수를 작은 함수로 쪼개기

파일을 나누기 전에 함수부터 손봤습니다. 원래 run() 하나가 90줄쯤 됐습니다.

# 전: 하나가 다 한다
def run():
    # 요청 보내고
    # 파싱하고
    # 리스트에 담고
    # Excel로 저장하고
    # 알림 보내고
    ...

이걸 이렇게 쪼갰습니다.

# 후: 하나가 하나만 한다
def fetch_page(url):
    """페이지 HTML만 가져온다"""
    response = requests.get(url, timeout=10)
    response.raise_for_status()
    return response.text


def parse_items(html):
    """HTML에서 필요한 값만 뽑아 리스트로 돌려준다"""
    soup = BeautifulSoup(html, "html.parser")
    items = []
    for row in soup.select("div.item"):
        items.append({
            "title": row.select_one(".title").get_text(strip=True),
            "price": row.select_one(".price").get_text(strip=True),
        })
    return items

기준은 하나로 잡았습니다. 함수 이름을 짓기 어려우면 두 가지 일을 하고 있는 겁니다.

fetch_and_parse_and_save 같은 이름이 나오면 쪼갤 신호였습니다. and가 들어가면 일단 의심했습니다.

함수마다 """..."""로 한 줄 설명을 붙였습니다. docstring이라고 부르는데, 몇 주 뒤에 다시 읽을 때 도움이 됐습니다.


5. 2단계: 파일을 역할별로 나누기

이제 폴더를 이렇게 만들었습니다.

my_crawler/
├── main.py          # 실행 순서만
├── config.py        # 설정과 .env 읽기
├── crawler.py       # 요청과 파싱
├── storage.py       # Excel 저장
├── notifier.py      # Discord 알림
├── config.ini
└── .env

각 파일에는 아까 쪼갠 함수를 성격대로 옮겨 담았습니다. crawler.py에는 fetch_page와 parse_items, storage.py에는 save_to_excel, notifier.py에는 지난 글에서 만든 send_discord가 들어갔습니다.

같은 폴더 안이면 파일 이름이 곧 모듈 이름이 됩니다. crawler.py는 import crawler로 부릅니다.


6. 3단계: main.py는 순서만 남기기

정리하고 나니 main.py가 이만큼 줄었습니다.

import logging

from config import load_settings
from crawler import fetch_page, parse_items
from storage import save_to_excel
from notifier import send_discord


def main():
    settings = load_settings()

    try:
        html = fetch_page(settings["target_url"])
        items = parse_items(html)
        save_to_excel(items, settings["output_dir"])
        send_discord(f"[성공] 크롤링 완료 / {len(items)}건 저장")
    except Exception as e:
        logging.exception("크롤링 실패")
        send_discord(f"[실패] 크롤링 중단\n원인: {e}")


if __name__ == "__main__":
    main()

220줄이 30줄이 됐습니다. 사라진 게 아니라 옆 파일로 옮겨간 것뿐인데, main.py만 봐도 이 프로그램이 뭘 하는지 순서가 읽힙니다.

이제 선택자를 고칠 일이 생기면 crawler.py만 열면 됩니다. 저장 형식을 바꾸고 싶으면 storage.py만 봅니다. 8월 초에 고생했던 그 작업을 다시 해보니 3분도 안 걸렸습니다.

if __name__ == "__main__":은 이 파일을 직접 실행할 때만 main()을 부르라는 뜻입니다. 다른 파일에서 import main을 해도 크롤링이 멋대로 돌지 않습니다.


7. 삽질했던 부분들

crawler.py에서 notifier.py를 부르고, notifier.py에서 다시 crawler.py를 부르게 짰다가 순환 참조 오류를 봤습니다. 서로 마주 보고 부르면 Python이 어느 쪽을 먼저 읽어야 할지 몰라서 멈춥니다. 알림은 main.py에서만 부르도록 정리하니 해결됐습니다.

작업 스케줄러가 갑자기 실패하기 시작했습니다. 파일 위치가 바뀌었는데 스케줄러에는 옛날 경로가 그대로였습니다. 시작 위치도 새 폴더로 바꿔줘야 했습니다.

한 번에 다 쪼개려다가 중간에 뭐가 뭔지 몰라 원상복구한 적도 있습니다. 두 번째 시도에서는 파일 하나 옮길 때마다 실행해서 되는지 확인했습니다. 이 방법이 훨씬 빨랐습니다.

파일 이름을 logging.py로 지었다가 표준 라이브러리 logging을 못 읽는 오류가 났습니다. Python이 내 파일을 먼저 찾아버립니다. 이름 짓기 전에 import 이름이 이미 있는 건 아닌지 확인하는 습관이 생겼습니다.


8. 아웃트로: 고칠 자리를 바로 찾는다는 것

오늘은 한 파일에 쌓인 크롤러를 함수와 파일 단위로 쪼개봤습니다.

핵심 정리

  1. 코드가 하는 일을 순서대로 적어보면 나눌 기준이 보인다
  2. 함수는 하나가 하나만 하게, 이름에 and가 들어가면 쪼갤 신호
  3. 파일은 역할별로 나누고 main.py에는 순서만 남기기
  4. 같은 폴더면 파일 이름이 곧 모듈 이름, import 파일명으로 부르기
  5. 한 번에 다 옮기지 말고 하나 옮길 때마다 실행해서 확인하기

이제 할 수 있는 것들

  • 고칠 곳을 파일 이름만 보고 찾아가기
  • 새 기능을 기존 코드 건드리지 않고 새 파일로 붙이기
  • 크롤링 코드만 따로 떼서 다른 사이트에 재사용하기
  • main.py 한 장으로 프로그램 흐름 설명하기

비전공자 Python 공부를 시작하고 스물다섯 번째 글인데, 새 기능을 배우는 것보다 이미 짠 코드를 다시 읽을 수 있게 만드는 일이 더 어려웠습니다. 업무 자동화는 결국 몇 달 뒤의 나도 알아볼 수 있어야 계속 쓰이더군요.

다음 포스팅에서는 쪼갠 함수들이 제대로 도는지 확인하는 방법을 다뤄보겠습니다. assert로 간단한 검사를 붙여보고, 사이트에 접속하지 않고도 파싱 함수만 따로 시험하는 방법까지 정리해볼 생각입니다. 매번 실제 사이트를 긁어서 확인하다가 차단당할 뻔했거든요.