성능 측정과 최적화
스크립트가 느리면 두 가지를 먼저 의심합니다. 반복문 안에서 외부 명령어를 호출하거나, 같은 파일을 여러 번 읽는 것입니다. 두 문제 모두 패턴만 알면 간단히 해결됩니다.
그 전에 먼저 어디가 느린지 측정해야 합니다. 측정 없는 최적화는 막연한 추측입니다.
time 명령어
가장 간단한 측정 방법입니다. 명령어나 스크립트 앞에 time을 붙이면 실행 시간을 출력합니다.
time ./slow_script.sh
실행 결과입니다.
real 0m3.421s
user 0m0.183s
sys 0m0.245s
세 가지 시간의 의미입니다.
| 항목 | 의미 |
|---|---|
| real | 실제 경과 시간 (벽시계 시간) |
| user | CPU가 사용자 코드를 실행한 시간 |
| sys | CPU가 커널 코드를 실행한 시간 |
real이 user + sys보다 훨씬 크다면 I/O 대기나 외부 명령어 실행 오버헤드가 주 원인입니다.
실행 시간 측정 — SECONDS와 date
스크립트 안에서 구간별 실행 시간을 측정합니다.
# 파일: measure_time.sh#!/usr/bin/env bash# 방법 1: SECONDS 변수 (초 단위, 소수점 없음)SECONDS=0# ... 작업 수행 ...sleep 2echo "경과 시간: ${SECONDS}초" # 2초# 방법 2: date +%s%N (나노초 단위, 밀리초까지 측정 가능)start_ns=$(date +%s%N)# ... 작업 수행 ...sleep 1end_ns=$(date +%s%N)elapsed_ms=$(( (end_ns - start_ns) / 1000000 ))echo "경과 시간: ${elapsed_ms}ms" # 약 1000ms# 방법 3: 구간별 측정benchmark() { local label="$1" local start=$SECONDS shift "$@" # 전달받은 명령어 실행 echo "[benchmark] ${label}: $((SECONDS - start))초"}benchmark "파일 정렬" sort -u /etc/passwd -o /tmp/sorted.txtbenchmark "단어 빈도" awk '{for(i=1;i<=NF;i++) freq[$i]++} END{for(w in freq) print freq[w],w}' /etc/passwd
성능 병목 패턴과 최적화
반복문 안에서 외부 명령어를 호출하는 것이 가장 흔한 병목입니다. 외부 명령어를 실행할 때마다 새 프로세스를 생성하는 오버헤드가 발생합니다.
| 느린 패턴 | 빠른 패턴 | 이유 |
|---|---|---|
cat file | grep |
grep file |
불필요한 cat 제거 |
for + echo 반복 |
printf 사용 |
서브쉘 최소화 |
$(cut -d: -f1) 반복 |
${var%%:*} |
외부 명령어 → 내장 확장 |
| 루프에서 파일 반복 읽기 | 변수에 한 번 저장 | I/O 최소화 |
$(wc -l < file) 루프 |
루프 밖에서 한 번 실행 | 프로세스 생성 최소화 |
실제 비교 예시입니다.
# 파일: performance_compare.sh#!/usr/bin/env bashTESTFILE="/etc/passwd"echo "=== 테스트 1: cat + grep vs grep ==="time (for i in {1..100}; do cat "$TESTFILE" | grep "root" > /dev/null; done)echo "---"time (for i in {1..100}; do grep "root" "$TESTFILE" > /dev/null; done)echo ""echo "=== 테스트 2: 외부 명령어 vs 매개변수 확장 ==="# 느린 방법: cut으로 첫 번째 필드 추출time ( while IFS= read -r line; do echo "$line" | cut -d: -f1 done < "$TESTFILE") > /dev/nullecho "---"# 빠른 방법: 매개변수 확장time ( while IFS=: read -r username _rest; do echo "$username" done < "$TESTFILE") > /dev/null
실행 결과(시스템에 따라 다름)입니다.
=== 테스트 1: cat + grep vs grep ===
real 0m0.412s ← cat + grep (느림)
---
real 0m0.198s ← grep만 (약 2배 빠름)
=== 테스트 2: 외부 명령어 vs 매개변수 확장 ===
real 0m0.891s ← cut 사용 (느림)
---
real 0m0.043s ← 매개변수 확장 (약 20배 빠름)
루프 안에서 외부 명령어 최소화
가장 효과적인 최적화입니다.
# 파일: loop_optimization.sh#!/usr/bin/env bash# 느린 방법: 루프마다 wc 실행echo "=== 느린 방법 ==="total_lines=0for file in /etc/*.conf; do lines=$(wc -l < "$file") # 파일마다 wc 프로세스 생성 total_lines=$((total_lines + lines))doneecho "총 줄 수: $total_lines"# 빠른 방법: wc를 한 번만 실행echo "=== 빠른 방법 ==="total_lines=$(cat /etc/*.conf | wc -l) # 한 번의 wcecho "총 줄 수: $total_lines"# 또는 xargs 활용total_lines=$(wc -l /etc/*.conf | tail -1 | awk '{print $1}')echo "총 줄 수: $total_lines"
병렬 처리 — xargs -P와 GNU Parallel
독립적인 작업이 많을 때는 병렬로 실행해서 시간을 줄일 수 있습니다.
# 파일: parallel_processing.sh#!/usr/bin/env bash# 방법 1: xargs -P (병렬 프로세스 개수 지정)# CPU 코어 수만큼 병렬로 파일 압축find /tmp/logs -name "*.log" | xargs -P 4 -I{} gzip {}# 방법 2: GNU Parallel (더 세밀한 제어)# parallel이 설치되어 있는 경우# find /tmp/logs -name "*.log" | parallel -j4 gzip {}# 예시: 10개 URL을 4개씩 병렬로 다운로드urls=( "https://example.com/file1.txt" "https://example.com/file2.txt" # ...)printf '%s\n' "${urls[@]}" | xargs -P 4 -I{} curl -sO {}
xargs -P N은 최대 N개의 프로세스를 동시에 실행합니다. CPU 코어 수를 동적으로 얻으려면 $(nproc) 또는 $(sysctl -n hw.ncpu)를 사용합니다.
실습 — 느린 스크립트 최적화 후 성능 비교
10,000줄 로그 파일에서 에러 라인의 IP 주소를 추출하는 스크립트를 최적화합니다.
#!/usr/bin/env bash# 새 파일: optimize_demo.sh# 테스트 데이터 생성generate_test_log() { local lines="${1:-10000}" local file="/tmp/test.log" for i in $(seq 1 "$lines"); do local level if (( i % 10 == 0 )); then level="ERROR" else level="INFO" fi echo "$(date '+%Y-%m-%d %H:%M:%S') [$level] 192.168.1.$((RANDOM % 254 + 1)) request processed" done > "$file" echo "$file"}LOG_FILE=$(generate_test_log 10000)echo "테스트 파일: $LOG_FILE ($(wc -l < "$LOG_FILE")줄)"echo ""# 느린 방법: 루프에서 grep + cutecho "=== 느린 방법 ==="time ( error_ips=() while IFS= read -r line; do if echo "$line" | grep -q "ERROR"; then ip=$(echo "$line" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+') error_ips+=("$ip") fi done < "$LOG_FILE" echo "에러 IP 개수: ${#error_ips[@]}")echo ""# 빠른 방법: grep + awk 한 번에echo "=== 빠른 방법 ==="time ( count=$(grep "ERROR" "$LOG_FILE" | \ grep -oE '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' | \ wc -l) echo "에러 IP 개수: $count")rm -f "$LOG_FILE"
빠른 방법이 수십 배 이상 빠릅니다. 쉘 내장 루프 대신 grep, awk 같은 스트림 처리 도구를 파이프라인으로 연결하면 대용량 파일도 효율적으로 처리할 수 있습니다.