이미지 로딩 중...
AI Generated
2025. 11. 5. · 5 Views
Shell Script 실무 활용 팁
실무에서 바로 사용할 수 있는 Shell Script 고급 기법과 최적화 팁을 소개합니다. 자동화, 에러 처리, 성능 최적화까지 실전 노하우를 담았습니다.
목차
- 안전한 스크립트 작성을 위한 set 옵션
- 강력한 문자열 처리와 변수 확장
- 병렬 처리로 성능 극대화하기
- 함수와 모듈화로 재사용 가능한 스크립트 만들기
- 배열과 연관 배열 활용하기
- 프로세스 대체와 명령 치환 마스터하기
- 신호 처리와 트랩으로 안정적인 스크립트 만들기
- 고급 리다이렉션과 파일 디스크립터 활용
1. 안전한 스크립트 작성을 위한 set 옵션
시작하며
여러분이 배포 스크립트를 실행했는데 중간에 에러가 났지만 스크립트가 계속 실행되어 더 큰 문제를 일으킨 경험이 있나요? 또는 변수명을 잘못 입력해서 빈 값으로 처리되어 예상치 못한 결과가 나온 적이 있나요?
이런 문제들은 실제 운영 환경에서 치명적인 장애로 이어질 수 있습니다. 특히 자동화된 배포나 시스템 관리 스크립트에서 이런 실수는 서비스 중단으로 직결될 수 있죠.
바로 이럴 때 필요한 것이 Shell Script의 set 옵션입니다. 몇 줄의 설정만으로 스크립트의 안정성을 크게 향상시킬 수 있습니다.
개요
간단히 말해서, set 옵션은 Shell Script의 동작 방식을 제어하는 내장 명령어입니다. 스크립트 실행 중 에러 발생 시 즉시 중단하거나, 미정의 변수 사용을 방지하는 등 안전장치 역할을 합니다.
실무에서 CI/CD 파이프라인을 구성하거나 서버 프로비저닝 스크립트를 작성할 때 필수적입니다. 예를 들어, 데이터베이스 마이그레이션 스크립트나 대량 파일 처리 작업에서 매우 유용합니다.
기존에는 각 명령어마다 에러 체크 코드를 작성했다면, 이제는 스크립트 상단에 몇 줄만 추가하면 전체적인 에러 처리가 가능합니다. 핵심 옵션으로는 -e(에러 시 종료), -u(미정의 변수 에러), -x(디버깅), -o pipefail(파이프라인 에러 감지) 등이 있습니다.
이러한 옵션들이 조합되면 견고하고 디버깅이 쉬운 스크립트를 만들 수 있습니다.
코드 예제
#!/bin/bash
# 안전한 스크립트 실행을 위한 설정
set -euo pipefail
# 디버깅이 필요한 경우 아래 주석 해제
# set -x
# 트랩으로 에러 발생 위치 추적
trap 'echo "Error at line $LINENO"' ERR
# 안전한 변수 사용
DB_NAME="${1:-default_db}" # 기본값 설정
BACKUP_DIR="/backup/${DB_NAME}"
# 디렉토리 생성 (실패 시 스크립트 중단)
mkdir -p "${BACKUP_DIR}"
# 백업 실행
mysqldump "${DB_NAME}" > "${BACKUP_DIR}/backup_$(date +%Y%m%d).sql"
설명
이것이 하는 일: set 옵션은 Shell Script의 전체적인 실행 환경을 설정하여, 에러 상황에서 스크립트가 어떻게 동작할지를 결정합니다. 첫 번째로, set -e 옵션은 명령어가 0이 아닌 종료 코드를 반환하면 즉시 스크립트를 중단시킵니다.
예를 들어 파일 복사가 실패했는데 다음 단계로 진행되는 것을 방지합니다. 이는 연쇄적인 오류를 막는 첫 번째 방어선입니다.
두 번째로, set -u 옵션은 정의되지 않은 변수를 사용하려 할 때 에러를 발생시킵니다. rm -rf /$UNDEFINED_VAR/* 같은 치명적인 실수를 방지할 수 있죠.
변수명 오타로 인한 사고를 원천적으로 차단합니다. 세 번째로, set -o pipefail은 파이프라인에서 하나라도 실패하면 전체를 실패로 처리합니다.
보통 파이프의 마지막 명령어 결과만 체크하는데, 이 옵션으로 중간 과정의 에러도 감지할 수 있습니다. 마지막으로, trap 명령어를 사용하면 에러 발생 시 정확한 라인 번호를 출력하여 디버깅을 쉽게 만들어줍니다.
특히 긴 스크립트에서 문제 지점을 빠르게 찾을 수 있습니다. 여러분이 이 설정을 사용하면 프로덕션 환경에서의 실수를 크게 줄이고, 문제 발생 시 빠른 대응이 가능해집니다.
특히 자동화된 배포나 백업 스크립트에서는 필수적인 안전장치입니다.
실전 팁
💡 스크립트 시작 부분에 set -euo pipefail을 항상 추가하는 습관을 들이세요. 이것만으로도 80% 이상의 실수를 방지할 수 있습니다.
💡 특정 명령의 실패를 무시하려면 command || true 형태로 작성하세요. 예: rm non_existent_file || true
💡 디버깅 시 set -x를 추가하면 실행되는 모든 명령어를 출력하여 문제를 쉽게 찾을 수 있습니다.
💡 함수 내부에서도 local 키워드로 지역 변수를 선언하여 변수 충돌을 방지하세요.
💡 CI/CD 파이프라인 스크립트에서는 set -e와 함께 각 단계별 로깅을 추가하여 실패 지점을 명확히 파악하세요.
2. 강력한 문자열 처리와 변수 확장
시작하며
여러분이 설정 파일에서 특정 값을 추출하거나, 파일명을 일괄 변경하거나, 경로에서 디렉토리명만 뽑아내야 할 때 어떻게 하시나요? sed나 awk를 사용하시나요?
사실 Bash 자체의 변수 확장 기능만으로도 대부분의 문자열 처리가 가능합니다. 외부 명령어를 호출하지 않아 성능도 더 빠르고, 코드도 더 간결해집니다.
이번에는 실무에서 자주 사용되는 Bash의 강력한 변수 확장과 문자열 처리 기법을 알아보겠습니다. 이것만 알아도 스크립트 작성 속도가 2배는 빨라집니다.
개요
간단히 말해서, Bash 변수 확장은 ${variable} 형태로 변수를 조작하는 내장 기능입니다. 문자열 자르기, 치환, 대소문자 변환, 기본값 설정 등이 모두 가능합니다.
파일 경로 처리, 설정값 파싱, 로그 분석 등 거의 모든 Shell Script 작업에서 활용됩니다. 예를 들어, 수백 개의 파일 확장자를 변경하거나, 환경 변수를 동적으로 처리할 때 매우 유용합니다.
기존에는 echo $var | sed 's/old/new/' 같은 방식을 사용했다면, 이제는 ${var//old/new}로 한 번에 처리할 수 있습니다. 프로세스 생성 오버헤드가 없어 훨씬 빠릅니다.
주요 기능으로는 부분 문자열 추출(${var:offset:length}), 패턴 매칭 삭제(${var#pattern}), 치환(${var/old/new}), 대소문자 변환(${var^^}) 등이 있습니다. 이들을 조합하면 복잡한 문자열 처리도 가능합니다.
코드 예제
#!/bin/bash
# 다양한 변수 확장 예제
FILE_PATH="/home/user/documents/report_2024.pdf"
# 파일명만 추출
FILENAME="${FILE_PATH##*/}" # report_2024.pdf
# 디렉토리만 추출
DIR="${FILE_PATH%/*}" # /home/user/documents
# 확장자 제거
NAME="${FILENAME%.*}" # report_2024
# 확장자만 추출
EXT="${FILENAME##*.}" # pdf
# 문자열 치환과 변환
NEW_NAME="${NAME/2024/2025}" # report_2025
UPPER="${NEW_NAME^^}" # REPORT_2025
# 배열과 함께 사용
FILES=(*.txt)
echo "Found ${#FILES[@]} text files" # 파일 개수
설명
이것이 하는 일: Bash 변수 확장은 변수의 값을 다양한 방식으로 조작하고 변환하는 내장 메커니즘입니다. 별도의 프로세스를 생성하지 않고 Shell 내부에서 직접 처리합니다.
첫 번째로, ${var##*/}와 ${var%/*} 같은 패턴 매칭 삭제를 살펴보겠습니다. ##는 왼쪽에서 가장 긴 패턴을, %는 오른쪽에서 가장 짧은 패턴을 삭제합니다.
파일 경로에서 파일명이나 디렉토리를 추출할 때 완벽한 도구입니다. 두 번째로, ${var/old/new}는 문자열 치환을 수행합니다.
한 번만 치환하려면 /를, 모두 치환하려면 //를 사용합니다. 이는 설정 파일의 템플릿 처리나 동적 스크립트 생성에 매우 유용합니다.
세 번째로, ${var^^}와 ${var,,}는 대소문자 변환을 담당합니다. 환경 변수 정규화나 파일명 표준화 작업에서 자주 사용됩니다.
첫 글자만 변환하려면 ^나 ,를 하나만 사용하면 됩니다. 네 번째로, ${#var}는 문자열 길이를, ${#array[@]}는 배열 크기를 반환합니다.
입력 검증이나 배열 순회에 필수적인 기능입니다. 여러분이 이러한 변수 확장을 마스터하면 Shell Script 작성 속도가 현저히 빨라지고, 코드도 더 읽기 쉬워집니다.
특히 DevOps 작업에서 파일 처리나 설정 관리 시 큰 효율성 향상을 경험하실 수 있습니다.
실전 팁
💡 ${var:-default}를 사용하면 변수가 비어있을 때 기본값을 제공할 수 있어 안전한 스크립트 작성이 가능합니다.
💡 파일 경로 작업 시 dirname과 basename 대신 변수 확장을 사용하면 성능이 10배 이상 빨라집니다.
💡 ${!prefix*}를 사용하면 특정 접두사로 시작하는 모든 변수명을 가져올 수 있어 동적 설정 처리에 유용합니다.
💡 복잡한 치환은 여러 단계로 나누어 처리하면 가독성이 좋아집니다. 한 줄에 모든 것을 하려고 하지 마세요.
3. 병렬 처리로 성능 극대화하기
시작하며
여러분이 수천 개의 로그 파일을 분석하거나, 대량의 이미지를 변환하거나, 여러 서버에 동시에 명령을 실행해야 한다면 어떻게 하시나요? for 루프로 하나씩 처리하면 몇 시간이 걸릴 작업이죠.
실제로 많은 개발자들이 Shell Script는 느리다고 생각하지만, 이는 순차 처리만 사용하기 때문입니다. 병렬 처리를 활용하면 작업 시간을 10분의 1로 줄일 수 있습니다.
이번에는 GNU Parallel, xargs, 백그라운드 작업(&)을 활용한 병렬 처리 기법을 알아보겠습니다. CPU 코어를 최대한 활용하여 작업 효율을 극대화하는 방법을 배워봅시다.
개요
간단히 말해서, Shell Script에서의 병렬 처리는 여러 작업을 동시에 실행하여 전체 처리 시간을 단축하는 기법입니다. CPU의 모든 코어를 활용하여 처리 속도를 크게 향상시킬 수 있습니다.
대량의 파일 처리, API 호출, 데이터 변환, 서버 배포 등 반복적인 작업에서 필수적입니다. 예를 들어, 1000개의 이미지 리사이징이나 100대 서버의 로그 수집 같은 작업에서 극적인 성능 향상을 보입니다.
기존에는 for 루프로 순차적으로 처리했다면, 이제는 모든 CPU 코어를 동시에 활용하여 병렬로 처리할 수 있습니다. 8코어 CPU에서는 이론적으로 8배 빠른 처리가 가능합니다.
주요 도구로는 GNU Parallel(가장 강력), xargs -P(간단한 병렬화), 백그라운드 작업과 wait(기본 병렬화) 등이 있습니다. 각 도구는 상황에 맞는 장단점이 있어 적절히 선택해야 합니다.
코드 예제
#!/bin/bash
# 병렬 처리 예제 - 이미지 변환
# 방법 1: GNU Parallel 사용 (가장 강력)
find . -name "*.jpg" | parallel -j 8 convert {} {.}_thumb.png
# 방법 2: xargs 병렬 처리
find . -name "*.log" | xargs -P 4 -I {} gzip {}
# 방법 3: 백그라운드 작업과 wait
process_file() {
echo "Processing $1"
# 실제 처리 로직
sleep 1
}
export -f process_file
# 동시 실행 수 제한
MAX_JOBS=4
for file in *.txt; do
while [ $(jobs -r | wc -l) -ge $MAX_JOBS ]; do
sleep 0.1
done
process_file "$file" &
done
wait # 모든 백그라운드 작업 완료 대기
설명
이것이 하는 일: 병렬 처리는 여러 작업을 동시에 실행하여 시스템 자원을 최대한 활용하고 전체 작업 시간을 단축시킵니다. 첫 번째로, GNU Parallel은 가장 강력한 병렬 처리 도구입니다.
-j 옵션으로 동시 작업 수를 지정하고, 자동으로 부하 분산과 출력 정렬을 처리합니다. 복잡한 명령어 조합이나 원격 서버 작업에도 사용할 수 있어 매우 유연합니다.
두 번째로, xargs의 -P 옵션은 간단한 병렬화에 적합합니다. 표준 입력으로 받은 항목들을 지정된 수만큼 병렬로 처리합니다.
GNU Parallel보다 단순하지만, 대부분의 시스템에 기본으로 설치되어 있어 접근성이 좋습니다. 세 번째로, 백그라운드 작업(&)과 wait 조합은 가장 기본적인 병렬화 방법입니다.
각 작업을 백그라운드로 실행하고 wait으로 완료를 기다립니다. 동시 실행 수를 제한하려면 jobs 명령어로 현재 실행 중인 작업 수를 체크합니다.
네 번째로, 프로세스 간 통신이 필요한 경우 named pipe나 임시 파일을 활용합니다. 각 병렬 작업의 결과를 수집하고 통합하는 데 유용합니다.
여러분이 이러한 병렬 처리 기법을 활용하면 대량 데이터 처리 시간을 획기적으로 단축할 수 있습니다. 특히 로그 분석, 이미지 처리, 배치 작업에서 10배 이상의 성능 향상을 경험하실 수 있습니다.
실전 팁
💡 CPU 코어 수는 nproc 명령어로 확인하고, 병렬 작업 수는 코어 수의 1.5~2배로 설정하면 최적 성능을 얻을 수 있습니다.
💡 I/O 집약적 작업은 CPU 코어 수보다 많은 병렬 처리가 가능하지만, CPU 집약적 작업은 코어 수를 초과하지 마세요.
💡 GNU Parallel의 --bar 옵션을 사용하면 진행 상황을 시각적으로 확인할 수 있어 긴 작업에 유용합니다.
💡 메모리 사용량을 모니터링하면서 병렬 수를 조정하세요. 너무 많은 병렬 작업은 메모리 부족을 일으킬 수 있습니다.
💡 로그 파일이 뒤섞이지 않도록 각 병렬 작업의 출력을 별도 파일로 리다이렉트하고 나중에 병합하세요.
4. 함수와 모듈화로 재사용 가능한 스크립트 만들기
시작하며
여러분이 비슷한 코드를 여러 스크립트에 복사-붙여넣기 하고 있다면, 이제 그만하실 시간입니다. 똑같은 에러 처리, 로깅, 유효성 검사 코드를 매번 다시 작성하는 것은 시간 낭비일 뿐만 아니라 유지보수의 악몽입니다.
한 곳에서 버그를 수정했는데 다른 스크립트에는 여전히 버그가 남아있는 경험, 다들 있으시죠? 이런 문제는 코드 중복 때문에 발생합니다.
Shell Script도 다른 프로그래밍 언어처럼 함수와 모듈로 구조화할 수 있습니다. 이번에는 재사용 가능하고 유지보수가 쉬운 Shell Script를 작성하는 방법을 알아보겠습니다.
개요
간단히 말해서, Shell Script의 함수는 반복적으로 사용되는 코드를 캡슐화하여 재사용 가능하게 만드는 구조입니다. 별도의 라이브러리 파일로 분리하여 여러 스크립트에서 공유할 수도 있습니다.
로깅, 에러 처리, 설정 파싱, 유효성 검사 등 공통 기능을 함수로 만들면 코드 품질이 크게 향상됩니다. 예를 들어, 모든 스크립트에서 동일한 로깅 형식을 사용하거나, 표준화된 에러 메시지를 출력할 수 있습니다.
기존에는 긴 스크립트를 위에서 아래로 쭉 작성했다면, 이제는 기능별로 함수를 만들고 main 함수에서 조합하는 구조적 프로그래밍이 가능합니다. 핵심 기능으로는 함수 정의와 호출, 지역 변수(local), 반환값 처리, source를 통한 외부 스크립트 로드 등이 있습니다.
이를 활용하면 대규모 Shell Script 프로젝트도 체계적으로 관리할 수 있습니다.
코드 예제
#!/bin/bash
# lib/common.sh - 공통 라이브러리
log() {
local level="$1"
shift
echo "[$(date +'%Y-%m-%d %H:%M:%S')] [$level] $*" >&2
}
die() {
log "ERROR" "$@"
exit 1
}
retry() {
local max_attempts="$1"
local delay="$2"
shift 2
local attempt=1
until "$@"; do
if [ $attempt -ge $max_attempts ]; then
log "ERROR" "Command failed after $max_attempts attempts: $*"
return 1
fi
log "WARN" "Attempt $attempt failed, retrying in ${delay}s..."
sleep "$delay"
((attempt++))
done
}
# main.sh - 메인 스크립트
source lib/common.sh
log "INFO" "Starting application"
retry 3 5 curl -f http://api.example.com || die "API unreachable"
설명
이것이 하는 일: 함수와 모듈화는 Shell Script를 논리적 단위로 분리하여 코드 재사용성을 높이고 유지보수를 쉽게 만듭니다. 첫 번째로, 함수 정의는 기능을 캡슐화하는 기본 단위입니다.
function_name() { ... } 형태로 정의하고, 매개변수는 $1, $2 등으로 접근합니다.
함수 내부에서 local 키워드로 지역 변수를 선언하면 전역 변수 오염을 방지할 수 있습니다. 두 번째로, source 또는 . 명령어로 외부 스크립트를 로드할 수 있습니다.
이를 통해 공통 함수들을 별도 파일로 분리하고 필요한 곳에서 불러와 사용합니다. 마치 다른 언어의 import나 require와 같은 역할입니다.
세 번째로, 에러 처리와 로깅을 표준화합니다. 예제의 log 함수는 일관된 형식으로 로그를 출력하고, die 함수는 에러 메시지와 함께 스크립트를 종료합니다.
모든 스크립트에서 동일한 방식으로 사용할 수 있습니다. 네 번째로, retry 같은 고급 패턴을 함수로 구현하면 복잡한 로직도 간단하게 재사용할 수 있습니다.
네트워크 요청이나 불안정한 작업에 재시도 로직을 쉽게 추가할 수 있습니다. 여러분이 이러한 모듈화 기법을 적용하면 수백 줄의 스크립트도 관리 가능한 작은 단위로 나눌 수 있습니다.
특히 팀 프로젝트에서 공통 라이브러리를 공유하면 개발 속도와 코드 품질이 동시에 향상됩니다.
실전 팁
💡 함수명은 동사로 시작하고 명확한 의미를 담아 작성하세요. 예: validate_input, process_file, send_notification
💡 함수의 반환값은 return으로, 출력값은 echo로 구분하세요. 반환값은 상태 코드(0=성공), 출력값은 실제 데이터입니다.
💡 공통 라이브러리는 ~/.bash_lib/ 같은 중앙 위치에 모아두고 PATH에 추가하면 어디서나 사용할 수 있습니다.
💡 함수 시작 부분에 간단한 사용법 주석을 추가하면 다른 개발자(미래의 자신 포함)가 쉽게 이해할 수 있습니다.
5. 배열과 연관 배열 활용하기
시작하며
여러분이 여러 서버의 정보를 관리하거나, 설정값들을 그룹화하거나, 동적으로 생성되는 데이터를 처리해야 할 때 단순 변수만으로는 한계가 있죠? 변수명에 숫자를 붙여가며 SERVER1, SERVER2, SERVER3...
이런 식으로 만들고 계신가요? Bash 4.0부터 지원되는 연관 배열(Associative Array)을 사용하면 이런 문제를 깔끔하게 해결할 수 있습니다.
마치 다른 언어의 HashMap이나 Dictionary처럼 키-값 쌍으로 데이터를 관리할 수 있습니다. 이번에는 일반 배열과 연관 배열을 활용하여 복잡한 데이터 구조를 다루는 방법을 알아보겠습니다.
설정 관리, 데이터 집계, 동적 처리에 매우 유용한 기법들입니다.
개요
간단히 말해서, Bash의 배열은 여러 값을 하나의 변수에 저장하는 데이터 구조입니다. 일반 배열은 인덱스로, 연관 배열은 문자열 키로 접근합니다.
서버 목록 관리, 설정값 그룹화, 통계 데이터 수집, 명령줄 인자 파싱 등에 필수적입니다. 예를 들어, 각 환경별 데이터베이스 연결 정보나 파일별 처리 상태를 추적할 때 매우 효과적입니다.
기존에는 여러 개의 단순 변수를 만들거나 임시 파일을 사용했다면, 이제는 메모리 내에서 구조화된 데이터를 효율적으로 관리할 수 있습니다. 주요 기능으로는 배열 선언(declare -a/declare -A), 요소 추가/삭제, 반복 처리, 크기 확인, 키/값 추출 등이 있습니다.
이를 조합하면 복잡한 데이터 처리도 간단하게 구현할 수 있습니다.
코드 예제
#!/bin/bash
# 일반 배열 사용
servers=("web01" "web02" "db01" "db02")
# 요소 추가
servers+=("cache01")
# 배열 순회
for server in "${servers[@]}"; do
echo "Checking $server..."
ping -c 1 "$server" > /dev/null 2>&1 && echo " ✓ $server is up"
done
# 연관 배열 (Bash 4.0+)
declare -A config
config[host]="localhost"
config[port]="3306"
config[user]="admin"
config[pass]="secret"
# 연관 배열 순회
for key in "${!config[@]}"; do
echo "$key = ${config[$key]}"
done
# 동적 데이터 집계
declare -A file_counts
while IFS= read -r file; do
ext="${file##*.}"
((file_counts[$ext]++))
done < <(find . -type f)
echo "File statistics:"
for ext in "${!file_counts[@]}"; do
echo " .$ext: ${file_counts[$ext]} files"
done
설명
이것이 하는 일: 배열과 연관 배열은 여러 관련 데이터를 하나의 변수로 관리하여 코드를 간결하고 효율적으로 만듭니다. 첫 번째로, 일반 배열은 순서가 있는 데이터 목록을 저장합니다.
array=(value1 value2) 형태로 초기화하고, ${array[@]}로 모든 요소에 접근합니다. 서버 목록이나 파일 목록 같은 순차적 데이터 처리에 이상적입니다.
두 번째로, 연관 배열은 키-값 쌍으로 데이터를 저장합니다. declare -A로 선언하고, array[key]=value 형태로 값을 할당합니다.
설정 파일 파싱이나 데이터 그룹화에 매우 유용합니다. ${!array[@]}로 모든 키를, ${array[@]}로 모든 값을 가져올 수 있습니다.
세 번째로, 배열의 동적 처리가 가능합니다. += 연산자로 요소를 추가하고, unset array[index]로 삭제할 수 있습니다.
실행 중에 데이터가 변하는 상황에서도 유연하게 대응할 수 있습니다. 네 번째로, 파일 통계 예제처럼 연관 배열을 카운터로 사용할 수 있습니다.
각 파일 확장자별로 개수를 세는 등의 집계 작업이 매우 간단해집니다. SQL의 GROUP BY와 유사한 효과를 얻을 수 있습니다.
여러분이 배열을 적극 활용하면 복잡한 데이터 처리 로직을 크게 단순화할 수 있습니다. 특히 여러 서버 관리, 동적 설정 처리, 로그 분석 등에서 코드 가독성과 유지보수성이 현저히 향상됩니다.
실전 팁
💡 배열을 함수 인자로 전달할 때는 "${array[@]}"처럼 따옴표로 감싸야 공백이 포함된 요소도 제대로 전달됩니다.
💡 연관 배열은 Bash 4.0 이상에서만 지원되므로, 스크립트 시작 부분에 버전 체크를 추가하세요: [[ ${BASH_VERSION%%.*} -lt 4 ]] && exit 1
💡 배열 크기는 ${#array[@]}로, 특정 요소의 길이는 ${#array[index]}로 확인할 수 있습니다.
💡 JSON 데이터를 파싱할 때 jq와 연관 배열을 조합하면 매우 강력한 데이터 처리가 가능합니다.
💡 대용량 배열은 메모리를 많이 사용하므로, 필요한 경우 임시 파일이나 데이터베이스 사용을 고려하세요.
6. 프로세스 대체와 명령 치환 마스터하기
시작하며
여러분이 임시 파일을 만들어서 데이터를 전달하고, 다시 그 파일을 읽고, 마지막에 삭제하는 번거로운 작업을 반복하고 있다면, 이제 그런 방식은 잊어버리세요. 임시 파일 없이도 프로세스 간 데이터를 주고받을 수 있습니다.
또한 두 명령의 출력을 동시에 비교하거나, 한 명령의 출력을 여러 명령의 입력으로 동시에 사용해야 할 때 어떻게 하시나요? 프로세스 대체를 사용하면 이 모든 것이 가능합니다.
이번에는 Shell Script의 숨겨진 강력한 기능인 프로세스 대체(Process Substitution)와 명령 치환(Command Substitution)을 마스터해보겠습니다.
개요
간단히 말해서, 프로세스 대체는 <(command) 형태로 명령의 출력을 파일처럼 사용하는 기법이고, 명령 치환은 $(command) 형태로 명령의 출력을 변수나 인자로 사용하는 기법입니다. 로그 비교, 실시간 데이터 처리, 파이프라인 구성, 동적 설정 로드 등에 매우 유용합니다.
예를 들어, 두 서버의 설정 파일을 실시간으로 비교하거나, 여러 소스의 데이터를 동시에 처리할 때 필수적입니다. 기존에는 임시 파일을 생성하고 삭제하는 번거로운 과정이 필요했다면, 이제는 메모리 내에서 직접 처리하여 더 빠르고 깔끔한 코드를 작성할 수 있습니다.
주요 기능으로는 입력 프로세스 대체(<()), 출력 프로세스 대체(>()), 명령 치환($()), Here Document(<<EOF) 등이 있습니다. 이들을 조합하면 복잡한 데이터 흐름도 우아하게 처리할 수 있습니다.
코드 예제
#!/bin/bash
# 프로세스 대체 - 두 명령 출력 비교
diff <(ls -la /etc) <(ssh remote "ls -la /etc")
# 여러 로그 파일 동시 모니터링
tail -f /var/log/app.log | tee >(grep ERROR > errors.log) \
>(grep WARN > warnings.log) \
>(grep INFO > info.log)
# 명령 치환과 함께 사용
config=$(cat <<EOF
server {
host: $(hostname)
date: $(date +%Y-%m-%d)
users: $(who | wc -l)
}
EOF
)
# 동적 파일 목록 처리
while IFS= read -r file; do
echo "Processing: $file"
# 파일 처리 로직
done < <(find . -type f -name "*.log" -mtime -7)
# 백업과 압축을 동시에
tar cf - /data | tee >(gzip > backup.tar.gz) \
>(bzip2 > backup.tar.bz2) \
> /dev/null
설명
이것이 하는 일: 프로세스 대체와 명령 치환은 프로세스 간 데이터 교환을 파일 시스템을 거치지 않고 직접 수행하는 고급 Shell 기능입니다. 첫 번째로, <(command) 형태의 입력 프로세스 대체는 명령의 출력을 마치 파일인 것처럼 다룰 수 있게 합니다.
diff나 comm 같은 파일 비교 명령에 실시간 데이터를 전달할 때 매우 유용합니다. 실제로는 /dev/fd/의 파일 디스크립터를 사용합니다.
두 번째로, >(command) 형태의 출력 프로세스 대체는 하나의 출력을 여러 명령으로 동시에 보낼 수 있게 합니다. tee 명령과 조합하면 로그를 여러 기준으로 분류하거나 여러 형식으로 동시 저장할 수 있습니다.
세 번째로, $(command) 형태의 명령 치환은 명령 실행 결과를 문자열로 치환합니다. 백틱(`)보다 가독성이 좋고 중첩도 가능합니다.
동적으로 값을 생성하거나 설정 파일을 구성할 때 필수적입니다. 네 번째로, < <(command) 패턴은 while 루프와 함께 사용하여 명령 출력을 한 줄씩 처리할 때 유용합니다.
파이프와 달리 서브쉘을 생성하지 않아 루프 내에서 변수를 수정해도 유지됩니다. 여러분이 이러한 기법을 마스터하면 복잡한 데이터 파이프라인을 간결하게 구성할 수 있습니다.
특히 실시간 로그 처리, 서버 간 설정 동기화, 멀티 포맷 백업 등에서 코드를 크게 단순화할 수 있습니다.
실전 팁
💡 프로세스 대체는 bash와 zsh에서만 지원되므로, 이식성이 필요한 경우 shebang을 #!/bin/bash로 명시하세요.
💡 >(command) 사용 시 명령이 비동기로 실행되므로, 필요한 경우 wait을 사용하여 완료를 기다리세요.
💡 명령 치환 내에서 에러가 발생해도 무시되므로, 중요한 명령은 별도로 에러 체크를 하세요.
💡 대용량 데이터는 프로세스 대체보다 파이프가 더 효율적일 수 있으므로 상황에 맞게 선택하세요.
7. 신호 처리와 트랩으로 안정적인 스크립트 만들기
시작하며
여러분의 스크립트가 실행 중에 Ctrl+C로 중단되었을 때, 임시 파일이 그대로 남아있거나 락 파일이 해제되지 않아 다음 실행이 막힌 경험이 있으신가요? 또는 스크립트가 비정상 종료되어 데이터베이스 연결이 끊기지 않은 채로 남아있었나요?
이런 문제들은 적절한 신호 처리가 없어서 발생합니다. 스크립트가 어떤 이유로든 종료될 때 정리 작업을 수행해야 하는데, 이를 놓치면 시스템에 쓰레기가 쌓이게 됩니다.
이번에는 trap 명령을 사용한 신호 처리로 어떤 상황에서도 깔끔하게 정리되는 안정적인 스크립트를 만드는 방법을 알아보겠습니다.
개요
간단히 말해서, trap은 특정 신호(signal)를 받았을 때 실행할 명령을 지정하는 Shell 내장 명령어입니다. 스크립트 종료 시 자동으로 정리 작업을 수행하도록 보장합니다.
임시 파일 정리, 락 파일 해제, 데이터베이스 연결 종료, 프로세스 정리 등 모든 정리 작업에 필수적입니다. 예를 들어, 배포 스크립트가 중단되어도 시스템을 일관된 상태로 유지할 수 있습니다.
기존에는 스크립트 끝에 정리 코드를 넣었지만 비정상 종료 시 실행되지 않았다면, 이제는 어떤 방식으로 종료되든 항상 정리 작업이 실행되도록 보장할 수 있습니다. 주요 신호로는 EXIT(종료), INT(Ctrl+C), TERM(종료 요청), ERR(에러 발생), DEBUG(디버깅) 등이 있습니다.
각 신호에 맞는 핸들러를 설정하여 견고한 스크립트를 만들 수 있습니다.
코드 예제
#!/bin/bash
# 전역 변수로 정리할 리소스 추적
TEMP_DIR=""
LOCK_FILE="/var/lock/myscript.lock"
PID_FILE="/var/run/myscript.pid"
# 정리 함수 정의
cleanup() {
local exit_code=$?
echo "Cleaning up... (exit code: $exit_code)"
# 임시 디렉토리 삭제
[[ -n "$TEMP_DIR" && -d "$TEMP_DIR" ]] && rm -rf "$TEMP_DIR"
# 락 파일 해제
[[ -f "$LOCK_FILE" ]] && rm -f "$LOCK_FILE"
# PID 파일 삭제
[[ -f "$PID_FILE" ]] && rm -f "$PID_FILE"
# 자식 프로세스 종료
jobs -p | xargs -r kill 2>/dev/null
echo "Cleanup completed"
exit $exit_code
}
# 신호 트랩 설정
trap cleanup EXIT INT TERM
trap 'echo "Error at line $LINENO"; exit 1' ERR
# 락 파일 생성 (중복 실행 방지)
if ! mkdir "$LOCK_FILE" 2>/dev/null; then
echo "Script is already running"
exit 1
fi
# PID 저장
echo $$ > "$PID_FILE"
# 임시 디렉토리 생성
TEMP_DIR=$(mktemp -d)
echo "Working in $TEMP_DIR"
설명
이것이 하는 일: trap은 시스템 신호를 가로채서 사용자 정의 함수를 실행하도록 하여, 예상치 못한 종료 상황에서도 필요한 정리 작업을 수행합니다. 첫 번째로, cleanup 함수를 정의하여 모든 정리 작업을 한 곳에 모읍니다.
종료 코드를 보존하여 스크립트가 왜 종료되었는지 추적할 수 있습니다. 임시 파일, 락, PID 파일 등 모든 리소스를 체계적으로 정리합니다.
두 번째로, trap cleanup EXIT INT TERM으로 주요 종료 신호를 모두 처리합니다. EXIT는 정상 종료, INT는 Ctrl+C, TERM은 kill 명령에 대응합니다.
이렇게 하면 어떤 방식으로 종료되든 cleanup이 실행됩니다. 세 번째로, ERR 트랩으로 에러 발생 시 라인 번호를 출력합니다.
디버깅에 매우 유용하며, set -e와 함께 사용하면 에러 발생 지점을 정확히 파악할 수 있습니다. 네 번째로, 락 파일로 중복 실행을 방지합니다.
mkdir은 원자적(atomic) 연산이므로 경쟁 조건(race condition) 없이 안전하게 락을 구현할 수 있습니다. PID 파일도 함께 사용하여 실행 중인 프로세스를 추적합니다.
여러분이 이러한 신호 처리를 구현하면 운영 환경에서 안정적으로 동작하는 스크립트를 만들 수 있습니다. 특히 크론 작업이나 시스템 관리 스크립트에서 리소스 누수를 방지하는 데 필수적입니다.
실전 팁
💡 trap에서 여러 명령을 실행하려면 함수로 묶거나 세미콜론으로 구분하세요: trap 'cmd1; cmd2; cmd3' EXIT
💡 디버깅 시 trap 'echo "Executing: $BASH_COMMAND"' DEBUG를 사용하면 모든 명령을 추적할 수 있습니다.
💡 cleanup 함수에서는 에러를 무시하도록 || true를 사용하여 정리 작업이 중단되지 않게 하세요.
💡 SIGKILL(kill -9)은 trap으로 잡을 수 없으므로, 가능한 SIGTERM을 사용하여 정상적인 종료를 유도하세요.
💡 스크립트가 백그라운드로 실행될 때는 HUP(hangup) 신호도 처리하는 것이 좋습니다.
8. 고급 리다이렉션과 파일 디스크립터 활용
시작하며
여러분이 스크립트의 출력을 로그 파일에 저장하면서 동시에 화면에도 보여주고 싶다면 어떻게 하시나요? 또는 특정 함수의 모든 출력을 자동으로 로그 파일로 리다이렉트하고 싶다면요?
표준 출력(stdout)과 표준 에러(stderr)를 각각 다른 파일에 저장하면서, 동시에 둘을 합쳐서 또 다른 파일에 저장해야 한다면? 이런 복잡한 요구사항들이 실무에서는 자주 발생합니다.
이번에는 파일 디스크립터(File Descriptor)를 직접 다루는 고급 리다이렉션 기법을 알아보겠습니다. 복잡한 로깅 요구사항도 우아하게 해결할 수 있습니다.
개요
간단히 말해서, 파일 디스크립터는 열린 파일을 가리키는 정수값이며, Shell에서는 0(stdin), 1(stdout), 2(stderr) 외에도 3-9번을 사용자가 정의할 수 있습니다. 복잡한 로깅 시스템, 다중 출력 처리, 동적 리다이렉션, 입출력 백업과 복원 등에 필수적입니다.
예를 들어, 디버그 모드에서만 상세 로그를 출력하거나, 함수별로 다른 로그 파일을 사용할 수 있습니다. 기존에는 각 명령마다 리다이렉션을 지정했다면, 이제는 전체 코드 블록이나 함수 단위로 출력을 제어할 수 있습니다.
코드가 깔끔해지고 로깅 로직이 단순해집니다. 주요 기법으로는 파일 디스크립터 복사(>&), 열기(exec N>), 닫기(exec N>&-), 읽기/쓰기 모드(<>), Here String(<<<) 등이 있습니다.
이들을 조합하면 매우 정교한 입출력 제어가 가능합니다.
코드 예제
#!/bin/bash
# 로그 파일 설정
LOG_FILE="/var/log/app.log"
ERROR_FILE="/var/log/app.error"
exec 3>&1 4>&2 # stdout/stderr 백업
exec 1>"$LOG_FILE" 2>"$ERROR_FILE" # 리다이렉트
# 화면과 파일에 동시 출력 함수
log() {
echo "$@" | tee -a /dev/fd/3
}
# 조건부 디버그 출력
DEBUG=${DEBUG:-0}
if [[ $DEBUG -eq 1 ]]; then
exec 5>/var/log/debug.log
debug() { echo "[DEBUG] $*" >&5; }
else
exec 5>/dev/null
debug() { :; }
fi
# 함수별 로깅
process_data() {
exec 6>&1 7>&2 # 현재 상태 저장
exec 1>>"process.log" 2>&1 # 함수 로그
echo "Processing started at $(date)"
# 처리 로직
exec 1>&6 2>&7 # 원래 상태 복원
exec 6>&- 7>&- # 디스크립터 닫기
}
# 읽기/쓰기 동시 처리
exec 8<>/tmp/bidirectional.txt
echo "Writing data" >&8
cat <&8
설명
이것이 하는 일: 파일 디스크립터를 직접 조작하여 입출력 스트림을 정밀하게 제어하고, 복잡한 로깅 요구사항을 효율적으로 구현합니다. 첫 번째로, exec 3>&1 4>&2로 현재 stdout과 stderr를 백업합니다.
이후 원본 출력이 필요할 때 3번과 4번 디스크립터를 사용할 수 있습니다. 로그 파일로 리다이렉트한 후에도 화면 출력이 필요할 때 유용합니다.
두 번째로, 조건부 디버그 출력을 구현합니다. DEBUG 변수에 따라 5번 디스크립터를 디버그 로그나 /dev/null로 연결합니다.
이렇게 하면 디버그 코드를 제거하지 않고도 on/off할 수 있습니다. 세 번째로, 함수별로 다른 로그 파일을 사용할 수 있습니다.
함수 시작 시 현재 상태를 저장하고, 함수의 모든 출력을 특정 파일로 리다이렉트한 후, 함수 종료 시 원래 상태로 복원합니다. 네 번째로, <> 연산자로 읽기/쓰기를 동시에 할 수 있는 양방향 파일 디스크립터를 만듭니다.
파이프나 소켓 통신, 임시 버퍼 구현에 활용할 수 있습니다. 여러분이 이러한 고급 기법을 활용하면 엔터프라이즈 수준의 로깅 시스템을 Shell Script만으로도 구현할 수 있습니다.
특히 복잡한 배치 작업이나 시스템 관리 스크립트에서 디버깅과 모니터링이 훨씬 쉬워집니다.
실전 팁
💡 파일 디스크립터 3-9를 사용할 때는 항상 사용 후 닫아주세요(exec N>&-). 리소스 누수를 방지합니다.
💡 lsof -p $$로 현재 스크립트가 열고 있는 모든 파일 디스크립터를 확인할 수 있습니다.
💡 프로세스 대체와 함께 사용하면 더 강력합니다: exec 3< <(tail -f /var/log/syslog)
💡 로그 로테이션을 구현하려면 신호 핸들러에서 파일 디스크립터를 다시 열면 됩니다.
💡 표준 에러를 표준 출력으로 합치되 순서를 유지하려면 2>&1을 명령어 뒤가 아닌 리다이렉션 뒤에 써야 합니다.