로깅 패턴
스크립트가 실행될 때 무슨 일이 일어나는지 기록하는 것이 로깅입니다. 개발 중에는 디버그 메시지가 도움이 되고, 운영 중에는 에러와 경고만 남기는 것이 좋습니다. 로그를 화면에도 출력하고 파일에도 저장하고 싶을 때도 있습니다.
이 절에서는 단순한 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 하나만 수정하면 됩니다.