자동화 스크립트를 작업 스케줄러에 걸어두고 며칠이 지난 뒤, 결과물이 안 만들어져 있는 걸 발견합니다. 그런데 스케줄러 기록에는 “성공(0x0)”만 찍혀 있습니다. 스케줄러는 프로세스가 실행됐는지만 보고, 스크립트가 중간에 무엇 때문에 멈췄는지는 알지 못합니다. 남아 있는 단서가 없으면 결국 같은 상황을 손으로 재현해봐야 하는데, 그게 매번 30분씩 걸립니다. 반대로 로그 한 줄만 있으면 1분입니다. 이번 글에서는 스크립트 로그 관리를 세 단계로 정리합니다. 날짜별 파일로 남기기, 자동으로 분할하기, 오래된 건 알아서 지우기입니다.
print로 버티면 안 되는 이유
개발 중에는 print()로 충분합니다. 콘솔에 바로 보이니까요. 문제는 그 스크립트가 사람 손을 떠나는 순간입니다.
- 작업 스케줄러나 cron이 실행하면 콘솔 창 자체가 없습니다. 출력은 어디에도 남지 않고 버려집니다.
- 터미널에서 돌렸더라도 창을 닫으면 끝입니다. 스크롤 버퍼를 넘긴 줄은 이미 사라졌습니다.
- 언제 찍힌 줄인지 알 수 없습니다. 시각이 없으면 “어제 그 실패”를 찾아낼 방법이 없습니다.
- 정상 로그와 오류가 뒤섞입니다. 심각도 구분이 없어서 나중에 걸러낼 수가 없습니다.
파이썬 표준 라이브러리의 logging은 이 네 가지를 한 번에 해결합니다. 추가 설치도 필요 없습니다.
1단계: 날짜별 로그 파일로 남기기
가장 단순하고, 스케줄러로 돌리는 스크립트에 가장 잘 맞는 방식입니다. 파일명에 날짜를 박아두면 하루치가 한 파일에 모이고, 어떤 날의 기록을 볼지도 파일 이름만 보고 고를 수 있습니다.
import logging
from datetime import datetime
from pathlib import Path
# 로그 폴더는 반드시 스크립트 파일 기준 절대경로로 잡습니다.
# 작업 스케줄러는 "시작 위치"를 비워두면 작업 디렉터리를 C:\Windows\System32로
# 잡기 때문에, 상대경로로 쓰면 로그가 엉뚱한 곳에 생깁니다.
LOG_DIR = Path(__file__).resolve().parent / "logs"
LOG_DIR.mkdir(exist_ok=True)
log_path = LOG_DIR / ("run-" + datetime.now().strftime("%Y-%m-%d") + ".log")
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
handlers=[
# encoding을 빼면 윈도우에서 CP949로 저장돼 한글이 깨집니다
logging.FileHandler(log_path, encoding="utf-8"),
logging.StreamHandler(), # 손으로 실행할 때는 콘솔에도 같이 출력
],
)
logging.info("작업 시작")
try:
total = 0
for f in Path("data").glob("*.csv"):
total += 1
logging.info("처리: %s (%d바이트)", f.name, f.stat().st_size)
logging.info("완료 — CSV %d개 처리", total)
except Exception:
logging.exception("작업 실패") # 트레이스백까지 함께 기록
raise
이렇게 하면 logs/run-2026-10-04.log 같은 파일이 날마다 하나씩 생깁니다. 실패했을 때 파일을 열어보면 이런 내용이 들어 있습니다.
2026-10-04 09:00:01 [INFO] 작업 시작
2026-10-04 09:00:01 [INFO] 처리: sales-01.csv (20483바이트)
2026-10-04 09:00:02 [INFO] 처리: sales-02.csv (19771바이트)
2026-10-04 09:00:02 [ERROR] 작업 실패
Traceback (most recent call last):
File "D:\scripts\merge.py", line 28, in <module>
df = pd.read_csv(f)
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xb0 in position 12
어느 파일을 읽다가 터졌는지, 몇 번째 줄에서 났는지, 원인이 인코딩 문제였다는 것까지 한눈에 보입니다. logging.exception()이 트레이스백을 그대로 파일에 박아주기 때문입니다. logging.error()만 쓰면 메시지 한 줄만 남아서 정작 필요한 정보가 빠집니다.
파이썬 3.8 이하에서는
logging.basicConfig(encoding=...)이 동작하지 않습니다. 위 예제처럼FileHandler를 직접 만들어encoding을 넘기는 방식은 모든 버전에서 안전합니다.
2단계: 자정마다 자동 분할하고 보관 개수 제한하기
하루 종일 떠 있는 프로그램이라면 파일명에 날짜를 박는 방식이 어울리지 않습니다. 시작할 때 날짜가 고정돼서, 자정을 넘겨도 전날 파일에 계속 쌓이기 때문입니다. 이때는 TimedRotatingFileHandler를 씁니다.
import logging
from logging.handlers import TimedRotatingFileHandler
handler = TimedRotatingFileHandler(
filename=str(LOG_DIR / "run.log"),
when="midnight", # 자정을 넘기면 새 파일로 갈아탑니다
interval=1,
backupCount=14, # 14개까지만 보관. 더 오래된 건 알아서 삭제
encoding="utf-8",
)
handler.suffix = "%Y-%m-%d" # run.log.2026-10-04 형태로 쌓입니다
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
handlers=[handler],
)
핵심은 backupCount입니다. 분할만 해주는 게 아니라 개수를 넘긴 오래된 파일을 직접 삭제해줍니다. 별도 정리 스크립트 없이 보관 기간이 지켜집니다.
다만 알아둘 점이 있습니다. 분할 여부는 로그를 기록하는 순간에 판단합니다. 계속 실행되는 프로그램이라면 자정에 정확히 갈리지만, 하루 한 번 돌고 끝나는 스크립트는 다음 실행 때가 되어야 비로소 파일이 갈립니다. 그래서 단발성 스케줄러 작업이라면 1단계 방식이 더 단순하고 예측 가능합니다. 둘을 섞어 쓸 필요는 없습니다.
3단계: 오래된 로그 자동 삭제
1단계 방식을 택했다면 정리는 직접 해줘야 합니다. 방치하면 수천 개 파일이 쌓입니다. 한 줄이 100바이트인 로그를 하루 200줄씩 1년 모으면 7MB 정도라 용량 자체는 부담이 아니지만, 파일 개수가 수백 개를 넘어가면 폴더를 열어보는 것조차 번거로워집니다.
import time
from pathlib import Path
def cleanup_logs(log_dir, keep_days=14, dry_run=True):
"""keep_days보다 오래된 로그 파일을 지우고, 지운(또는 지울) 목록을 돌려줍니다."""
deadline = time.time() - keep_days * 86400
targets = []
for f in Path(log_dir).glob("*.log*"): # 분할된 run.log.2026-09-01까지 포함
if f.stat().st_mtime < deadline:
targets.append(f.name)
if not dry_run:
f.unlink()
return targets
if __name__ == "__main__":
# 처음 한 번은 dry_run=True로 돌려서 지울 목록만 눈으로 확인하세요
removed = cleanup_logs("logs", keep_days=14, dry_run=False)
print("정리한 로그 " + str(len(removed)) + "개: " + ", ".join(removed))
이 함수를 본 작업 스크립트 맨 앞에서 한 번 호출하면 됩니다. 별도 스케줄러 등록이 필요 없습니다. 어차피 스크립트가 돌 때마다 정리 기회가 생기니까요.
dry_run 인자를 둔 이유가 있습니다. 파일을 지우는 코드는 글로브 패턴 하나만 잘못 적어도 엉뚱한 걸 날립니다. 처음 한 번은 dry_run=True로 돌려서 목록만 확인하고, 의도한 파일만 걸렸을 때 False로 바꾸는 습관을 권합니다.
파워셸로 돌리는 스크립트라면
윈도우에서 .ps1로 자동화를 돌린다면 같은 패턴을 파워셸로도 그대로 만들 수 있습니다. 로그 기록과 정리를 한 파일에 넣었습니다.
# ---- 날짜별 로그 파일 준비 ----
$logDir = Join-Path $PSScriptRoot 'logs'
if (-not (Test-Path $logDir)) { New-Item -ItemType Directory $logDir | Out-Null }
$log = Join-Path $logDir ('run-' + (Get-Date -Format 'yyyy-MM-dd') + '.log')
function Write-Log($msg) {
$line = (Get-Date -Format 'yyyy-MM-dd HH:mm:ss') + ' ' + $msg
Add-Content -Path $log -Value $line -Encoding utf8
Write-Host $line
}
Write-Log '작업 시작'
# 외부 명령이 토해내는 출력까지 같은 파일에 남기려면 *>> 로 모든 스트림을 붙입니다
node app.js *>> $log
Write-Log ('node 종료 코드: ' + $LASTEXITCODE)
# ---- 14일보다 오래된 로그 정리 ----
Get-ChildItem $logDir -Filter '*.log' |
Where-Object { $_.LastWriteTime -lt (Get-Date).AddDays(-14) } |
Remove-Item -Force
$PSScriptRoot는 스크립트 파일이 있는 폴더입니다. 작업 디렉터리와 무관하게 항상 같은 곳에 로그가 생기므로, 스케줄러에서 돌릴 때 경로 사고를 막아줍니다.
방식별 비교
| 방식 | 설정 난이도 | 시각·심각도 | 보관 관리 | 적합한 경우 |
|---|---|---|---|---|
print()만 | 없음 | 없음 | 불가 | 개발 중 임시 확인 |
>> 리다이렉션 | 매우 쉬움 | 없음 | 수동 | 이미 만들어둔 스크립트에 급히 붙일 때 |
| 날짜별 파일 + logging | 쉬움 | 있음 | 정리 코드 필요 | 스케줄러로 하루 몇 번 도는 스크립트 |
| TimedRotatingFileHandler | 보통 | 있음 | 자동 | 계속 떠 있는 서버·감시 프로그램 |
실수하기 쉬운 지점
- 로그 경로를 상대경로로 쓰는 것 — 가장 흔한 사고입니다. 작업 스케줄러에서 “시작 위치”를 비워두면 작업 디렉터리가
C:\Windows\System32가 되어, 로그가 그 폴더에 생기거나 권한 때문에 아예 안 만들어집니다. 항상 스크립트 파일 기준 절대경로로 잡으세요. - 인코딩 미지정 —
FileHandler에encoding="utf-8"을 빼면 윈도우 기본 코드페이지(CP949)로 저장돼서 한글 메시지가 다른 편집기에서 깨집니다. 참고로 Windows PowerShell 5.1의-Encoding utf8은 BOM이 붙은 UTF-8입니다. - 비밀정보를 그대로 찍는 것 — API 키, 토큰, 비밀번호를 로그에 남기지 마세요. 로그 파일은 평문이고 백업·클라우드 동기화에 같이 끌려 들어갑니다. 꼭 남겨야 하면 앞 네 글자만 찍습니다.
- 로그 형식을 매번 다르게 쓰는 것 —
format을 고정해두면 나중에findstr이나Select-String으로ERROR만 뽑아내기가 쉽습니다. 형식이 들쭉날쭉하면 그게 안 됩니다. - 성공했을 때 아무것도 안 남기는 것 — 실패만 기록하면 “실행 자체가 안 된 것”과 “실행됐지만 할 일이 없던 것”을 구분할 수 없습니다. 시작과 끝은 항상 남기세요.
마무리
지금 돌아가는 스크립트 하나를 열어서 print(를 logging.info(로 바꾸고, 맨 위에 1단계의 설정 블록을 붙이는 것부터 시작하면 됩니다. 5분이면 끝나는 작업이고, 다음에 그 스크립트가 조용히 실패했을 때 원인을 찾는 시간이 30분에서 1분으로 줄어듭니다.
다음 글에서는 AI로 글을 여러 편 뽑았을 때 그 글들이 서로 얼마나 겹치는지, n-gram으로 중복도를 직접 재보는 방법을 다루겠습니다.