실습: 안전한 임시 파일 관리
지금까지 배운 내용을 하나로 합칩니다. mktemp로 임시 파일을 만들고, trap으로 어떤 상황에서도 삭제를 보장하고, PID 파일로 중복 실행을 막고, 진행 상황을 실시간으로 표시하는 스크립트입니다.
시나리오는 이렇습니다. 대용량 로그 파일들이 있고, 각 파일에서 ERROR 패턴을 추출해 하나의 보고서로 만들어야 합니다. 이 작업은 시간이 걸리고, 중간에 중단될 수도 있습니다. 어떤 상황에서도 임시 파일이 남지 않아야 하고, 두 번 동시에 실행되면 안 됩니다.
safe_processor.sh 전체 코드
#!/usr/bin/env bash# 파일: safe_processor.sh# 용도: 로그 파일 대량 처리 (안전한 임시 파일 관리 실습)set -euo pipefail# ─── 설정 ─────────────────────────────────────────────────SCRIPT_NAME=$(basename "$0" .sh)PID_FILE="/tmp/${SCRIPT_NAME}.pid"LOG_DIR="${1:-/var/log}" # 처리할 로그 디렉토리 (기본: /var/log)OUTPUT_FILE="${2:-./error_report_$(date +%Y%m%d).txt}" # 출력 파일PATTERN="ERROR" # 검색 패턴# ─── 전역 변수 ───────────────────────────────────────────TEMP_DIR=""TOTAL_FILES=0PROCESSED=0ERRORS_FOUND=0START_TIME=$(date +%s)# ─── 유틸리티 함수 ───────────────────────────────────────log() { echo "[$(date '+%H:%M:%S')] $*"}progress() { local current="$1" local total="$2" local file="$3" local percent=$(( current * 100 / total )) local bar_width=30 local filled=$(( bar_width * percent / 100 )) local bar bar=$(printf '%0.s#' $(seq 1 "$filled") 2>/dev/null) bar+=$(printf '%0.s.' $(seq 1 $((bar_width - filled)) ) 2>/dev/null) printf "\r[%s] %3d%% (%d/%d) %s" "$bar" "$percent" "$current" "$total" "$file"}# ─── 클린업 함수 ─────────────────────────────────────────cleanup() { local exit_code=$? echo "" # 진행 표시줄 다음 줄로 이동 log "정리 시작..." # 1. 임시 디렉토리 삭제 if [[ -n "$TEMP_DIR" && -d "$TEMP_DIR" ]]; then rm -rf "$TEMP_DIR" log "임시 디렉토리 삭제 완료" fi # 2. PID 파일 삭제 rm -f "$PID_FILE" # 3. 종료 통계 local end_time end_time=$(date +%s) local elapsed=$((end_time - START_TIME)) echo "" echo "===== 실행 결과 =====" echo "처리 파일: ${PROCESSED}/${TOTAL_FILES}개" echo "발견된 오류: ${ERRORS_FOUND}건" echo "소요 시간: ${elapsed}초" if [[ $exit_code -ne 0 ]]; then echo "상태: 비정상 종료 (코드: $exit_code)" else echo "상태: 정상 완료" fi echo "=====================" exit "$exit_code"}trap cleanup EXIT# ─── Ctrl+C 처리 ─────────────────────────────────────────on_interrupt() { echo "" log "중단 요청을 받았습니다. 정리 후 종료합니다..." exit 130 # 관례: Ctrl+C 종료 코드}trap on_interrupt INT# ─── 중복 실행 방지 ──────────────────────────────────────if [[ -f "$PID_FILE" ]]; then existing_pid=$(cat "$PID_FILE") if kill -0 "$existing_pid" 2>/dev/null; then log "오류: 이미 실행 중입니다. (PID: $existing_pid)" log "중단하려면: kill $existing_pid" exit 1 else log "경고: 이전 PID 파일 발견. 제거 후 계속합니다." rm -f "$PID_FILE" fifiecho $$ > "$PID_FILE"log "시작. PID: $$"# ─── 임시 디렉토리 생성 ──────────────────────────────────TEMP_DIR=$(mktemp -d --suffix=".processor")log "임시 작업 디렉토리: $TEMP_DIR"# ─── 처리할 파일 목록 수집 ───────────────────────────────log "로그 파일 탐색 중: $LOG_DIR"mapfile -t log_files < <(find "$LOG_DIR" -maxdepth 2 -name "*.log" -type f 2>/dev/null | sort)TOTAL_FILES=${#log_files[@]}if [[ $TOTAL_FILES -eq 0 ]]; then log "처리할 .log 파일이 없습니다: $LOG_DIR" exit 0filog "총 ${TOTAL_FILES}개 파일 처리 시작"# ─── 파일별 처리 (병렬 + 진행 표시) ─────────────────────for log_file in "${log_files[@]}"; do PROCESSED=$((PROCESSED + 1)) filename=$(basename "$log_file") progress "$PROCESSED" "$TOTAL_FILES" "$filename" # 임시 디렉토리에서 개별 결과 파일 생성 result_file="${TEMP_DIR}/${PROCESSED}.result" # ERROR 패턴 추출 (파일이 없거나 권한 없으면 건너뜀) if error_lines=$(grep "$PATTERN" "$log_file" 2>/dev/null); then count=$(echo "$error_lines" | wc -l) ERRORS_FOUND=$((ERRORS_FOUND + count)) { echo "=== $log_file (${count}건) ===" echo "$error_lines" echo "" } > "$result_file" fidoneecho "" # 진행 표시줄 후 줄바꿈# ─── 결과 취합 ───────────────────────────────────────────log "결과 취합 중..."{ echo "로그 오류 분석 보고서" echo "생성 시각: $(date)" echo "분석 대상: $LOG_DIR" echo "총 파일: ${TOTAL_FILES}개" echo "총 오류: ${ERRORS_FOUND}건" echo "" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "" # 개별 결과 파일들을 순서대로 합치기 for result_file in "${TEMP_DIR}"/*.result; do [[ -f "$result_file" ]] && cat "$result_file" done} > "$OUTPUT_FILE"log "보고서 저장 완료: $OUTPUT_FILE"
스크립트 실행과 테스트
먼저 테스트용 로그 파일을 준비합니다.
# 테스트 로그 파일 생성mkdir -p /tmp/test_logsfor i in {1..5}; do { echo "[INFO] 서버 시작" echo "[ERROR] 데이터베이스 연결 실패 ($i)" echo "[INFO] 재시도 중..." echo "[ERROR] 타임아웃 발생 ($i)" echo "[INFO] 복구 완료" } > "/tmp/test_logs/app${i}.log"doneecho "테스트 파일 생성 완료"ls /tmp/test_logs/
스크립트를 실행합니다.
chmod +x safe_processor.sh# 기본 실행./safe_processor.sh /tmp/test_logs ./report.txt
정상 실행 결과입니다.
[10:45:00] 시작. PID: 23456
[10:45:00] 임시 작업 디렉토리: /tmp/tmp.XyZ789.processor
[10:45:00] 로그 파일 탐색 중: /tmp/test_logs
[10:45:00] 총 5개 파일 처리 시작
[##############################] 100% (5/5) app5.log
[10:45:01] 결과 취합 중...
[10:45:01] 보고서 저장 완료: ./report.txt
[10:45:01] 정리 시작...
[10:45:01] 임시 디렉토리 삭제 완료
===== 실행 결과 =====
처리 파일: 5/5개
발견된 오류: 10건
소요 시간: 1초
상태: 정상 완료
=====================
생성된 보고서를 확인합니다.
cat report.txt
로그 오류 분석 보고서
생성 시각: Thu Apr 24 10:45:01 KST 2026
분석 대상: /tmp/test_logs
총 파일: 5개
총 오류: 10건
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
=== /tmp/test_logs/app1.log (2건) ===
[ERROR] 데이터베이스 연결 실패 (1)
[ERROR] 타임아웃 발생 (1)
=== /tmp/test_logs/app2.log (2건) ===
[ERROR] 데이터베이스 연결 실패 (2)
...
비정상 종료 테스트
스크립트가 중단됐을 때 임시 파일이 남지 않는지 확인합니다.
# 터미널 1: 스크립트 실행 (느리게 동작하도록 수정 필요 시 sleep 추가)./safe_processor.sh /tmp/test_logs ./report.txt# 실행 중 Ctrl+C 누르기
Ctrl+C를 누르면 다음이 출력됩니다.
[######...............] 40% (2/5) app2.log
[10:45:02] 중단 요청을 받았습니다. 정리 후 종료합니다...
[10:45:02] 정리 시작...
[10:45:02] 임시 디렉토리 삭제 완료
===== 실행 결과 =====
처리 파일: 2/5개
발견된 오류: 4건
소요 시간: 2초
상태: 비정상 종료 (코드: 130)
=====================
임시 디렉토리가 삭제되었는지 확인합니다.
ls /tmp/*.processor 2>/dev/null || echo "임시 파일 없음 (정상)"
임시 파일 없음 (정상)
다른 터미널에서 kill로 종료해도 동일하게 cleanup이 실행됩니다.
# 터미널 2에서kill -TERM $(cat /tmp/safe_processor.pid)
중복 실행 방지 테스트
두 번째 실행 시도가 거부되는지 확인합니다.
# 터미널 1: 실행 중 유지./safe_processor.sh /tmp/test_logs ./report.txt &# 터미널 2: 두 번째 실행 시도./safe_processor.sh /tmp/test_logs ./report.txt
두 번째 실행 결과입니다.
[10:45:05] 오류: 이미 실행 중입니다. (PID: 23456)
[10:45:05] 중단하려면: kill 23456
이 스크립트는 PART 08에서 배운 핵심 패턴들을 모두 담고 있습니다. mktemp로 안전한 임시 파일 생성, trap EXIT로 종료 보장, PID 파일로 중복 방지, 진행 상황 표시까지 실전에서 바로 응용할 수 있는 구조입니다. 이 패턴을 PART 11의 견고한 스크립트 작성에서 더 발전시켜 나갑니다.