소프트웨어 엔지니어들은 개발 과정에서 명령줄을 매일 사용하지만, 배열은 명령줄의 여러 기능 중에서도 상대적으로 잘 알려지지 않은 기능에 속합니다(정규식 연산자 =~만큼 생소하지는 않지만요). 하지만 낯선 문법과 어색한 표기법을 감수할 만한 가치가 있습니다. Bash 배열은 생각보다 훨씬 강력한 도구입니다.
잠깐, 왜 굳이 Bash 배열일까?
Bash에 관해 글을 쓰는 일은 쉽지 않습니다. 문법의 사소한 특이점만 나열하는 설명서 수준으로 전락하기 십상이기 때문입니다. 하지만 걱정하지 마세요. 이 글의 목적은 여러분이 직접 매뉴얼(RTFM)을 뒤적이지 않도록 핵심만 전달하는 것입니다.
실제로 유용한 예제
그럼 실무에서 바로 적용할 수 있는 시나리오를 살펴보겠습니다. 회사에서 내부 데이터 파이프라인의 실행 시간을 평가하고 최적화하는 새로운 프로젝트를 이끌고 있다고 가정해 봅시다. 첫 단계로 파라미터 스윕(parameter sweep)을 수행하여 파이프라인이 스레드를 얼마나 효율적으로 활용하는지 확인하고 싶습니다.
설명을 단순화하기 위해 파이프라인을 컴파일된 C++ 블랙박스로 간주하고, 조정할 수 있는 유일한 파라미터는 데이터 처리에 할당되는 스레드 수라고 하겠습니다. 즉, ./pipeline --threads 4 형태로 실행하는 방식입니다.
Bash 배열의 기본
먼저 테스트하려는 --threads 값들을 담은 배열을 정의해 보겠습니다.
allThreads=(1 2 4 8 16 32 64 128)
이 예제에서 모든 요소는 숫자지만, 반드시 그럴 필요는 없습니다. Bash 배열은 숫자와 문자열을 함께 담을 수 있으며, myArray=(1 2 "three" 4 "five") 같은 표현도 유효합니다. 다른 Bash 변수와 마찬가지로 등호(=) 주위에는 공백을 두면 안 된다는 점에 유의하세요. 공백이 있으면 Bash는 변수 이름을 실행할 프로그램으로 인식하고, =를 첫 번째 인수로 처리해 버립니다!
배열을 초기화했으니 이제 몇 가지 요소를 꺼내 보겠습니다. 그런데 echo $allThreads를 실행하면 첫 번째 요소만 출력된다는 것을 발견하게 될 겁니다.
그 이유를 이해하려면 한 걸음 물러나서 Bash에서 변수를 출력하는 일반적인 방식을 다시 살펴볼 필요가 있습니다. 다음 상황을 생각해 보세요.
type="article"
echo "Found 42 $type"
변수 $type이 단수형 명사로 주어졌고, 문장 끝에 s를 붙여 복수형으로 만들고 싶다고 해봅시다. 단순히 $type 뒤에 s를 붙일 수는 없습니다. 그러면 전혀 다른 변수인 $types가 되어 버리기 때문입니다. echo "Found 42 "$type"s"처럼 문자열을 잘게 쪼개는 우회법도 있지만, 가장 좋은 해결책은 중괄호를 사용하는 것입니다. echo "Found 42 ${type}s"처럼 말이죠. 중괄호를 사용하면 변수 이름이 어디서 시작하고 끝나는지 Bash에게 명확하게 알려줄 수 있습니다(흥미롭게도 이는 JavaScript/ES6의 템플릿 리터럴에서 변수와 표현식을 삽입할 때 쓰는 문법과 동일합니다).
결국 일반적인 Bash 변수에는 중괄호가 필수는 아니지만, 배열에는 중괄호가 필요하다는 결론에 도달합니다. 덕분에 접근할 인덱스를 지정할 수 있는데, 예를 들어 echo ${allThreads[1]}은 배열의 두 번째 요소를 반환합니다. 대괄호 없이 echo $allThreads[1]이라고 쓰면 Bash는 [1]을 문자열로 취급해 그대로 출력해 버립니다.
그렇습니다. Bash 배열 문법은 참 기묘합니다. 그래도 적어도 다른 언어와 달리 0부터 인덱싱한다는 점은 위안입니다(당신 말입니다, R 언어).
배열 순회하기
앞선 예제들에서는 정수 인덱스를 사용했습니다. 하지만 그렇지 않은 경우도 두 가지 있습니다. 첫째, 인덱스가 담긴 변수 $i를 이용해 i번째 요소를 가져오려면 echo ${allThreads[$i]}처럼 작성하면 됩니다. 둘째, 배열의 모든 요소를 출력하려면 숫자 인덱스 대신 @ 기호를 사용하면 됩니다(@를 '전체(all)'라고 기억하면 쉽습니다). echo ${allThreads[@]} 형태입니다.
배열 요소 순회
이를 바탕으로 $allThreads 배열을 순회하며 각 스레드 값마다 파이프라인을 실행해 보겠습니다.
for t in ${allThreads[@]}; do
./pipeline --threads $t
done
배열 인덱스 순회
이번에는 조금 다른 접근 방식을 살펴보겠습니다. 배열 요소를 순회하는 대신 배열 인덱스를 순회하는 방법입니다.
for i in ${!allThreads[@]}; do
./pipeline --threads ${allThreads[$i]}
done
코드를 분해해 보겠습니다. 앞서 확인했듯이 ${allThreads[@]}는 배열의 모든 요소를 의미합니다. 여기에 느낌표를 붙여 ${!allThreads[@]}로 만들면 배열의 모든 인덱스 목록(이 경우 0부터 7까지)이 반환됩니다. 다시 말해, for 루프는 모든 인덱스 $i를 순회하면서 $allThreads에서 i번째 요소를 읽어 --threads 파라미터의 값으로 설정하는 것입니다.
눈이 좀 아프긴 하지만, 굳이 소개하는 데는 이유가 있습니다. 루프 안에서 인덱스와 값을 모두 알아야 하는 경우가 종종 있기 때문입니다. 예를 들어 배열의 첫 번째 요소를 건너뛰고 싶을 때, 인덱스를 활용하면 루프 내부에서 따로 증가시켜야 하는 추가 변수를 만들지 않아도 됩니다.
배열 채우기
지금까지 관심 있는 각 --threads 값마다 파이프라인을 실행할 수 있었습니다. 이제 파이프라인의 출력이 초 단위 실행 시간이라고 가정해 봅시다. 매 반복마다 그 출력을 캡처해 별도의 배열에 저장하고, 마지막에 다양한 분석을 할 수 있도록 준비하고 싶습니다.
알아두면 유용한 문법
코드로 들어가기 전에 두 가지 문법을 더 소개해야 합니다. 첫째, Bash 명령의 출력 결과를 받아오는 방법입니다. 다음 문법을 사용하면 명령의 출력을 $output 변수에 저장할 수 있습니다.
output=$( ./my_script.sh )
둘째, 방금 받아온 값을 배열에 추가(append)하는 방법입니다. 문법이 꽤 익숙하게 보일 겁니다.
myArray+=( "newElement1" "newElement2" )
파라미터 스윕 완성
모든 것을 조합하면 파라미터 스윕을 수행하는 스크립트가 완성됩니다.
allThreads=(1 2 4 8 16 32 64 128)
allRuntimes=()
for t in ${allThreads[@]}; do
runtime=$(./pipeline --threads $t)
allRuntimes+=( $runtime )
done
짜잔, 이걸로 끝입니다!
그 외에 어떤 활용이 있을까?
이 글에서는 배열을 활용한 파라미터 스윕 시나리오를 다뤘습니다. 하지만 Bash 배열을 쓸 이유는 이것만이 아니라고 약속드립니다. 실전에서 바로 쓸 수 있는 두 가지 예제를 더 소개합니다.
로그 알림 자동화
이 시나리오에서는 애플리케이션이 여러 모듈로 나뉘어 있고, 각 모듈마다 고유한 로그 파일을 가지고 있다고 가정합니다. cron 작업 스크립트를 작성하여 특정 모듈에 문제 징후가 보이면 담당자에게 이메일을 보낼 수 있습니다.
# 로그 목록과 각 문제의 통보 대상
logPaths=("api.log" "auth.log" "jenkins.log" "data.log")
logEmails=("jay@email" "emma@email" "jon@email" "sophia@email")
# 각 로그에서 문제 징후 확인
for i in ${!logPaths[@]};
do
log=${logPaths[$i]}
stakeholder=${logEmails[$i]}
numErrors=$( tail -n 100 "$log" | grep "ERROR" | wc -l )
# 최근 오류가 5건 초과면 관계자에게 경고
if [[ "$numErrors" -gt 5 ]];
then
emailRecipient="$stakeholder"
emailSubject="WARNING: ${log} showing unusual levels of errors"
emailBody="${numErrors} errors found in log ${log}"
echo "$emailBody" | mailx -s "$emailSubject" "$emailRecipient"
fi
done
API 조회
Medium 포스트에 가장 많이 댓글을 달는 사용자가 누구인지 분석하고 싶다고 해봅시다. 데이터베이스에 직접 접근할 수 없어 SQL은 사용할 수 없지만, API는 활용할 수 있습니다!
API 인증과 토큰에 관한 긴 논의를 피하기 위해, 공개 API 테스트 서비스인 JSONPlaceholder를 엔드포인트로 사용하겠습니다. 각 포스트를 조회해 댓글 작성자들의 이메일을 받아온 뒤, 그 이메일들을 결과 배열에 추가하면 됩니다.
endpoint="https://jsonplaceholder.typicode.com/comments"
allEmails=()
# 처음 10개 포스트 조회
for postId in {1..10};
do
# 해당 포스트의 댓글 작성자 이메일을 가져오는 API 호출
response=$(curl "${endpoint}?postId=${postId}")
# jq로 JSON 응답을 배열로 파싱
allEmails+=( $( jq '.[].email' <<< "$response" ) )
done
여기서는 명령줄에서 JSON을 파싱하기 위해 jq 도구를 사용했습니다. jq의 문법은 이 글의 범위를 벗어나지만, 꼭 한번 살펴볼 것을 강력히 추천합니다.
짐작하겠지만, Bash 배열이 도움이 되는 시나리오는 무궁무진합니다. 이 글에서 소개한 예제들이 여러분에게 영감을 주었기를 바랍니다. 실무에서 활용한 다른 사례가 있다면 아래 댓글로 공유해 주세요.
아직 끝이 아닙니다!
이 글에서 다룬 배열 문법이 상당히 많으므로, 지금까지 배운 내용을 정리하고 추가로 다루지 않은 고급 팁도 함께 표로 정리했습니다.
| 문법 | 결과 |
|---|---|
arr=() |
빈 배열 생성 |
arr=(1 2 3) |
배열 초기화 |
${arr[2]} |
세 번째 요소 조회 |
${arr[@]} |
모든 요소 조회 |
${!arr[@]} |
배열 인덱스 조회 |
${#arr[@]} |
배열 크기 계산 |
arr[0]=3 |
첫 번째 요소 덮어쓰기 |
arr+=(4) |
값 추가(append) |
str=$(ls) |
ls 출력을 문자열로 저장 |
arr=( $(ls) ) |
ls 출력을 파일 배열로 저장 |
${arr[@]:s:n} |
인덱스 s부터 n개 요소 조회 |
마지막으로 남기는 생각
지금까지 살펴본 것처럼 Bash 배열의 문법은 확실히 낯설지만, 이 글이 여러분에게 그 강력함을 확신시켜 주었기를 바랍니다. 문법에 익숙해지고 나면 어느새 Bash 배열을 자주 손이 가게 되어 있을 겁니다.
Bash인가, Python인가?
그렇다면 이런 질문이 자연스럽게 따라옵니다. 언제 Python 같은 다른 스크립팅 언어 대신 Bash 배열을 써야 할까?
핵심은 의존성(dependency)입니다. 문제를 커맨드라인 도구 호출만으로 해결할 수 있다면 Bash를 사용하는 편이 좋습니다. 반면 스크립트가 더 큰 Python 프로젝트의 일부라면 Python을 사용하는 것이 자연스럽습니다.
예를 들어 파라미터 스윕을 Python으로 구현할 수도 있지만, 결국 Bash를 감싸는 래퍼(wrapper) 코드를 작성하는 데 그치게 됩니다.
import subprocess
all_threads = [1, 2, 4, 8, 16, 32, 64, 128]
all_runtimes = []
# 각 스레드 수마다 파이프라인 실행
for t in all_threads:
cmd = './pipeline --threads {}'.format(t)
# subprocess 모듈로 출력 결과 받아오기
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=True)
output = p.communicate()[0]
all_runtimes.append(output)
이 예제에서는 어차피 커맨드라인을 피할 수 없으므로, Bash를 직접 사용하는 편이 더 낫습니다.
간단한 홍보 한 줄
이 글은 제가 OSCON에서 진행했던 라이브 코딩 워크숍 You Don't Know Bash를 바탕으로 작성되었습니다. 슬라이드도, 프레젠테이션 클릭커도 없이, 저와 청중이 함께 명령줄에 타이핑하며 Bash의 신비로운 세계를 탐험하는 세션이었습니다.