컴파일러의 결정성은 ‘전체 입력 상태’에 대한 함수로는 성립하지만, 실제 빌드에서는 입력 상태를 완전히 고정하지 못해 산출물이 흔들린다는 관점에서 결정성·재현 가능 빌드·신뢰할 수 있는 툴체인의 차이와 재현 가능 빌드를 위한 실무 기법, 그리고 LLM 시대의 엔지니어링적 함의를 정리한다.
2026년 2월 22일
Betteridge의 법칙은 “아니다”라고 말하고, 보통의 개발자 경험 관점에서는 그 답이 대체로 맞다. (그리고 네, 당신 말이 맞아요! 제가 이 글을 쓰는 데 ChatGPT 도움을 받았다는 걸 알 수 있게 em—dash도 하나 넣어둘게요.)
내 생각은 이렇다. 이 질문에는 컴퓨터과학 답과 엔지니어링 답이 있다.
나는 2000년대에 Ksplice에서 일했는데, 거기서는 RAM 안에서 실행 중인 리눅스 커널을 패치해서 재부팅 없이 보안 업데이트를 적용했다. 죽어서 크래시 나는 커널의
plaintextobjdump
출력을 읽는 일이 매일 루틴은 아니었지만, 자주 해야 할 정도로 충분히 많아서 “컴파일러 출력 vs 소스의 의도”라는 문제가 더 이상 이론이 아니게 됐다.
형식적으로는:
artifact = F(
source,
flags,
compiler binary,
linker + assembler,
libc + runtime,
env vars,
filesystem view,
locale + timezone,
clock,
kernel behavior,
hardware/concurrency schedule
)
대부분의 팀은
plaintextsource
그리고 많아야
plaintextflags
정도만 고정한 다음, 나머지는 전부 “노이즈”라고 부른다. 재현 불가능성은 바로 그 “노이즈” 속에 산다.
나는 이걸 2000년대 Ksplice에서 뼈저리게 배웠다. 우리는 예전 버전과 새 버전의 컴파일된 출력을 diff해서, 라이브 커널 메모리에 핫패치를 꿰매 넣는 방식으로 재부팅 없는 리눅스 커널 업데이트를 만들었다. 대부분의 diff는 변경된 C 코드에 깔끔하게 대응했다. 그런데 가끔은 의미론적 소스 변경이 없는데도 diff가 폭발하곤 했다. 예를 들면 레지스터 할당이 달라지거나, 패스 동작이 달라지거나, 섹션/레이아웃이 바뀌는 식이다. 의도는 같은데 기계어가 달라졌다.
구체적인 역사적 흔적을 원한다면, GCC 버그 18574에 대해 포인터 해시 불안정성이 순회 순서와 SSA 합치기(coalescing)에 영향을 준다는 gcc-bugs 스레드가 있다.
이 구분은 중요하다:
서로 관련은 있지만, 동등한 보장을 의미하진 않는다.
댓글러가 이 점에서는 맞다. 컴파일러는 의미론(semantics)을 보존해야 한다. 정의된 동작(defined behavior)을 가진 프로그램이라면, 출력은 소스 언어의 추상 기계(abstract machine)와 관찰 가능하게 동등(observationally equivalent)해야 한다.
즉, 외부로 드러나는 동작이 같기만 하면 명령어 순서, 레지스터 선택, 인라인 전략, 블록 레이아웃은 컴파일러가 마음대로 해도 된다. 실무에서 “관찰 가능한 동작”이란 바이트-대-바이트로 동일한 명령어가 아니라, I/O 효과, volatile 접근, 원자적 동기화 보장, 정의된 반환값 같은 것들을 뜻한다.
중요한 단서들:
plaintext__DATE__
,
plaintext__TIME__
,
plaintext__TIMESTAMP__
plaintext/home/fragmede/projects/foo
)
plaintextLC_ALL
)
plaintextar
,
plaintextranlib
)
ASLR 참고: ASLR은 방출(emitted)되는 바이너리를 직접 랜덤화하지 않는다. 프로세스 메모리 레이아웃을 랜덤화한다. 하지만 컴파일러 패스 동작이 포인터의 정체성/순서에 의존한다면, ASLR이 간접적으로 결과를 교란할 수는 있다.
그래서 “컴파일러는 결정적이다”는 말은 정리(theorem) 차원에서는 종종 맞고, 운영(operational) 차원에서는 틀리기 쉽다. 그리고 산출물이 재현 가능하더라도, Ken Thompson의 Reflections on Trusting Trust는 여전히 적용된다.
또 하나: 컴파일러는 새 기술이 아니다. Grace Hopper의 A-0 시스템은 1952년 UNIVAC 시절까지 거슬러 올라간다. ChatGPT는 4년 됐는데 컴파일러는 74년?
Debian과 더 넓은 reproducible-builds 노력(대략 2013년 이후)이 이를 메인스트림으로 밀어 올렸다: 같은 소스 + 같은 빌드 지시사항이면, 비트 단위로 동일한 산출물이 나와야 한다.
실무 플레이북:
plaintextTZ=UTC
,
plaintextLC_ALL=C
) *
plaintextSOURCE_DATE_EPOCH
설정
plaintext-ffile-prefix-map
,
plaintext-fdebug-prefix-map
)
plaintextar -D
)
이렇게 하면:
지금은 이런 게 되었나? 많은 생태계에서 대체로 그렇다. 하지만 컴파일러, 링커, 패키징, 빌드 시스템 전반에 걸친 수년간의 매우 의도적인 작업이 필요했다. 손만 휘저으며 “순수하다”고 선언해서 온 게 아니라, 이상한 엣지 케이스들을 갈아 넣으며 여기까지 왔다.
요즘은 “LLM이 비결정적이면 바이브코딩이 제정신인가?”라는 형태로 이 논의가 다시 나온다. 다시 말하지만: 컴퓨터과학 답을 원하나, 엔지니어링 답을 원하나?
우리는 LLM로 정지 문제(halting problem)를 해결했고, 또 해결하지 못했다. 형식적인 의미에서 정지 문제를 해결한 건 전혀 아니다. 하지만 실용적으로는, 내가
plaintextfor
루프를 쓰고 조건을 망치면 LLM이 코드를 보고 “너 지금 바보짓 하는 중”이라고 말해주고, 그걸 고쳐주기도 한다.
엔지니어링은 완벽히 결정적인 지능에 의존해본 적이 없다. 통제된 인터페이스, 테스트 오라클, 재현 가능한 파이프라인, 관측 가능성(observability)에 의존해왔다. 나는 comma.ai를 일상적으로 쓸 정도로 AI에 호의적이지만, 생성된 코드 주변에는 결정적인 검증 게이트를 여전히 원한다. 내 여자친구는 내가 운전하는 것보다 그것(주행 보조)이 운전할 때가 더 부드럽고 덜 들쭉날쭉하다고 선호하는데, 이는 “확률적 시스템”과 “운영적으로 더 나은 결과”가 공존할 수 있다는 유용한 상기다.
LLM 보조 코딩에서도 같은 패턴이다:
컴퓨터과학 답: 비결정성은 무섭다.
엔지니어링 답: 경계 조건을 통제하고, 출력을 검증하고, 출시한다.
그리고 그래, 이 논증의 일부는 실존적이다. 우리 대부분은 아직 철학으로 돈 버는 게 아니라 월세 내는 일을 하고 있으니까. 그래서 일을 앞으로 밀어주는 도구를 쓰고, 그다음 필요한 가드레일을 세운다.