Skip to main content
입문

배포 산출물의 동일성과 승격: 테스트한 것을 그대로 배포했는가

산출물과 파일 묶음을 다이제스트로 식별하고, 검증 결과와 환경 기록을 같은 식별자에 연결해 재빌드 없이 다음 환경으로 승격한다.

마지막 검증 재검증 정책: 제품 버전 의존: 새 주요 버전마다 재검증
목차
표준·구현·측정·해석 표시는 무엇인가요?
  • 표준웹 표준이나 언어 명세가 정한 동작
  • 구현특정 기술이나 브라우저가 실제로 구현한 동작
  • 측정명시한 환경에서 직접 실행해 관찰한 결과
  • 해석앞선 근거에서 도출한 설계 판단
  • 미확인아직 공식 근거나 재현 결과를 확인하지 못한 내용

30초 요약

테스트한 결과와 배포한 결과가 같은지 확인하려면 한 문장을 기억한다.

만든 것을 식별하고, 식별한 산출물을 검증하며, 그 결과를 식별자에 연결하고, 같은 산출물을 승격한 뒤 환경의 식별자를 확인한다.

실제 흐름은 다섯 칸이다.

산출물 묶음 → 식별 → 식별한 산출물 검증·결과 연결 → 같은 묶음 승격 → 환경의 식별자 확인
  1. 배포 후보 파일마다 콘텐츠 다이제스트를 계산한다.
  2. 여러 파일과 플랫폼 결과를 하나의 릴리스 매니페스트에 묶고 그 목록도 식별한다.
  3. 테스트·정책 결과가 어느 릴리스 식별자를 검증했는지 기록한다.
  4. 다음 환경에서 다시 빌드하지 않고 같은 릴리스 식별자에 배포 자격을 준다.
  5. 배포 뒤 환경이 실제로 사용하는 릴리스와 설정 식별자를 확인한다.

같은 커밋(저장소에 기록한 특정 소스 상태), 버전명, 파일 이름이나 초록색 테스트 화면만으로는 이 고리가 닫히지 않는다.

커밋과 버전명은 산출물 자체가 아니다

같은 커밋에서도 다음 입력이 달라지면 결과가 달라질 수 있다.

  • 빌드 도구와 의존성 버전
  • 운영체제·CPU와 작업 경로
  • 빌드 시각, 난수와 파일 순서
  • 빌드 시 주입한 환경 값
  • 압축, 패키징과 서명 단계

7편은 이런 입력을 통제하고 독립 재빌드 결과를 비교하는 방법을 다뤘다. 이번 글의 질문은 조금 다르다. 이미 만든 여러 후보 중 어느 결과를 테스트했고, 바로 그 결과를 어느 환경에 배포했는가를 추적한다.

커밋 8ab...  ──빌드 A──> web.tar.gz  sha256:111...
              └─빌드 B──> web.tar.gz  sha256:222...

파일 이름과 커밋이 같아도 다이제스트가 다르면 서로 다른 산출물이다. A를 테스트하고 B를 배포했다면 ‘같은 소스를 배포했다’고 말할 수는 있어도 ‘테스트한 산출물을 배포했다’고 말할 수는 없다.

콘텐츠 다이제스트는 선택한 바이트를 가리킨다

다이제스트는 ‘무엇의 바이트를 계산했는가’라는 범위와 함께 써야 한다.

  • 압축 전 폴더인가, 최종 압축 파일인가?
  • 서명 전 패키지인가, 서명된 최종 패키지인가?
  • 여러 플랫폼을 가리키는 상위 목록인가, 한 플랫폼 이미지인가?
  • 변환된 코드 위치를 원본 코드에 연결하는 소스 맵을 포함하는가, 사용자에게 배포할 파일만 포함하는가?

같은 범위를 고른 뒤에만 값을 비교한다. 다이제스트가 같으면 선택한 바이트가 같다는 강한 실용적 근거가 되지만, 테스트 통과·안전성·제작자 신뢰까지 알려 주지는 않는다.

이름과 태그는 다이제스트의 별칭으로만 쓴다

web:stable, mobile:1.8.0 같은 이름은 사람이 찾고 대화하기 쉽다. 그러나 이름이 같은 내용을 영원히 가리킨다고 가정하지 않는다.

web:stable ──어제──> sha256:111...
web:stable ──오늘──> sha256:222...

따라서 기록에는 둘을 함께 남긴다.

읽는 이름: web:stable
고정 참조: registry.example/web@sha256:111...

고정 참조는 가리키는 내용이 바뀌지 않는 식별 방법이다. 태그를 승격 표시로 쓸 수는 있지만, 검증과 배포 기록의 최종 비교는 태그가 그때 가리킨 다이제스트로 한다.

여러 파일은 릴리스 매니페스트로 묶는다

React를 사용하는 Web 앱 하나도 HTML, JavaScript 조각, CSS, 이미지와 변환된 코드 위치를 원본 코드에 연결하는 소스 맵처럼 여러 파일을 만든다. React Native 릴리스는 iOS·Android 패키지와 각 JavaScript 소스 맵을 함께 추적할 수 있다. 파일 하나의 다이제스트만 기록하면 나머지가 바뀌거나 빠져도 알아차리지 못한다.

다음은 특정 제품 형식이 아닌 개념 예시다.

{
  "release": "frontend-2026.08.04.1",
  "artifacts": [
    { "role": "web-assets", "uri": "web.tar.gz", "sha256": "111..." },
    { "role": "web-source-maps", "uri": "web-maps.tar.gz", "sha256": "222..." },
    { "role": "android-upload", "uri": "app.aab", "sha256": "333..." }
  ]
}

role은 파일이 실행용인지 오류 위치 복원용인지 같은 책임을 나타내고, uri는 가져올 위치를 나타낸다. 실제 목록에는 크기, 플랫폼과 파일 형식도 기록할 수 있다. 중요한 것은 다음이다.

  • 파일 목록과 각 다이제스트가 고정돼 있는가?
  • 매니페스트 자체도 다이제스트나 바뀌지 않는 릴리스 ID로 식별되는가?
  • 검증·승격·배포 기록이 파일 이름이 아니라 이 묶음 식별자를 가리키는가?

검증 기록은 무엇을 검증했는지 가리켜야 한다

테스트 기록도 최소한 다음 질문에 답해야 한다.

대상:    release-manifest@sha256:abc...
정책:    browser-e2e-v3
환경:    chromium-140 / test-api-17
결과:    passed
실행:    run-8421

이 예시는 SLSA 형식이나 특정 CI 제품 문법이 아니다. 앱 테스트 결과를 릴리스 식별자에 연결하기 위한 개념 기록이다. run-8421 passed만 남기면 그 실행이 어떤 파일을 받았는지 알 수 없다.

검증 정책도 식별한다. 테스트 코드와 브라우저 버전이 바뀌면 같은 산출물에 대한 질문 자체가 달라질 수 있기 때문이다. ‘모든 테스트 통과’보다 어떤 필수 정책이 어느 대상에 통과했는지 남긴다.

승격은 새 빌드가 아니다

release@sha256:abc...
  ├─ 개발 배포 완료
  ├─ 스테이징 검증 통과
  └─ 운영 배포 자격 부여  ← 승격

승격이 파일을 실제로 복사해야 한다는 뜻은 아니다. 저장소의 같은 다이제스트를 다음 환경이 가리키게 할 수도 있다. 핵심은 릴리스 식별자가 바뀌지 않는 것이다.

반대로 다음 흐름은 승격이 아니다.

스테이징: 커밋 8ab... → 빌드 → sha256:111... → 테스트 통과
운영:     커밋 8ab... → 다시 빌드 → sha256:222... → 배포

운영 결과가 좋을 수도 있지만 스테이징의 통과 증거는 111...을 가리킨다. 222...에는 같은 증거가 없다. 운영 전용 빌드가 반드시 필요하다면 새 산출물로 식별하고 필요한 검증을 다시 연결한다.

환경 설정은 산출물과 따로 식별한다

환경별 서버 주소나 기능 설정을 어떻게 넣는지에 따라 동일성 경계가 달라진다.

빌드 시 넣는 설정

PUBLIC_API_URL=https://staging.example → web.tar.gz sha256:111...
PUBLIC_API_URL=https://api.example     → web.tar.gz sha256:222...

설정이 JavaScript 안에 들어가 바이트를 바꾸면 두 결과는 다른 산출물이다. 같은 커밋이라도 스테이징에서 테스트한 파일을 운영에 그대로 승격한 것이 아니다.

실행 시 읽는 설정

앱 산출물:     web.tar.gz       sha256:111...
스테이징 설정: runtime.json     sha256:aaa...
운영 설정:     runtime.json     sha256:bbb...

앱 산출물은 같게 유지할 수 있지만 실제 배포 단위는 앱 다이제스트 + 환경 설정 다이제스트다. 설정 검증과 접근 권한도 별도 관문이 필요하다. ‘같은 산출물’과 ‘같은 실행 상태’를 같은 말로 쓰지 않는다.

조건부 권장은 다음과 같다. 환경마다 다시 빌드할 이유가 없다면 실행 시 설정을 분리하면 같은 산출물을 승격하기 쉽다. 설정 로딩 실패와 산출물·설정의 호환성도 함께 설계해야 한다.

배포 기록이 증거 고리를 닫는다

배포 명령이 시작되거나 성공 코드를 반환했다는 사실만으로는 부족할 수 있다. 배포 도구가 완료됐다고 판단한 뒤 다음을 찾을 수 있어야 한다.

환경:       production
릴리스:     release-manifest@sha256:abc...
실행 설정:  runtime-config@sha256:bbb...
배포 실행:  deploy-291
상태:       ready
시각:       2026-08-04T...

확인 방법은 플랫폼마다 다르다. 컨테이너 환경은 실제 실행 중인 이미지 다이제스트를 조회할 수 있고, 정적 Web 배포는 배포 서비스가 바꾸지 않는 릴리스 ID와 파일 매니페스트를 연결할 수 있다. 중요한 것은 ‘운영 배포 버튼을 눌렀다’가 아니라 운영 환경의 현재 상태가 검증된 식별자를 가리킨다는 증거다.

Web과 React는 파일 집합 전체를 추적한다

React는 산출물 식별 방식을 정하지 않는다. Vite·Webpack 같은 빌드 도구와 호스팅 방식이 실제 결과를 정한다. 정적 Web 앱이라면 다음을 분리한다.

  • 사용자에게 배포하는 HTML, JavaScript, CSS와 이미지
  • 오류 위치를 원본으로 되돌리는 소스 맵
  • 서버 렌더링을 한다면 서버 실행 이미지
  • 실행 시 읽는 공개 설정

내용 기반 파일 이름이 있어도 파일 하나만 확인하지 않는다. 빌드가 나눈 파일 조각인 이전 청크가 남거나 HTML이 새 청크를 가리키지 않으면 개별 파일은 올바르더라도 배포 묶음은 잘못될 수 있다. 파일 목록과 각 다이제스트, 진입 HTML이 가리키는 관계까지 릴리스 단위로 검증한다.

소스 맵은 사용자에게 공개하지 않더라도 같은 릴리스 식별자에 연결할 수 있다. 그래야 운영 오류의 생성 위치가 어느 원본 코드와 맵을 사용해야 하는지 찾을 수 있다.

React Native Android는 한 파일의 바이트 비교로 끝나지 않는다

따라서 업로드한 app.aab와 Android 기기에 설치되는 앱 파일인 APK는 같은 파일이 아니다.

React Native 소스
  → 업로드 AAB 다이제스트
  → Play 릴리스·versionCode
  → 기기별 APK 생성
  → 앱 서명 키로 서명
  → 사용자 장치의 설치 APK 집합

versionCode는 Android가 새 버전과 이전 버전의 순서를 판단하는 번호이지 업로드 AAB나 모든 기기별 APK의 바이트 다이제스트가 아니다. 이 경계에서는 한 파일의 동일성 대신 변환 관계를 추적한다.

  • 어느 AAB 다이제스트를 어느 Play 릴리스에 올렸는가?
  • 앱의 고유 식별 이름인 package name과 versionCode는 무엇인가?
  • Play가 어느 앱 서명 인증서를 사용했는가?
  • 문제가 난 장치가 받은 앱 버전과 CPU·화면 밀도용 APK 구성은 무엇인가?

이 기록은 AAB와 APK가 같은 바이트라는 주장이 아니다. 검증한 업로드 산출물에서 플랫폼이 어떤 규칙으로 설치 산출물을 만들었는지 연결하는 증거다. iOS나 다른 스토어도 중간 변환과 서명 경계가 있다면 플랫폼 공식 절차에 맞는 식별자를 따로 기록해야 한다.

다이제스트·서명·증명은 다른 질문에 답한다

  • 다이제스트: 선택한 바이트가 같은가?
  • 디지털 서명: 신뢰하기로 한 키가 이 내용에 서명했고 이후 바뀌지 않았는가?
  • 빌드 출처 증명: 어떤 소스·빌더·과정이 이 산출물을 만들었다고 기록됐는가?
  • 검증 기록: 어떤 정책이나 테스트가 어느 대상에 어떤 결과를 냈는가?
  • 배포 기록: 어느 환경이 어느 릴리스와 설정을 사용하게 됐는가?

증명(attestation, 어떤 사실이 맞다고 서명해 남긴 진술)이 있다는 사실만 보지 않는다. 어떤 산출물을 대상으로 누가 무엇을 진술했는지, 서명을 어떤 신뢰 기준으로 검증했는지 확인한다. 브라우저의 전체 사용자 흐름을 확인하는 E2E 테스트 통과, 취약점 검사와 사용자 안전성은 출처 증명과 별도 정책이다.

동일성이 끊겼을 때 확인하는 순서

문제가 생기면 다음 순서로 한 고리씩 확인한다.

  1. 환경 — 문제가 난 환경이 현재 가리키는 릴리스와 설정 식별자는 무엇인가?
  2. 승격 — 그 식별자가 어느 이전 환경과 승인에서 넘어왔는가?
  3. 검증 — 필수 테스트·정책 기록이 정확히 그 식별자를 대상으로 하는가?
  4. 매니페스트 — 파일 목록, 역할과 각 다이제스트가 완전한가?
  5. 산출물 — 저장소에서 다시 가져온 실제 바이트가 기록한 다이제스트와 맞는가?
  6. 출처 — 어떤 소스·빌더·입력으로 만들어졌는가?

예를 들어 운영 오류가 스테이징에서는 없었다면 ‘환경 차이’라고 먼저 단정하지 않는다.

운영 릴리스 ID == 스테이징 검증 ID ?
  ├─ 아니오 → 재빌드, 바뀐 태그, 잘못된 승격부터 조사
  └─ 예

운영 설정 ID == 승인된 의도한 운영 설정 ID ?
  ├─ 아니오 → 잘못된 설정 배포부터 조사
  └─ 예

운영 설정의 검증 기록과 앱 산출물 호환성 증거가 있는가?
  ├─ 아니오 → 설정 검증·호환성 경계 조사
  └─ 예 → 실제 환경·트래픽·데이터 차이 조사

첫 비교로 추측 범위를 크게 줄일 수 있다.

다음 글과의 경계

이번 글은 테스트한 산출물과 배포한 산출물을 같은 식별자와 기록으로 연결하는 데 집중했다. 다음 내용은 분리한다.

  • 배포 뒤 사용자 문제를 어떤 신호로 찾는가: 다음 12편 관측성 신호
  • 설치된 네이티브 앱과 JavaScript 업데이트의 호환성: 14편 앱 버전과 호환성 계약
  • 여러 업데이트 파일을 전부 적용하거나 전혀 적용하지 않는 법: 16편 원자적 업데이트
  • 어느 사용자까지 같은 릴리스를 넓힐지 판단하는 법: 17편 점진 배포와 카나리 배포
  • 문제가 생긴 릴리스에서 이전 식별자로 돌아가는 법: 18편 롤백
  • 공급망 전체의 키 관리·서명 정책과 공격 모델: 별도 보안 글

마지막으로 한 문장을 다시 떠올린다. 만든 것을 식별하고, 식별한 산출물을 검증하며, 그 결과를 식별자에 연결하고, 같은 산출물을 승격한 뒤 환경의 식별자를 확인한다.

Active recall

기억에서 꺼내 보기

답을 쓰지 않아도 됩니다. 먼저 머릿속으로 답한 뒤 펼쳐서 정답·이유·흔한 오해를 비교하세요.

  1. 01같은 커밋에서 빌드한 두 파일은 같은 산출물일까?

    정답

    커밋은 소스 상태를 가리킬 뿐이므로, 최종 파일이나 릴리스 매니페스트의 콘텐츠 다이제스트를 비교해야 한다.

    왜 그런가

    시간·도구·환경과 서명처럼 소스 밖 입력이 결과 바이트를 바꿀 수 있다.

    관련 설명 다시 읽기
    지금 어느 정도 기억났나요?

    선택한 상태와 다음 복습일은 이 브라우저에만 저장됩니다.

  2. 02`web:stable` 같은 태그가 같으면 같은 컨테이너 이미지를 배포할까?

    정답

    태그는 다른 이미지에 다시 연결될 수 있으므로, 실제 이미지 다이제스트가 같은지 확인해야 한다.

    왜 그런가

    사람이 읽는 이름은 탐색에 유용하지만 바이트가 바뀌지 않는 식별자를 대신하지 못한다.

    관련 설명 다시 읽기
    지금 어느 정도 기억났나요?

    선택한 상태와 다음 복습일은 이 브라우저에만 저장됩니다.

  3. 03브라우저의 전체 사용자 흐름을 확인하는 E2E 테스트가 통과했다는 기록만 있으면 배포 후보의 자격을 증명할까?

    정답

    아니다. 검증 정책과 결과에 더해 그 실행이 확인한 산출물 또는 릴리스 매니페스트 식별자가 기록돼야 한다.

    왜 그런가

    대상이 없으면 새로 만든 다른 파일이 그 통과 결과를 빌려 쓸 수 있다.

    관련 설명 다시 읽기
    지금 어느 정도 기억났나요?

    선택한 상태와 다음 복습일은 이 브라우저에만 저장됩니다.

  4. 04스테이징에서 통과한 소스를 운영 설정으로 다시 빌드하는 것도 승격일까?

    정답

    새 빌드가 바이트를 바꾸면 새로운 산출물이다. 승격은 이미 검증한 릴리스 묶음의 자격을 다음 환경으로 옮기는 일이다.

    왜 그런가

    운영에서 다시 만든 결과는 스테이징이 확인한 대상과 같다는 증거가 없다.

    관련 설명 다시 읽기
    지금 어느 정도 기억났나요?

    선택한 상태와 다음 복습일은 이 브라우저에만 저장됩니다.

  5. 05Google Play에 올리는 게시용 앱 묶음 AAB와 Android 기기에 설치되는 앱 파일 APK의 바이트를 직접 비교하면 같을까?

    정답

    아니다. Google Play가 AAB에서 기기별 APK를 생성하고 앱 서명 키로 서명하므로 두 파일은 역할과 바이트 경계가 다르다.

    왜 그런가

    업로드 산출물과 스토어가 만든 배포 산출물을 한 파일로 보지 말고 변환·서명 관계를 추적해야 한다.

    관련 설명 다시 읽기
    지금 어느 정도 기억났나요?

    선택한 상태와 다음 복습일은 이 브라우저에만 저장됩니다.

출처와 검증 범위

아래 날짜는 링크를 마지막으로 열어 본 날입니다. 문서 상단의 검증일은 글의 설명과 적용 범위를 다시 확인한 날입니다.

  1. DORA Capabilities — Continuous integrationDORA, Google Cloud · 공식 문서 · 확인 2026-08-04
  2. Site Reliability Engineering — Release EngineeringGoogle SRE · 공식 문서 · 확인 2026-08-04
  3. Cryptographic checksumsReproducible Builds · 공식 문서 · 확인 2026-08-04
  4. What's in a build environment?Reproducible Builds · 공식 문서 · 확인 2026-08-04
  5. Stable order for inputsReproducible Builds · 공식 문서 · 확인 2026-08-04
  6. RandomnessReproducible Builds · 공식 문서 · 확인 2026-08-04
  7. Build pathReproducible Builds · 공식 문서 · 확인 2026-08-04
  8. SOURCE_DATE_EPOCH specificationReproducible Builds · 표준 · 확인 2026-08-04
  9. Embedded signaturesReproducible Builds · 공식 문서 · 확인 2026-08-04
  10. Image digestsDocker Docs · 공식 문서 · 확인 2026-08-04
  11. SLSA v1.2 — Verification Summary AttestationSLSA · 표준 · 확인 2026-08-04
  12. Artifact attestationsGitHub Docs · 공식 문서 · 확인 2026-08-04
  13. Cloud Deploy TerminologyGoogle Cloud Documentation · 공식 문서 · 확인 2026-08-04
  14. Cloud Deploy service architectureGoogle Cloud Documentation · 공식 문서 · 확인 2026-08-04
  15. Env Variables and ModesVite · 공식 문서 · 확인 2026-08-04
  16. About Android App BundlesAndroid Developers · 공식 문서 · 확인 2026-08-04
  17. Sign your appAndroid Developers · 공식 문서 · 확인 2026-08-04
  18. Version your appAndroid Developers · 공식 문서 · 확인 2026-08-04
  19. Publishing to Google Play StoreReact Native · 공식 문서 · 확인 2026-08-04
이 문서의 마지막까지 읽었습니다.