LODY/정리

Ubuntu 서버 운영 / 셸의 선택: dash와 bash

셸의 선택: dash와 bash

🟣 로컬에선 되는데 서버에선 안 된다

배포 스크립트를 하나 짜서 서버에 올렸다. 로컬 맥에서는 잘 돌던 스크립트가 서버에서는 [[: not found 같은 오류를 뱉으며 멈췄다. 같은 리눅스 문법인데 한쪽에서만 깨지는 게 처음엔 이해가 안 됐다.

원인은 스크립트를 해석하는 셸이 서로 달랐다는 데 있었다. 로컬에서는 bash로 실행했는데, 서버에서는 sh로 실행했고, 우분투에서 /bin/shbash가 아니라 dash라는 다른 셸을 가리킨다. 두 셸은 같은 명령을 상당 부분 공유하지만, 완전히 같지는 않다. 이 사소해 보이는 차이가 실제 배포를 멈춰 세웠기 때문에, 셸이 대체 무엇이고 왜 우분투가 굳이 두 개를 나눠 쓰는지를 제대로 봐야 했다.


🟣 셸은 하나의 프로그램이다

셸을 특별한 무언가로 생각하기 쉽지만, 셸도 그냥 실행 파일 하나다. 사용자가 입력한 줄을 읽어서, 그것을 명령과 인자로 해석하고, 해당 프로그램을 실행해 주는 명령 해석기다. 서버에 SSH로 접속하면 로그인 과정의 마지막에 이 셸 프로그램이 하나 떠서 나를 맞이한다.

어떤 셸이 뜨는지는 사용자 계정마다 정해져 있다. /etc/passwd 파일의 각 줄 맨 끝 필드가 그 사용자의 로그인 셸 경로다.

$ grep "^$USER" /etc/passwd
lody:x:1000:1000:Lody:/home/lody:/bin/bash

맨 뒤의 /bin/bash가 로그인했을 때 실행될 셸이다. 이 값이 /bin/sh인 계정으로 접속하면 dash가 뜨고, /bin/bash면 bash가 뜬다. 셸은 이렇게 계정 설정 파일에 박혀 있는 하나의 프로그램 경로일 뿐이고, 바꾸고 싶으면 이 필드를 바꾸면 된다.

여기서 한 가지 구분이 필요하다. 로그인할 때 대화용으로 뜨는 셸(로그인 셸)과, 스크립트를 실행할 때 커널이 잠깐 띄우는 셸은 다를 수 있다. 앞의 배포 문제가 정확히 이 지점에서 났다. 내가 대화용으로 쓰는 셸은 bash였지만, 스크립트가 sh script.sh로 실행되는 순간 그 스크립트를 해석한 것은 dash였다.


🟣 dash와 bash는 무엇이 다른가

두 셸의 관계를 이해하려면 표준과 구현을 나눠 봐야 한다. 유닉스 셸에는 POSIX가 정한 sh 명세가 있다. 변수, 조건문, 반복문, 파이프 같은 기본 문법을 규정한 최소 공통 규격이다. dash와 bash는 이 규격의 서로 다른 구현이다.

dash는 Debian Almquist Shell의 줄임말로, POSIX sh 명세를 거의 그대로만 구현한 가볍고 작은 셸이다. 규격에 없는 편의 기능은 대부분 넣지 않았다. bash는 Bourne Again Shell로, POSIX 규격을 지키면서 그 위에 수많은 확장을 얹은 큰 셸이다. 배열, [[ ]] 확장 조건식, source, 프로세스 치환 같은 것들이 이 확장에 속한다.

POSIX sh 명세(공통 규격)dash규격만 최소 구현bash규격 + 확장 기능구현구현 후 확장

그래서 문제가 한 방향으로만 터진다. bash로 쓴 스크립트가 dash에서 깨지지, 그 반대는 드물다. bash 전용 문법을 흔히 bashism이라 부르는데, 앞서 스크립트를 멈춰 세운 [[ ]]가 대표적인 bashism이다. dash에는 이 확장이 없으니 [[를 알 수 없는 명령으로 보고 멈춘 것이다. POSIX 규격에 있는 [ ](test)만 썼다면 양쪽에서 다 돌았을 것이다.


🟣 왜 우분투 기본 sh는 dash인가

여기서 자연스러운 의문이 생긴다. bash가 dash의 기능을 다 포함하는데, 왜 우분투는 /bin/sh를 굳이 기능이 적은 dash로 걸어 두는가. 답은 부팅 속도와 자원에 있다.

시스템이 부팅될 때 커널은 수많은 초기화 스크립트를 실행한다. 각 스크립트는 그 안에서 또 다른 명령들을 부르고, 그때마다 /bin/sh가 짧게 떴다 사라지기를 반복한다. 부팅 한 번에 셸이 수천 번 실행되는 셈이다. 이때 셸 하나가 얼마나 빨리 뜨고 얼마나 적은 메모리를 쓰느냐가 부팅 전체 시간에 그대로 곱해진다.

bash는 기능이 많은 만큼 실행 파일이 크고 초기화에 더 많은 일을 한다. dash는 작고 가벼워서 뜨고 지는 속도가 훨씬 빠르다. 데비안이 2006년 무렵 /bin/sh의 연결을 bash에서 dash로 바꾼 이유가 이것이고, 우분투도 그 결정을 물려받았다. 실제로 이 변경만으로 부팅 시간이 눈에 띄게 줄었다는 것이 당시 근거였다. 자주 실행되는 셸일수록 무겁지 않아야 한다는 판단이다.

정리하면 우분투는 셸을 용도에 따라 나눠 쓴다. 시스템이 스크립트를 대량으로 돌리는 /bin/sh 자리에는 가벼운 dash를 두고, 사람이 직접 앉아서 명령을 치는 로그인 셸 자리에는 기능이 풍부한 bash를 둔다. 속도가 중요한 곳과 편의가 중요한 곳을 갈라 놓은 것이다.


🟣 로그인 셸을 bash로 고정하기

새로 만든 계정이나 최소 설치 환경에서는 로그인 셸이 dash로 잡혀 있는 경우가 있다. 대화형으로 쓰기에는 bash가 편하니, 로그인 셸을 bash로 바꾼다. 앞서 본 /etc/passwd의 마지막 필드를 직접 고쳐도 되지만, 정식 도구는 chsh다.

$ chsh -s /bin/bash
$ grep "^$USER" /etc/passwd   # 마지막 필드가 /bin/bash로 바뀐다

시스템 전체의 /bin/sh가 무엇을 가리키는지는 별개의 문제다. 이건 계정 설정이 아니라 dpkg-reconfigure dash로 관리하는 심볼릭 링크이고, 웬만하면 dash로 두는 편이 안전하다. 부팅 스크립트들이 dash를 전제로 쓰였기 때문이다. 바꾸고 싶은 것은 내 로그인 셸이지 시스템의 sh가 아니다라는 구분이 중요하다.


🟣 스크립트가 어느 셸로 도는지는 shebang이 정한다

배포 사고의 진짜 교훈은 스크립트의 첫 줄에 있었다. 스크립트 맨 위의 #!로 시작하는 줄을 shebang이라 하고, 커널은 이 줄을 보고 어떤 해석기로 이 파일을 실행할지 정한다.

#!/bin/sh      # dash로 해석된다 (bashism 쓰면 깨진다)
#!/bin/bash    # bash로 해석된다 (bashism 써도 된다)

#!/bin/sh로 시작하는 스크립트는 우분투에서 dash로 돈다. 여기에 [[ ]]나 배열 같은 bashism을 넣으면 다른 환경에서는 몰라도 우분투 서버에서는 깨진다. 두 가지 선택지가 있다. 이식성을 원하면 shebang을 #!/bin/sh로 두되 POSIX 문법만 쓰고, bash 기능이 꼭 필요하면 shebang을 #!/bin/bash로 명시해서 해석기를 확정한다. 어느 쪽이든 첫 줄과 본문의 문법을 일치시키는 것이 핵심이다.

이걸 눈으로 확인하는 습관도 들였다. shellcheck라는 정적 분석 도구가 스크립트를 훑어 bashism이 섞였는지, shebang과 문법이 맞는지 미리 잡아 준다. 배포 파이프라인에 이걸 한 줄 끼워 두니 같은 사고가 다시 나지 않았다.


🟣 정리

  • 셸은 특별한 존재가 아니라 명령을 해석해 실행하는 하나의 프로그램이고, 어떤 셸이 뜰지는 /etc/passwd의 마지막 필드에 박혀 있다.
  • dash와 bash는 POSIX sh 명세의 서로 다른 구현이다. dash는 규격만 최소로 담았고, bash는 규격 위에 배열·[[ ]] 같은 확장을 얹었다.
  • 우분투가 /bin/sh를 dash로 두는 이유는 부팅 때 셸이 수천 번 실행되기 때문이다. 자주 뜨는 셸은 가벼워야 부팅이 빠르다.
  • bashism으로 쓴 스크립트를 #!/bin/sh로 실행하면 dash가 해석하다 깨진다. shebang과 본문 문법을 일치시키고 shellcheck로 미리 검증한다.
  • 로그인 셸(chsh)과 시스템의 /bin/sh(심볼릭 링크)는 다른 대상이다. 바꾸려는 게 무엇인지부터 구분해야 한다.

실무 감각으로 남는 것은 "환경마다 셸이 다를 수 있다"는 전제다. 면접에서 "sh와 bash의 차이"를 물으면 대개 문법 나열로 끝나지만, 규격과 구현의 관계, 그리고 부팅 속도라는 운영상의 이유까지 이어 말할 수 있으면 이해의 결이 달라진다. 셸로 서버에 들어왔으니, 이제 그 셸로 무엇을 할 수 있는지가 정해져야 한다. 다음 장은 사용자와 권한 모델이다.