새 서버에 접속했더니 python3 명령어는 실행되는데 python은 command not found가 납니다. .bashrc에 PATH를 추가했는데 재접속해도 반영이 안 됩니다. cron에 등록한 스크립트는 터미널에서 잘 되는데 자동 실행하면 항상 실패합니다.
이 모든 상황의 공통 원인은 환경변수와 dotfiles를 모르기 때문입니다. 이 모듈을 마치면 PATH 추적, 설정 즉시 적용, cron 환경변수 문제를 스스로 진단하고 해결할 수 있게 됩니다.
환경변수 & dotfiles
- 1지역 변수와 export 환경변수의 차이를 이해하고 자식 프로세스 전달 범위를 설명할 수 있다
- 2env, printenv, set으로 환경변수를 조회하고 unset으로 제거할 수 있다
- 3.bashrc와 .bash_profile의 로드 순서와 적용 시점 차이를 설명할 수 있다
- 4$PATH 구조를 이해하고 새 경로를 추가해 언어 런타임 버전을 전환할 수 있다
- 5source로 설정을 즉시 적용하고, alias를 영구 등록하며, cron 스크립트에서 누락된 환경변수를 보완할 수 있다
env | sortecho $PATH | tr ':' '\n'source ~/.bashrc셸 스크립트 문법(export, 변수 치환)을 지원하지 않으므로 KEY=VALUE 형식만 사용합니다
지역 변수 vs 전역 환경변수 — export의 역할
배포 스크립트를 짜다 보면 스크립트 안에서 설정한 변수가 자식 스크립트에서 안 보이는 상황을 만납니다. 또는 반대로, 터미널에서 export DB_HOST=localhost를 설정했는데 cron으로 실행하면 해당 변수가 없다는 에러가 납니다. 이 차이가 지역 변수와 환경변수의 구분에서 비롯됩니다. export를 붙이느냐 마느냐 한 단어 차이지만, 그 변수가 자식 프로세스에 전달되는지가 달라집니다. 스크립트 간 데이터 전달, 환경 설정 파일 구조 설계, cron 오류 디버깅 — 이 모든 상황에서 이 개념이 기준점이 됩니다.
쉘에서 변수를 만드는 방법은 두 가지입니다. export 여부에 따라 그 변수가 자식 프로세스에게 전달되는지가 결정됩니다.
확대
지역 변수 (Local Variable)
현재 쉘 세션에만 존재하며, 자식 프로세스(새 쉘, 실행한 스크립트 등)에는 전달되지 않습니다.
# 지역 변수 선언
MY_NAME="alice"
echo $MY_NAME
# 출력: alice
# 자식 쉘에서는 보이지 않음
bash -c 'echo $MY_NAME'
# 출력: (아무것도 출력 안 됨)
전역 환경변수 (Environment Variable) — export 사용
export를 사용하면 현재 쉘과 그 쉘에서 실행되는 모든 자식 프로세스에게 변수가 전달됩니다.
# 환경변수로 내보내기
export MY_NAME="alice"
# 자식 쉘에서도 접근 가능
bash -c 'echo $MY_NAME'
# 출력: alice
# 선언과 동시에 export
export JAVA_HOME="/usr/lib/jvm/java-17-openjdk"
# 기존 지역 변수를 나중에 export
VERSION="3.11"
export VERSION
변수 확인 방법 비교 — env, printenv, set 명령어가 각각 어떤 변수를 보여주는지 비교합니다:
| 명령어 | 보여주는 것 |
|---|---|
set | 지역 변수 + 환경변수 + 쉘 함수 모두 |
env | 현재 프로세스의 환경변수만 |
printenv | 환경변수만 (특정 변수 지정 가능) |
printenv HOME | HOME 변수 하나만 출력 |
# 환경변수 전체 목록
env
# 출력 예시:
# HOME=/home/alice
# USER=alice
# SHELL=/bin/bash
# LANG=en_US.UTF-8
# PATH=/usr/local/bin:/usr/bin:/bin
# TERM=xterm-256color
# ...
# 특정 변수만 확인
printenv HOME
# 출력: /home/alice
printenv JAVA_HOME
# 출력: /usr/lib/jvm/java-17-openjdk
# set은 함수와 지역 변수까지 포함하므로 출력이 매우 많음
# 특정 변수를 찾을 때는 grep과 함께 사용
set | grep MY_NAME
# 출력: MY_NAME=alice
변수 삭제 — unset 명령으로 환경변수나 쉘 변수를 제거합니다:
unset MY_NAME
echo $MY_NAME
# 출력: (빈 줄)
중요한 개념: export는 현재 세션과 그 자식들에게만 적용됩니다. 다음 로그인 세션에서도 환경변수를 유지하려면 반드시 dotfile(.bashrc 또는 .bash_profile)에 기록해야 합니다.
환경변수 선언과 제거(export와 unset)
환경변수 안전하게 지우기: unset
unset은 현재 셸의 변수 이름과 값을 제거해 이후에 시작하는 자식 프로세스로 더 이상 상속되지 않게 합니다. 이미 실행 중인 프로세스가 가진 환경 사본, 셸 히스토리, CI 로그·프로세스 목록 등에 남은 값까지 지우는 명령은 아니므로 비밀값을 안전하게 폐기하는 만능 방법으로 오해하면 안 됩니다.
⚠️ 비밀번호·토큰은 명령줄 인자나
echo로 출력하지 말고 Secret Manager/CI 보안 변수로 주입하세요. 사용 후에는unset하고, 노출 가능성이 있으면 해당 자격증명을 즉시 폐기·교체하고 로그를 정리합니다.
$ export MY_VAR="secret"
$ unset MY_VAR
$ echo $MY_VAR # 아무것도 출력되지 않음
PATH 환경변수 확장 시의 덮어쓰기 참사 예방
새 소프트웨어 바이너리 경로를 $PATH에 추가할 때, 기존 경로를 누락하는 중대한 실수를 저지르면 ls, cp 같은 시스템 기본 명령어가 먹통이 됩니다. 반드시 기존 $PATH를 뒤에 붙여 확장해야 합니다.
# 올바른 예시 (기존 PATH를 포함하여 뒤에 붙임)
$ export PATH="/opt/newapp/bin:$PATH"
# 절망적인 오답 예시 (기존 PATH 유실, 기본 명령어 마비됨!)
$ export PATH="/opt/newapp/bin"
dotfiles 로드 순서 — 언제 어떤 파일이 실행되는가
확대
Linux 쉘은 시작 조건에 따라 서로 다른 설정 파일을 읽습니다. 이 순서를 모르면 "분명히 .bashrc에 썼는데 왜 적용이 안 되지?"라는 상황에 빠집니다.
쉘의 두 가지 시작 모드 — Login Shell과 Non-Login Shell은 로드하는 설정 파일이 다릅니다:
| 모드 | 설명 | 예시 |
|---|---|---|
| Login Shell | 로그인 시 최초로 실행되는 쉘 | SSH 접속, 콘솔 로그인, bash --login |
| Interactive Non-Login Shell | 이미 로그인된 상태에서 새 터미널 열기 | 터미널 에뮬레이터, bash 명령 실행 |
| Non-Interactive Shell | 사용자 입력 없이 실행되는 쉘 | 스크립트 실행, cron 작업 |
로드 순서 — Login Shell (SSH 접속 등) — SSH 접속 시 쉘이 읽는 파일 순서입니다:
1. /etc/profile ← 시스템 전체 설정 (root만 수정)
2. /etc/profile.d/*.sh ← 패키지가 추가하는 개별 설정 파일들
3. ~/.bash_profile ← 사용자 로그인 설정 (없으면 아래 순서로 시도)
또는 ~/.bash_login
또는 ~/.profile
로드 순서 — Interactive Non-Login Shell (새 터미널 탭) — 터미널 앱에서 새 탭을 열 때 읽는 파일 순서입니다:
1. /etc/bash.bashrc ← 시스템 전체 interactive 설정
2. ~/.bashrc ← 사용자 interactive 설정
실무에서 쓰는 표준 패턴:
대부분의 Linux 배포판에서 ~/.bash_profile이 ~/.bashrc를 직접 불러오도록 설정되어 있습니다. 이렇게 하면 로그인 쉘이든 인터랙티브 쉘이든 동일한 설정이 적용됩니다.
# ~/.bash_profile 일반적인 내용
# Login shell에서 .bashrc를 읽도록 연결
if [ -f ~/.bashrc ]; then
source ~/.bashrc
fi
이 패턴 덕분에 개발자가 실제로 수정하는 파일은 거의 항상 ~/.bashrc 하나입니다.
시스템 전역 vs 사용자별 설정 — 어느 파일을 수정하느냐에 따라 적용 범위와 필요 권한이 달라집니다:
| 파일 | 적용 범위 | 수정 권한 |
|---|---|---|
/etc/environment | 시스템 전체 (모든 사용자, 모든 쉘) | root |
/etc/profile | 시스템 전체 login shell | root |
/etc/profile.d/*.sh | 시스템 전체 (패키지별 분리) | root |
~/.bash_profile | 해당 사용자 login shell | 해당 사용자 |
~/.bashrc | 해당 사용자 interactive shell | 해당 사용자 |
/etc/environment 특이점 — 이 파일은 쉘 문법이 아닌 단순 KEY=VALUE 형식만 허용합니다:
# /etc/environment는 쉘 스크립트가 아닙니다
# export 키워드 없이 KEY=VALUE 형식만 사용
cat /etc/environment
# 출력:
# PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
# LANG="en_US.UTF-8"
# 이 파일은 PAM(Pluggable Authentication Modules)이 읽으므로
# SSH, GUI 로그인, sudo 등 모든 경우에 적용됨
어떤 파일에 무엇을 써야 하는가:
~/.bashrc: alias, 함수, PATH 추가, 프롬프트(PS1) 설정 — 대부분의 개인 설정~/.bash_profile: 로그인 시 한 번만 실행되어야 하는 것 (보통.bashrc호출만)/etc/environment: 시스템 전체에 적용할 단순 KEY=VALUE 변수 (쉘 문법 사용 불가)
실습 — 기본기 다지기
실습 전 디렉토리와 예제 파일을 먼저 준비합니다.
# 실습 디렉토리 준비
mkdir -p /tmp/linux/part2/exam_11 && cd /tmp/linux/part2/exam_11
# .env 파일 생성
cat > /tmp/linux/part2/exam_11/.env << 'EOF'
APP_ENV=development
DB_HOST=localhost
DB_PORT=5432
APP_PORT=8080
SECRET_KEY=dev-secret-change-in-prod
EOF
# 환경변수 로드 테스트 스크립트 생성
cat > /tmp/linux/part2/exam_11/app.sh << 'EOF'
#!/bin/bash
source .env
echo "Environment: $APP_ENV"
echo "DB: $DB_HOST:$DB_PORT"
echo "App Port: $APP_PORT"
EOF
chmod +x /tmp/linux/part2/exam_11/app.sh
이제 실습을 진행합니다.
서버에 처음 접속했을 때 현재 환경이 어떻게 설정되어 있는지 파악하는 것이 첫 번째 습관입니다.
# 모든 환경변수 출력 (알파벳 정렬)
env | sort
출력 예시:
DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus
HOME=/home/alice
LANG=en_US.UTF-8
LOGNAME=alice
MAIL=/var/mail/alice
PATH=/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/home/alice/.local/bin:/home/alice/bin
PWD=/home/alice
SHELL=/bin/bash
TERM=xterm-256color
USER=alice
XDG_RUNTIME_DIR=/run/user/1000
# 특정 변수 하나만 확인
printenv PATH
# 출력: /usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/home/alice/.local/bin
# 변수 존재 여부 확인 (스크립트에서 유용)
if [ -z "$JAVA_HOME" ]; then
echo "JAVA_HOME이 설정되지 않았습니다"
fi
# 출력: JAVA_HOME이 설정되지 않았습니다
# 현재 로그인한 사용자 확인
echo "사용자: $USER, 홈 디렉토리: $HOME, 쉘: $SHELL"
# 출력: 사용자: alice, 홈 디렉토리: /home/alice, 쉘: /bin/bash
자주 사용하는 내장 환경변수 — 쉘이 기본으로 제공하는 변수들로 경로, 사용자, 홈 디렉터리 등을 담고 있습니다:
echo $HOME # 홈 디렉토리: /home/alice
echo $USER # 현재 사용자: alice
echo $SHELL # 기본 쉘: /bin/bash
echo $PWD # 현재 디렉토리: /home/alice/projects
echo $OLDPWD # 이전 디렉토리 (cd - 명령의 동작 원리)
echo $HOSTNAME # 호스트명: web-server-01
echo $LANG # 언어 설정: en_US.UTF-8
echo $TERM # 터미널 타입: xterm-256color
echo $$ # 현재 쉘의 PID: 12345
echo $? # 직전 명령의 종료 코드: 0 (성공) 또는 1 이상 (실패)
- 먼저 env | sort 출력에서 PATH 항목을 찾는다 — 그 다음 echo $PATH | tr ':' '\n' 으로 경로를 한 줄씩 분리해 어느 디렉토리가 먼저 검색되는지 순서를 확인한다
- bash -c 'echo $MY_VAR' 의 출력이 비어 있으면 export 없이 선언한 것이다 — 동일 변수를 export 후 같은 명령을 실행하면 값이 출력된다. 이 두 결과의 차이가 지역 변수 vs 환경변수의 핵심이다
- 새 터미널(비로그인 셸)에서 export 설정 변수가 사라지면 → ~/.bashrc에 기록되지 않은 것. .bashrc에 추가 후 source ~/.bashrc 하면 즉시 적용되고 이후 모든 새 터미널에서도 유지된다
$PATH는 쉘이 명령어를 찾는 디렉토리 목록입니다. 콜론(:)으로 구분되며, 왼쪽에서 오른쪽 순서로 검색합니다.
# PATH 현재 값 확인
echo $PATH
# 출력: /usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin
# 보기 좋게 한 줄씩 출력
echo $PATH | tr ':' '\n'
# 출력:
# /usr/local/bin
# /usr/bin
# /bin
# /usr/local/sbin
# /usr/sbin
# 특정 명령어가 어디서 실행되는지 확인
which python3
# 출력: /usr/bin/python3
which java
# 출력: /usr/lib/jvm/java-17-openjdk/bin/java
# which가 없는 명령어는 PATH에 없다는 뜻
which myapp
# 출력: which: no myapp in (/usr/local/bin:/usr/bin:/bin:...)
PATH에 디렉토리 추가 — PATH 앞이나 뒤에 디렉터리를 추가해 명령어 검색 경로를 확장합니다:
# 현재 세션에서만 유효한 PATH 추가 (맨 앞에 추가 — 우선순위 높음)
export PATH="/opt/myapp/bin:$PATH"
# 확인
echo $PATH
# 출력: /opt/myapp/bin:/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin
# 맨 뒤에 추가 (우선순위 낮음 — 기존 명령이 있으면 기존 것 사용)
export PATH="$PATH:/opt/legacy/bin"
PATH 추가를 영구적으로 ~/.bashrc에 기록 — 재부팅 후에도 PATH가 유지되도록 설정 파일에 추가합니다:
# ~/.bashrc 파일 끝에 추가
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc
# 변경사항 즉시 적용
source ~/.bashrc
# 확인
echo $PATH | tr ':' '\n' | grep local
# 출력: /home/alice/.local/bin
type 명령으로 명령어 종류 확인 — 명령어가 실행 파일인지, alias인지, 내장 명령인지 구분합니다:
# which보다 더 많은 정보를 제공
type ls
# 출력: ls is aliased to `ls --color=auto'
type cd
# 출력: cd is a shell builtin
type python3
# 출력: python3 is /usr/bin/python3
type ll
# 출력: ll is aliased to `ls -alF'
.bashrc를 수정한 후 로그아웃하지 않고 즉시 적용하는 방법이 source 명령입니다. . (점)은 source의 동의어입니다.
# ~/.bashrc를 수정했다면
nano ~/.bashrc
# (편집 후 저장)
# 방법 1: source 명령
source ~/.bashrc
# 방법 2: . (점) 명령 — 완전히 동일한 동작
. ~/.bashrc
# 방법 3: bash를 다시 실행 (새 프로세스 생성 — source와 다름)
exec bash # 현재 쉘을 새 bash로 교체
source vs 새 쉘 실행의 차이 — source는 현재 쉘에서, bash script.sh는 자식 쉘에서 실행해 변수 전달 방식이 다릅니다:
# source는 현재 쉘 컨텍스트에서 실행
# → 지역 변수, 현재 디렉토리 등이 유지됨
export TEST_VAR="hello"
source ~/.bashrc # TEST_VAR가 유지됨 (현재 쉘에서 실행)
echo $TEST_VAR
# 출력: hello
# bash 스크립트로 실행하면 자식 프로세스에서 실행
# → 환경변수 변경이 부모 쉘에 영향을 주지 않음
bash some-script.sh # 자식 프로세스 실행, 부모 쉘에 영향 없음
source 실전 활용 패턴 — .env 파일 로드, 설정 파일 즉시 반영 등 현장에서 자주 쓰는 source 패턴입니다:
# 환경 설정 파일을 분리해서 관리
# ~/.bashrc 내용:
cat ~/.bashrc
# ...
# source ~/.bash_aliases # alias 모음
# source ~/.bash_functions # 함수 모음
# source ~/.bash_private # 비밀 키 등 (git에 올리지 않음)
# 각 파일이 존재할 때만 읽도록 방어적으로 작성
if [ -f ~/.bash_aliases ]; then
source ~/.bash_aliases
fi
특정 파일이 실제로 로드되고 있는지 디버그 — 설정 파일이 로드되는지 확인하고 로드 순서 문제를 진단합니다:
# ~/.bashrc 최상단에 임시로 추가해서 확인
# echo "~/.bashrc loaded at $(date)" >> /tmp/shell-debug.log
# 어떤 순서로 파일이 로드되는지 추적
bash --login -x 2>&1 | grep "source\|^\+" | head -30
alias는 긴 명령을 짧게 줄이는 쉘 기능입니다. 생산성에 직접적인 영향을 주므로 자신만의 alias 세트를 만드는 것이 중요합니다.
# 현재 세션에서 alias 정의
alias ll='ls -alF'
alias la='ls -A'
alias l='ls -CF'
alias grep='grep --color=auto'
# alias 목록 확인
alias
# 출력:
# alias grep='grep --color=auto'
# alias l='ls -CF'
# alias la='ls -A'
# alias ll='ls -alF'
# alias 사용
ll /etc
# 출력:
# total 1084
# drwxr-xr-x. 141 root root 8192 Mar 26 09:15 ./
# drwxr-xr-x. 18 root root 235 Mar 10 14:22 ../
# -rw-r--r--. 1 root root 16 Mar 10 14:22 adjtime
# ...
# alias 삭제
unalias ll
실무에서 자주 쓰는 alias 모음 — 반복되는 긴 명령어를 짧게 줄이는 현장 alias 모음입니다:
# 안전 장치
alias rm='rm -i' # 삭제 전 확인 요청
alias cp='cp -i' # 덮어쓰기 전 확인
alias mv='mv -i' # 이동 전 확인
# 디렉토리 이동
alias ..='cd ..'
alias ...='cd ../..'
alias ....='cd ../../..'
# 자주 가는 디렉토리
alias proj='cd ~/projects'
alias logs='cd /var/log'
# 시스템 정보
alias df='df -hT' # 사람이 읽기 좋은 형식
alias du='du -sh' # 디렉토리 용량 요약
alias ps='ps auxf' # 트리 형태로 프로세스 표시
alias ports='ss -tulnp' # 열린 포트 확인
alias myip='curl -s ifconfig.me' # 외부 IP 확인
# Git 단축어
alias gs='git status'
alias ga='git add'
alias gc='git commit'
alias gp='git push'
alias gl='git log --oneline --graph --decorate'
alias를 영구적으로 저장하는 패턴 — alias를 설정 파일에 추가해 재부팅 후에도 유지합니다:
# ~/.bash_aliases 파일 생성 (관리하기 쉽도록 별도 파일로 분리)
cat > ~/.bash_aliases << 'EOF'
# 안전 장치
alias rm='rm -i'
alias cp='cp -i'
alias mv='mv -i'
# 탐색
alias ll='ls -alF --color=auto'
alias la='ls -A'
alias ..='cd ..'
alias ...='cd ../..'
# 시스템
alias df='df -hT'
alias ports='ss -tulnp'
alias myip='curl -s ifconfig.me'
# Git
alias gs='git status'
alias gl='git log --oneline --graph --decorate -20'
EOF
# ~/.bashrc에 자동 로드 추가
echo '' >> ~/.bashrc
echo '# alias 파일 로드' >> ~/.bashrc
echo 'if [ -f ~/.bash_aliases ]; then' >> ~/.bashrc
echo ' source ~/.bash_aliases' >> ~/.bashrc
echo 'fi' >> ~/.bashrc
# 즉시 적용
source ~/.bashrc
# 확인
alias rm
# 출력: alias rm='rm -i'
자주 쓰는 스크립트를 어느 디렉토리에서든 명령어처럼 실행하려면, 스크립트가 위치한 디렉토리를 PATH에 추가해야 합니다.
# 개인 스크립트 디렉토리 생성
mkdir -p ~/bin
# 스크립트 예시 작성
cat > ~/bin/server-info << 'EOF'
#!/bin/bash
echo "=== 서버 정보 ==="
echo "호스트명: $(hostname)"
echo "IP 주소: $(hostname -I | awk '{print $1}')"
echo "OS: $(cat /etc/os-release | grep PRETTY_NAME | cut -d'"' -f2)"
echo "커널: $(uname -r)"
echo "CPU 코어: $(nproc)"
echo "메모리: $(free -h | awk '/^Mem/{print $2}') 전체, $(free -h | awk '/^Mem/{print $7}') 사용 가능"
echo "디스크: $(df -h / | awk 'NR==2{print $2}') 전체, $(df -h / | awk 'NR==2{print $4}') 사용 가능"
echo "업타임: $(uptime -p)"
EOF
# 실행 권한 부여
chmod +x ~/bin/server-info
# ~/bin을 PATH에 추가 (이미 /etc/profile.d/가 추가한 경우도 있음)
# 현재 세션에서 테스트
export PATH="$HOME/bin:$PATH"
# 어느 디렉토리에서든 실행 가능한지 확인
cd /tmp
server-info
출력 예시:
=== 서버 정보 ===
호스트명: web-server-01
IP 주소: 192.168.1.100
OS: Red Hat Enterprise Linux 9.4 (Plow)
커널: 5.14.0-570.52.1.el9_6.x86_64
CPU 코어: 4
메모리: 7.6Gi 전체, 5.2Gi 사용 가능
디스크: 50G 전체, 35G 사용 가능
업타임: up 15 days, 3 hours, 42 minutes
# ~/.bashrc에 영구 등록
cat >> ~/.bashrc << 'EOF'
# 개인 스크립트 디렉토리
if [ -d "$HOME/bin" ]; then
export PATH="$HOME/bin:$PATH"
fi
# ~/.local/bin도 있으면 추가 (pip install --user 등이 여기에 설치)
if [ -d "$HOME/.local/bin" ]; then
export PATH="$HOME/.local/bin:$PATH"
fi
EOF
source ~/.bashrc
# PATH에 추가되었는지 확인
echo $PATH | tr ':' '\n' | grep -E "bin$"
# 출력:
# /home/alice/bin
# /home/alice/.local/bin
실무 — Java/Python/Node.js 버전 전환
확대
실무에서는 프로젝트마다 다른 언어 버전이 필요한 경우가 많습니다. nvm, pyenv, sdkman 같은 도구들도 결국 내부적으로 PATH를 조작하는 방식으로 동작합니다. 그 원리를 직접 이해하면 어떤 도구든 트러블슈팅이 쉬워집니다.
원리: PATH 앞부분에 원하는 버전의 bin 디렉토리를 추가
PATH는 왼쪽에서 오른쪽으로 검색하므로, 맨 앞에 추가된 디렉토리가 항상 우선 실행됩니다.
# 현재 Java 버전 확인
java -version
# 출력: openjdk version "11.0.22" 2024-01-16
# OpenJDK Runtime Environment (build 11.0.22+7)
# 시스템에 설치된 Java 버전들 확인
ls /usr/lib/jvm/
# 출력:
# java-11-openjdk-amd64
# java-17-openjdk-amd64
# java-21-openjdk-amd64
# Java 17로 전환 (현재 세션만)
export JAVA_HOME="/usr/lib/jvm/java-17-openjdk-amd64"
export PATH="$JAVA_HOME/bin:$PATH"
java -version
# 출력: openjdk version "17.0.10" 2024-01-16
# OpenJDK Runtime Environment (build 17.0.10+7)
# ~/.bashrc에 영구 적용
cat >> ~/.bashrc << 'EOF'
# Java 환경 설정
export JAVA_HOME="/usr/lib/jvm/java-17-openjdk-amd64"
export PATH="$JAVA_HOME/bin:$PATH"
EOF
Python 버전 전환 (pyenv 원리 이해) — pyenv는 PATH 앞에 shim을 넣어 버전별 실행 파일을 전환합니다:
# pyenv를 설치하지 않고 수동으로 같은 원리 구현
# 여러 버전의 Python이 /opt/python/ 아래 설치되어 있다고 가정
ls /opt/python/
# 출력: 3.9.18 3.10.13 3.11.8 3.12.2
# Python 3.11 사용
export PATH="/opt/python/3.11.8/bin:$PATH"
python3 --version
# 출력: Python 3.11.8
# pyenv가 실제로 하는 일 확인 (pyenv 설치 시)
cat ~/.pyenv/shims/python3
# 실제로는 PATH 앞부분에 shims 디렉토리를 추가하고
# shim 스크립트가 올바른 버전으로 호출을 중계함
# pyenv 설치 시 ~/.bashrc에 들어가는 내용
export PYENV_ROOT="$HOME/.pyenv"
export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init -)" # pyenv가 PATH를 조작하는 함수 실행
Node.js 버전 전환 (nvm 원리) — nvm도 PATH를 조작해 활성화된 버전의 node를 먼저 찾도록 합니다:
# nvm 없이 수동으로 Node 버전 전환
ls /opt/node/
# 출력: v18.20.2 v20.12.0 v22.1.0
# Node 20 사용
export PATH="/opt/node/v20.12.0/bin:$PATH"
node --version
# 출력: v20.12.0
npm --version
# 출력: 10.5.0
# nvm 설치 시 ~/.bashrc에 들어가는 내용
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # nvm 로드
[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"
프로젝트별 자동 버전 전환 — .nvmrc 또는 .python-version 활용 — 프로젝트 디렉터리에 들어오면 지정된 버전으로 자동 전환합니다:
# 프로젝트 디렉토리에 버전 파일 생성
echo "20" > ~/projects/my-app/.nvmrc
echo "3.11" > ~/projects/ml-project/.python-version
# ~/.bashrc에 디렉토리 이동 시 자동 전환 함수 추가
cat >> ~/.bashrc << 'EOF'
# 디렉토리 이동 시 .nvmrc 자동 감지
cd() {
builtin cd "$@"
if [ -f ".nvmrc" ] && command -v nvm &>/dev/null; then
nvm use --silent
fi
}
EOF
버전 전환 확인 스크립트 — 현재 활성화된 Python/Node 버전과 PATH를 한 번에 확인합니다:
cat ~/bin/check-versions << 'EOF'
#!/bin/bash
echo "=== 현재 활성 런타임 버전 ==="
command -v java &>/dev/null && echo "Java: $(java -version 2>&1 | head -1)"
command -v python3 &>/dev/null && echo "Python: $(python3 --version)"
command -v node &>/dev/null && echo "Node: $(node --version)"
command -v npm &>/dev/null && echo "npm: $(npm --version)"
echo ""
echo "=== 런타임 위치 ==="
command -v java &>/dev/null && echo "java: $(which java)"
command -v python3 &>/dev/null && echo "python3: $(which python3)"
command -v node &>/dev/null && echo "node: $(which node)"
EOF
chmod +x ~/bin/check-versions
check-versions
# 출력:
# === 현재 활성 런타임 버전 ===
# Java: openjdk version "17.0.10" 2024-01-16
# Python: Python 3.11.8
# Node: v20.12.0
# npm: 10.5.0
#
# === 런타임 위치 ===
# java: /usr/lib/jvm/java-17-openjdk-amd64/bin/java
# python3: /opt/python/3.11.8/bin/python3
# node: /opt/node/v20.12.0/bin/node
트러블슈팅
상황: git status를 실행하면 command not found가 납니다. 분명히 예전엔 됐는데 갑자기 안 되거나, 새 서버에서 처음 접속했을 때 발생합니다.
원인: (1) 프로그램이 설치되지 않음, (2) 설치는 됐지만 실행 파일 경로가 PATH에 없음, (3) 오타 — 세 가지 중 하나입니다. PATH가 .bashrc 오타로 오염되거나, 다른 도구가 PATH를 덮어쓴 경우에도 발생합니다.
진단: 설치 여부와 PATH를 단계별로 확인합니다.
# 1단계: 실행 파일이 시스템 어디에 있는지 확인
which git # PATH 검색
find /usr -name "git" -type f 2>/dev/null
# /usr/bin/git ← 설치는 됐는데 PATH에 없는 것
# 2단계: 발견된 경로로 직접 실행 (PATH와 무관하게)
/usr/bin/git --version
# git version 2.39.3 ← 실행 됨 = PATH 문제 확정
# 3단계: 현재 PATH에서 무엇이 빠져 있는지 확인
echo $PATH | tr ':' '\n'
# /usr/local/bin
# /home/alice/bin
# ← /usr/bin이 없음
# PATH 오염 원인 추적
grep -n "PATH" ~/.bashrc ~/.bash_profile ~/.profile 2>/dev/null
해결:
# 임시 복구 (현재 세션)
export PATH="/usr/bin:$PATH"
git status # 이제 됨
# PATH가 완전히 망가진 경우 기본값으로 리셋
export PATH="/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin"
# .bashrc에서 문제 줄 수정 후 적용
nano ~/.bashrc
source ~/.bashrc
상황: .bashrc를 수정하고 source ~/.bashrc를 실행했더니 ls, cat, which 같은 기본 명령어까지 전부 command not found가 납니다. 터미널이 거의 사용 불가 상태가 됐습니다.
원인: export PATH="/opt/myapp" 처럼 $PATH를 빠뜨리고 경로를 완전히 덮어쓴 경우 발생합니다. 기존 /usr/bin, /bin 경로가 모두 날아가면서 시스템 명령어들을 찾을 수 없게 됩니다.
진단: 현재 PATH가 무엇인지 확인합니다.
# 현재 PATH 확인
echo $PATH
# /opt/myapp ← /usr/bin, /bin이 없음
# .bashrc에서 문제 줄 찾기 (절대 경로 사용 - PATH 없이)
/usr/bin/grep -n "PATH" /home/$USER/.bashrc
# 45: export PATH="/opt/myapp" ← $PATH 누락
해결:
# PATH 즉시 복구 (export는 bash 내장 명령이므로 PATH 불필요)
export PATH="/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin"
# 이제 기본 명령어 사용 가능해짐
grep -n "PATH" ~/.bashrc
# 45: export PATH="/opt/myapp"
# 문제 줄 수정
nano ~/.bashrc
# export PATH="/opt/myapp:$PATH" ← $PATH 추가
source ~/.bashrc # 적용
# 예방: .bashrc 수정 전 백업 습관화
cp ~/.bashrc ~/.bashrc.backup.$(date +%Y%m%d)
# 새 PATH 설정은 서브쉘에서 먼저 테스트
(export PATH="/opt/newapp/bin:$PATH"; newapp --version)
상황: 터미널에서 직접 ~/bin/deploy.sh를 실행하면 잘 되는데, crontab에 등록하고 자동으로 돌리면 항상 실패합니다. 로그에는 command not found가 찍혀 있습니다.
원인: cron은 매우 제한된 환경변수(PATH=/usr/bin:/bin, SHELL=/bin/sh)만 가집니다. 터미널에서 설정한 JAVA_HOME, PYENV_ROOT, NVM_DIR 등은 cron 환경에서 존재하지 않습니다.
진단: cron 실제 환경변수를 파일로 덤프해 확인합니다.
# crontab에 1회성 디버그 작업 추가
crontab -e
# * * * * * env > /tmp/cron-env.txt 2>&1
# 1분 후 확인
cat /tmp/cron-env.txt
# HOME=/home/alice
# LOGNAME=alice
# PATH=/usr/bin:/bin ← /opt/maven/bin이 없음
# SHELL=/bin/sh ← bash가 아닌 sh
# ← JAVA_HOME, MAVEN_HOME 모두 없음
# cron과 동일한 환경에서 스크립트 테스트
env -i HOME=$HOME LOGNAME=$USER PATH=/usr/bin:/bin SHELL=/bin/sh bash ~/bin/deploy.sh
해결:
# 방법 1 (권장): 스크립트 내에서 환경변수 명시적 선언
cat > ~/bin/deploy.sh << 'EOF'
#!/bin/bash
export JAVA_HOME="/usr/lib/jvm/java-17-openjdk-amd64"
export MAVEN_HOME="/opt/maven"
export PATH="$JAVA_HOME/bin:$MAVEN_HOME/bin:/usr/local/bin:/usr/bin:/bin"
cd /home/alice/projects/my-app || exit 1
mvn clean package -q >> /var/log/deploy.log 2>&1
EOF
chmod +x ~/bin/deploy.sh
# 방법 2: crontab 상단에 환경변수 선언
crontab -e
# SHELL=/bin/bash
# PATH=/opt/maven/bin:/usr/lib/jvm/java-17/bin:/usr/bin:/bin
# JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64
# 0 2 * * * /home/alice/bin/deploy.sh >> /var/log/deploy.log 2>&1
# cron 로그 확인 (실행 자체가 안 될 때)
journalctl -u crond --since "1 hour ago"
grep CRON /var/log/syslog | tail -20
심화 — 파일은 바꿨는데 프로세스는 옛 값을 붙든다
심화: 환경은 프로세스마다 복사된 스냅샷이다
지금까지는 "어느 파일에 쓰면 적용되나"를 다뤘습니다. 그런데 파일을 올바르게 고쳤는데도 이미 실행 중인 프로세스에는 그 변경이 통하지 않는 경우가 있습니다. 이 벽을 이해하려면 환경변수가 '살아 있는 공유 저장소'가 아니라 '프로세스마다 복사된 사본'이라는 사실을 알아야 합니다.
- 환경은 exec 시점의 스냅샷입니다. 프로세스가 시작(fork 후 exec)될 때 부모의 environ을 통째로 복사해 자기 메모리에 들고 갑니다. 이후 부모가 값을 바꿔도 이미 뜬 자식에게는 반영되지 않고, 반대로 자식이 바꾼 값도 부모로 돌아가지 않습니다. 상태는 시작하는 순간 한 번 아래로만 복사됩니다.
- 그래서 dotfile·
/etc/environment를 고쳐도, 그 전에 시작된 셸·데몬·GUI 세션은 옛 값을 그대로 붙듭니다. 새 값은 "그 파일을 새로 읽고 시작한 프로세스"부터 보입니다. 방금 연 SSH 세션에선 새 값이 나오는데 오래된 데몬은 옛 값을 쓰는 불일치가 여기서 생깁니다. - 실행 중 프로세스의 실제 환경은
/proc/PID/environ에 있습니다. 추측 대신 증거로 확인하세요:
# 특정 프로세스가 지금 들고 있는 환경변수 (변수는 NUL로 구분됨)
cat /proc/$(pgrep -f myapp | head -1)/environ | tr '\0' '\n' | sort
# 내 셸 프로세스의 환경도 같은 방식으로 볼 수 있다
cat /proc/self/environ | tr '\0' '\n' | grep PATH
- 반영시키려면 그 프로세스를 재시작해야 합니다. 실행 중 프로세스의 환경을 밖에서 안전하게 바꾸는 표준 방법은 없기 때문입니다. systemd 서비스면 유닛의
Environment=/EnvironmentFile=을 갱신한 뒤systemctl daemon-reload && systemctl restart <서비스>./etc/environment처럼 PAM이 로그인 시 1회 읽는 변수는, 이미 로그인된 세션과 그 자식에는 안 들어오므로 재로그인이 필요합니다.
정리하면, 환경변수 문제는 두 층입니다 — "어느 파일에 쓰나(적용 대상)"와 "언제 읽히나(스냅샷 시점)". 앞 절이 첫 번째 층이라면, 이 절의 스냅샷 개념이 두 번째 층입니다.
상황: /etc/environment(또는 서비스의 EnvironmentFile)에서 DB_HOST를 새 값으로 수정하고 저장했습니다. 그런데 오래 떠 있던 애플리케이션은 여전히 옛 DB로 붙습니다. 더 헷갈리는 건, 새 SSH 세션에서 echo $DB_HOST를 하면 새 값이 정상적으로 나온다는 점입니다. "파일도 맞고 셸에서도 새 값인데 왜 서비스만 옛날이지?"
원인: 그 서비스는 파일을 수정하기 전에 시작됐고, 시작 시점에 복사한 environ 스냅샷을 그대로 들고 있습니다. 파일을 나중에 바꿔도 이미 뜬 프로세스의 사본은 자동 갱신되지 않습니다. 새 셸에서 새 값이 보이는 건, 그 셸이 수정 후에 시작돼 파일을 새로 읽었기 때문입니다 — 서비스의 값과는 무관합니다.
진단: 그 서비스가 지금 실제로 들고 있는 값을 직접 확인합니다 — cat /proc/$(pgrep -f myapp | head -1)/environ | tr '\0' '\n' | grep DB_HOST. 여기서 옛 값이 나오는데 새 셸의 echo $DB_HOST는 새 값이면, "파일은 새 값, 프로세스는 옛 값"이라는 불일치가 확정됩니다. 이 대조가 원인을 못 박는 결정적 증거입니다.
해결: 값을 바꾼 뒤 해당 프로세스를 재시작해야 새 스냅샷을 뜹니다. systemd 서비스면 EnvironmentFile 갱신 → systemctl daemon-reload → systemctl restart myapp 순서로. /etc/environment처럼 로그인 세션에 걸린 변수라면, 그 세션에서 뜬 프로세스는 재로그인(또는 서비스 재시작)해야 반영됩니다. source나 export는 지금 이 셸과 앞으로의 자식에만 영향을 줄 뿐, 이미 따로 떠 있는 서비스에는 닿지 않는다는 점을 기억하세요.
고급 활용 — dotfiles 체계적으로 관리하기
새 서버에 접속할 때마다 alias를 다시 설정하고, PATH를 다시 추가하고, 프롬프트를 다시 꾸미는 상황이 반복된다면, dotfiles를 체계적으로 관리할 때입니다. .bashrc를 Git 저장소로 관리하면 어느 서버에서든 git clone 한 번으로 동일한 환경을 복원할 수 있습니다. 아래는 실무에서 바로 쓸 수 있는 .bashrc 구조와 Git 기반 dotfiles 관리 패턴입니다.
# 실무에서 권장하는 ~/.bashrc 구조
cat ~/.bashrc
#!/bin/bash
# ~/.bashrc — Alice의 쉘 설정
# 마지막 수정: 2026-03-26
# 1. 인터랙티브 쉘이 아니면 즉시 종료 (중요!)
case $- in
*i*) ;;
*) return;;
esac
# 2. 히스토리 설정
HISTCONTROL=ignoreboth # 중복 및 공백으로 시작하는 명령 저장 안 함
HISTSIZE=10000 # 메모리에 저장할 히스토리 수
HISTFILESIZE=20000 # 파일에 저장할 히스토리 수
shopt -s histappend # 쉘 종료 시 히스토리 파일에 추가 (덮어쓰지 않음)
# 3. 쉘 동작 설정
shopt -s checkwinsize # 터미널 크기 변경 시 자동 업데이트
shopt -s globstar # **/ 패턴 지원
# 4. PATH 설정
if [ -d "$HOME/bin" ]; then
export PATH="$HOME/bin:$PATH"
fi
if [ -d "$HOME/.local/bin" ]; then
export PATH="$HOME/.local/bin:$PATH"
fi
# 5. 개발 환경
export JAVA_HOME="/usr/lib/jvm/java-17-openjdk-amd64"
export PATH="$JAVA_HOME/bin:$PATH"
export EDITOR="vim"
export VISUAL="vim"
export PAGER="less"
# 6. alias 로드
if [ -f ~/.bash_aliases ]; then
source ~/.bash_aliases
fi
# 7. 프롬프트 설정 (PS1)
# \u = 사용자, \h = 호스트, \w = 현재 디렉토리
PS1='\[\033[01;32m\]\u@\h\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]\$ '
# 8. 자동완성
if ! shopt -oq posix; then
if [ -f /usr/share/bash-completion/bash_completion ]; then
source /usr/share/bash-completion/bash_completion
fi
fi
dotfiles를 Git으로 관리하는 방법 — 설정 파일을 Git 저장소로 관리해 서버마다 일관된 환경을 유지합니다:
# dotfiles 저장소 초기화
mkdir -p ~/dotfiles
cd ~/dotfiles
git init
# 심볼릭 링크 방식으로 관리
cp ~/.bashrc ~/dotfiles/bashrc
ln -sf ~/dotfiles/bashrc ~/.bashrc
cp ~/.bash_aliases ~/dotfiles/bash_aliases
ln -sf ~/dotfiles/bash_aliases ~/.bash_aliases
# Git에 추가
cd ~/dotfiles
git add bashrc bash_aliases
git commit -m "초기 dotfiles 설정"
# GitHub에 올려두면 새 서버에서 바로 복원 가능
git remote add origin git@github.com:alice/dotfiles.git
git push -u origin main
새 서버에 dotfiles 설치 스크립트 — 새 서버에서 한 번의 실행으로 개인 환경 설정을 모두 적용합니다:
cat > ~/dotfiles/install.sh << 'EOF'
#!/bin/bash
# 새 서버에 dotfiles 설치
DOTFILES_DIR="$HOME/dotfiles"
echo "dotfiles 설치 시작..."
# 기존 파일 백업
for file in bashrc bash_aliases bash_profile; do
if [ -f "$HOME/.$file" ] && [ ! -L "$HOME/.$file" ]; then
mv "$HOME/.$file" "$HOME/.$file.backup.$(date +%Y%m%d)"
echo "백업: ~/.$file → ~/.$file.backup.$(date +%Y%m%d)"
fi
done
# 심볼릭 링크 생성
ln -sf "$DOTFILES_DIR/bashrc" "$HOME/.bashrc"
ln -sf "$DOTFILES_DIR/bash_aliases" "$HOME/.bash_aliases"
ln -sf "$DOTFILES_DIR/bash_profile" "$HOME/.bash_profile"
echo "설치 완료. 'source ~/.bashrc' 실행하세요."
EOF
chmod +x ~/dotfiles/install.sh
빠른 참고표
환경변수 관련 명령어 요약 — 자주 쓰는 환경변수 관련 명령어와 용도를 한눈에 정리했습니다:
| 명령어 | 용도 | 예시 |
|---|---|---|
export VAR=value | 환경변수 설정 및 내보내기 | export DEBUG=true |
unset VAR | 변수 삭제 | unset DEBUG |
printenv VAR | 특정 변수 값 출력 | printenv HOME |
env | 모든 환경변수 출력 | env | sort |
set | 변수 + 함수 모두 출력 | set | grep PATH |
source FILE | 파일을 현재 쉘에서 실행 | source ~/.bashrc |
. FILE | source의 단축형 | . ~/.bashrc |
which CMD | 명령어 위치 확인 | which python3 |
type CMD | 명령어 종류 확인 | type ls |
alias NAME='CMD' | alias 정의 | alias ll='ls -alF' |
unalias NAME | alias 삭제 | unalias ll |
alias | 모든 alias 목록 | alias |
dotfiles 로드 순서 요약 — 쉘 시작 모드별 설정 파일 로드 순서를 시각화합니다:
SSH 로그인 (Login Shell) — 순서대로 로드
/etc/environment— PAM이 읽음, 모든 로그인에 적용/etc/profile— 시스템 전체 login shell 설정/etc/profile.d/*.sh— 패키지별 시스템 설정~/.bash_profile— 사용자 login shell 설정 (보통~/.bashrc호출)
새 터미널 탭 (Interactive Non-Login Shell)
/etc/bash.bashrc— 시스템 전체 interactive 설정~/.bashrc— 사용자 interactive 설정
스크립트 / cron (Non-Interactive, Non-Login)
- 아무 설정 파일도 자동으로 읽지 않음! → 필요한 환경변수는 스크립트 내에서 직접 설정
명령어·단축키 빠른 참조
이 모듈에서 다룬 환경변수·dotfiles 명령을 실전 옵션과 함께 모았습니다.
| 명령어/단축키 | 용도 | 자주 쓰는 예 |
|---|---|---|
export | 변수를 자식 프로세스에 전달 | export PATH="/opt/app/bin:$PATH" |
unset | 변수 제거(민감정보 소거) | unset DB_PASSWORD |
env / printenv | 환경변수 목록·단일 조회 | env | sort, printenv HOME |
set | 지역변수·함수까지 전체 조회 | set | grep MY_VAR |
echo $PATH | tr ':' '\n' | PATH를 한 줄씩 확인 | 검색 순서(왼→오) 파악 |
which / type | 명령 위치·종류 확인 | type ls(alias·builtin 구분) |
source / . | 설정을 현재 셸에 즉시 적용 | source ~/.bashrc |
alias / unalias | 명령 단축 정의·해제 | alias rm='rm -i' |
~/.bashrc | 대화형 셸 개인 설정(alias·PATH) | 새 터미널마다 로드 |
~/.bash_profile | 로그인 셸 설정(보통 .bashrc 호출) | [ -f ~/.bashrc ] && . ~/.bashrc |
/etc/environment | 시스템 전역 KEY=VALUE(셸 문법 불가) | JAVA_HOME=/usr/lib/jvm/... |
/proc/PID/environ | 실행 중 프로세스의 실제 환경값 | tr '\0' '\n' < /proc/PID/environ |
관련 모듈로 더 깊이:
- 백엔드 개발자를 위한 실전 Bash 쉘 스크립트 핵심 가이드 — 스크립트 안에서 환경 변수를 안전하게 다루고 시크릿을 주입하는 법
- cron으로 리눅스 주기적 반복 작업(배치) 예약 및 백업 자동화 — cron이 dotfiles를 읽지 않아 환경 변수가 비는 함정과 해결
- 리눅스 부팅 시 데몬 프로세스 자동 실행 및 관리 가이드 — 유닛 파일의
Environment=로 서비스에 변수를 명시적으로 전달하기
다음 모듈에서는 프로세스 관리(Process Management)를 다룹니다 — ps, top, kill, nice, pgrep 등으로 실행 중인 프로세스를 조회하고 제어하는 방법을 배웁니다.