실행 결과는 입력으로 결정되지 않는다

리눅스에서 동일한 명령을 입력했는데 결과가 달라지는 경우는 흔하다. 이 현상은 예외가 아니라 구조적인 결과다. 많은 경우 사용자는 입력이 실행을 결정한다고 가정한다. 그러나 실제로는 입력은 실행의 시작 조건일 뿐이다. 실행 결과는 입력이 아니라 시스템 상태에 의해 결정된다. 이 차이를 이해하지 못하면, 동일한 명령이 왜 다른 결과를 만드는지 설명할 수 없다.

입력은 단순한 문자열이다. 문자열은 실행 가능한 객체가 아니다. 시스템은 이 문자열을 해석하고, 실행 가능한 구조로 변환해야 한다. 이 과정에서 입력은 여러 환경 요소와 결합된다. 이 결합 결과가 실행 대상과 실행 방식에 영향을 준다. 따라서 입력 자체는 결과를 보장하지 않는다. 입력은 단지 결정 과정의 일부다.

이 구조를 직접 확인해보면, 입력 중심 사고가 왜 잘못되었는지 명확해진다.

$ which python
/usr/bin/python

$ mkdir -p /tmp/testbin
$ echo -e '#!/bin/sh\necho fake python' > /tmp/testbin/python
$ chmod +x /tmp/testbin/python

$ PATH=/tmp/testbin:$PATH
$ python
fake python

이 실험에서 입력은 항상 python으로 동일하다. 그러나 결과는 완전히 달라진다. 기존에는 /usr/bin/python이 실행되었다. 이후에는 /tmp/testbin/python이 실행된다. 입력은 바뀌지 않았지만 결과는 바뀐다. 이 변화는 PATH라는 환경 요소 때문이다.

이 결과는 중요한 사실을 드러낸다. 실행 결과는 입력 문자열이 아니라, 입력을 해석하는 환경에 의해 결정된다. 따라서 실행을 이해하려면 입력이 아니라 해석 구조를 봐야 한다. 다음 단계에서는 이 해석이 단일 결정이 아니라 여러 선택의 누적이라는 점을 설명한다.

실행은 하나의 결정이 아니라 여러 개의 선택이다

실행은 단일 이벤트처럼 보인다. 그러나 실제로는 여러 개의 선택이 순차적으로 이루어진 결과다. 각 선택은 이전 선택의 결과를 기반으로 한다. 이 구조 때문에 실행은 복잡해진다. 동시에 이 구조 때문에 실행 결과는 예측 가능해진다. 예측이 어려운 것이 아니라, 기준이 명확하지 않은 것이다.

각 선택은 “가능한 후보 중 하나를 선택하는 과정”이다. 예를 들어, 동일한 이름이 여러 경로에 존재할 수 있다. 시스템은 그 중 하나를 선택한다. 파일이 존재하더라도 실행 방식이 여러 개일 수 있다. 시스템은 그 중 하나를 선택한다. 이처럼 실행은 단일 경로가 아니라 선택의 연쇄다.

이 선택 구조는 눈에 보이지 않기 때문에 혼란을 만든다. 그러나 일부는 관찰할 수 있다.

$ type -a ls
ls is /bin/ls
ls is /usr/bin/ls

이 결과는 동일한 이름이 여러 위치에 존재할 수 있음을 보여준다. 그러나 실제 실행에서는 하나만 선택된다. 어떤 것이 선택되는지는 규칙에 의해 결정된다. 이 규칙은 사용자에게 명시적으로 보이지 않는다. 그러나 결과에는 직접적인 영향을 준다.

또 다른 예시는 alias다.

$ alias ls='echo override'
$ ls
override

이 경우 동일한 ls라는 입력이 완전히 다른 동작을 만든다. 이 변화는 실행 대상이 바뀌었기 때문이다. 이 역시 선택의 결과다. 시스템은 파일이 아니라 alias를 선택했다.

이 구조의 핵심은 명확하다. 실행은 “정해진 경로”가 아니라 선택된 경로다. 그리고 이 선택은 여러 단계에서 이루어진다. 각 단계의 선택 기준이 다르면 결과도 달라진다. 따라서 실행 결과를 이해하려면, 선택이 어떻게 이루어지는지를 이해해야 한다. 다음 단계에서는 그 중 가장 중요한 선택 기준인 PATH를 분석한다.

PATH는 검색이 아니라 “우선순위 시스템”이다

많은 설명에서 PATH는 단순한 탐색 리스트로 설명된다. 그러나 이 설명은 충분하지 않다. PATH는 단순히 파일을 찾는 기능이 아니다. PATH는 여러 후보 중 하나를 선택하는 규칙이다. 즉, PATH는 검색이 아니라 우선순위 시스템이다.

PATH는 디렉토리의 순서를 정의한다. 이 순서는 단순한 나열이 아니다. 이 순서는 선택 우선순위를 의미한다. 동일한 이름의 실행 파일이 여러 위치에 존재할 경우, PATH의 앞쪽에 있는 것이 선택된다. 이 선택은 첫 번째 일치에서 종료된다. 이후 후보는 고려되지 않는다.

이 구조는 실험으로 명확하게 드러난다.

$ echo $PATH
/usr/bin:/bin

$ which ls
/usr/bin/ls

$ PATH=/bin:/usr/bin
$ which ls
/bin/ls

이 경우 ls라는 입력은 동일하다. 그러나 PATH 순서가 바뀌면서 선택 결과가 달라진다. 실행되는 파일이 변경된다. 이 변화는 단순한 위치 차이가 아니다. 서로 다른 버전의 프로그램이 실행될 수도 있다. 따라서 결과 자체가 달라질 수 있다.

이 구조는 PATH를 단순한 설정값이 아니라, 실행 결과를 결정하는 정책으로 만든다. PATH를 변경하는 것은 실행 환경을 변경하는 것과 동일하다. 이 점을 이해하지 못하면, 실행 결과가 왜 달라지는지 설명할 수 없다.

또한 PATH는 명시적이지 않다. 사용자는 PATH를 직접 확인하지 않는 경우가 많다. 그러나 PATH는 항상 실행에 영향을 준다. 이 숨겨진 영향 때문에 실행 결과는 예측하기 어려워 보인다. 실제로는 예측 불가능한 것이 아니라, 기준이 숨겨져 있을 뿐이다.

이 단계까지 오면 하나의 구조가 보인다. 입력은 고정되어 있다. 그러나 선택 기준이 바뀌면 결과가 바뀐다. 이 선택은 파일 경로 수준에서 끝나지 않는다. 다음 단계에서는 선택된 파일이 실제로 어떻게 해석되는지를 다룬다.

파일은 실행 대상이 아니라 “해석 대상”이다

PATH를 통해 특정 파일 경로가 선택되었다고 해서 실행이 확정되는 것은 아니다. 이 시점에서 확보된 것은 단순한 파일이다. 그러나 모든 파일이 동일한 방식으로 실행되는 것은 아니다. 시스템은 이 파일이 어떤 방식으로 처리되어야 하는지를 다시 판단해야 한다. 즉, 파일은 실행 대상이 아니라 해석 대상이다.

파일은 형태에 따라 다르게 처리된다. 대표적으로 바이너리와 스크립트는 전혀 다른 경로를 가진다. 바이너리는 시스템이 직접 실행할 수 있는 형식이다. 반면 스크립트는 텍스트일 뿐이다. 텍스트 자체는 실행할 수 없다. 따라서 실행을 위해서는 추가적인 해석이 필요하다. 이 차이는 실행 방식 자체를 바꾼다.

이 차이는 파일의 내부 구조를 보면 드러난다.

$ file /bin/ls
/bin/ls: ELF 64-bit LSB executable

$ echo -e '#!/bin/sh\necho hello' > test.sh
$ chmod +x test.sh

$ file test.sh
test.sh: POSIX shell script, ASCII text executable

위 결과에서 /bin/ls는 ELF 형식이다. 이 형식은 커널이 직접 이해할 수 있다. 반면 test.sh는 텍스트 파일이다. 실행 가능하다고 표시되어 있지만, 실제로는 코드가 아니다. 따라서 시스템은 이 파일을 그대로 실행하지 않는다.

이 차이는 실행 결과에 직접적인 영향을 준다. 동일하게 실행 권한이 있어도, 처리 방식이 다르다. 바이너리는 직접 실행된다. 스크립트는 다른 프로그램을 통해 실행된다. 즉, 실행 대상은 파일이 아니라 파일의 해석 결과다.

이 구조는 중요한 사실을 보여준다. 실행은 “파일을 실행하는 것”이 아니다. 실행은 “파일을 해석한 결과를 실행하는 것”이다. 따라서 파일이 무엇이냐보다, 어떻게 해석되느냐가 더 중요하다. 다음 단계에서는 이 해석을 강제하는 구조인 shebang을 분석한다.

shebang은 실행 주체를 바꾼다

스크립트 파일이 실행될 수 있는 이유는 shebang 때문이다. 그러나 shebang은 단순한 문법이 아니다. shebang은 실행 대상을 재정의하는 규칙이다. 즉, 파일을 실행하는 것이 아니라, 다른 프로그램을 실행하도록 만드는 장치다.

shebang은 파일의 첫 줄에 위치한다. #!로 시작하는 이 줄은 인터프리터 경로를 지정한다. 시스템은 이 정보를 기반으로 실행 방식을 다시 결정한다. 이 과정에서 기존 실행 대상은 유지되지 않는다. 대신 새로운 실행 대상이 선택된다. 즉, 실행 주체가 바뀐다.

이 구조는 실험으로 명확하게 드러난다.

$ echo -e '#!/bin/sh\necho shell' > test.sh
$ chmod +x test.sh
$ ./test.sh
shell

이 파일은 직접 실행된 것처럼 보인다. 그러나 실제로는 /bin/sh가 실행된 것이다. test.sh는 실행 대상이 아니라 입력으로 전달된 것이다.

이 차이를 더 명확하게 보기 위해 shebang을 변경해보면 결과가 바뀐다.

$ echo -e '#!/bin/echo\nhello world' > test.sh
$ chmod +x test.sh
$ ./test.sh
./test.sh

여기서 echo가 실행된다. 그리고 test.sh는 인자로 전달된다. 즉, 파일의 내용은 실행되지 않는다. 실행된 것은 echo다. shebang이 실행 주체를 완전히 바꾼다.

이 구조는 실행이 고정된 것이 아니라 재해석 가능하다는 점을 보여준다. 동일한 파일이라도 shebang이 다르면 완전히 다른 결과가 나온다. 따라서 실행 대상은 파일 경로가 아니라, 재해석된 실행 구조다.

이 단계에서 중요한 결론이 나온다. 실행은 한 번 결정되지 않는다. 실행은 여러 번 재정의된다. 그리고 그 재정의는 결과를 바꾼다. 다음 단계에서는 이 재해석된 실행이 기존 상태 위에서 어떻게 적용되는지를 분석한다.

실행은 새로 시작되지 않는다 — 기존 상태 위에서 바뀐다

많은 설명에서 프로그램 실행은 “새로운 프로세스가 시작된다”로 표현된다. 그러나 이 표현은 중요한 구조를 가린다. 실제로 실행은 항상 기존 상태 위에서 이루어진다. 즉, 실행은 생성이 아니라 상태 변경이다. 이 차이를 이해하지 못하면 실행 이후의 동작을 설명할 수 없다.

실행 이전에는 이미 실행 컨텍스트가 존재한다. 이 컨텍스트는 현재 실행 중인 상태를 포함한다. 새로운 실행은 이 상태를 완전히 버리지 않는다. 일부는 유지되고, 일부만 변경된다. 특히 커널이 관리하는 상태는 유지된다. 대표적으로 파일 디스크립터와 환경 변수는 그대로 남는다.

이 구조는 간단한 실험으로 확인할 수 있다.

$ echo -e '#include <unistd.h>\nint main(){write(1,"hello\n",6);}' > test.c
$ gcc test.c -o test

$ ./test > out.txt
$ cat out.txt
hello

여기서 test 프로그램은 표준 출력으로 데이터를 보낸다. 그러나 실제 출력은 파일로 저장된다. 이는 실행 전에 설정된 출력 구조가 그대로 유지되기 때문이다. 프로그램은 이를 알지 못한다. 그러나 결과는 달라진다.

이 현상은 중요한 사실을 드러낸다. 실행은 코드에 의해 완전히 결정되지 않는다. 실행 결과는 이미 존재하던 상태에 의해 영향을 받는다. 코드가 동일해도, 상태가 다르면 결과가 달라진다.

또한 이 구조는 실행이 “완전히 새로 시작되는 것”이 아님을 보여준다. 실행은 기존 컨텍스트 위에 새로운 코드가 적용되는 과정이다. 이 때문에 이전 단계에서 정의된 구조가 그대로 유지된다.

결론적으로 실행은 독립적인 이벤트가 아니다. 실행은 기존 상태를 기반으로 한 변환이다. 이 변환은 이전 단계에서 결정된 요소들을 유지한다. 따라서 실행 결과를 이해하려면, 코드뿐만 아니라 상태를 함께 봐야 한다. 다음 단계에서는 이 상태 중에서도 결과에 직접적인 영향을 주는 입출력 구조를 분석한다.

입출력 구조는 실행 결과를 왜곡한다

프로그램의 결과는 코드에 의해 결정된다고 생각하기 쉽다. 그러나 실제로는 출력 결과조차 코드만으로 결정되지 않는다. 프로그램은 단지 데이터를 특정 파일 디스크립터로 보낼 뿐이다. 그 디스크립터가 어디를 가리키는지는 실행 이전에 이미 결정되어 있다. 따라서 동일한 코드라도, 입출력 구조가 다르면 결과는 달라진다.

파일 디스크립터는 숫자로 표현되는 핸들이다. 표준 입력은 0이다. 표준 출력은 1이다. 표준 에러는 2다. 이 디스크립터들은 특정 자원을 가리킨다. 기본적으로는 터미널을 가리킨다. 그러나 이 연결은 고정되어 있지 않다. 실행 전에 다른 자원으로 변경할 수 있다. 이 변경이 리다이렉션이다.

이 구조는 간단한 실험으로 확인할 수 있다.

$ echo hello
hello

$ echo hello > out.txt
$ cat out.txt
hello

첫 번째 명령은 터미널로 출력된다. 두 번째 명령은 파일로 출력된다. 코드 자체는 동일하다. echo는 동일하게 표준 출력으로 데이터를 보낸다. 그러나 결과는 완전히 다르게 나타난다. 이 차이는 출력 대상이 변경되었기 때문이다.

이 구조는 더 복잡한 형태에서도 동일하게 작동한다.

$ ls /notfound > out.txt 2>&1
$ cat out.txt
ls: cannot access '/notfound': No such file or directory

여기서는 에러 출력까지 파일로 이동한다. 2>&1은 표준 에러를 표준 출력과 동일한 대상으로 연결한다. 이 연결 역시 실행 이전에 결정된다. 프로그램은 이를 인식하지 못한다. 그러나 결과는 완전히 바뀐다.

이 구조는 중요한 사실을 드러낸다. 실행 결과는 코드의 결과가 아니다. 실행 결과는 코드와 입출력 구조의 결합 결과다. 따라서 출력이 예상과 다르게 나타난다면, 코드가 아니라 구조를 먼저 봐야 한다. 다음 단계에서는 이 구조를 더 은밀하게 바꾸는 요소인 환경 변수를 다룬다.

환경 변수는 실행을 보이지 않게 바꾼다

환경 변수는 실행 과정에서 명시적으로 드러나지 않는다. 그러나 실행 결과에 지속적으로 영향을 준다. 특히 많은 프로그램은 환경 변수를 기반으로 동작을 변경한다. 이 변화는 코드 수정 없이 발생한다. 따라서 환경 변수는 실행 결과를 “보이지 않게” 바꾸는 요소다.

환경 변수는 단순한 설정값이 아니다. 실행 컨텍스트의 일부다. 프로그램은 시작될 때 환경 변수를 전달받는다. 이후 이 값을 기반으로 동작을 결정한다. 이 과정은 외부에서 관찰하기 어렵다. 그러나 결과에는 직접적인 영향을 준다.

이 구조는 간단한 실험으로 확인할 수 있다.

$ echo 'echo $MY_VAR' > test.sh
$ chmod +x test.sh

$ ./test.sh

$ MY_VAR=hello ./test.sh
hello

첫 번째 실행에서는 아무 것도 출력되지 않는다. 두 번째 실행에서는 hello가 출력된다. 입력은 동일하다. 파일도 동일하다. 그러나 결과는 다르다. 이 차이는 환경 변수 때문이다.

환경 변수는 실행 대상 선택에도 영향을 준다. PATH 역시 환경 변수다. 따라서 환경 변수는 실행 대상과 실행 결과를 동시에 바꾼다. 이 영향은 코드 외부에서 발생한다. 따라서 코드만으로 실행 결과를 예측하는 것은 불가능하다.

또한 환경 변수는 전역적으로 적용될 수 있다. 쉘 설정 파일이나 시스템 설정에 의해 정의된다. 이 경우 사용자는 자신이 어떤 환경에서 실행하고 있는지 인지하지 못할 수 있다. 이 숨겨진 상태가 실행 결과를 바꾼다.

결론적으로 실행은 코드와 입력만으로 설명할 수 없다. 실행은 환경을 포함한 전체 상태에 의해 결정된다. 이 상태가 보이지 않기 때문에 실행 결과는 불안정해 보인다. 다음 단계에서는 이러한 차이가 오류로 나타나는 구조를 분석한다.

실행 실패는 랜덤이 아니라 “결정 실패”다

실행이 실패하는 경우는 종종 예측 불가능하게 보인다. 동일한 명령이 어떤 환경에서는 동작하고, 다른 환경에서는 실패한다. 이 현상은 랜덤처럼 보인다. 그러나 실제로는 특정 단계에서 결정이 실패한 결과다. 실행은 여러 선택의 연쇄다. 실패도 이 연쇄 중 특정 지점에서 발생한다.

가장 흔한 실패는 실행 대상 선택 단계에서 발생한다.

$ unknown_command
bash: unknown_command: command not found

이 경우 시스템은 실행 대상을 찾지 못했다. 이는 입력 문제가 아니라, 선택 규칙 문제다. PATH에 해당 이름이 존재하지 않는다. 따라서 실행이 시작되지 않는다.

다음으로는 실행 방식 결정 단계에서 실패할 수 있다.

$ echo 'echo hello' > test.sh
$ chmod +x test.sh
$ ./test.sh
bash: ./test.sh: cannot execute: Exec format error

이 경우 파일은 존재하고 실행 권한도 있다. 그러나 실행 방식이 정의되지 않았다. shebang이 없기 때문이다. 시스템은 이 파일을 어떻게 실행해야 하는지 결정하지 못한다. 따라서 실행이 실패한다.

또 다른 실패는 권한 문제다.

$ chmod -x test.sh
$ ./test.sh
bash: ./test.sh: Permission denied

이 경우 실행 대상과 실행 방식은 모두 결정되었다. 그러나 실행 조건이 충족되지 않았다. 따라서 실행이 차단된다.

이 모든 실패는 공통점을 가진다. 실행 자체가 실패한 것이 아니다. 결정 과정 중 하나가 실패한 것이다. 따라서 실행 실패를 이해하려면, 어느 단계에서 결정이 실패했는지를 찾아야 한다.

이 구조를 이해하면 실행 문제는 더 이상 모호하지 않다. 각 단계는 명확한 역할을 가진다. 실패도 그 역할에 대응된다. 따라서 문제를 단계별로 분석할 수 있다. 이 관점은 실행을 디버깅 가능한 구조로 만든다.

실행은 코드 실행이 아니라 “결정 구조의 결과”다

지금까지의 내용을 종합하면 하나의 결론으로 수렴한다. 실행은 코드가 실행되는 사건이 아니다. 실행은 이미 결정된 구조 위에 코드가 적용된 결과다. 즉, 실행의 본질은 코드가 아니라 결정 구조다. 이 관점을 가지지 않으면, 실행 결과를 일관되게 설명할 수 없다.

일반적으로 실행은 “프로그램을 실행한다”는 표현으로 이해된다. 이 표현은 단순하지만 중요한 요소를 생략한다. 실행 이전에는 이미 여러 단계의 선택이 이루어진다. 실행 대상이 선택된다. 실행 방식이 결정된다. 실행 주체가 재정의된다. 실행 컨텍스트가 구성된다. 입출력 구조가 확정된다. 이 모든 과정이 완료된 이후에야 코드가 적용된다. 따라서 코드 실행은 마지막 단계일 뿐이다.

이 구조를 실험적으로 확인하려면, 코드 자체는 동일하게 유지하고 실행 환경만 바꾸면 된다.

$ echo -e '#include <stdio.h>\nint main(){printf("hello\n");}' > test.c
$ gcc test.c -o test

$ ./test
hello

$ ./test > out.txt
$ cat out.txt
hello

두 경우 모두 동일한 코드가 실행된다. 그러나 결과의 형태는 다르다. 첫 번째는 터미널에 출력된다. 두 번째는 파일에 저장된다. 코드에는 차이가 없다. 입력도 동일하다. 결과가 달라진 이유는 실행 구조가 다르기 때문이다. 이 차이는 실행 이전에 이미 결정되었다.

이 구조는 실행을 바라보는 기준을 바꾼다. 실행 결과를 이해하려면, 코드 분석만으로는 부족하다. 실행 이전에 어떤 선택이 이루어졌는지를 봐야 한다. PATH가 무엇인지 확인해야 한다. 환경 변수가 어떻게 설정되어 있는지 확인해야 한다. 입출력 구조가 어떻게 연결되어 있는지 확인해야 한다. 이 모든 요소가 결합되어 결과를 만든다.

또한 이 관점은 시스템 전반으로 확장된다. 서비스 실행, 컨테이너 실행, 파이프라인 구성도 동일한 구조를 따른다. 실행 대상이 정의된다. 실행 환경이 구성된다. 실행 구조가 확정된다. 이후 코드가 적용된다. 즉, 실행은 개별 프로그램의 문제가 아니라, 시스템 전체에서 반복되는 패턴이다.

이 패턴의 핵심은 일관성이다. 입력이 무엇이든, 동일한 방식으로 해석되고 결정된다. 단지 각 단계에서 선택되는 값이 다를 뿐이다. 이 때문에 실행은 예측 가능하다. 예측이 어려운 이유는 구조가 복잡하기 때문이다. 구조를 이해하면, 결과는 설명 가능해진다.

결론적으로 실행은 더 이상 “프로그램을 실행하는 행위”가 아니다. 실행은 상태를 기반으로 한 결정 구조의 최종 결과다. 이 구조를 기준으로 보면, 실행은 더 이상 블랙박스가 아니다. 각 단계는 명확한 역할을 가진다. 각 단계의 선택은 결과에 직접적으로 반영된다. 이 연결을 이해하면, 새로운 환경에서도 실행 결과를 예측할 수 있다.