이미지 로딩 중...
AI Generated
2025. 11. 18. · 3 Views
배열과 연관 배열 완벽 가이드
셸 스크립트에서 여러 개의 데이터를 효율적으로 관리하는 방법을 배웁니다. 일반 배열과 연관 배열의 차이점, 선언 방법, 데이터 접근 및 조작 방법을 실무 예제와 함께 상세히 다룹니다.
목차
1. 배열_선언과_초기화
시작하며
여러분이 서버 관리 스크립트를 작성하는데, 10개의 서버 IP 주소를 변수에 저장해야 하는 상황을 겪어본 적 있나요? ip1="192.168.1.1", ip2="192.168.1.2" 이렇게 일일이 변수를 만들다 보면 코드가 너무 길어지고, 나중에 관리하기도 정말 힘들어집니다.
이런 문제는 실제 개발 현장에서 자주 발생합니다. 비슷한 종류의 데이터가 여러 개 있을 때, 변수를 하나하나 만들면 코드가 복잡해지고 반복문으로 처리하기도 어려워집니다.
만약 서버가 100개라면? 생각만 해도 끔찍하죠.
바로 이럴 때 필요한 것이 배열입니다. 배열을 사용하면 여러 개의 관련된 데이터를 하나의 이름으로 묶어서 관리할 수 있고, 반복문으로 쉽게 처리할 수 있습니다.
개요
간단히 말해서, 배열은 여러 개의 값을 하나의 이름 아래 저장할 수 있는 데이터 보관함입니다. 마치 사물함에 여러 개의 물건을 넣어두고, 각 칸에 번호를 매겨서 관리하는 것과 같습니다.
배열이 왜 필요한지 실무 관점에서 생각해보면, 로그 파일 목록을 처리하거나, 여러 사용자 이름을 관리하거나, 설정 값들을 한 곳에 모아두고 싶을 때 매우 유용합니다. 예를 들어, 백업해야 할 디렉토리 목록을 배열로 만들어두면 반복문 하나로 모든 디렉토리를 순회하며 백업할 수 있습니다.
기존에는 변수를 여러 개 선언하고 일일이 처리했다면, 이제는 배열 하나로 묶어서 간단하게 관리할 수 있습니다. 코드도 짧아지고, 나중에 항목을 추가하거나 수정하기도 훨씬 쉬워집니다.
배열의 핵심 특징은 첫째, 순서가 있다는 것입니다(0번부터 시작). 둘째, 같은 종류의 데이터를 모아서 관리하기 좋다는 것입니다.
셋째, 반복문과 함께 사용하면 엄청난 시너지를 발휘한다는 것입니다. 이러한 특징들이 스크립트를 간결하고 유지보수하기 쉽게 만들어줍니다.
코드 예제
# 방법 1: 괄호를 사용한 배열 선언
servers=("web1.example.com" "web2.example.com" "db1.example.com")
# 방법 2: 인덱스를 지정하여 선언
backup_dirs[0]="/home"
backup_dirs[1]="/etc"
backup_dirs[2]="/var/log"
# 방법 3: 빈 배열 선언 후 추가
users=()
users+=("alice")
users+=("bob")
# 방법 4: 명령어 결과를 배열로 저장
log_files=($(ls /var/log/*.log))
설명
이것이 하는 일: 배열을 선언하고 초기화하는 것은 여러 개의 데이터를 하나의 변수명 아래 저장하는 첫 단계입니다. Bash에서는 다양한 방법으로 배열을 만들 수 있습니다.
첫 번째로, 가장 일반적인 방법은 괄호 안에 공백으로 구분된 값들을 나열하는 것입니다. servers=("web1" "web2" "db1")처럼 작성하면, servers라는 배열에 세 개의 서버 이름이 저장됩니다.
이 방법은 처음부터 모든 값을 알고 있을 때 가장 편리합니다. 두 번째로, 인덱스를 직접 지정하는 방법이 있습니다.
backup_dirs[0]="/home"처럼 각 위치에 값을 할당하는 방식인데, 이렇게 하면 원하는 위치에 값을 넣을 수 있습니다. 심지어 backup_dirs[5]="test"처럼 중간을 건너뛰고 값을 넣을 수도 있습니다.
빈 인덱스는 자동으로 빈 문자열로 채워집니다. 세 번째로, 빈 배열을 만들고 나중에 += 연산자로 값을 추가하는 방법입니다.
users=()로 빈 배열을 만들고, users+=("새이름")으로 계속 추가할 수 있습니다. 이 방식은 스크립트 실행 중에 동적으로 데이터를 수집할 때 매우 유용합니다.
네 번째로, 명령어의 실행 결과를 배열로 저장하는 방법입니다. log_files=($(ls *.log))처럼 작성하면, ls 명령어의 결과를 공백으로 나누어 배열에 저장합니다.
단, 파일 이름에 공백이 있으면 문제가 생길 수 있으니 주의해야 합니다. 여러분이 이 코드를 사용하면 데이터를 체계적으로 관리할 수 있고, 나중에 반복문으로 쉽게 처리할 수 있습니다.
특히 서버 목록, 파일 목록, 사용자 목록처럼 비슷한 종류의 데이터가 여러 개 있을 때 배열을 사용하면 코드가 훨씬 깔끔해지고 확장하기도 쉬워집니다.
실전 팁
💡 배열에 공백이 포함된 값을 저장할 때는 반드시 큰따옴표로 감싸주세요. names=("John Doe" "Jane Smith")처럼 작성하면 각 이름이 하나의 요소로 저장됩니다.
💡 명령어 결과를 배열로 저장할 때는 파일 이름에 공백이 있을 수 있으니 주의하세요. 대신 mapfile이나 readarray 명령어를 사용하면 더 안전합니다.
💡 배열을 선언할 때 declare -a array_name을 사용하면 명시적으로 일반 배열임을 표시할 수 있습니다. 코드 가독성이 좋아집니다.
💡 스크립트 시작 부분에 설정 값들을 배열로 모아두면, 나중에 설정을 변경하기가 훨씬 쉽습니다. 하드코딩된 값들을 한 곳에 모아두는 습관을 들이세요.
💡 빈 배열인지 확인하려면 ${#array[@]}로 길이를 체크하세요. 0이면 빈 배열입니다.
2. 배열_요소_접근
시작하며
여러분이 배열에 10개의 서버 주소를 저장해두었는데, 그중에서 3번째 서버에만 접속해야 하는 상황을 상상해보세요. 아니면 모든 서버 주소를 화면에 출력해야 할 수도 있겠죠.
배열을 만들었으면 이제 그 안의 데이터를 꺼내 쓸 수 있어야 합니다. 이런 문제는 실무에서 계속 마주치게 됩니다.
특정 위치의 값만 필요할 때도 있고, 모든 값을 한 번에 사용해야 할 때도 있습니다. 배열의 첫 번째 값만 가져오거나, 마지막 값만 필요한 경우도 자주 있습니다.
바로 이럴 때 필요한 것이 배열 요소 접근 방법입니다. Bash에서는 인덱스를 사용해서 원하는 위치의 값을 가져올 수 있고, 특별한 문법으로 모든 값을 한 번에 가져올 수도 있습니다.
개요
간단히 말해서, 배열 요소 접근은 배열 안에 저장된 값을 꺼내서 사용하는 것입니다. 마치 사물함의 특정 번호 칸을 열어서 물건을 꺼내는 것과 같습니다.
왜 이것이 중요한지 실무 관점에서 생각해보면, 로그 파일 목록 중 첫 번째 파일만 분석하거나, 마지막으로 백업된 디렉토리를 확인하거나, 모든 사용자에게 메일을 보낼 때 각 사용자 이름을 하나씩 꺼내야 합니다. 예를 들어, 서버 모니터링 스크립트에서 특정 서버의 상태만 확인할 때 매우 유용합니다.
기존에는 각 값을 별도의 변수에 저장했다면, 이제는 배열 인덱스로 간단하게 접근할 수 있습니다. ${array[0]}로 첫 번째 값을, ${array[@]}로 모든 값을 한 번에 가져올 수 있습니다.
배열 접근의 핵심 특징은 첫째, 인덱스가 0부터 시작한다는 것입니다(첫 번째 요소는 [0]). 둘째, [@]와 [*]를 사용하면 모든 요소에 접근할 수 있다는 것입니다.
셋째, 음수 인덱스를 사용하면 뒤에서부터 접근할 수도 있습니다. 이러한 특징들이 배열을 유연하게 활용할 수 있게 해줍니다.
코드 예제
# 서버 목록 배열 생성
servers=("web1.example.com" "web2.example.com" "db1.example.com" "cache1.example.com")
# 특정 인덱스의 요소 접근 (0부터 시작)
echo "첫 번째 서버: ${servers[0]}"
echo "세 번째 서버: ${servers[2]}"
# 마지막 요소 접근 (음수 인덱스 사용)
echo "마지막 서버: ${servers[-1]}"
# 모든 요소 접근 - [@] 사용
echo "모든 서버: ${servers[@]}"
# 모든 요소 접근 - [*] 사용
echo "모든 서버: ${servers[*]}"
설명
이것이 하는 일: 배열에 저장된 값들을 필요에 따라 꺼내서 사용하는 것입니다. 특정 위치의 값 하나만 가져올 수도 있고, 모든 값을 한 번에 가져올 수도 있습니다.
첫 번째로, 가장 기본적인 방법은 ${array[인덱스]} 형식입니다. ${servers[0]}은 배열의 첫 번째 요소를 가져오고, ${servers[2]}는 세 번째 요소를 가져옵니다.
주의할 점은 인덱스가 0부터 시작한다는 것입니다. 처음에는 헷갈릴 수 있지만, 대부분의 프로그래밍 언어가 이 방식을 사용하므로 익숙해지면 자연스럽습니다.
두 번째로, 음수 인덱스를 사용하면 배열의 끝에서부터 접근할 수 있습니다. ${servers[-1]}은 마지막 요소를, ${servers[-2]}는 뒤에서 두 번째 요소를 가져옵니다.
이 기능은 Bash 4.3 버전부터 지원되며, 배열의 길이를 모를 때 마지막 요소에 접근하기 매우 편리합니다. 세 번째로, ${array[@]}와 ${array[*]}는 배열의 모든 요소를 한 번에 가져옵니다.
둘의 차이는 큰따옴표로 감쌌을 때 나타나는데, "${array[@]}"는 각 요소를 별도의 단어로 취급하고, "${array[*]}"는 모든 요소를 하나의 문자열로 합칩니다. 대부분의 경우 [@]를 사용하는 것이 안전합니다.
네 번째로, 존재하지 않는 인덱스에 접근하면 빈 문자열이 반환됩니다. ${servers[100]}처럼 범위를 벗어난 인덱스를 사용해도 에러가 발생하지 않고, 그냥 빈 값이 나옵니다.
이것은 편리하기도 하지만, 오타로 인한 버그를 찾기 어렵게 만들 수도 있으니 주의가 필요합니다. 여러분이 이 코드를 사용하면 배열의 특정 값을 정확하게 가져올 수 있고, 반복문과 결합하면 모든 요소를 순회하며 작업을 수행할 수 있습니다.
실무에서는 특히 첫 번째와 마지막 요소에 자주 접근하게 되므로, [0]과 [-1] 문법을 꼭 기억해두세요.
실전 팁
💡 배열 요소를 사용할 때는 항상 중괄호 {}를 사용하세요. $array[0]이 아니라 ${array[0]}이 올바른 문법입니다. 중괄호를 빼먹으면 예상치 못한 결과가 나올 수 있습니다.
💡 공백이 포함된 배열 요소를 다룰 때는 "${array[@]}"처럼 큰따옴표로 감싸야 각 요소가 올바르게 분리됩니다. 따옴표 없이 사용하면 공백을 기준으로 값이 쪼개집니다.
💡 배열의 마지막 요소에 접근할 때 ${array[-1]}이 동작하지 않는다면 Bash 버전을 확인하세요. 4.3 이전 버전에서는 ${array[${#array[@]}-1]} 방식을 사용해야 합니다.
💡 디버깅할 때 echo "${array[@]}"로 배열 전체를 출력해보면 예상한 값이 제대로 들어있는지 빠르게 확인할 수 있습니다.
💡 배열 요소가 비어있는지 확인하려면 [ -z "${array[0]}" ]처럼 -z 옵션을 사용하세요. 빈 문자열이면 참을 반환합니다.
3. 배열_순회_방법
시작하며
여러분이 100개의 로그 파일을 배열에 저장해두었는데, 각 파일을 하나씩 열어서 에러 메시지가 있는지 확인해야 하는 상황을 생각해보세요. 파일 하나하나를 수동으로 처리하기에는 너무 많고, 자동화가 절실히 필요합니다.
이런 문제는 실제 개발 현장에서 매일 발생합니다. 여러 서버에 같은 명령어를 실행하거나, 파일 목록을 처리하거나, 사용자 리스트를 순회하며 작업을 수행해야 할 때가 정말 많습니다.
하나씩 수동으로 하기에는 시간도 오래 걸리고 실수할 가능성도 높습니다. 바로 이럴 때 필요한 것이 배열 순회입니다.
반복문을 사용하면 배열의 모든 요소를 자동으로 하나씩 꺼내서 처리할 수 있고, 코드도 단 몇 줄이면 충분합니다.
개요
간단히 말해서, 배열 순회는 배열의 모든 요소를 처음부터 끝까지 하나씩 방문하면서 작업을 수행하는 것입니다. 마치 도서관의 책장을 처음부터 끝까지 훑어보면서 필요한 책을 찾는 것과 같습니다.
왜 이것이 중요한지 실무 관점에서 생각해보면, 백업 스크립트에서 여러 디렉토리를 순회하며 압축하거나, 서버 목록을 돌면서 상태를 확인하거나, 사용자 명단을 순회하며 권한을 설정할 때 필수적입니다. 예를 들어, 데이터베이스 백업 스크립트에서 여러 데이터베이스를 순회하며 덤프를 생성하는 경우에 매우 유용합니다.
기존에는 각 요소를 일일이 처리하는 코드를 반복해서 작성했다면, 이제는 for 루프 하나로 모든 요소를 자동으로 처리할 수 있습니다. 코드 양도 줄어들고, 새로운 항목을 추가해도 루프가 자동으로 처리합니다.
배열 순회의 핵심 특징은 첫째, for...in 문법으로 값을 직접 순회할 수 있다는 것입니다. 둘째, C 스타일 for 문으로 인덱스를 사용해서 순회할 수도 있습니다.
셋째, while 루프와 카운터를 결합해서 더 세밀한 제어도 가능합니다. 이러한 방법들이 상황에 따라 유연하게 배열을 처리할 수 있게 해줍니다.
코드 예제
# 파일 목록 배열
log_files=("app.log" "error.log" "access.log" "debug.log")
# 방법 1: for...in 루프로 값 순회 (가장 일반적)
for file in "${log_files[@]}"; do
echo "처리 중: $file"
# 실제 작업 수행
done
# 방법 2: C 스타일 for 루프로 인덱스 순회
for ((i=0; i<${#log_files[@]}; i++)); do
echo "인덱스 $i: ${log_files[i]}"
done
# 방법 3: while 루프와 카운터 사용
count=0
while [ $count -lt ${#log_files[@]} ]; do
echo "파일 $count: ${log_files[$count]}"
((count++))
done
설명
이것이 하는 일: 배열의 모든 요소를 자동으로 하나씩 꺼내면서 반복 작업을 수행합니다. 사람이 일일이 처리할 필요 없이 스크립트가 알아서 모든 항목을 처리해줍니다.
첫 번째로, 가장 많이 사용되는 방법은 for...in 루프입니다. for file in "${log_files[@]}"; do ...
done 구조에서, file 변수에 배열의 각 요소가 순서대로 할당됩니다. 첫 번째 반복에서는 "app.log"가, 두 번째에서는 "error.log"가 file에 들어갑니다.
이 방법은 인덱스가 필요 없고 값만 필요할 때 가장 간단하고 읽기 쉽습니다. 두 번째로, C 스타일 for 루프를 사용하면 인덱스로 순회할 수 있습니다.
for ((i=0; i<${#log_files[@]}; i++))에서 ${#log_files[@]}는 배열의 길이를 의미하고, i는 0부터 시작해서 배열 길이보다 작을 때까지 1씩 증가합니다. 이 방법은 인덱스가 필요하거나, 여러 배열을 동시에 순회할 때 유용합니다.
예를 들어, 파일 이름 배열과 파일 크기 배열을 함께 순회할 수 있습니다. 세 번째로, while 루프와 카운터를 사용하는 방법도 있습니다.
count 변수를 0으로 시작해서, 배열 길이보다 작은 동안 반복하면서 ((count++))로 증가시킵니다. 이 방법은 루프 안에서 카운터를 조작해야 할 때(예: 특정 조건에서 2씩 증가) 유용합니다.
네 번째로, 각 방법의 장단점을 이해하는 것이 중요합니다. for...in은 가장 간단하고 안전하지만 인덱스를 모릅니다.
C 스타일은 인덱스를 알 수 있지만 문법이 조금 복잡합니다. while 루프는 가장 유연하지만 코드가 길어집니다.
여러분이 이 코드를 사용하면 수백 개의 항목도 자동으로 처리할 수 있고, 사람의 실수 없이 정확하게 모든 요소를 순회할 수 있습니다. 실무에서는 90% 이상 for...in 방식을 사용하게 되므로, 이 방식을 먼저 익히시고 필요할 때 다른 방식을 활용하세요.
실전 팁
💡 배열을 순회할 때는 반드시 "${array[@]}"처럼 큰따옴표로 감싸세요. 공백이 포함된 요소가 올바르게 처리됩니다. 따옴표 없이 ${array[@]}만 쓰면 공백을 기준으로 값이 쪼개집니다.
💡 루프 안에서 배열을 수정하지 마세요. 순회 중에 요소를 추가하거나 삭제하면 예상치 못한 동작이 발생할 수 있습니다. 수정이 필요하면 별도의 배열을 만드세요.
💡 대용량 배열을 처리할 때는 진행 상황을 출력하는 것이 좋습니다. echo "처리 중: $i/${#array[@]}"처럼 현재 위치를 보여주면 스크립트가 멈춘 것인지 느린 것인지 알 수 있습니다.
💡 특정 조건에서 루프를 조기 종료하려면 break를, 현재 반복만 건너뛰려면 continue를 사용하세요. 예: [[ $file == *.tmp ]] && continue
💡 인덱스와 값이 모두 필요하면 for i in "${!array[@]}"; do ... "${array[i]}" ... done 패턴을 사용하세요. ${!array[@]}는 모든 인덱스를 반환합니다.
4. 연관_배열_declare_-A
시작하며
여러분이 서버 이름과 IP 주소를 매핑해서 저장하고 싶은데, 일반 배열로는 "web1"이 몇 번 인덱스인지 기억하기가 너무 힘든 상황을 겪어본 적 있나요? 서버 이름으로 바로 IP를 찾고 싶은데, 0, 1, 2 같은 숫자로만 접근하는 것은 너무 불편합니다.
이런 문제는 실제 시스템 관리 스크립트에서 정말 자주 발생합니다. 사용자 이름과 이메일을 매핑하거나, 설정 키와 값을 저장하거나, 파일 확장자와 MIME 타입을 연결할 때, 숫자 인덱스로는 관리가 어렵습니다.
코드를 나중에 읽어도 무엇을 의미하는지 알기 힘듭니다. 바로 이럴 때 필요한 것이 연관 배열입니다.
연관 배열을 사용하면 숫자 대신 의미 있는 문자열을 키로 사용할 수 있어서, 코드가 훨씬 읽기 쉽고 유지보수하기 좋아집니다.
개요
간단히 말해서, 연관 배열은 숫자 인덱스 대신 문자열 키를 사용하는 배열입니다. 마치 사전처럼 단어(키)를 찾으면 그에 해당하는 뜻(값)을 얻을 수 있습니다.
왜 이것이 필요한지 실무 관점에서 생각해보면, 설정 파일을 파싱해서 키-값 쌍으로 저장하거나, 환경별로 다른 데이터베이스 정보를 관리하거나, HTTP 상태 코드와 메시지를 매핑할 때 매우 유용합니다. 예를 들어, status_messages["200"]="OK" 이런 식으로 저장하면 나중에 상태 코드만으로 메시지를 바로 찾을 수 있습니다.
기존에는 숫자 인덱스를 사용해서 servers[0]="web1", servers[1]="192.168.1.1" 이렇게 따로 저장했다면, 이제는 server_ips["web1"]="192.168.1.1"처럼 의미 있는 이름으로 직접 연결할 수 있습니다. 코드를 읽을 때 훨씬 이해하기 쉽습니다.
연관 배열의 핵심 특징은 첫째, declare -A로 선언해야 한다는 것입니다(Bash 4.0 이상). 둘째, 키로 어떤 문자열이든 사용할 수 있다는 것입니다.
셋째, 키로 값을 찾는 속도가 매우 빠르다는 것입니다(해시 테이블 구조). 이러한 특징들이 복잡한 데이터 관계를 간단하게 표현할 수 있게 해줍니다.
코드 예제
# 연관 배열 선언 (declare -A 필수)
declare -A server_ips
# 값 할당
server_ips["web1"]="192.168.1.10"
server_ips["web2"]="192.168.1.11"
server_ips["db1"]="192.168.1.20"
# 선언과 동시에 초기화
declare -A http_codes=(
["200"]="OK"
["404"]="Not Found"
["500"]="Internal Server Error"
)
# 값 접근
echo "web1 서버 IP: ${server_ips["web1"]}"
echo "HTTP 404: ${http_codes["404"]}"
# 모든 키 출력
echo "모든 서버: ${!server_ips[@]}"
# 모든 값 출력
echo "모든 IP: ${server_ips[@]}"
설명
이것이 하는 일: 연관 배열은 의미 있는 이름(키)과 값을 쌍으로 저장해서, 나중에 이름만으로 값을 빠르게 찾을 수 있게 해줍니다. Python의 딕셔너리, JavaScript의 객체와 비슷한 개념입니다.
첫 번째로, 연관 배열을 사용하려면 반드시 declare -A로 선언해야 합니다. declare -A server_ips처럼 작성하면, Bash에게 "이것은 일반 배열이 아니라 연관 배열이야"라고 알려주는 것입니다.
이 선언을 빼먹으면 일반 배열로 취급되어 문자열 키가 제대로 작동하지 않으므로, 절대 잊어서는 안 됩니다. 두 번째로, 값을 할당하는 방법은 일반 배열과 비슷하지만 인덱스 자리에 문자열을 사용합니다.
server_ips["web1"]="192.168.1.10"처럼 대괄호 안에 큰따옴표로 감싼 문자열을 키로 사용합니다. 키는 공백도 포함할 수 있지만, 가능하면 간단하고 명확한 이름을 사용하는 것이 좋습니다.
세 번째로, 선언과 동시에 초기화할 수도 있습니다. declare -A array=(["key1"]="value1" ["key2"]="value2") 형식으로 작성하면, 여러 키-값 쌍을 한 번에 설정할 수 있습니다.
설정 데이터나 고정된 매핑을 저장할 때 코드가 깔끔해집니다. 네 번째로, 값에 접근하는 방법은 ${array["키"]} 형식입니다.
일반 배열과 비슷하지만 숫자 대신 문자열 키를 사용합니다. 특별히 ${!array[@]}를 사용하면 모든 키를 얻을 수 있고, ${array[@]}는 모든 값을 반환합니다.
이것은 연관 배열을 순회할 때 매우 유용합니다. 다섯 번째로, 키가 존재하는지 확인하려면 [[ -v array["키"] ]] 조건문을 사용할 수 있습니다.
존재하지 않는 키에 접근하면 빈 문자열이 반환되지만, 빈 문자열이 실제 값일 수도 있으므로 -v 옵션으로 정확하게 확인하는 것이 좋습니다. 여러분이 이 코드를 사용하면 복잡한 데이터 관계를 명확하게 표현할 수 있고, 코드를 읽는 사람이 즉시 의미를 이해할 수 있습니다.
실무에서는 설정 관리, 캐싱, 데이터 매핑 등 다양한 곳에서 연관 배열을 활용하게 됩니다.
실전 팁
💡 연관 배열은 Bash 4.0 이상에서만 지원됩니다. 스크립트 시작 부분에 [[ ${BASH_VERSINFO[0]} -ge 4 ]]로 버전을 체크하는 것이 안전합니다.
💡 키 이름에 공백이나 특수문자가 있으면 반드시 큰따옴표로 감싸세요. server_ips["web server 1"]="..."처럼 사용해야 합니다.
💡 연관 배열을 함수에 전달할 때는 주의가 필요합니다. 일반 배열처럼 전달되지 않으므로, declare -n을 사용한 참조 전달이나 직접 접근을 고려하세요.
💡 설정 파일을 연관 배열로 로드하면 관리가 쉬워집니다. while IFS='=' read key value; do config["$key"]="$value"; done < config.txt 패턴을 활용하세요.
💡 연관 배열을 JSON으로 변환하거나 그 반대로 할 때는 jq 같은 도구를 활용하면 편리합니다. 복잡한 데이터 구조를 다룰 때 유용합니다.
5. 배열_길이와_인덱스
시작하며
여러분이 배열에 파일 목록을 저장해두었는데, 도대체 몇 개의 파일이 있는지, 마지막 파일의 인덱스가 뭔지 알아야 하는 상황을 상상해보세요. 또는 배열이 비어있는지 확인해서 "처리할 파일이 없습니다"라는 메시지를 보여주고 싶을 수도 있습니다.
이런 문제는 실무 스크립트에서 정말 자주 마주칩니다. 배열의 크기를 알아야 반복문을 제대로 돌릴 수 있고, 빈 배열인지 확인해야 불필요한 작업을 방지할 수 있습니다.
또한 특정 인덱스가 유효한지 검증해야 하는 경우도 많습니다. 바로 이럴 때 필요한 것이 배열 길이와 인덱스 정보입니다.
Bash에서는 특별한 문법으로 배열의 크기를 알아내고, 어떤 인덱스가 실제로 존재하는지 확인할 수 있습니다.
개요
간단히 말해서, 배열 길이는 배열에 저장된 요소의 개수를 의미하고, 인덱스는 각 요소의 위치 번호를 의미합니다. 마치 책의 페이지 수를 세고, 특정 내용이 몇 페이지에 있는지 찾는 것과 같습니다.
왜 이것이 중요한지 실무 관점에서 생각해보면, 배치 작업에서 "총 100개 중 50개 처리 완료" 같은 진행 상황을 보여주거나, 배열이 비어있으면 에러 메시지를 출력하거나, 특정 인덱스 범위만 처리해야 할 때 필수적입니다. 예를 들어, 로그 파일 분석 스크립트에서 처리할 파일이 있는지 먼저 확인하는 경우에 매우 유용합니다.
기존에는 배열을 일일이 세거나 추측했다면, 이제는 ${#array[@]}로 정확한 길이를 알 수 있고, ${!array[@]}로 모든 인덱스를 확인할 수 있습니다. 추측이 아닌 정확한 정보로 스크립트를 작성할 수 있습니다.
배열 길이와 인덱스의 핵심 특징은 첫째, ${#array[@]}로 요소 개수를 알 수 있다는 것입니다. 둘째, ${!array[@]}로 모든 인덱스를 나열할 수 있다는 것입니다.
셋째, 일반 배열과 연관 배열 모두에서 동일한 문법을 사용할 수 있다는 것입니다. 이러한 특징들이 배열을 안전하고 정확하게 다룰 수 있게 해줍니다.
코드 예제
# 일반 배열
files=("app.log" "error.log" "access.log")
# 배열 길이 확인
echo "파일 개수: ${#files[@]}"
# 모든 인덱스 확인
echo "인덱스 목록: ${!files[@]}"
# 빈 배열 확인
if [ ${#files[@]} -eq 0 ]; then
echo "처리할 파일이 없습니다"
fi
# 연관 배열의 길이와 키
declare -A config
config["host"]="localhost"
config["port"]="3306"
echo "설정 항목 수: ${#config[@]}"
echo "설정 키 목록: ${!config[@]}"
# 특정 인덱스가 존재하는지 확인
if [[ -v files[1] ]]; then
echo "인덱스 1이 존재합니다: ${files[1]}"
fi
설명
이것이 하는 일: 배열에 저장된 요소의 개수를 세고, 어떤 인덱스에 값이 있는지 확인하는 작업입니다. 이 정보는 배열을 안전하게 다루고 조건에 따라 다른 처리를 하는 데 필수적입니다.
첫 번째로, ${#array[@]}는 배열의 길이, 즉 요소의 개수를 반환합니다. files 배열에 3개의 요소가 있다면 ${#files[@]}는 3을 반환합니다.
이 문법은 일반 배열과 연관 배열 모두에서 동일하게 작동하며, 배열이 비어있으면 0을 반환합니다. 주의할 점은 [@] 대신 [*]를 사용해도 같은 결과가 나오지만, 일관성을 위해 [@]를 사용하는 것이 권장됩니다.
두 번째로, ${!array[@]}는 배열의 모든 인덱스를 공백으로 구분된 문자열로 반환합니다. 일반 배열이면 "0 1 2"처럼 숫자가, 연관 배열이면 "host port db"처럼 키가 반환됩니다.
이것은 배열을 순회할 때 인덱스와 값을 모두 사용해야 하는 경우에 매우 유용합니다. for i in "${!files[@]}"; do echo "$i: ${files[i]}"; done 패턴으로 인덱스와 값을 함께 출력할 수 있습니다.
세 번째로, 배열이 비어있는지 확인하는 것은 매우 중요한 방어 코드입니다. if [ ${#files[@]} -eq 0 ]처럼 길이가 0인지 체크하면, 빈 배열에서 반복문을 돌리는 등의 문제를 방지할 수 있습니다.
실무에서는 외부 명령어로 배열을 채우는 경우가 많은데, 명령어가 실패하면 빈 배열이 되므로 이런 검증이 필수입니다. 네 번째로, [[ -v array[index] ]]는 특정 인덱스에 값이 존재하는지 확인하는 조건문입니다.
일반 배열에서 중간 인덱스를 건너뛰었거나, 연관 배열에서 특정 키가 있는지 확인할 때 사용합니다. 존재하지 않는 인덱스에 접근하면 빈 문자열이 반환되는데, 실제 값이 빈 문자열일 수도 있으므로 -v를 사용하는 것이 더 정확합니다.
다섯 번째로, 희소 배열(인덱스가 연속되지 않은 배열)에서는 길이와 최대 인덱스가 다를 수 있습니다. arr[0]="a", arr[5]="b"처럼 만들면 길이는 2지만 최대 인덱스는 5입니다.
이런 경우 for ((i=0; i<${#arr[@]}; i++))로 순회하면 빈 요소를 만날 수 있으므로, for i in "${!arr[@]}"로 실제 존재하는 인덱스만 순회하는 것이 안전합니다. 여러분이 이 코드를 사용하면 배열의 상태를 정확히 파악하고, 안전하게 처리할 수 있습니다.
특히 사용자 입력이나 외부 명령어로 배열을 채울 때는 반드시 길이를 체크해서 예외 상황을 처리하세요.
실전 팁
💡 배열 길이를 자주 사용한다면 변수에 저장해두세요. length=${#files[@]}로 저장하면 매번 계산하지 않아도 됩니다. 단, 배열이 변경되면 다시 계산해야 합니다.
💡 진행 상황을 보여줄 때 printf를 사용하면 깔끔합니다. printf "처리 중: %d/%d\n" "$current" "${#array[@]}"처럼 포맷을 지정할 수 있습니다.
💡 빈 배열을 체크하는 방법은 여러 가지인데, [ ${#arr[@]} -eq 0 ]이 가장 명확하고 읽기 쉽습니다. [ -z "${arr[*]}" ]도 동작하지만 덜 직관적입니다.
💡 연관 배열의 모든 키를 순회하려면 for key in "${!config[@]}"; do ... "${config[$key]}" ... done 패턴을 사용하세요. 키와 값을 모두 다룰 수 있습니다.
💡 배열의 마지막 인덱스를 구하려면 일반 배열은 $((${#arr[@]}-1)), 희소 배열은 arr_indices=("${!arr[@]}"); last=${arr_indices[-1]}을 사용하세요.
6. 배열_슬라이싱과_조작
시작하며
여러분이 100개의 로그 파일 경로를 배열에 저장했는데, 처음 10개만 처리하거나, 중간의 특정 범위만 추출하거나, 파일 확장자만 따로 모아야 하는 상황을 생각해보세요. 전체 배열을 새로 만들지 않고 일부분만 효율적으로 꺼내고 싶습니다.
이런 문제는 실무에서 정말 자주 발생합니다. 대용량 데이터를 페이지네이션으로 나눠서 처리하거나, 배열에서 특정 요소를 제거하거나, 두 배열을 합쳐야 할 때가 많습니다.
배열 전체를 복사해서 수정하면 비효율적이고 메모리도 낭비됩니다. 바로 이럴 때 필요한 것이 배열 슬라이싱과 조작 기법입니다.
Bash에서는 배열의 일부만 추출하거나, 요소를 추가/삭제하거나, 배열을 변형하는 다양한 방법을 제공합니다.
개요
간단히 말해서, 배열 슬라이싱은 배열의 일부분을 잘라내는 것이고, 배열 조작은 요소를 추가, 삭제, 변경하는 모든 작업을 의미합니다. 마치 문서에서 특정 페이지만 복사하거나, 페이지를 추가/삭제하는 것과 같습니다.
왜 이것이 중요한지 실무 관점에서 생각해보면, 대용량 로그를 처리할 때 처음 100줄만 샘플링하거나, 백업 파일 목록에서 오래된 것을 제거하거나, 여러 소스의 데이터를 하나의 배열로 합칠 때 필수적입니다. 예를 들어, 웹 크롤링 결과를 배치로 나눠서 처리할 때 슬라이싱이 매우 유용합니다.
기존에는 반복문으로 일일이 요소를 복사하거나 새 배열을 만들었다면, 이제는 ${array[@]:start:length} 문법 하나로 원하는 부분을 바로 추출할 수 있습니다. 코드가 간결해지고 실행 속도도 빨라집니다.
배열 조작의 핵심 특징은 첫째, 슬라이싱으로 부분 배열을 만들 수 있다는 것입니다. 둘째, += 연산자로 요소를 추가할 수 있다는 것입니다.
셋째, unset으로 특정 요소를 삭제할 수 있다는 것입니다. 넷째, 배열끼리 합치거나 필터링할 수 있다는 것입니다.
이러한 특징들이 배열을 유연하게 변형하고 재구성할 수 있게 해줍니다.
코드 예제
# 원본 배열
files=("log1.txt" "log2.txt" "log3.txt" "log4.txt" "log5.txt" "log6.txt")
# 슬라이싱: 인덱스 2부터 3개 요소 추출
subset=("${files[@]:2:3}")
echo "부분 배열: ${subset[@]}" # log3.txt log4.txt log5.txt
# 슬라이싱: 인덱스 2부터 끝까지
tail_files=("${files[@]:2}")
echo "뒤쪽 요소: ${tail_files[@]}"
# 요소 추가
files+=("log7.txt")
files+=("log8.txt" "log9.txt")
# 특정 인덱스 삭제
unset 'files[2]' # 인덱스 2 삭제
# 배열 합치기
arr1=("a" "b" "c")
arr2=("d" "e" "f")
combined=("${arr1[@]}" "${arr2[@]}")
echo "합친 배열: ${combined[@]}"
# 조건에 맞는 요소만 필터링
filtered=()
for item in "${files[@]}"; do
[[ $item == log[1-3]* ]] && filtered+=("$item")
done
설명
이것이 하는 일: 배열의 특정 부분만 선택하거나, 요소를 동적으로 추가/삭제하거나, 여러 배열을 하나로 합치는 등 배열을 자유롭게 변형하는 작업입니다. 이것은 데이터를 효율적으로 처리하고 메모리를 절약하는 데 매우 중요합니다.
첫 번째로, 배열 슬라이싱은 ${array[@]:start:length} 문법을 사용합니다. start는 시작 인덱스(0부터 시작), length는 추출할 요소의 개수입니다.
"${files[@]:2:3}"은 인덱스 2부터 3개 요소를 추출합니다. length를 생략하면 시작 인덱스부터 끝까지 모두 가져옵니다.
이 방법은 원본 배열을 수정하지 않고 새로운 배열을 만들기 때문에 안전합니다. 두 번째로, += 연산자를 사용하면 배열에 요소를 추가할 수 있습니다.
files+=("new.txt")는 배열 끝에 하나의 요소를 추가하고, files+=("new1.txt" "new2.txt")는 여러 요소를 한 번에 추가합니다. 이 방식은 반복문 안에서 조건에 맞는 항목을 수집할 때 매우 유용합니다.
예를 들어, 파일 목록을 순회하며 특정 조건을 만족하는 파일만 새 배열에 모을 수 있습니다. 세 번째로, unset 명령어로 특정 인덱스의 요소를 삭제할 수 있습니다.
unset 'files[2]'는 인덱스 2의 요소를 제거하지만, 주의할 점은 인덱스가 재정렬되지 않는다는 것입니다. 인덱스 0, 1, 3, 4...처럼 2가 빠진 희소 배열이 됩니다.
만약 인덱스를 다시 0부터 연속되게 만들고 싶다면 files=("${files[@]}")로 재할당하면 됩니다. 네 번째로, 여러 배열을 합치려면 combined=("${arr1[@]}" "${arr2[@]}") 패턴을 사용합니다.
이것은 arr1의 모든 요소와 arr2의 모든 요소를 순서대로 새 배열에 넣습니다. 세 개 이상의 배열도 같은 방식으로 합칠 수 있습니다.
이 방법은 여러 소스에서 데이터를 수집한 후 하나로 합쳐서 처리할 때 매우 편리합니다. 다섯 번째로, 조건에 맞는 요소만 필터링하는 것은 Bash에 내장 함수가 없어서 반복문을 사용해야 합니다.
for item in "${files[@]}"; do [[ 조건 ]] && filtered+=("$item"); done 패턴을 사용하면, 조건을 만족하는 요소만 새 배열에 추가됩니다. 예를 들어, .log 파일만 추출하거나, 특정 패턴에 맞는 항목만 선택할 수 있습니다.
여섯 번째로, 배열의 요소를 변형하려면(예: 모든 파일명을 대문자로) 반복문을 사용합니다. for i in "${!arr[@]}"; do arr[i]="${arr[i]^^}"; done처럼 작성하면 모든 요소를 대문자로 변환할 수 있습니다.
^^는 대문자 변환, ,,는 소문자 변환을 의미합니다. 여러분이 이 코드를 사용하면 대용량 데이터를 메모리 효율적으로 처리하고, 필요한 부분만 빠르게 추출하고, 동적으로 데이터를 수집할 수 있습니다.
실무에서는 특히 로그 분석, 파일 처리, 데이터 파이프라인에서 이러한 기법들이 자주 사용됩니다.
실전 팁
💡 슬라이싱할 때 큰따옴표를 꼭 사용하세요. subset=("${files[@]:2:3}")처럼 작성해야 공백이 포함된 요소가 제대로 처리됩니다. 따옴표를 빼먹으면 요소가 쪼개집니다.
💡 배열에서 요소를 삭제한 후에는 인덱스가 연속되지 않을 수 있습니다. 만약 연속된 인덱스가 필요하다면 arr=("${arr[@]}")로 재할당하여 인덱스를 재정렬하세요.
💡 대용량 배열을 처리할 때는 슬라이싱으로 청크 단위로 나눠서 처리하면 메모리 사용을 줄일 수 있습니다. for ((i=0; i<${#arr[@]}; i+=100)); do chunk=("${arr[@]:i:100}"); ... done 패턴을 활용하세요.
💡 배열에서 중복을 제거하려면 연관 배열을 활용할 수 있습니다. declare -A seen; unique=(); for item in "${arr[@]}"; do [[ ! -v seen[$item] ]] && unique+=("$item") && seen[$item]=1; done
💡 배열을 정렬하려면 readarray -t sorted < <(printf '%s\n' "${arr[@]}" | sort) 패턴을 사용하세요. 각 요소를 줄로 출력하고 sort로 정렬한 후 다시 배열로 읽어들입니다.