🤖

본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.

⚠️

본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.

이미지 로딩 중...

Bash 함수 작성과 활용 완벽 가이드 - 슬라이드 1/7
A

AI Generated

2025. 11. 17. · 24 Views

Bash 함수 작성과 활용 완벽 가이드

Bash 스크립트에서 함수를 작성하고 활용하는 방법을 초급 개발자를 위해 쉽게 설명합니다. 함수 정의부터 파라미터 처리, return과 exit의 차이, 지역 변수, 함수 라이브러리 만들기, 재귀 함수까지 실무에서 바로 사용할 수 있는 예제와 함께 알아봅니다.


목차

  1. 함수 정의와 호출
  2. 함수 파라미터 ($1, $2, ...)
  3. return vs exit
  4. 지역 변수 (local)
  5. 함수 라이브러리 만들기
  6. 재귀 함수 작성

1. 함수 정의와 호출

시작하며

여러분이 서버 관리 스크립트를 작성할 때 같은 코드를 여러 번 복사해서 붙여넣는 자신을 발견한 적 있나요? 예를 들어, 로그를 남기는 코드를 스크립트 곳곳에 반복해서 작성하다 보면 나중에 수정할 때 일일이 다 찾아서 고쳐야 하는 번거로움이 있습니다.

이런 문제는 실제 개발 현장에서 자주 발생합니다. 반복되는 코드는 유지보수를 어렵게 만들고, 실수를 유발하며, 스크립트의 가독성을 떨어뜨립니다.

또한 나중에 로직을 수정해야 할 때 여러 곳을 일일이 찾아 고쳐야 하는 고통을 겪게 됩니다. 바로 이럴 때 필요한 것이 함수입니다.

함수를 사용하면 반복되는 코드를 한 곳에 모아두고, 필요할 때마다 이름만 불러서 사용할 수 있습니다. 마치 레고 블록처럼 한 번 만들어두면 언제든지 꺼내 쓸 수 있는 것이죠.

개요

간단히 말해서, 함수는 특정 작업을 수행하는 코드 묶음에 이름을 붙여놓은 것입니다. 마치 요리 레시피처럼, 한번 만들어두면 언제든지 그 이름만 부르면 실행됩니다.

함수가 필요한 이유는 명확합니다. 같은 작업을 여러 번 해야 할 때 코드를 복사하지 않고 함수 이름만 호출하면 되기 때문입니다.

예를 들어, 파일을 백업하고 로그를 남기는 작업을 10군데에서 해야 한다면, 함수 없이는 같은 코드를 10번 작성해야 하지만, 함수를 만들면 딱 한 번만 작성하고 10번 호출하면 됩니다. 기존에는 긴 스크립트에 모든 코드를 순차적으로 나열했다면, 이제는 의미 있는 작업 단위로 함수를 만들어 조립할 수 있습니다.

이렇게 하면 스크립트가 훨씬 읽기 쉽고 관리하기 편해집니다. Bash에서 함수를 정의하는 방법은 두 가지가 있습니다.

첫 번째는 function 함수명 { } 형식이고, 두 번째는 함수명() { } 형식입니다. 두 방법 모두 동일하게 작동하므로 여러분이 편한 방식을 선택하면 됩니다.

함수를 호출할 때는 그냥 함수 이름만 적으면 되는데, 이때 괄호는 필요 없습니다.

코드 예제

#!/bin/bash

# 방법 1: function 키워드 사용
function greet_user {
    echo "안녕하세요! Bash 함수 세계에 오신 것을 환영합니다."
    echo "현재 시간은 $(date +%H:%M:%S) 입니다."
}

# 방법 2: 괄호 사용 (더 일반적)
backup_log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] 백업 작업을 시작합니다..."
    echo "파일들을 압축하고 있습니다..."
    echo "백업이 완료되었습니다!"
}

# 함수 호출 - 괄호 없이 이름만 사용
greet_user
echo "---"
backup_log

설명

이것이 하는 일: 위 코드는 두 가지 방법으로 함수를 정의하고 호출하는 방법을 보여줍니다. greet_user 함수는 환영 메시지와 현재 시간을 출력하고, backup_log 함수는 백업 작업의 진행 상황을 로그로 남깁니다.

첫 번째로, function greet_user { } 부분에서 함수를 정의합니다. function 키워드 다음에 함수 이름을 적고, 중괄호 안에 실행할 명령들을 넣습니다.

이 함수는 호출될 때마다 환영 메시지와 함께 현재 시간을 보여줍니다. $(date +%H:%M:%S) 부분은 명령어 치환을 통해 현재 시간을 가져오는데, 이것이 왜 중요한지는 실시간 정보를 함수에서 동적으로 처리할 수 있다는 점입니다.

두 번째로, backup_log() 형식으로 함수를 정의하는 방법을 보여줍니다. 이 방식은 function 키워드 없이 함수명 뒤에 빈 괄호를 붙이는 것으로, POSIX 표준을 따르며 더 널리 사용됩니다.

함수 내부에서는 날짜와 시간을 포맷팅하여 로그 메시지와 함께 출력합니다. 실무에서는 이런 로그 함수를 만들어두면 스크립트 전체에서 일관된 형식의 로그를 남길 수 있습니다.

세 번째로, 함수를 호출하는 부분입니다. greet_userbackup_log처럼 함수 이름만 적으면 됩니다.

다른 프로그래밍 언어처럼 괄호를 붙이지 않는다는 점이 Bash의 특징입니다. 함수는 정의된 순서대로 실행되며, 각 함수 내부의 명령들이 순차적으로 실행됩니다.

여러분이 이 코드를 사용하면 반복되는 작업을 깔끔하게 정리할 수 있고, 코드의 재사용성이 높아지며, 유지보수가 훨씬 쉬워집니다. 특히 복잡한 스크립트에서 함수를 사용하면 전체 구조가 명확해져서 나중에 다른 사람이 봐도 쉽게 이해할 수 있습니다.

또한 한 곳만 수정하면 그 함수를 사용하는 모든 곳에 변경사항이 반영되므로, 버그 수정이나 기능 개선이 훨씬 효율적입니다.

실전 팁

💡 함수는 반드시 호출하기 전에 정의되어야 합니다. 스크립트 상단에 모든 함수를 모아두는 것이 좋은 관행입니다.

💡 함수 이름은 의미를 명확히 전달하도록 짓되, backup_database, send_alert 처럼 동사로 시작하면 무엇을 하는지 한눈에 알 수 있습니다.

💡 function 키워드 방식과 괄호 방식 중 한 가지를 선택해 프로젝트 전체에서 일관되게 사용하세요. 팀에서는 보통 괄호 방식을 선호합니다.

💡 함수 내부에서 에러가 발생할 수 있는 명령은 반드시 에러 처리를 추가하세요. command || echo "에러 발생" 형태로 간단히 처리할 수 있습니다.

💡 긴 함수는 여러 개의 작은 함수로 나누는 것이 좋습니다. 하나의 함수는 하나의 명확한 작업만 수행하도록 설계하세요.


2. 함수 파라미터 ($1, $2, ...)

시작하며

여러분이 사용자 계정을 생성하는 스크립트를 만들 때, 매번 사용자 이름이 달라지는 상황을 생각해보세요. 계정마다 다른 이름, 다른 그룹, 다른 홈 디렉토리를 지정해야 한다면 어떻게 할까요?

함수를 호출할 때마다 다른 값을 전달할 수 있다면 훨씬 편리하겠죠. 이런 문제는 실제로 자동화 스크립트에서 매우 흔합니다.

같은 작업을 하지만 대상이나 설정값이 다를 때마다 새로운 함수를 만들 수는 없습니다. 이렇게 하면 함수가 수백 개가 되어버리고, 관리가 불가능해집니다.

바로 이럴 때 필요한 것이 함수 파라미터입니다. 파라미터를 사용하면 함수를 호출할 때 필요한 값들을 전달할 수 있고, 함수 내부에서 $1, $2 같은 특수 변수로 이 값들을 받아서 사용할 수 있습니다.

마치 자판기에 돈을 넣고 원하는 음료 번호를 누르는 것처럼, 함수에 값을 넣고 원하는 결과를 얻을 수 있습니다.

개요

간단히 말해서, 함수 파라미터는 함수를 호출할 때 함께 전달하는 값들입니다. 함수 내부에서는 $1이 첫 번째 파라미터, $2가 두 번째 파라미터를 의미하며, $@는 모든 파라미터를, $#은 파라미터의 개수를 나타냅니다.

파라미터가 필요한 이유는 함수를 유연하게 만들기 위해서입니다. 하나의 함수로 다양한 상황을 처리할 수 있게 되므로, 코드의 재사용성이 극대화됩니다.

예를 들어, 파일을 복사하는 함수를 만들 때 소스와 대상을 파라미터로 받으면, 어떤 파일이든 복사할 수 있는 범용 함수가 됩니다. 기존에는 함수 안에 값을 하드코딩했다면, 이제는 파라미터로 받아서 동적으로 처리할 수 있습니다.

이렇게 하면 같은 로직을 다른 데이터에 적용할 수 있어 훨씬 강력한 스크립트를 만들 수 있습니다. Bash 함수 파라미터의 핵심 특징은 위치 기반이라는 점입니다.

첫 번째로 전달한 값은 $1, 두 번째는 $2 식으로 순서대로 할당됩니다. 또한 $0은 함수 이름이 아닌 스크립트 이름을 가리킨다는 점을 기억하세요.

파라미터를 검증하는 것도 중요한데, $#으로 파라미터 개수를 확인하여 필수 파라미터가 모두 전달되었는지 검사할 수 있습니다.

코드 예제

#!/bin/bash

# 사용자 정보를 출력하는 함수
create_user() {
    # 파라미터 개수 확인
    if [ $# -lt 2 ]; then
        echo "에러: 사용자명과 그룹명을 입력해주세요"
        echo "사용법: create_user <사용자명> <그룹명> [홈디렉토리]"
        return 1
    fi

    local username=$1        # 첫 번째 파라미터
    local group=$2           # 두 번째 파라미터
    local homedir=${3:-/home/$username}  # 세 번째 파라미터 (기본값 설정)

    echo "사용자 생성 중..."
    echo "  - 사용자명: $username"
    echo "  - 그룹: $group"
    echo "  - 홈 디렉토리: $homedir"
    echo "총 $# 개의 파라미터를 받았습니다"
}

# 함수 호출 예제
create_user "john" "developers"
echo "---"
create_user "sarah" "admins" "/var/home/sarah"

설명

이것이 하는 일: 위 코드는 사용자 계정 생성 정보를 출력하는 함수를 만들고, 파라미터를 통해 다양한 사용자 정보를 전달하는 방법을 보여줍니다. 파라미터 검증과 기본값 설정까지 포함된 실무적인 예제입니다.

첫 번째로, if [ $# -lt 2 ] 부분에서 파라미터 개수를 검증합니다. $#은 함수에 전달된 파라미터의 개수를 나타내는 특수 변수입니다.

이 함수는 최소 2개의 파라미터(사용자명, 그룹명)가 필요하므로, 2개 미만이면 에러 메시지를 출력하고 return 1로 함수를 종료합니다. 이런 검증 로직은 실무에서 매우 중요한데, 잘못된 입력으로 인한 예상치 못한 동작을 방지할 수 있기 때문입니다.

두 번째로, local username=$1처럼 파라미터를 의미 있는 변수명에 할당합니다. $1은 첫 번째 파라미터, $2는 두 번째 파라미터를 의미합니다.

이렇게 변수에 할당하면 코드 가독성이 높아지고, 나중에 파라미터를 여러 번 사용할 때도 편리합니다. local 키워드를 사용하는 이유는 다음 섹션에서 자세히 다루겠지만, 함수 내부에서만 사용되는 지역 변수를 만들기 위해서입니다.

세 번째로, ${3:-/home/$username} 구문은 파라미터 확장 문법으로, 세 번째 파라미터가 제공되지 않았을 때 기본값을 설정합니다. 즉, 홈 디렉토리를 지정하지 않으면 자동으로 /home/사용자명 형식으로 만들어집니다.

이런 기본값 설정은 함수를 더 유연하게 만들어주며, 선택적 파라미터를 구현할 때 매우 유용합니다. 마지막으로, 함수를 호출할 때는 create_user "john" "developers"처럼 함수 이름 뒤에 공백으로 구분하여 값들을 나열합니다.

첫 번째 호출에서는 2개의 파라미터만, 두 번째 호출에서는 3개의 파라미터를 전달하는 것을 볼 수 있습니다. 이렇게 같은 함수로 다양한 경우를 처리할 수 있습니다.

여러분이 이 코드를 사용하면 하나의 함수로 수많은 다른 사용자를 처리할 수 있고, 파라미터 검증을 통해 안전한 스크립트를 만들 수 있으며, 기본값 설정으로 사용자 편의성을 높일 수 있습니다. 실무에서는 이런 패턴을 사용하여 견고하고 재사용 가능한 함수를 작성합니다.

실전 팁

💡 파라미터를 변수에 할당할 때는 항상 의미 있는 이름을 사용하세요. $1보다 $username이 훨씬 이해하기 쉽습니다.

💡 파라미터를 사용하기 전에 항상 $#으로 개수를 확인하고, 필수 파라미터가 없으면 사용법을 출력하는 것이 좋습니다.

💡 공백이 포함된 파라미터를 전달할 때는 반드시 따옴표로 감싸세요. create_user "John Doe" "developers" 처럼 작성해야 합니다.

💡 $@를 사용하면 모든 파라미터를 다른 명령어나 함수에 그대로 전달할 수 있습니다. 래퍼 함수를 만들 때 유용합니다.

💡 파라미터가 많아지면 (4개 이상) 가독성이 떨어지므로, 연관된 값들을 배열이나 다른 방식으로 묶는 것을 고려하세요.


3. return vs exit

시작하며

여러분이 여러 단계로 구성된 배포 스크립트를 작성하고 있다고 상상해보세요. 첫 번째 함수에서 설정 파일 검증에 실패했을 때, 전체 스크립트를 멈춰야 할까요, 아니면 그 함수만 종료하고 다음 작업을 계속해야 할까요?

이런 고민을 해본 적 있으신가요? 이런 문제는 실제 운영 환경에서 매우 중요합니다.

exit를 잘못 사용하면 스크립트 전체가 갑자기 종료되어 중요한 정리 작업(cleanup)을 못할 수도 있고, return을 써야 할 곳에 아무것도 쓰지 않으면 에러가 발생해도 스크립트가 계속 진행되어 더 큰 문제를 일으킬 수 있습니다. 바로 이럴 때 필요한 것이 returnexit의 차이를 정확히 아는 것입니다.

return은 함수만 종료하고 호출한 곳으로 돌아가지만, exit는 스크립트 전체를 종료합니다. 이 차이를 이해하면 더 안전하고 예측 가능한 스크립트를 작성할 수 있습니다.

개요

간단히 말해서, return은 함수를 종료하고 상태 코드(0-255)를 반환하며, exit는 전체 스크립트를 종료하고 쉘로 돌아갑니다. 둘 다 숫자를 반환할 수 있는데, 0은 성공, 1-255는 다양한 에러 상태를 의미합니다.

이 둘을 구분해서 사용해야 하는 이유는 명확합니다. 함수 내에서 에러가 발생했을 때, 그것이 치명적인지(스크립트 전체 중단) 아니면 회복 가능한지(함수만 종료 후 에러 처리)를 결정해야 하기 때문입니다.

예를 들어, 선택적 기능이 실패했다면 return 1로 함수만 종료하고 메인 로직은 계속 진행할 수 있습니다. 반면, 데이터베이스 연결 실패 같은 치명적 에러라면 exit 1로 전체를 중단해야 합니다.

기존에는 에러 처리 없이 스크립트를 작성했다면, 이제는 각 함수의 반환값을 확인하고 적절히 대응할 수 있습니다. return 값은 $? 변수로 확인할 수 있으며, 이를 통해 조건부 로직을 구현할 수 있습니다.

returnexit의 핵심 특징은 다음과 같습니다. return은 함수 내부에서만 사용 가능하며, 함수 외부에서 사용하면 에러가 발생합니다.

exit는 어디서든 사용 가능하지만, 함수 내부에서 사용하면 전체 스크립트가 종료되므로 주의해야 합니다. 또한 둘 다 마지막 명령의 종료 상태를 기본값으로 사용하므로, 명시적으로 숫자를 지정하는 것이 좋습니다.

코드 예제

#!/bin/bash

# return을 사용하는 함수 - 함수만 종료
check_file() {
    local filename=$1

    if [ ! -f "$filename" ]; then
        echo "경고: $filename 파일이 없습니다"
        return 1  # 함수만 종료, 스크립트는 계속
    fi

    echo "$filename 파일을 찾았습니다"
    return 0  # 성공
}

# exit를 사용하는 함수 - 스크립트 전체 종료
check_critical_file() {
    local filename=$1

    if [ ! -f "$filename" ]; then
        echo "치명적 에러: $filename 필수 파일이 없습니다"
        exit 1  # 스크립트 전체 종료
    fi

    echo "$filename 필수 파일 확인 완료"
}

# 실행 예제
echo "=== Return 예제 ==="
check_file "optional_config.txt"
echo "함수 반환값: $?"
echo "스크립트는 계속 실행됩니다"

echo -e "\n=== Exit 예제 (주석 처리) ==="
# check_critical_file "required_config.txt"  # 이 줄을 실행하면 스크립트가 여기서 종료됨
echo "이 메시지는 출력됩니다"

설명

이것이 하는 일: 위 코드는 returnexit의 차이를 명확하게 보여주는 두 개의 함수를 구현합니다. 하나는 선택적 파일을 확인하고, 다른 하나는 필수 파일을 확인하여 각각 다른 방식으로 에러를 처리합니다.

첫 번째로, check_file 함수는 return을 사용합니다. `[ !

-f "$filename" ]는 파일이 존재하지 않는지 확인하는 테스트 조건입니다. 파일이 없으면 경고 메시지를 출력하고 return 1`을 실행하는데, 이것은 함수만 종료하고 에러 상태(1)를 반환합니다.

중요한 점은 스크립트 자체는 계속 실행된다는 것입니다. 파일을 찾으면 return 0으로 성공을 반환하며, 관례적으로 0은 성공, 0이 아닌 값은 실패를 의미합니다.

두 번째로, check_critical_file 함수는 exit를 사용합니다. 이 함수도 파일 존재 여부를 확인하지만, 파일이 없으면 "치명적 에러"라고 표시하고 exit 1을 실행합니다.

여기서 핵심은 exit가 실행되는 순간 전체 스크립트가 종료되며, 그 이후의 모든 코드는 실행되지 않는다는 점입니다. 이런 방식은 필수 리소스가 없어서 더 이상 진행이 불가능할 때 사용합니다.

세 번째로, 실행 예제 부분을 보면 check_file 함수를 호출한 후 echo "함수 반환값: $?"로 반환값을 출력합니다. $?는 마지막으로 실행된 명령의 종료 상태를 담고 있는 특수 변수입니다.

파일이 없으면 1이, 있으면 0이 출력됩니다. 그리고 그 다음 줄의 echo 문이 실행되는 것을 볼 수 있는데, 이것은 return이 함수만 종료했기 때문입니다.

네 번째로, check_critical_file 호출 부분은 주석 처리되어 있습니다. 만약 이 주석을 제거하고 실행하면, 필수 파일이 없을 경우 exit 1이 실행되어 스크립트가 즉시 종료되고, 그 아래의 "이 메시지는 출력됩니다"는 절대 실행되지 않습니다.

이것이 exit의 강력하지만 조심스럽게 사용해야 하는 특성입니다. 여러분이 이 코드를 사용하면 에러 상황에서 적절한 처리 방식을 선택할 수 있습니다.

회복 가능한 에러는 return으로 처리하여 스크립트가 계속 진행되도록 하고, 치명적인 에러는 exit로 즉시 중단하여 더 큰 문제를 방지할 수 있습니다. 또한 반환값을 통해 에러의 종류를 구분할 수도 있습니다(예: return 1은 파일 없음, return 2는 권한 없음 등).

실전 팁

💡 함수 내에서는 기본적으로 return을 사용하세요. exit는 정말 치명적인 에러일 때만 사용해야 합니다.

💡 반환값은 의미를 부여하세요. 0은 성공, 1은 일반 에러, 2는 잘못된 사용법, 3은 파일 없음 등 규칙을 정하면 디버깅이 쉬워집니다.

💡 함수 호출 후 반환값을 확인하는 습관을 들이세요. func || handle_error 패턴을 사용하면 에러를 간단히 처리할 수 있습니다.

💡 스크립트 상단에 set -e를 추가하면 어떤 명령이든 실패하면 자동으로 스크립트가 종료됩니다. 하지만 이것은 양날의 검이므로 신중히 사용하세요.

💡 cleanup 작업이 필요하다면 trap 명령으로 EXIT 시그널을 잡아서 정리 코드를 실행하도록 설정하세요. 이렇게 하면 exit로 종료되어도 안전하게 마무리할 수 있습니다.


4. 지역 변수 (local)

시작하며

여러분이 여러 함수를 사용하는 복잡한 스크립트를 작성하고 있다고 가정해봅시다. 한 함수에서 count라는 변수를 사용했는데, 나중에 다른 함수에서도 같은 이름의 변수를 사용했더니 값이 이상하게 바뀌어 있는 경험을 해보셨나요?

마치 내 방에 두었던 물건을 누군가 몰래 옮겨놓은 것 같은 느낌이죠. 이런 문제는 변수의 스코프(scope), 즉 변수가 영향을 미치는 범위 때문에 발생합니다.

Bash에서 기본적으로 모든 변수는 전역(global) 변수입니다. 즉, 함수 안에서 만든 변수도 함수 밖에서 접근할 수 있고, 다른 함수에서 같은 이름의 변수를 만들면 서로 충돌하여 예상치 못한 버그가 발생합니다.

특히 큰 프로젝트에서는 이런 버그를 찾기가 매우 어렵습니다. 바로 이럴 때 필요한 것이 local 키워드입니다.

local을 사용하면 함수 내부에서만 사용되는 지역 변수를 만들 수 있습니다. 마치 각 함수가 자기만의 독립된 작업 공간을 갖는 것처럼, 변수들이 서로 간섭하지 않게 됩니다.

개요

간단히 말해서, local은 함수 내부에서만 유효한 지역 변수를 선언하는 키워드입니다. local 없이 선언된 변수는 전역 변수가 되어 스크립트 어디서든 접근 가능하지만, local로 선언하면 그 함수 안에서만 존재합니다.

지역 변수가 필요한 이유는 변수 이름 충돌을 방지하고 코드의 예측 가능성을 높이기 위해서입니다. 각 함수가 독립적으로 동작하면 디버깅이 쉬워지고, 함수를 재사용하기도 편리합니다.

예를 들어, 여러 함수에서 temp, result, i 같은 흔한 변수명을 사용할 때, 모두 local로 선언하면 서로 영향을 주지 않습니다. 기존에는 모든 변수를 전역으로 사용하여 어느 함수에서든 접근 가능했다면, 이제는 함수마다 독립된 변수 공간을 가질 수 있습니다.

이렇게 하면 함수가 자기만의 상태를 안전하게 관리할 수 있고, 다른 코드에 의한 부작용(side effect)을 걱정하지 않아도 됩니다. Bash 지역 변수의 핵심 특징은 다음과 같습니다.

첫째, local은 함수 내부에서만 사용할 수 있습니다. 둘째, 지역 변수는 함수가 종료되면 자동으로 사라집니다.

셋째, 같은 이름의 전역 변수가 있어도 함수 내부에서는 지역 변수가 우선합니다. 넷째, 중첩된 함수 호출에서도 각 함수는 자신만의 지역 변수를 가집니다.

코드 예제

#!/bin/bash

# 전역 변수
counter=0

# local을 사용하지 않는 함수 (나쁜 예)
bad_function() {
    counter=10  # 전역 변수를 수정함!
    temp="함수 내부 값"
    echo "bad_function: counter=$counter, temp=$temp"
}

# local을 사용하는 함수 (좋은 예)
good_function() {
    local counter=20  # 지역 변수, 전역 변수에 영향 없음
    local temp="지역 값"
    local result=$(( counter * 2 ))

    echo "good_function: counter=$counter, temp=$temp, result=$result"
}

# 또 다른 함수 - 독립적인 지역 변수 사용
calculate() {
    local num1=$1
    local num2=$2
    local result=$(( num1 + num2 ))

    echo "계산 결과: $num1 + $num2 = $result"
    return $result
}

# 실행 예제
echo "초기 전역 counter: $counter"
bad_function
echo "bad_function 후 전역 counter: $counter (변경됨!)"
echo "bad_function 후 temp: $temp (함수 밖에서도 접근 가능!)"

echo "---"
good_function
echo "good_function 후 전역 counter: $counter (변경 안됨)"
echo "good_function 후 temp: $temp (빈 값, 접근 불가)"

설명

이것이 하는 일: 위 코드는 local을 사용하지 않았을 때와 사용했을 때의 차이를 명확하게 보여줍니다. 전역 변수와 지역 변수가 어떻게 다르게 동작하는지, 그리고 왜 지역 변수를 사용해야 하는지 실제 예제로 설명합니다.

첫 번째로, 스크립트 시작 부분에 counter=0이라는 전역 변수를 선언합니다. 이 변수는 스크립트 전체에서 접근 가능합니다.

bad_function에서는 local 없이 counter=10을 실행하는데, 이것은 전역 변수 counter를 수정합니다. 이것이 문제인 이유는 함수가 의도치 않게 외부 상태를 변경하여, 나중에 다른 부분에서 counter를 사용할 때 예상과 다른 값이 들어있을 수 있기 때문입니다.

temp 변수도 마찬가지로 함수 밖에서도 접근 가능해집니다. 두 번째로, good_function에서는 모든 변수에 local을 붙입니다.

local counter=20은 함수 내부에서만 유효한 새로운 변수를 만듭니다. 전역 counter와는 완전히 별개의 변수이며, 함수 내부에서는 지역 변수가 우선합니다.

local result=$(( counter * 2 ))처럼 변수 선언과 동시에 값을 할당할 수도 있습니다. 이렇게 하면 함수가 완전히 독립적으로 동작하며, 외부 코드에 전혀 영향을 주지 않습니다.

세 번째로, calculate 함수는 파라미터를 지역 변수에 할당하는 좋은 패턴을 보여줍니다. local num1=$1처럼 파라미터를 의미 있는 이름의 지역 변수로 만들면, 코드를 읽기 쉬워지고 $1, $2보다 명확합니다.

모든 중간 계산 값도 local로 선언하여 함수의 캡슐화를 유지합니다. 네 번째로, 실행 예제 부분에서 결과를 확인합니다.

bad_function 호출 후에는 전역 counter가 10으로 바뀌어 있고, temp 변수도 여전히 접근 가능합니다. 반면 good_function 호출 후에는 전역 counter가 여전히 0이고, temp는 빈 값(undefined)입니다.

이것은 지역 변수가 함수 종료와 함께 사라졌기 때문입니다. 여러분이 이 코드를 사용하면 함수 간 변수 충돌을 완전히 방지할 수 있고, 각 함수가 독립적으로 동작하도록 만들 수 있으며, 버그를 줄이고 코드의 안정성을 크게 높일 수 있습니다.

특히 대규모 스크립트에서는 모든 함수 내부 변수를 local로 선언하는 것이 거의 필수적입니다. 이렇게 하면 나중에 스크립트를 수정하거나 확장할 때 예상치 못한 부작용을 걱정하지 않아도 됩니다.

실전 팁

💡 함수 내부의 모든 변수는 기본적으로 local로 선언하는 습관을 들이세요. 정말 전역으로 필요한 경우에만 예외를 두세요.

💡 파라미터를 받을 때도 local username=$1 형태로 지역 변수에 할당하면 코드 가독성이 크게 향상됩니다.

💡 반복문의 카운터 변수(i, j 등)도 반드시 local로 선언하세요. 중첩된 반복문에서 변수 충돌이 자주 발생합니다.

💡 local -r variable=value 형식으로 읽기 전용 지역 변수를 만들 수 있습니다. 상수처럼 사용할 값에 유용합니다.

💡 디버깅할 때 declare -p variable 명령으로 변수의 속성(전역/지역, 타입 등)을 확인할 수 있습니다.


5. 함수 라이브러리 만들기

시작하며

여러분이 여러 개의 배포 스크립트를 관리하고 있다고 생각해보세요. 각 스크립트마다 로그 출력, 에러 처리, 파일 백업 같은 동일한 함수들을 복사해서 붙여넣고 있지 않나요?

10개의 스크립트에 같은 로그 함수가 있는데, 로그 포맷을 바꾸려면 10군데를 모두 수정해야 하는 상황 말이죠. 이런 문제는 코드 중복으로 인한 유지보수의 악몽입니다.

한 곳을 수정하면 다른 곳도 모두 찾아서 고쳐야 하고, 빠뜨린 곳이 있으면 버그가 발생합니다. 또한 새로운 스크립트를 만들 때마다 이전 스크립트에서 함수들을 복사해야 하므로 비효율적입니다.

바로 이럴 때 필요한 것이 함수 라이브러리입니다. 자주 사용하는 함수들을 별도의 파일로 만들어두고, 필요할 때마다 source 명령으로 불러와서 사용하는 것입니다.

마치 레고 블록 세트처럼, 재사용 가능한 부품들을 모아두고 조립하여 사용하는 방식입니다.

개요

간단히 말해서, 함수 라이브러리는 재사용 가능한 함수들을 모아놓은 별도의 스크립트 파일입니다. source 또는 . 명령을 사용하여 다른 스크립트에서 이 라이브러리를 불러오면, 라이브러리의 모든 함수를 마치 현재 스크립트에 정의된 것처럼 사용할 수 있습니다.

함수 라이브러리가 필요한 이유는 코드 재사용성과 유지보수성을 극대화하기 위해서입니다. 공통 기능을 한 곳에 모아두면, 수정이 필요할 때 한 번만 고치면 그것을 사용하는 모든 스크립트에 반영됩니다.

예를 들어, 로그 포맷을 변경하거나 에러 처리 로직을 개선할 때, 라이브러리 파일 하나만 수정하면 됩니다. 기존에는 각 스크립트마다 함수를 개별적으로 정의했다면, 이제는 여러 스크립트에서 동일한 라이브러리를 공유하여 사용할 수 있습니다.

이렇게 하면 프로젝트 전체의 일관성도 유지되고, 새로운 스크립트를 만들 때도 훨씬 빠르게 작성할 수 있습니다. 함수 라이브러리의 핵심 특징은 다음과 같습니다.

첫째, 라이브러리 파일은 보통 .sh 확장자를 사용하며, 실행 권한은 필요 없습니다(source로 불러오기만 하므로). 둘째, 일반적으로 lib/ 또는 utils/ 디렉토리에 정리하여 관리합니다.

셋째, 라이브러리는 독립적으로 동작하도록 설계하며, 가능한 한 외부 의존성을 최소화합니다. 넷째, 잘 설계된 라이브러리는 주석과 문서화가 잘 되어 있어 다른 사람도 쉽게 사용할 수 있습니다.

코드 예제

# ===== 파일: lib/common_functions.sh =====
#!/bin/bash
# 공통 함수 라이브러리

# 색상 코드 상수
readonly RED='\033[0;31m'
readonly GREEN='\033[0;32m'
readonly YELLOW='\033[1;33m'
readonly NC='\033[0m' # No Color

# 로그 출력 함수
log_info() {
    echo -e "${GREEN}[INFO]${NC} $(date '+%Y-%m-%d %H:%M:%S') - $*"
}

log_error() {
    echo -e "${RED}[ERROR]${NC} $(date '+%Y-%m-%d %H:%M:%S') - $*" >&2
}

log_warning() {
    echo -e "${YELLOW}[WARNING]${NC} $(date '+%Y-%m-%d %H:%M:%S') - $*"
}

# 파일 백업 함수
backup_file() {
    local source_file=$1
    local backup_dir=${2:-./backups}

    if [ ! -f "$source_file" ]; then
        log_error "백업할 파일이 없습니다: $source_file"
        return 1
    fi

    mkdir -p "$backup_dir"
    local backup_name="$(basename "$source_file").$(date +%Y%m%d_%H%M%S).bak"
    cp "$source_file" "$backup_dir/$backup_name"
    log_info "백업 완료: $backup_dir/$backup_name"
}

# ===== 파일: deploy.sh (라이브러리 사용 예제) =====
#!/bin/bash

# 라이브러리 불러오기
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/lib/common_functions.sh"

# 라이브러리 함수 사용
log_info "배포 스크립트를 시작합니다"
backup_file "config.conf" "./backups"
log_warning "이것은 테스트 배포입니다"
log_info "배포가 완료되었습니다"

설명

이것이 하는 일: 위 코드는 실무에서 자주 사용하는 로그 출력과 파일 백업 함수를 라이브러리로 만들고, 다른 스크립트에서 이를 불러와 사용하는 완전한 예제입니다. 두 개의 파일로 구성되어 있으며, 실제 프로젝트에서 바로 사용할 수 있는 패턴입니다.

첫 번째로, lib/common_functions.sh 파일은 함수 라이브러리입니다. 맨 위에 readonly로 색상 코드 상수들을 정의하는데, 이것은 터미널에서 컬러 출력을 하기 위한 ANSI 이스케이프 코드입니다.

readonly를 사용하면 이 값들이 실수로 변경되는 것을 방지할 수 있습니다. 이런 상수들을 라이브러리에 정의해두면 모든 스크립트에서 일관된 색상을 사용할 수 있습니다.

두 번째로, log_info, log_error, log_warning 세 가지 로그 함수를 정의합니다. 각 함수는 로그 레벨에 따라 다른 색상으로 메시지를 출력하며, 타임스탬프를 자동으로 추가합니다.

$*는 함수에 전달된 모든 파라미터를 하나의 문자열로 합친 것입니다. log_error>&2를 사용하여 표준 에러(stderr)로 출력하는데, 이렇게 하면 에러 메시지를 별도로 리다이렉션할 수 있습니다.

이런 로그 함수들은 거의 모든 스크립트에서 필요하므로 라이브러리로 만들기에 완벽한 후보입니다. 세 번째로, backup_file 함수는 파일 백업 기능을 제공합니다.

소스 파일의 존재 여부를 확인하고, 백업 디렉토리를 생성한 후, 타임스탬프가 포함된 백업 파일을 만듭니다. ${2:-./backups} 구문은 두 번째 파라미터가 제공되지 않으면 기본값으로 ./backups를 사용한다는 의미입니다.

이런 범용 함수를 라이브러리에 두면 여러 스크립트에서 일관된 방식으로 백업을 처리할 수 있습니다. 네 번째로, deploy.sh 파일에서 라이브러리를 사용하는 방법을 보여줍니다.

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 부분은 현재 스크립트의 디렉토리 경로를 얻는 표준 방법입니다. 이렇게 하면 어느 위치에서 스크립트를 실행하든 상대 경로로 라이브러리를 찾을 수 있습니다.

source "$SCRIPT_DIR/lib/common_functions.sh" 명령으로 라이브러리를 불러오면, 그 안의 모든 함수와 변수를 현재 스크립트에서 사용할 수 있습니다. 다섯 번째로, 라이브러리를 불러온 후에는 log_info, backup_file 같은 함수들을 마치 현재 스크립트에 정의된 것처럼 자유롭게 호출할 수 있습니다.

이것이 함수 라이브러리의 핵심 장점입니다. 여러분이 10개의 스크립트를 작성한다면, 각각에서 이 라이브러리를 불러와 동일한 함수들을 사용할 수 있고, 나중에 로그 포맷을 변경하고 싶으면 라이브러리 파일 하나만 수정하면 모든 스크립트에 적용됩니다.

여러분이 이 패턴을 사용하면 프로젝트 전체에서 코드 중복을 제거할 수 있고, 유지보수가 훨씬 쉬워지며, 새로운 스크립트를 빠르게 작성할 수 있고, 팀 전체가 일관된 코딩 스타일을 유지할 수 있습니다. 특히 DevOps 환경에서 많은 자동화 스크립트를 관리할 때 함수 라이브러리는 필수적입니다.

실전 팁

💡 라이브러리 파일 맨 위에 간단한 설명과 사용 예제를 주석으로 작성해두면 나중에 다른 사람(또는 미래의 자신)이 사용하기 쉽습니다.

💡 source 대신 . 명령도 사용할 수 있습니다(. ./lib/common.sh). 둘은 완전히 동일하며 .가 더 짧습니다.

💡 라이브러리가 이미 불러와졌는지 확인하려면 [[ -n ${LIB_LOADED:-} ]] && return 같은 가드를 라이브러리 상단에 추가하세요. 그리고 LIB_LOADED=1을 설정하면 중복 로딩을 방지할 수 있습니다.

💡 환경에 따라 다른 라이브러리를 불러오려면 if [ "$ENV" = "production" ]; then source lib/prod.sh; else source lib/dev.sh; fi 패턴을 사용하세요.

💡 라이브러리를 시스템 전역에서 사용하려면 /usr/local/lib/bash/ 같은 표준 경로에 설치하고, /etc/profile.d/에 source 명령을 추가하는 것을 고려하세요.


6. 재귀 함수 작성

시작하며

여러분이 디렉토리 안의 모든 파일을 찾아야 하는 상황을 생각해보세요. 하지만 그 디렉토리 안에는 또 다른 디렉토리들이 있고, 그 안에 또 디렉토리가 있습니다.

몇 단계 깊이인지 알 수 없는 이런 구조에서 모든 파일을 어떻게 찾을까요? 반복문으로는 깊이를 알 수 없어 처리하기 어렵습니다.

이런 문제는 트리 구조나 계층적 데이터를 다룰 때 자주 발생합니다. 디렉토리 탐색, 파일 시스템 순회, JSON 데이터 처리, 수학적 계산(팩토리얼, 피보나치 등) 같은 작업들이 이에 해당합니다.

깊이를 미리 알 수 없는 구조에서는 일반적인 반복문으로는 한계가 있습니다. 바로 이럴 때 필요한 것이 재귀 함수입니다.

재귀 함수는 자기 자신을 호출하는 함수로, 문제를 더 작은 동일한 문제로 나누어 해결합니다. 마치 러시아 인형(마트료시카)처럼, 큰 인형 안에 작은 인형이 있고, 그 안에 더 작은 인형이 있는 구조를 처리할 때 완벽합니다.

개요

간단히 말해서, 재귀 함수는 함수 내부에서 자기 자신을 호출하는 함수입니다. 매번 호출할 때마다 문제의 크기가 작아지고, 더 이상 나눌 수 없는 기본 케이스(base case)에 도달하면 재귀를 멈추고 결과를 반환합니다.

재귀 함수가 필요한 이유는 특정 유형의 문제를 자연스럽고 우아하게 해결할 수 있기 때문입니다. 특히 트리나 계층 구조를 다룰 때 재귀적 접근이 반복문보다 훨씬 이해하기 쉽고 간결합니다.

예를 들어, 디렉토리의 모든 하위 디렉토리를 탐색할 때, 재귀를 사용하면 각 디렉토리에서 같은 로직을 반복하기만 하면 됩니다. 기존에는 복잡한 중첩 반복문이나 스택 자료구조를 직접 구현해야 했다면, 이제는 재귀 함수로 같은 작업을 훨씬 간단하게 표현할 수 있습니다.

코드의 의도가 명확해지고, 버그가 줄어들며, 유지보수가 쉬워집니다. 재귀 함수의 핵심 특징은 다음과 같습니다.

첫째, 반드시 종료 조건(base case)이 있어야 합니다. 그렇지 않으면 무한 재귀에 빠져 스택 오버플로우가 발생합니다.

둘째, 재귀 호출마다 문제의 크기가 작아져야 합니다. 셋째, 각 재귀 호출은 독립적인 지역 변수를 가지므로 서로 간섭하지 않습니다.

넷째, 재귀 깊이가 너무 깊으면 성능 문제가 발생할 수 있으므로, 매우 큰 데이터셋에는 반복문이 더 적합할 수 있습니다.

코드 예제

#!/bin/bash

# 팩토리얼 계산 (수학적 재귀 예제)
factorial() {
    local n=$1

    # 종료 조건 (base case): 0! = 1, 1! = 1
    if [ "$n" -le 1 ]; then
        echo 1
        return
    fi

    # 재귀 호출: n! = n * (n-1)!
    local prev=$(factorial $(( n - 1 )))
    echo $(( n * prev ))
}

# 디렉토리 크기 계산 (실용적 예제)
calculate_dir_size() {
    local dir=$1
    local total=0

    # 현재 디렉토리의 파일들 처리
    for item in "$dir"/*; do
        if [ -f "$item" ]; then
            # 파일이면 크기 추가
            local size=$(stat -f%z "$item" 2>/dev/null || stat -c%s "$item" 2>/dev/null)
            total=$(( total + size ))
        elif [ -d "$item" ]; then
            # 디렉토리면 재귀 호출
            local subdir_size=$(calculate_dir_size "$item")
            total=$(( total + subdir_size ))
        fi
    done

    echo $total
}

# 카운트다운 (간단한 재귀 예제)
countdown() {
    local num=$1

    # 종료 조건
    if [ "$num" -le 0 ]; then
        echo "발사!"
        return
    fi

    echo "$num..."
    sleep 1
    countdown $(( num - 1 ))  # 재귀 호출
}

# 실행 예제
echo "=== 팩토리얼 예제 ==="
echo "5! = $(factorial 5)"

echo -e "\n=== 카운트다운 예제 ==="
countdown 5

설명

이것이 하는 일: 위 코드는 세 가지 재귀 함수 예제를 보여줍니다. 팩토리얼 계산으로 수학적 재귀 개념을 설명하고, 디렉토리 크기 계산으로 실용적인 사용 사례를 보여주며, 카운트다운으로 재귀의 동작 방식을 시각적으로 이해할 수 있게 합니다.

첫 번째로, factorial 함수는 재귀의 기본 원리를 가장 명확하게 보여줍니다. if [ "$n" -le 1 ]은 종료 조건(base case)으로, n이 1 이하면 더 이상 재귀하지 않고 1을 반환합니다.

이것이 없으면 함수가 영원히 자기 자신을 호출하여 스택 오버플로우가 발생합니다. 종료 조건을 통과하면 factorial $(( n - 1 ))로 자기 자신을 호출하되, 매번 n을 1씩 줄입니다.

예를 들어 factorial 5는 내부에서 factorial 4를 호출하고, 그것은 factorial 3을 호출하는 식으로 계속됩니다. 마지막에 factorial 1이 1을 반환하면, 그 값이 위로 전파되어 최종 결과가 계산됩니다.

두 번째로, calculate_dir_size 함수는 실무에서 사용할 수 있는 재귀의 좋은 예입니다. 디렉토리 안의 모든 항목을 순회하면서, 파일이면 크기를 더하고, 디렉토리면 재귀 호출로 그 안의 크기를 계산합니다.

for item in "$dir"/*는 현재 디렉토리의 모든 항목을 반복합니다. [ -f "$item" ]로 파일인지 확인하고, stat 명령으로 크기를 가져옵니다(Linux와 macOS의 차이를 처리하기 위해 두 가지 명령을 시도합니다).

디렉토리를 만나면 calculate_dir_size "$item"로 재귀 호출하여 하위 디렉토리의 총 크기를 얻습니다. 이런 방식으로 아무리 깊은 디렉토리 구조라도 모두 탐색할 수 있습니다.

세 번째로, countdown 함수는 재귀가 어떻게 작동하는지 눈으로 볼 수 있는 예제입니다. 숫자를 출력하고 1초 대기한 후, 숫자를 1 줄여서 자기 자신을 다시 호출합니다.

0 이하가 되면 "발사!"를 출력하고 종료합니다. 이것을 실행하면 "5...

4... 3...

2... 1...

발사!" 같은 카운트다운을 볼 수 있는데, 각 숫자마다 함수가 새로운 재귀 호출을 만들고 있는 것입니다. 재귀 함수의 동작 원리를 좀 더 자세히 설명하면, 각 함수 호출은 콜 스택(call stack)이라는 메모리 영역에 쌓입니다.

factorial 5를 호출하면 스택에 추가되고, 그 안에서 factorial 4를 호출하면 또 스택에 추가되는 식입니다. 종료 조건에 도달하면 스택의 맨 위부터 하나씩 제거되면서 결과가 계산됩니다.

이것이 재귀가 작동하는 메커니즘이며, 스택 공간이 한정되어 있기 때문에 재귀 깊이가 너무 깊으면 "stack overflow" 에러가 발생할 수 있습니다. 여러분이 이 패턴을 사용하면 계층 구조를 다루는 문제를 매우 우아하게 해결할 수 있고, 복잡한 반복문 없이 간결한 코드를 작성할 수 있으며, 특히 트리 탐색, 파일 시스템 처리, 재귀적 데이터 구조 다루기에 매우 효과적입니다.

하지만 항상 종료 조건을 명확히 하고, 재귀 깊이가 합리적인 범위 내인지 확인해야 합니다.

실전 팁

💡 재귀 함수를 작성할 때 가장 먼저 종료 조건(base case)을 정의하세요. 이것이 없으면 무한 재귀에 빠집니다.

💡 디버깅할 때는 재귀 호출마다 현재 상태를 출력하면 실행 흐름을 이해하는 데 큰 도움이 됩니다. echo "재귀 깊이: $depth, 값: $value" 같은 식으로요.

💡 Bash에서 재귀 깊이는 보통 수백~수천 레벨까지 가능하지만, 매우 깊은 재귀가 필요하다면 Python이나 다른 언어를 고려하세요.

💡 디렉토리 순회나 파일 찾기 같은 작업은 재귀 함수를 직접 만들기보다 find 명령을 사용하는 것이 더 효율적이고 안전합니다. 재귀는 개념 학습이나 특수한 경우에 사용하세요.

💡 재귀와 반복문 중 고민될 때는 이렇게 생각하세요: 문제가 자연스럽게 부분 문제로 나뉘면 재귀, 정해진 횟수만큼 반복하면 반복문이 적합합니다.


#Bash#Functions#Shell Script#Linux#Automation#Linux,Bash,Shell

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.