shebang(#!)이란 무엇인가

shebang은 스크립트 파일의 첫 줄에 작성하는 #! 형태의 선언이며, 이 파일을 어떤 인터프리터로 실행할지 운영체제에 알려주는 실행 지정자다.
필요한 이유는 스크립트가 바이너리 실행 파일이 아니기 때문이다. 운영체제는 파일을 직접 실행하라는 요청을 받으면, 이 파일을 해석할 프로그램이 무엇인지 먼저 알아야 한다.
핵심 차이는 이것이다. bash script.sh처럼 사용자가 인터프리터를 직접 지정하는 실행과 달리, ./script.sh처럼 파일을 직접 실행할 때는 shebang이 실행 기준점이 된다.

핵심 요약

shebang은 스크립트의 첫 줄에서 실행 인터프리터를 지정하는 구조다.
같은 스크립트라도 어떤 인터프리터로 실행하느냐에 따라 문법 해석과 실행 결과가 달라질 수 있기 때문에, shebang은 단순 표기가 아니라 실행 계약 선언에 가깝다.
#!/bin/bash는 고정 경로를 직접 지정하는 방식이고, #!/usr/bin/env bash는 현재 환경의 PATH를 기준으로 실행 파일을 찾는 방식이라는 점이 다르다.

왜 필요한가

스크립트 파일은 텍스트다.
운영체제 입장에서 텍스트 파일은 그 자체로 CPU가 바로 실행할 수 있는 형식이 아니다.
바이너리 실행 파일은 내부 형식이 정해져 있어서 커널이 로딩하고 실행 흐름을 시작할 수 있지만, 셸 스크립트나 Python 스크립트는 누군가가 내용을 읽고 해석해야 한다.
바로 이 지점에서 “누가 해석할 것인가”가 필요해진다.

문제는 사용자가 스크립트를 실행하는 방식이 둘로 나뉜다는 점이다.
첫 번째는 인터프리터를 직접 지정하는 방식이다. 예를 들어 bash script.sh, python3 app.py처럼 실행하면, 사용자가 이미 해석기를 정해 준 상태다.
이 경우 운영체제는 bash 또는 python3라는 실행 파일을 먼저 실행하고, 그 프로그램이 대상 스크립트를 읽도록 넘겨주면 된다.
즉, 이 실행 방식에서는 shebang이 없어도 실행 자체는 가능하다.

하지만 두 번째 방식은 다르다.
사용자가 아래처럼 파일 자체를 실행하는 경우다.

./script.sh

이때 운영체제는 script.sh를 실행 파일처럼 다뤄야 한다.
그런데 이 파일은 바이너리가 아니라 텍스트다.
그렇다면 커널은 이 파일을 누구에게 넘겨야 할지 알아야 한다.
이 질문에 대한 답이 shebang이다.

기존 한계는 명확하다.
스크립트에 shebang이 없다면, 파일을 직접 실행했을 때 운영체제는 명확한 해석 경로를 갖지 못한다.
환경에 따라 에러가 나거나, 셸이 우회적으로 처리하거나, 사용자가 기대한 것과 다른 인터프리터가 사용될 수 있다.
즉, 같은 파일이 시스템마다 다르게 취급될 여지가 생긴다.

shebang은 이 한계를 해결한다.
파일 내부에 “이 파일은 /bin/bash로 실행하라” 또는 “현재 환경에서 찾은 python3로 실행하라”는 선언을 넣어 둠으로써, 실행 주체를 스크립트 자체가 명시한다.
그 결과 실행 방식이 표준화된다.
사용자는 ./script.sh만 입력하면 되고, 시스템은 첫 줄의 선언을 보고 적절한 인터프리터를 호출한다.

실제 영향은 단순 편의성에 그치지 않는다.
배포 스크립트, 운영 자동화 스크립트, cron 작업, CI 파이프라인, 컨테이너 엔트리포인트, 로컬 개발 도구 모두 “어떤 인터프리터를 전제로 작성되었는가”가 중요하다.
shebang이 없거나 잘못되면, 실행 실패뿐 아니라 더 위험한 문제가 생긴다.
스크립트는 실행되지만 다른 인터프리터가 읽어서 일부 문법만 조용히 오동작할 수 있다.
즉, shebang은 실행 여부만 결정하는 것이 아니라 동작의 일관성까지 보장하는 구조다.

예제

예제 1. 가장 기본적인 bash shebang

아래는 가장 흔한 형태의 bash 스크립트다.

#!/bin/bash
echo "hello"

파일명을 hello.sh라고 하고 실행 권한을 부여한다.

chmod +x hello.sh

그 다음 파일 자체를 직접 실행한다.

./hello.sh

결과는 다음과 같다.

hello

왜 이렇게 되는가.
사용자는 hello.sh라는 파일을 직접 실행했지만, 실제 내부 동작은 “커널이 첫 줄의 #!/bin/bash를 보고 /bin/bash를 실행한 뒤, 그 bash에게 hello.sh를 읽게 하는 방식”이다.
즉, 겉으로는 파일을 실행한 것처럼 보이지만, 실제로는 bash가 해석 실행한 것이다.

이 예제가 중요한 이유는 shebang의 역할을 가장 직접적으로 보여주기 때문이다.
스크립트 안에 인터프리터 정보가 들어 있기 때문에, 사용자는 실행 시점에 bash를 따로 적을 필요가 없다.
운영 스크립트, 설치 스크립트, 서버 초기화 스크립트처럼 “파일 자체를 바로 실행하는 흐름”에서 이 방식이 기본이 된다.

같은 상황을 인터프리터 직접 지정 방식으로 쓰면 아래와 같다.

bash hello.sh

이것도 결과는 동일하다.

hello

하지만 의미는 다르다.
이 경우 shebang이 없어도 bash를 직접 호출했기 때문에 bash가 파일을 읽는다.
반대로 ./hello.sh는 shebang이 있어야 실행 기준이 명확해진다.
즉, shebang은 “파일 직접 실행” 문맥에서 가치가 생긴다.

예제 2. shebang 없이 직접 실행할 때

이번에는 shebang이 없는 파일을 보자.

echo "hello"

이 파일도 실행 권한을 준다.

chmod +x hello.sh

그리고 직접 실행한다.

./hello.sh

환경에 따라 다음과 같은 에러가 날 수 있다.

Exec format error

또는 셸이 개입하는 방식에 따라 다른 반응이 나타날 수도 있다.
핵심은 실행 기준이 명확하지 않다는 점이다.

왜 이런 일이 생기는가.
운영체제는 실행 요청을 받으면 먼저 파일 형식을 확인한다.
바이너리 포맷이라면 그 규칙대로 로드한다.
shebang이 있는 텍스트 파일이라면 지정된 인터프리터를 호출한다.
그런데 일반 텍스트 파일에 아무 정보도 없으면, 이 파일을 실행 가능한 형식으로 볼 근거가 부족하다.

이 예제에서 중요한 점은 “shebang이 없으면 무조건 안 된다”가 아니다.
bash hello.sh처럼 실행하면 된다.
문제는 shebang이 없을 때 파일 자체가 스스로 실행 문맥을 설명하지 못한다는 것이다.
즉, 실행 책임이 전부 사용자에게 넘어간다.
이 상태는 수동 실행에서는 버틸 수 있지만, 자동화 환경에서는 불안정해진다.

실무에서는 이런 종류의 문제가 자주 드러난다.
로컬에서는 bash script.sh로 실행해서 잘 되는데, 배포 서버에서는 엔트리포인트나 스케줄러가 ./script.sh처럼 실행해서 실패하는 경우가 있다.
겉보기에는 같은 스크립트지만, 실행 방식이 다르면 결과도 달라진다.
shebang은 이 차이를 제거한다.

예제 3. Python 스크립트에서의 shebang

shebang은 셸 스크립트 전용이 아니다.
인터프리터 언어라면 구조는 같다.

#!/usr/bin/python3
print("hello python")

파일명을 app.py라고 하고 실행 권한을 부여한다.

chmod +x app.py

직접 실행한다.

./app.py

결과는 다음과 같다.

hello python

왜 이렇게 되는가.
첫 줄의 #!/usr/bin/python3가 “이 파일은 Python 3 인터프리터로 처리하라”는 실행 지시 역할을 하기 때문이다.
운영체제는 Python 문법을 이해하지 않는다.
대신 python3를 실행해서 파일 해석을 위임한다.

이 예제가 의미 있는 이유는, shebang이 스크립트의 언어적 전제를 드러낸다는 점 때문이다.
이 파일은 Python 2가 아니라 Python 3 문법을 전제로 작성되었다는 사실을 첫 줄에서 선언하고 있다.
즉, 단순히 실행만 되게 하는 것이 아니라 문법 호환 범위까지 명시한다.

예를 들어 아래 코드는 Python 3 문법인 f-string을 사용한다.

#!/usr/bin/python3

name = "world"
print(f"hello {name}")

결과는 다음과 같다.

hello world

이 스크립트를 Python 2 환경에 연결하면 문법 오류가 날 수 있다.
따라서 shebang은 “이 코드가 어느 런타임을 가정하는가”를 명확히 적는 자리다.
CLI 도구, 내부 자동화 유틸리티, 배치 보조 스크립트에서 이 차이는 실제 장애로 이어진다.

예제 4. /usr/bin/env를 사용하는 방식

이번에는 env 방식을 보자.

#!/usr/bin/env python3
print("hello")

직접 실행한다.

chmod +x app.py
./app.py

결과는 다음과 같다.

hello

겉으로 보면 앞의 Python 예제와 비슷하다.
하지만 내부 의미는 다르다.

#!/usr/bin/python3는 “정확히 /usr/bin/python3를 사용하라”는 뜻이다.
반면 #!/usr/bin/env python3는 “먼저 /usr/bin/env를 실행하고, 그 env가 현재 PATH에서 python3를 찾아 실행하라”는 뜻이다.
즉, 인터프리터를 절대 경로로 박아 두지 않고, 환경 변수 기반 탐색에 맡긴다.

이 방식이 필요한 상황은 명확하다.
개발 환경마다 python3의 설치 위치가 다를 수 있다.
가상환경을 사용하면 PATH의 우선순위에 따라 다른 python3가 잡히기도 한다.
컨테이너 이미지마다 기본 경로도 다를 수 있다.
이럴 때 절대 경로 고정 방식은 유연성이 떨어진다.

예를 들어 아래처럼 가상환경을 활성화한 상태를 생각해 보자.

python3 -V
which python3

결과 예시는 다음과 같을 수 있다.

Python 3.12.2
/home/user/project/.venv/bin/python3

이 환경에서 #!/usr/bin/env python3는 현재 가상환경의 python3를 찾을 수 있다.
반면 #!/usr/bin/python3는 시스템 전역 Python을 직접 사용할 가능성이 있다.
즉, env 방식은 “현재 실행 컨텍스트를 존중하는 방식”이다.

실무 의미는 분명하다.
로컬 개발, 가상환경, pyenv, 컨테이너, CI 환경처럼 인터프리터 위치가 고정되지 않는 경우 env 방식이 더 적합하다.
반대로 특정 운영 서버에서 정확히 지정된 경로의 인터프리터만 사용해야 한다면 절대 경로 방식이 더 명확하다.
즉, 이식성 중심이면 env, 고정성 중심이면 절대 경로라는 구도로 이해하면 된다.

예제 5. bash 문법이 필요한데 /bin/sh를 쓰는 경우

다음 스크립트는 bash 문법을 사용한다.

#!/bin/sh

name="world"

if [[ -n "$name" ]]; then
  echo "hello $name"
fi

직접 실행하면 일부 환경에서 다음과 같은 오류가 날 수 있다.

[[: not found

또는 조건문이 의도대로 처리되지 않을 수 있다.

왜 이런 일이 생기는가.
[[ ... ]]는 bash에서 제공하는 확장 문법이다.
하지만 /bin/sh는 bash와 동일한 의미가 아니다.
시스템에 따라 /bin/sh가 dash나 다른 POSIX shell을 가리킬 수 있다.
즉, shebang에서 sh를 선택한 순간, 문법 기준이 달라진다.

올바른 형태는 아래다.

#!/bin/bash

name="world"

if [[ -n "$name" ]]; then
  echo "hello $name"
fi

이제 실행하면:

hello world

이 예제가 중요한 이유는, shebang이 단순 경로 지정이 아니라 문법 해석 범위 지정이라는 점을 보여주기 때문이다.
같은 텍스트라도 어떤 인터프리터가 읽느냐에 따라 의미가 달라진다.
즉, shebang은 스크립트의 실행기만 정하는 것이 아니라 언어 사양을 선택하는 선언이다.

실무 적용

1. 배포 스크립트에서 실행 환경을 고정하는 경우

상황은 배포 자동화 스크립트를 여러 서버에서 공통으로 사용하는 경우다.
스크립트는 파일 복사, 프로세스 재시작, 환경 변수 로딩, 조건 분기 같은 bash 문법을 포함한다.
문제는 실행 위치마다 기본 shell이 다를 수 있다는 점이다. sh가 bash가 아닌 경우도 있고, 운영 계정에 따라 PATH 구성이 다를 수도 있다.

이 상태에서 shebang이 없으면 스크립트 실행 방식이 호출자에 종속된다.
어떤 운영자는 bash deploy.sh로 실행하고, 어떤 자동화 도구는 ./deploy.sh로 실행할 수 있다.
그러면 동일 스크립트인데도 환경에 따라 다른 해석 결과가 나온다.
스크립트의 동작 책임이 코드가 아니라 실행자에게 흩어진다.

해결 방법은 스크립트 자체에 bash 기준을 명시하는 것이다.

#!/bin/bash
set -e

echo "deploy start"

또는 환경 이식성을 고려한다면 다음처럼 쓸 수 있다.

#!/usr/bin/env bash
set -e

echo "deploy start"

효과는 “누가 실행하든 bash 기준으로 읽힌다”는 점이다.
즉, 코드가 실행 문맥을 스스로 설명하게 된다.
운영 절차가 바뀌거나 호출 주체가 달라져도 실행 기준이 흔들리지 않는다.
이 구조는 배포 실패를 줄이는 것이 아니라, 더 정확히 말하면 실행 경로의 모호성을 제거하는 구조다.

2. Python 기반 내부 도구를 명령어처럼 배포하는 경우

상황은 사내에서 사용하는 작은 Python 도구를 배포하는 경우다.
예를 들어 로그 정리, CSV 변환, API 호출 보조, 설정 파일 생성 같은 유틸리티가 이에 해당한다.
사용자는 이 도구를 일반 명령처럼 실행하고 싶어 한다.

문제는 shebang이 없으면 아래처럼 항상 인터프리터를 앞에 붙여야 한다는 점이다.

python3 tool.py

이 방식은 실행 가능하지만, 도구를 명령어처럼 다루기 어렵다.
또한 운영 환경이 바뀌면 python 대신 python3, 혹은 특정 venv의 경로를 신경 써야 한다.
즉, 사용자가 매번 런타임 정보를 알고 있어야 한다.

해결 방법은 shebang과 실행 권한을 함께 설정하는 것이다.

#!/usr/bin/env python3

print("run internal tool")
chmod +x tool.py
./tool.py

효과는 사용자 관점에서 도구가 “실행 파일처럼 보이게 된다”는 점이다.
실제로는 Python 인터프리터가 실행되지만, 사용자는 런타임을 의식하지 않는다.
이 방식은 개발자 경험을 좋게 만든다는 표현보다, 더 정확히 말하면 실행 인터페이스를 단순화한다.
스크립트의 사용 방법이 코드 내부에 녹아들기 때문이다.

3. cron이나 스케줄러에서 실행 기준을 고정하는 경우

상황은 서버에서 주기적으로 실행되는 스크립트가 있는 경우다.
로그 정리, 파일 백업, 주기적 API 동기화, 상태 점검 작업이 여기에 포함된다.
이 작업은 보통 사람이 직접 실행하지 않고 cron이나 시스템 스케줄러가 실행한다.

문제는 스케줄러 환경이 인터랙티브 셸과 다르다는 점이다.
PATH가 제한될 수 있고, 기본 shell 가정도 다를 수 있다.
로컬 터미널에서는 잘 동작하던 스크립트가 스케줄러에서는 실패하는 이유가 여기에 있다.
즉, 사용자가 수동 실행할 때의 문맥과 자동 실행 문맥이 다르다.

해결 방법은 shebang으로 인터프리터를 명시하고, 필요한 경우 환경 경로까지 명확히 관리하는 것이다.

#!/bin/bash
date
echo "cron job running"

또는 Python이라면:

#!/usr/bin/env python3
print("scheduled task")

효과는 작업 실행 주체가 사람이든 스케줄러든 관계없이 해석 기준이 고정된다는 점이다.
cron은 단순히 파일을 실행 요청만 하면 되고, 파일 스스로 어떤 런타임을 사용할지 설명한다.
이 구조는 스케줄러의 단순성을 유지하면서 스크립트의 책임 범위를 분명하게 만든다.
즉, 장애 원인을 “호출 환경 문제인지, 코드 문제인지” 분리하기 쉬워진다.

4. 컨테이너나 가상환경에서 런타임 선택을 유연하게 유지하는 경우

상황은 Docker 컨테이너, Python venv, pyenv, 여러 개발 서버처럼 런타임 위치가 달라질 수 있는 환경이다.
문제는 인터프리터 절대 경로가 환경에 묶인다는 점이다.
예를 들어 어떤 컨테이너에서는 python3가 /usr/local/bin/python3에 있고, 다른 환경에서는 /usr/bin/python3에 있을 수 있다.

절대 경로를 박아 두면 특정 환경에서는 동작하지만, 다른 환경에서는 실행 실패가 난다.
즉, 코드가 환경 구조를 과도하게 가정하게 된다.
이 상태는 환경이 늘어날수록 유지보수가 어려워진다.

해결 방법은 env 방식이다.

#!/usr/bin/env python3
print("portable script")

효과는 PATH가 가리키는 현재 환경의 python3를 사용한다는 점이다.
가상환경이 활성화되어 있으면 그 python3가 선택된다.
컨테이너 내부 PATH가 다르면 그 기준을 따른다.
즉, 코드가 특정 절대 경로보다 “현재 실행 컨텍스트”를 우선한다.

이 방식은 무조건 더 낫다는 뜻은 아니다.
운영 서버에서 정확한 경로를 강제해야 하는 경우에는 절대 경로가 더 적절하다.
중요한 것은 shebang 선택도 설계라는 점이다.
환경 다양성을 허용할 것인지, 특정 실행 파일을 강제할 것인지에 따라 선언이 달라진다.

흔한 실수

실수 1. shebang을 첫 줄이 아닌 곳에 쓰는 경우

잘못된 사용은 아래와 같다.

# deploy script
#!/bin/bash
echo "start"

실제 결과는 shebang이 무시되거나, 파일이 일반 텍스트처럼 취급될 수 있다.
왜 틀렸는가.
운영체제는 shebang을 “파일의 첫 줄”에서만 인식한다.
중간 줄에 있는 #!는 단지 텍스트일 뿐이다.

올바른 방법은 shebang을 반드시 첫 줄에 두는 것이다.

#!/bin/bash
# deploy script
echo "start"

이 실수가 자주 문제를 만드는 이유는, 작성자는 shebang을 넣었다고 생각하지만 시스템은 그렇지 않기 때문이다.
즉, 코드 가독성 문제처럼 보여도 실제로는 실행 계약이 깨지는 문제다.

실수 2. 실행 권한 없이 직접 실행하려는 경우

잘못된 사용은 아래와 같다.

#!/bin/bash
echo "hello"

그리고 바로:

./script.sh

실제 결과는 보통 다음과 같다.

Permission denied

왜 틀렸는가.
shebang은 인터프리터를 지정할 뿐이고, 파일 자체를 실행 가능하게 만드는 권한까지 부여하지는 않는다.
직접 실행은 운영체제 입장에서 “실행 가능한 파일”이어야 한다.
즉, shebang과 실행 권한은 별개다.

올바른 방법은 실행 권한을 먼저 부여하는 것이다.

chmod +x script.sh
./script.sh

이 실수는 shebang의 역할을 과대해석해서 생긴다.
shebang은 실행기를 지정할 뿐이다.
파일 권한 모델까지 대체하지 않는다.

실수 3. bash 문법을 쓰면서 /bin/sh를 지정하는 경우

잘못된 사용은 다음과 같다.

#!/bin/sh

files=(a b c)
echo "${files[0]}"

실제 결과는 배열 문법 오류나 예기치 않은 실패다.
왜 틀렸는가.
배열, [[ ... ]], brace expansion 일부, process substitution 등은 bash 확장 문법이다.
/bin/sh는 POSIX shell을 가리키는 추상 인터페이스에 가깝고, 실제 구현은 bash가 아닐 수 있다.

올바른 방법은 스크립트가 bash 문법에 의존한다면 명시적으로 bash를 지정하는 것이다.

#!/bin/bash

files=(a b c)
echo "${files[0]}"

이 실수는 “sh도 shell이고 bash도 shell이니까 비슷하겠지”라는 전제에서 나온다.
하지만 shebang은 범주를 지정하는 자리가 아니라 정확한 해석기를 지정하는 자리다.
문법이 다르면 shebang도 달라져야 한다.

실수 4. Python 3 스크립트인데 python만 적는 경우

잘못된 사용은 아래와 같다.

#!/usr/bin/env python
name = "world"
print(f"hello {name}")

실제 결과는 환경에 따라 정상일 수도 있고, f-string 문법 오류가 날 수도 있다.
왜 틀렸는가.
python이라는 이름이 어떤 버전을 가리키는지는 시스템마다 다르다.
어떤 환경에서는 Python 3지만, 어떤 환경에서는 여전히 Python 2이거나 심볼릭 링크 구성이 다를 수 있다.

올바른 방법은 스크립트가 Python 3 문법을 전제로 한다면 명확히 python3를 적는 것이다.

#!/usr/bin/env python3
name = "world"
print(f"hello {name}")

이 실수는 shebang이 인터프리터만 찾으면 된다고 오해해서 생긴다.
실제로는 “어느 버전의 언어 사양으로 해석할지”까지 포함된다.
즉, shebang은 런타임 이름뿐 아니라 문법 기준 버전도 반영해야 한다.

실수 5. env 사용법을 잘못 이해하는 경우

잘못된 사용은 아래와 같다.

#!/usr/bin/env /bin/bash
echo "hello"

실제 결과는 실행 실패 또는 env 사용 오류다.
왜 틀렸는가.
env는 절대 경로를 실행하기 위한 장치가 아니라, 환경 변수 PATH를 기준으로 명령 이름을 찾는 도구다.
즉, bash처럼 명령 이름을 넘겨야 한다.

올바른 방법은 다음과 같다.

#!/usr/bin/env bash
echo "hello"

이 실수는 env를 “인터프리터를 한 번 더 감싸는 문법” 정도로만 이해해서 생긴다.
하지만 핵심은 PATH 탐색이다.
절대 경로를 쓸 거라면 env를 거칠 이유가 거의 없다.

실수 6. PATH에 인터프리터가 없는데 env 방식을 쓰는 경우

잘못된 사용은 다음과 같다.

#!/usr/bin/env python3
print("hello")

실제 결과는 다음과 같을 수 있다.

env: python3: No such file or directory

왜 틀렸는가.
env 방식은 편리하지만, 전제가 있다.
현재 PATH 안에 해당 명령이 있어야 한다.
즉, env는 경로 문제를 없애는 것이 아니라, 경로 선택 책임을 PATH에 넘기는 것이다.

올바른 방법은 두 가지다.
하나는 PATH를 올바르게 설정하는 것이다.
다른 하나는 운영 환경이 고정돼 있다면 절대 경로를 직접 적는 것이다.

#!/usr/bin/python3
print("hello")

이 실수는 env를 “항상 더 좋은 방식”으로 오해할 때 생긴다.
실제로는 환경 유연성을 얻는 대신 PATH 의존성을 수용하는 방식이다.
즉, 장점과 전제가 함께 있다.

관련 개념

인터프리터

인터프리터는 스크립트 내용을 읽고 해석하여 실행하는 프로그램이다.
bash, sh, python3, perl, ruby가 대표적이다.
shebang은 결국 “어느 인터프리터가 이 파일을 읽을 것인가”를 지정하는 선언이다.

실행 권한

실행 권한은 파일을 직접 실행 대상으로 삼을 수 있게 하는 권한이다.
shebang이 있어도 실행 권한이 없으면 ./script.sh 형태로는 실행되지 않는다.
즉, shebang은 실행기 지정이고, 실행 권한은 파일 실행 허용이다.

PATH

PATH는 명령어를 찾을 디렉터리 목록을 담는 환경 변수다.
/usr/bin/env python3는 바로 이 PATH를 이용해 python3를 찾는다.
즉, env 방식의 동작 원리는 PATH 탐색과 연결된다.

shell과 /bin/sh, /bin/bash의 차이

shell은 명령 해석기의 범주다.
하지만 /bin/sh/bin/bash는 동일한 것이 아니다.
특정 스크립트가 bash 확장 문법을 사용한다면, shebang에서도 bash를 정확히 지정해야 한다.

더 깊이 보기

shebang은 shell 기능처럼 보이지만, 실제로는 더 아래 레벨에서 작동한다.
핵심은 사용자가 파일을 직접 실행할 때 호출되는 시스템 호출 흐름에 있다.
보통 프로그램 실행은 execve() 계열 시스템 호출을 통해 요청된다.
커널은 이 호출을 받으면 대상 파일이 어떤 형식인지 검사한다.

바이너리 실행 파일이라면 ELF 같은 포맷 규칙에 따라 로딩한다.
반면 첫 두 바이트가 #!인 파일이라면, 커널은 이 파일을 인터프리터 스크립트로 본다.
그리고 첫 줄에서 인터프리터 경로와 필요 시 인자를 읽는다.
그 후 실제 실행 대상은 스크립트 파일이 아니라, 그 인터프리터 실행 파일이 된다.
즉, 커널이 “스크립트를 직접 실행하는 것”이 아니라 “인터프리터를 대신 실행해 주는 것”에 가깝다.

이 구조를 간단히 쓰면 다음과 같다.

사용자
→ ./script.sh 실행 요청
→ 커널이 #! 확인
→ 지정된 인터프리터 실행
→ 인터프리터가 script.sh 내용을 읽어 해석

이 흐름 때문에 shebang은 단순 주석이 아니다.
문자열처럼 보여도, 커널이 특별 취급하는 실행 메타데이터다.
즉, 코드 내부 설명이 아니라 실행 프로토콜의 일부다.

또한 shebang의 선택은 시스템 관점에서 “실행 책임을 어디에 둘 것인가”와 연결된다.
절대 경로 방식은 책임을 스크립트에 둔다.
정확히 이 경로의 실행기를 쓰겠다는 선언이기 때문이다.
반면 env 방식은 실행기 선택 일부를 PATH 환경에 위임한다.
즉, 스크립트는 “python3라는 이름의 인터프리터를 원한다”고 말하고, 실제 위치는 환경이 결정한다.

이 차이는 배포 전략과 연결된다.
단일 서버, 고정 이미지, 엄격한 운영 환경에서는 절대 경로가 더 예측 가능하다.
반대로 다양한 개발 환경, 가상환경, 도구 체인 혼합 환경에서는 env 방식이 더 자연스럽다.
즉, 어느 것이 무조건 맞는 것이 아니라 환경 고정성 대 환경 이식성 사이의 선택이다.

마지막으로 shebang은 문법 호환성과도 깊이 연결된다.
/bin/sh/bin/bash의 차이, pythonpython3의 차이, 특정 런타임 옵션 유무는 모두 실행 결과를 바꾼다.
즉, shebang은 프로그램을 “무엇으로 열 것인가” 수준의 문제가 아니라, 어떤 언어 규칙으로 읽고 어떤 런타임으로 동작시킬 것인가를 정하는 선언이다.
그래서 shebang은 짧지만, 스크립트 전체의 실행 정체성을 결정한다.

정리

shebang은 스크립트 파일 첫 줄에서 실행 인터프리터를 지정하는 선언이다.
직접 실행 시 운영체제가 무엇을 호출할지 결정하게 해 주며, 동시에 스크립트가 전제하는 문법과 런타임 기준을 명시한다.
#!/bin/bash는 고정 경로 방식이고, #!/usr/bin/env bash는 PATH 기반 탐색 방식이다.
결국 shebang은 편의 문법이 아니라, 스크립트의 실행 방식과 호환 범위를 고정하는 실행 계약이다.