iBetter Books
수정

성능 측정과 최적화

스크립트가 느리면 두 가지를 먼저 의심합니다. 반복문 안에서 외부 명령어를 호출하거나, 같은 파일을 여러 번 읽는 것입니다. 두 문제 모두 패턴만 알면 간단히 해결됩니다.

그 전에 먼저 어디가 느린지 측정해야 합니다. 측정 없는 최적화는 막연한 추측입니다.

time 명령어

가장 간단한 측정 방법입니다. 명령어나 스크립트 앞에 time을 붙이면 실행 시간을 출력합니다.

time ./slow_script.sh

실행 결과입니다.

real    0m3.421s
user    0m0.183s
sys     0m0.245s

세 가지 시간의 의미입니다.

항목 의미
real 실제 경과 시간 (벽시계 시간)
user CPU가 사용자 코드를 실행한 시간
sys CPU가 커널 코드를 실행한 시간

realuser + 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 같은 스트림 처리 도구를 파이프라인으로 연결하면 대용량 파일도 효율적으로 처리할 수 있습니다.