본문 바로가기
TYLER SONGBlog
← Blog index
CS학습5 min read

Shell: 사용자 명령을 해석해 커널에 전달하는 명령어 해석기

셸(Shell)은 사용자가 입력한 명령을 해석하고 운영체제 커널에 실행을 요청하는 프로그램이다. 입력을 파싱하고 fork, exec, wait 시스템 콜로 자식 프로세스를 실행하며, 파이프와 리다이렉션으로 프로그램을 조합할 수 있게 한다. 이 글은 셸의 실행 원리부터 기본 스크립트 작성, 실무 사용 시 주의점까지 다룬다.

By 송민성

1. 개념

셸(Shell)은 사용자와 운영체제 커널(Kernel) 사이에서 명령을 해석하고 실행을 중개하는 프로그램이다. 사용자가 입력한 문자열을 읽어 프로그램을 실행하고, 그 결과를 터미널에 돌려준다. 커널을 감싸는 껍질(shell)이라는 뜻에서 이름이 붙었다.

  • 터미널(Terminal): 입출력을 담당하는 창 또는 장치. 셸과는 다른 존재다.
  • 셸(Shell): 터미널에서 입력을 받아 해석하는 프로그램. bash, zsh, sh, fish 등이 있다.
  • 커널(Kernel): 프로세스, 파일, 메모리를 실제로 관리하는 운영체제의 핵심.

현재 사용 중인 셸은 다음과 같이 확인한다.

shell
echo "$SHELL" # 로그인 셸 경로 echo "$0" # 현재 실행 중인 셸 이름

2. 왜 사용하는가

  • 조합 가능성: 작은 프로그램 여러 개를 파이프(pipe)로 연결해 복잡한 작업을 처리한다.
  • 자동화: 반복 작업을 스크립트로 만들어 배포, 백업, 빌드에 활용한다.
  • 서버 접근성: 원격 서버(SSH)에서는 GUI 없이 셸이 기본 인터페이스다.
  • 재현성: 명령을 텍스트로 기록하면 같은 작업을 누구나 똑같이 수행할 수 있다.

3. 동작 원리

셸은 기본적으로 다음 루프를 반복한다(REPL 형태).

  1. 프롬프트를 출력하고 한 줄을 읽는다.
  2. 문자열을 단어 단위로 분리하고, 변수 치환($VAR), 글로빙(*.txt), 따옴표 처리 등을 수행한다.
  3. 명령이 내장 명령(builtin, 예: cd, export)이면 셸 프로세스가 직접 실행한다.
  4. 외부 프로그램이면 fork()로 자식 프로세스를 만들고, 자식에서 exec 계열 호출로 프로그램을 교체 실행한다.
  5. 부모(셸)는 wait 계열 호출로 자식의 종료를 기다리고, 종료 코드(exit status)를 받는다.

cd가 내장 명령인 이유는, 자식 프로세스에서 작업 디렉터리를 바꿔도 부모 셸에는 영향이 없기 때문이다.

외부 프로그램은 환경 변수 PATH에 나열된 디렉터리를 앞에서부터 탐색해 찾는다.

표준 스트림과 파일 디스크립터(File Descriptor)

번호이름기본 연결
0stdin키보드
1stdout터미널
2stderr터미널
  • 리다이렉션: >(덮어쓰기), >>(추가), <(입력), 2>(stderr)
  • 파이프: a | b는 a의 stdout을 b의 stdin에 연결한다. 두 프로세스는 동시에 실행된다.

종료 코드는 $?로 확인하며, 0은 성공, 0이 아닌 값은 실패를 뜻한다.

4. 코드 예제

파이프와 리다이렉션

shell
# 현재 디렉터리의 .log 파일 중 ERROR가 포함된 줄 수 세기 cat *.log 2>/dev/null | grep "ERROR" | wc -l # 결과를 파일로 저장 (stdout, stderr 분리) ls /etc /not-exist > out.txt 2> err.txt echo "exit status: $?"

변수, 조건문, 반복문

shell
#!/usr/bin/env bash set -euo pipefail name="${1:-world}" # 첫 번째 인자, 없으면 기본값 world echo "Hello, ${name}" if [[ -d "$HOME" ]]; then echo "홈 디렉터리가 존재합니다." fi for f in *.txt; do [[ -e "$f" ]] || continue # 매칭되는 파일이 없을 때 대비 echo "파일: $f ($(wc -l < "$f")줄)" done

함수와 종료 코드

shell
#!/usr/bin/env bash set -euo pipefail check_file() { local path="$1" if [[ ! -f "$path" ]]; then echo "파일 없음: $path" >&2 return 1 fi echo "확인 완료: $path" } check_file "$0" check_file "/tmp/does-not-exist" || echo "실패 코드: $?"

스크립트는 chmod +x script.sh 후 ./script.sh로 실행하거나, bash script.sh로 직접 실행한다.

5. 시간 복잡도 또는 성능 특성

셸에는 단일한 시간복잡도가 없지만 다음 특성을 알아두면 유용하다.

  • 프로세스 생성 비용: 외부 명령은 호출할 때마다 프로세스를 생성한다. 반복문 안에서 grep, awk, sed를 수만 번 호출하면 느려지므로, 가능하면 한 번의 호출로 전체 입력을 처리하거나 셸 내장 기능([[ ]], 파라미터 확장)을 쓴다.
  • 파이프라인 병렬성: 파이프로 연결된 프로세스들은 동시에 실행되며 데이터를 스트리밍으로 처리한다. 전체 파일을 메모리에 올리지 않고도 큰 파일을 처리할 수 있다.
  • 파이프 버퍼: 파이프에는 커널 버퍼 크기 제한이 있어, 소비자가 느리면 생산자가 대기(블로킹)한다.

6. 실무 사용 사례

  • 로그 분석: grep, awk, sort | uniq -c | sort -nr로 빈도 집계.
  • CI/CD 스크립트: 빌드, 테스트, 배포 단계를 스크립트로 연결.
  • 환경 설정: .bashrc, .zshrc에 별칭(alias)과 환경 변수 정의.
  • 배치 작업: cron과 결합한 백업, 정리 작업.
  • 컨테이너: Dockerfile의 RUN, ENTRYPOINT 스크립트.
shell
# 접근 로그에서 상태 코드별 개수 집계 (공백 구분 로그의 9번째 필드 가정) awk '{print $9}' access.log | sort | uniq -c | sort -nr

7. 주의할 점

  • 변수는 따옴표로 감싼다: rm $file은 공백이 포함되면 여러 인자로 쪼개진다. rm -- "$file"처럼 쓴다.
  • `set -euo pipefail` 사용: 오류 시 중단(-e), 미정의 변수 사용 금지(-u), 파이프 중간 실패 감지(pipefail). 다만 -e에는 조건문 내부 등 예외 동작이 있으므로 만능이 아니다.
  • 셸마다 문법이 다르다: [[ ]], 배열 등은 bash 확장이며 POSIX sh에서는 동작하지 않을 수 있다. 셔뱅(shebang)을 명확히 지정한다.
  • 위험한 명령: rm -rf "$DIR/"에서 DIR이 비어 있으면 의도치 않은 경로를 지울 수 있다. ${DIR:?}로 빈 값을 방지한다.
  • 복잡해지면 다른 언어를 고려: 데이터 구조나 오류 처리가 복잡하면 Python 등이 더 안전하다.
  • `>` 주의: 기존 파일을 경고 없이 덮어쓴다.

8. 핵심 정리

  • 셸은 명령을 해석해 커널에 실행을 요청하는 프로그램이며, 터미널과는 구분된다.
  • 외부 명령은 fork → exec → wait 흐름으로 실행되고, 내장 명령은 셸이 직접 처리한다.
  • stdin(0), stdout(1), stderr(2)와 리다이렉션, 파이프로 프로그램을 조합한다.
  • 종료 코드 0은 성공이며, $?로 확인한다.
  • 스크립트에서는 변수를 따옴표로 감싸고 set -euo pipefail을 기본으로 고려한다.
  • 반복문 안의 외부 명령 호출은 프로세스 생성 비용이 쌓이므로 주의한다.