iBetter Books
수정

로깅 패턴

스크립트가 실행될 때 무슨 일이 일어나는지 기록하는 것이 로깅입니다. 개발 중에는 디버그 메시지가 도움이 되고, 운영 중에는 에러와 경고만 남기는 것이 좋습니다. 로그를 화면에도 출력하고 파일에도 저장하고 싶을 때도 있습니다.

이 절에서는 단순한 echo 대신 레벨, 색상, 타임스탬프를 갖춘 로깅 라이브러리를 직접 작성합니다.

로그 레벨

로그 레벨은 메시지의 중요도를 나타냅니다. 일반적으로 다섯 가지를 사용합니다.

레벨 숫자 용도
DEBUG 0 상세한 실행 흐름, 변수 값
INFO 1 정상 동작 정보
WARN 2 잠재적 문제, 주의 필요
ERROR 3 오류 발생, 작업 실패
FATAL 4 치명적 오류, 즉시 종료 필요

LOG_LEVEL=WARN으로 설정하면 WARN 이상(WARN, ERROR, FATAL)만 출력하고 DEBUG, INFO는 숨깁니다. 운영 환경에서는 INFO로, 문제 추적 중에는 DEBUG로 낮추는 식으로 활용합니다.

구조화된 로깅 함수 — 기초

타임스탬프와 레벨 태그를 포함한 기본 로깅 함수입니다.

# 파일: lib_logging_v1.sh# 로그 레벨 정의declare -A LOG_LEVELS=([DEBUG]=0 [INFO]=1 [WARN]=2 [ERROR]=3 [FATAL]=4)LOG_LEVEL="${LOG_LEVEL:-INFO}"LOG_FILE="${LOG_FILE:-}"_log() {    local level="$1"    shift    local message="$*"    local timestamp    timestamp=$(date '+%Y-%m-%d %H:%M:%S')    # 현재 설정 레벨보다 낮은 메시지는 출력하지 않음    local level_num="${LOG_LEVELS[$level]:-1}"    local threshold="${LOG_LEVELS[$LOG_LEVEL]:-1}"    if [[ $level_num -lt $threshold ]]; then        return 0    fi    local formatted="[${timestamp}] [${level}] ${message}"    # 파일 출력 (LOG_FILE이 설정된 경우)    if [[ -n "$LOG_FILE" ]]; then        echo "$formatted" >> "$LOG_FILE"    fi    # 화면 출력 (ERROR/FATAL은 stderr)    if [[ "$level" == "ERROR" || "$level" == "FATAL" ]]; then        echo "$formatted" >&2    else        echo "$formatted"    fi}log_debug() { _log "DEBUG" "$@"; }log_info()  { _log "INFO"  "$@"; }log_warn()  { _log "WARN"  "$@"; }log_error() { _log "ERROR" "$@"; }log_fatal() { _log "FATAL" "$@"; exit 1; }

사용 예입니다.

source lib_logging_v1.shlog_info  "서비스 시작"log_debug "설정 파일 로드 완료"   # LOG_LEVEL=INFO면 출력 안 됨log_warn  "디스크 사용량 80% 초과"log_error "데이터베이스 연결 실패"

실행 결과(LOG_LEVEL=INFO 기본값)입니다.

[2026-04-24 09:30:12] [INFO] 서비스 시작
[2026-04-24 09:30:12] [WARN] 디스크 사용량 80% 초과
[2026-04-24 09:30:12] [ERROR] 데이터베이스 연결 실패

log_debug 메시지는 LOG_LEVEL이 INFO이므로 출력되지 않습니다.

색상 추가

터미널에서 로그 레벨별로 색상을 다르게 표시하면 가독성이 높아집니다.

# 파일: lib_logging_v2.sh# ANSI 색상 코드COLOR_RESET='\033[0m'COLOR_GRAY='\033[0;90m'COLOR_GREEN='\033[0;32m'COLOR_YELLOW='\033[0;33m'COLOR_RED='\033[0;31m'COLOR_RED_BOLD='\033[1;31m'declare -A LOG_LEVELS=([DEBUG]=0 [INFO]=1 [WARN]=2 [ERROR]=3 [FATAL]=4)declare -A LOG_COLORS=(    [DEBUG]="$COLOR_GRAY"    [INFO]="$COLOR_GREEN"    [WARN]="$COLOR_YELLOW"    [ERROR]="$COLOR_RED"    [FATAL]="$COLOR_RED_BOLD")LOG_LEVEL="${LOG_LEVEL:-INFO}"LOG_FILE="${LOG_FILE:-}"# 터미널 여부 확인 (파일 리다이렉션 시 색상 코드 제거)_is_terminal() {    [[ -t 1 ]]}_log() {    local level="$1"    shift    local message="$*"    local timestamp    timestamp=$(date '+%Y-%m-%d %H:%M:%S')    local level_num="${LOG_LEVELS[$level]:-1}"    local threshold="${LOG_LEVELS[$LOG_LEVEL]:-1}"    [[ $level_num -lt $threshold ]] && return 0    local plain_msg="[${timestamp}] [${level}] ${message}"    # 파일에는 색상 없이 저장    if [[ -n "$LOG_FILE" ]]; then        echo "$plain_msg" >> "$LOG_FILE"    fi    # 화면에는 색상 포함    if _is_terminal; then        local color="${LOG_COLORS[$level]:-}"        printf "${color}%s${COLOR_RESET}\n" "$plain_msg"    else        echo "$plain_msg"    fi}log_debug() { _log "DEBUG" "$@"; }log_info()  { _log "INFO"  "$@"; }log_warn()  { _log "WARN"  "$@"; }log_error() { _log "ERROR" "$@"; }log_fatal() { _log "FATAL" "$@"; exit 1; }

터미널에서는 레벨별로 색이 다르게 나타나고, 파일로 리다이렉션하면 색상 코드 없이 저장됩니다.

호출자 정보 포함

어느 함수의 몇 번째 줄에서 로그를 남겼는지 알 수 있으면 디버깅이 훨씬 쉬워집니다.

# 파일: lib_logging_caller.sh (lib_logging_v2.sh에 추가)_log() {    local level="$1"    shift    local message="$*"    local timestamp    timestamp=$(date '+%Y-%m-%d %H:%M:%S')    local level_num="${LOG_LEVELS[$level]:-1}"    local threshold="${LOG_LEVELS[$LOG_LEVEL]:-1}"    [[ $level_num -lt $threshold ]] && return 0    # 호출자 정보 (DEBUG 레벨에서만 포함)    local caller_info=""    if [[ "${LOG_LEVELS[$level]}" -eq 0 ]]; then        local caller_file="${BASH_SOURCE[2]:-unknown}"        local caller_line="${BASH_LINENO[1]:-0}"        local caller_func="${FUNCNAME[2]:-main}"        caller_info=" (${caller_file##*/}:${caller_line} ${caller_func})"    fi    local plain_msg="[${timestamp}] [${level}]${caller_info} ${message}"    [[ -n "$LOG_FILE" ]] && echo "$plain_msg" >> "$LOG_FILE"    echo "$plain_msg"}

DEBUG 레벨 메시지에는 파일명:줄번호:함수명이 자동으로 붙습니다.

tee를 활용한 화면+파일 동시 출력

로깅 함수 대신 tee를 활용해 간단하게 화면과 파일에 동시에 출력하는 방법도 있습니다.

# 파일: tee_logging.sh#!/usr/bin/env bashLOG_FILE="/tmp/myscript_$(date '+%Y%m%d').log"# 모든 출력(stdout + stderr)을 파일로도 저장exec > >(tee -a "$LOG_FILE") 2>&1echo "이 메시지는 화면과 파일에 모두 출력됩니다."ls /없는경로   # 에러 메시지도 파일에 저장됨echo "완료"

exec > >(tee -a "$LOG_FILE") 2>&1은 스크립트 전체의 stdout과 stderr을 tee로 보내는 구문입니다. 이후의 모든 출력이 화면과 파일 양쪽에 기록됩니다. 단, 파일에도 색상 코드가 그대로 들어갈 수 있으니 주의합니다.

실습 — 로깅 라이브러리 작성

지금까지의 내용을 종합한 완성된 로깅 라이브러리입니다. source로 불러서 어느 스크립트에서든 재사용할 수 있습니다.

#!/usr/bin/env bash# 새 파일: lib_logging.sh# lib_logging.sh — Bash 로깅 라이브러리# 사용법: source lib_logging.sh# 환경변수:#   LOG_LEVEL — 출력 레벨 (DEBUG/INFO/WARN/ERROR/FATAL, 기본: INFO)#   LOG_FILE  — 로그 파일 경로 (기본: 없음, 화면만 출력)# ---- 색상 코드 ----_LOG_COLOR_RESET='\033[0m'_LOG_COLORS=([DEBUG]='\033[0;90m' [INFO]='\033[0;32m' [WARN]='\033[0;33m' [ERROR]='\033[0;31m' [FATAL]='\033[1;31m')# ---- 로그 레벨 ----declare -A _LOG_LEVELS=([DEBUG]=0 [INFO]=1 [WARN]=2 [ERROR]=3 [FATAL]=4)LOG_LEVEL="${LOG_LEVEL:-INFO}"LOG_FILE="${LOG_FILE:-}"# ---- 내부 로그 함수 ----_log_write() {    local level="$1"    local message="$2"    local timestamp    timestamp=$(date '+%Y-%m-%d %H:%M:%S')    # 레벨 필터    local level_num="${_LOG_LEVELS[$level]:-1}"    local threshold="${_LOG_LEVELS[$LOG_LEVEL]:-1}"    [[ $level_num -lt $threshold ]] && return 0    local plain_msg="[${timestamp}] [${level}] ${message}"    # 파일에 저장 (색상 없이)    if [[ -n "$LOG_FILE" ]]; then        echo "$plain_msg" >> "$LOG_FILE"    fi    # 화면 출력 (터미널이면 색상, 아니면 plain)    if [[ -t 1 || -t 2 ]]; then        local color="${_LOG_COLORS[$level]:-}"        if [[ "$level" == "ERROR" || "$level" == "FATAL" ]]; then            printf "${color}%s${_LOG_COLOR_RESET}\n" "$plain_msg" >&2        else            printf "${color}%s${_LOG_COLOR_RESET}\n" "$plain_msg"        fi    else        if [[ "$level" == "ERROR" || "$level" == "FATAL" ]]; then            echo "$plain_msg" >&2        else            echo "$plain_msg"        fi    fi}# ---- 공개 함수 ----log_debug() { _log_write "DEBUG" "$*"; }log_info()  { _log_write "INFO"  "$*"; }log_warn()  { _log_write "WARN"  "$*"; }log_error() { _log_write "ERROR" "$*"; }log_fatal() { _log_write "FATAL" "$*"; exit 1; }

사용 예시입니다.

#!/usr/bin/env bash# 새 파일: use_logging.sh# 로깅 라이브러리 로드# shellcheck source=lib_logging.shsource "$(dirname "$0")/lib_logging.sh"# 환경변수로 설정 제어# LOG_LEVEL=DEBUG ./use_logging.sh# LOG_FILE=/tmp/app.log ./use_logging.shlog_info  "애플리케이션 시작"log_debug "디버그 모드 정보 (LOG_LEVEL=DEBUG일 때만 출력)"log_warn  "디스크 사용량 높음"log_error "파일을 찾을 수 없습니다"log_info  "처리 완료"

실행 방법과 결과입니다.

# 기본 (INFO 이상만 출력)./use_logging.sh# DEBUG 포함 출력LOG_LEVEL=DEBUG ./use_logging.sh# 파일로도 저장LOG_FILE=/tmp/app.log LOG_LEVEL=DEBUG ./use_logging.shcat /tmp/app.log

이 라이브러리 하나를 프로젝트에 두고 모든 스크립트에서 source로 불러 사용하면, 일관된 형식의 로그가 남습니다. 나중에 로그 형식을 바꾸고 싶을 때도 lib_logging.sh 하나만 수정하면 됩니다.