컴파일러는 결정적일까? 재현 빌드의 함정

제목: 컴파일러는 결정적일까? “이론은 Yes, 현실은 No”로 끝나는 이유
컴파일은 분명 기계적인 작업인데, 왜 어떤 날은 빌드 결과가 미묘하게 달라질까요?
오늘은 “**컴파일러는 결정적(deterministic)인가?**”라는 질문을 컴퓨터과학 관점 vs 엔지니어링 관점으로 나눠 정리해볼게요.
1) “결정적 컴파일러”와 “재현 가능한 빌드”는 다르다
요약하면, 컴파일러는 ‘모든 입력 상태’가 같으면 결정적일 수 있어요.
하지만 실무 빌드는 보통 그 “모든 입력”을 고정하지 않아서 결과물이 흔들립니다.
원문에서 말하는 컴파일 산출물은 사실상 아래처럼 엄청 많은 요소의 함수예요. 팀에서는 보통 source와 flags만 관리하고 나머지는 “노이즈”로 치부하는데, 바로 그 노이즈가 비재현성(non-reproducibility)의 본진이 됩니다.
- 입력에 포함되는 것들(예시)
compiler binary(컴파일러 바이너리), 링커/어셈블러libc + runtime, 환경변수, 파일시스템 뷰locale/timezone,clock, 커널 동작- 하드웨어/동시성 스케줄(concurrency schedule)
이 차이를 구분하면 좋아요.
- Deterministic compiler: 같은 “완전한 입력 튜플”이면 같은 출력
- Reproducible build: 서로 독립된 빌더가 “비트 단위로 동일한 결과” 재생산
- Reliable toolchain: 달라도 기능적으로 거의 문제 없게 나오는 경향
셋은 관련은 있지만, 동일한 보장(guarantee)이 아니에요.
2) 컴파일러의 계약(Contract)은 “바이트 동일성”이 아니라 “의미(semantics)”예요
요약하면 컴파일러에게 기대하는 건 **동작의 동등성(observational equivalence)**이지, 기계어가 똑같이 나오라는 게 아니에요.
즉, 레지스터 할당, 인라이닝, 블록 레이아웃, 명령어 순서가 달라져도 외부에서 관측 가능한 동작이 같으면 OK라는 쪽이 기본 계약입니다.
여기서 “관측 가능한 동작”은 대체로 이런 것들을 뜻해요.
- I/O 효과
volatile접근- 원자적 동기화(atomic) 보장
- 정의된 리턴값/예외 등
다만 중요한 함정도 같이 따라옵니다.
- **Undefined Behavior(정의되지 않은 동작)**가 있으면 의미 보장이 약해지거나 깨질 수 있어요.
- 타이밍, 마이크로아키텍처 사이드채널, 정확한 메모리 레이아웃은 보통 언어 차원의 계약 밖인 경우가 많아요.
- 재현 가능한 빌드는 의미 보존보다 더 빡세요. “동작”이 아니라 “같은 비트”가 목표니까요.

3) 빌드 결과를 흔드는 엔트로피(Entropy) 원인들
요약하면, 결과물을 바꾸는 건 거창한 버그만이 아니고 사소한 환경 차이인 경우가 많아요.
특히 디버그 정보, 파일 경로, 정렬 순서, 병렬성 같은 “주변부 요소”가 출력에 섞이면 비트 동일성을 깨기 쉽습니다.
원문에서 꼽은 대표적인 원인은 아래와 같아요.
__DATE__,__TIME__,__TIMESTAMP__처럼 시간이 박히는 매크로- DWARF 등 디버그 정보에 절대 경로가 들어가는 경우
- 빌드 경로 누출(
/home/.../project) LC_ALL같은 locale에 따라 정렬/비교가 달라지는 동작- 파일시스템 순회(iteration) 순서 차이
- 병렬 빌드/링크의 레이스로 인한 순서 변화
ar,ranlib아카이브 멤버 순서/메타데이터- build ID, UUID, random seed
- 빌드 중 네트워크 fetch
- 툴체인 버전 스큐(skew), 호스트 커널/라이브러리 차이
- (역사적 사례) 포인터/해시 순서 불안정이 최적화 패스에 간접 영향
여기서 흥미로운 포인트가 ASLR이에요. ASLR 자체가 바이너리를 랜덤하게 “생성”하진 않지만, 컴파일러 내부 패스가 포인터 식별/순서에 의존하면 ASLR이 간접적으로 결과를 흔들 수 있다는 설명이 나옵니다.
4) 재현 가능한 빌드(Reproducible Builds)는 “의도적인 엔지니어링”의 결과
요약하면, 재현성은 덤으로 얻어지는 성질이 아니라 설계하고 갈아 넣어야 생기는 특성이에요.
Debian을 중심으로 2013년 즈음부터 재현 가능한 빌드 운동이 본격화되면서, 생태계 전체(컴파일러/링커/패키징/빌드 시스템)가 함께 개선됐다는 맥락이 중요합니다.
실무에서 바로 써먹을 플레이북은 이런 형태예요.
- 툴체인/의존성 고정(freeze)
- 환경 안정화:
TZ=UTC,LC_ALL=C SOURCE_DATE_EPOCH설정(시간 요인 제거)- 변동 메타데이터 정규화/제거
- 경로 접두어 정규화:
-ffile-prefix-map,-fdebug-prefix-map - 결정적 아카이브:
ar -D - 빌드 그래프에서 네트워크 제거
- hermetic 컨테이너/샌드박스에서 빌드
- CI에서 빌더 간 산출물 diff를 지속 수행
이걸 하면 단순히 “다시 빌드가 된다”를 넘어, **검증 가능(verifiable)**하고 통제된(hermetic) 파이프라인을 만들 수 있어요. 큰 조직일수록 공급망(supply chain) 관점에서 이게 전환율(?)급으로 중요해집니다. 배포 속도도 결국 “재현/검증 비용”에 좌우되거든요.

5) LLM 코딩이 “비결정적”이어도, 엔지니어링은 가드레일로 굴러간다
요약하면, 원문은 이 논의를 LLM(대규모 언어 모델)로 확장해요.
“바이브코딩(vibecoding)이 LLM의 비결정성 때문에 위험한가?”에 대해, 컴퓨터과학적 공포는 타당하지만 엔지니어링 답은 ‘경계 조건을 통제하고, 검증하고, 통과하면 배포’입니다.
LLM 보조 코딩에서 실무적으로 유효한 패턴은 다음이에요.
- 입력을 제약하기(요구사항/컨텍스트를 명확히)
- 출력을 테스트 가능하게 만들기(테스트 오라클, 정적 분석)
- 결정적인 CI로 게이트(gate) 세우기
- 산출물은 재현 가능하게 만들기
- 확률적 생성은 “업스트림”으로 두고, 배포 단계의 진실은 검증으로 확보하기
즉, “완벽히 결정적인 지능”이 아니라 통제된 인터페이스 + 검증 파이프라인이 제품을 안전하게 만든다는 주장입니다. 우리 일이 철학이 아니라 결국 “렌트 내는 비즈니스”라는 언급도, 현업 감각으로는 꽤 정확한 포인트고요.
마무리: “결정성”을 믿기보다 “재현성/검증”을 설계해요
컴파일러는 이론적으로는 결정적일 수 있지만, 현실 빌드는 입력 상태가 워낙 넓어서 그냥 두면 흔들리는 게 기본값이에요.
그래서 중요한 건 “컴파일러가 결정적인가?”라는 질문 하나로 끝내는 게 아니라, 우리 팀의 빌드/배포 파이프라인이 재현 가능하고 검증 가능한가를 점검하는 거예요.
오늘부터 바로 해볼 액션을 하나만 고른다면, CI에서 **서로 다른 빌더로 같은 커밋을 빌드해 해시를 비교(diff)**하는 것부터 추천해요.
한 번만 해봐도 “노이즈”라고 부르던 것들이 실제로 얼마나 많은 비용을 만들고 있었는지, 체감이 확 옵니다.






