본문으로 건너뛰기
고급약 45분

형상·빌드·배포 연계 — 버전과 테스트 결과를 묶어 추적 가능하게

커밋·빌드 아티팩트·환경 좌표·테스트 결과를 하나의 식별자로 묶어 "이 버전에서 무엇이 통과했는가"에 답할 수 있게 만든다. 불변 아티팩트와 다이제스트 기반 승격, 릴리스 판정에 붙이는 품질 증거 요약 형식, 추적 가능성 사슬, 그리고 재현을 위한 아티팩트 보관 정책을 설계한다.

  • #TestOps
  • #형상관리
  • #추적가능성
  • #릴리스판정
  • #상관ID
  • #아티팩트보관

개념

릴리스 회의에서 답하지 못하는 질문

배포 직전 회의에서 반복되는 질문이 하나 있다. "그래서 이 버전에서 무엇이 검증됐나요." 이 질문에 즉시 답하는 조직은 드물다. 보통 이런 대화가 된다 — 어제 회귀 테스트가 돌았고, 통과율이 높았고, 그런데 그게 지금 배포하려는 빌드였는지는 확실하지 않고, 환경은 스테이징이었던 것 같다.

자동화가 아무리 많아도 결과가 버전에 붙어 있지 않으면 판정에 쓸 수 없다. 테스트 결과와 배포 대상 사이의 연결이 사람의 기억이면, 그 연결은 회의실에서 끊긴다. 이 레슨은 그 연결을 데이터로 만드는 일을 다룬다.

이 레슨의 경계 — 두 종류의 상관 ID

"상관 ID"라는 말이 이 커리큘럼에 두 번 나오고, 가리키는 것이 다르다.

m55 레슨 3의 상관 ID이 레슨의 상관 식별자
잇는 대상테스트 실행 ↔ 서버 로그·트레이스커밋 ↔ 빌드 ↔ 환경 ↔ 테스트 결과 ↔ 릴리스
목적실패 1건의 원인을 5분 안에 좁힌다버전 단위로 무엇이 검증됐는지 조회한다
수명실패 조사 기간(며칠)릴리스 이력(수년, 감사 대응 포함)
쓰는 사람실패를 조사하는 엔지니어릴리스 판정자, 감사·규제 대응, 사고 조사

둘은 같이 쓰면 가장 좋다. 실행 레코드에 두 ID 를 모두 남기면, 릴리스 증거에서 출발해 특정 실패의 서버 로그까지 한 줄로 내려갈 수 있다. 다만 설계 목적이 달라서 혼동하면 어느 쪽도 제대로 안 된다 — 깊이를 위한 ID 와 추적을 위한 ID 는 따로 정의한다.

묶는 다섯 축

추적 가능한 상태란 다섯 축이 한 레코드에서 서로를 가리키는 상태다.

① 소스      커밋 식별자, 브랜치, 태그
② 빌드      빌드 번호 + 산출 아티팩트의 다이제스트(내용 해시)
③ 환경      환경 좌표 해시 (레슨 3) — 구성·스키마·시드·의존 서비스
④ 실행      run_id — 테스트 실행 1회를 가리키는 식별자
⑤ 결과      테스트별 상태·소요·분류 라벨 (레슨 5의 실행 레코드)

핵심은 ②의 다이제스트 다. 빌드 번호는 재빌드하면 바뀌고, 커밋만으로는 "같은 커밋을 다시 빌드한 다른 산출물"을 구분할 수 없다. 아티팩트의 내용 해시를 기록하면 스테이징에서 검증한 것과 운영에 올리는 것이 같은 물건인지 를 기계적으로 확인할 수 있다. 이것이 없으면 "테스트는 통과했지만 배포된 건 다른 빌드"라는 사고가 가능해진다.

여기서 나오는 운영 원칙이 불변 아티팩트와 승격(promotion) 이다.

한 번 빌드, 여러 환경 승격
 
빌드 1회 → 아티팩트 A (digest sha256:7f3a…)
  ↓ 배포          dev     환경좌표 h1   → 테스트 결과 R1
  ↓ 같은 A 배포   staging 환경좌표 h2   → 테스트 결과 R2
  ↓ 같은 A 배포   prod    환경좌표 h3   → 운영 모니터링
 
금지: 환경마다 다시 빌드한다 (아티팩트가 달라져 검증 결과가 승계되지 않는다)

환경마다 재빌드하는 파이프라인에서는 스테이징의 테스트 결과를 운영 배포의 근거로 쓸 수 없다. 논리적으로 다른 산출물이기 때문이다. 이 원칙은 m14 레슨 1·4에서 다룬 파이프라인 설계의 전제이기도 하지만, 다이제스트를 결과에 기록해 그 전제가 지켜졌음을 증명하는 일 은 TestOps 쪽 책임이다.

식별자 설계 — run_id 를 어떻게 만드는가

run_id 는 짧고, 사람이 읽을 수 있고, 충돌하지 않고, 다른 축으로 역추적 가능해야 한다. 실무에서 쓰기 좋은 형태는 구성 요소를 합성하는 방식이다.

run_id = {시각}-{커밋 앞 7자}-{환경}-{실행 종류}-{짧은 난수}
 
예) 20261003T0210-7f3a1c9-staging-nightly-k3x

이 형식의 장점은 눈으로 보고도 맥락을 알 수 있다는 것이다. 알림 메시지나 티켓 제목에 그대로 넣을 수 있고, 로그 검색 키로도 쓸 수 있다. 데이터베이스에는 이 문자열과 함께 각 축을 별도 열로도 넣는다 — 문자열 파싱에 의존하면 형식을 바꾸는 날 모든 쿼리가 깨진다.

그리고 run_id 는 테스트 데이터에도 심는다. 레슨 3에서 데이터 소유자 식별에 쓴 그 식별자가 바로 이것이다. 같은 ID 가 실행 레코드·생성 데이터·아티팩트 경로·알림 메시지에 모두 들어가면, 어느 지점에서 출발해도 나머지를 찾을 수 있다.

품질 증거 요약 — 릴리스 판정에 붙이는 한 장

품질 게이트(m14 레슨 4)는 기계가 자동으로 차단하는 기준이다. 반면 릴리스 판정은 사람이 하는 결정이고, 그 결정에는 게이트 통과 여부보다 더 많은 맥락이 필요하다. 그 맥락을 정해진 형식으로 제공하는 것이 품질 증거 요약이다.

{
  "release": "2026.10.03-r1",
  "artifact_digest": "sha256:7f3a1c9e4b2d...",
  "commit": "7f3a1c9",
  "verified_on": [
    { "env": "staging", "env_coordinate": "h2c81f", "run_id": "20261003T0210-7f3a1c9-staging-nightly-k3x" }
  ],
  "summary": {
    "total": 512,
    "passed": 486,
    "failed": 4,
    "flaky_pass": 7,
    "quarantined": 15,
    "skipped": 0
  },
  "open_failures": [
    { "test_id": "chk-order-refund-02", "triage": "product-defect", "ticket": "QA-4821", "severity": "major" },
    { "test_id": "chk-search-rank-11", "triage": "test-defect", "ticket": "QA-4830", "severity": "minor" }
  ],
  "coverage_gap": [
    "환불 부분취소 — 격리 중(QA-4702), 수동 확인으로 대체함",
    "결제 3DS 실패 경로 — 자동화 없음, 수동 체크리스트 항목 17"
  ],
  "notes": "결제 모의 서버 1.8.0 기준. 실기기 iOS 검증은 야간 전체 실행 범위 밖."
}

이 형식에서 중요한 네 부분은 숫자가 아니다.

  • verified_on — 어떤 환경 좌표에서 검증했는지. 운영과 다른 좌표였다면 그 차이가 판정의 전제가 된다.
  • quarantined — 격리된 15개는 검증되지 않았다는 뜻이다. 통과율만 보면 숨는다.
  • coverage_gap — 격리·미자동화로 생긴 공백과 대체 수단. 레슨 4의 장부에서 온다.
  • open_failures 의 triage — 남은 실패가 제품 결함인지 테스트 결함인지. 레슨 7의 분류 결과가 그대로 들어온다.

통과율 하나로 릴리스를 판정하지 않게 만드는 것 이 이 형식의 목적이다. "95% 통과"는 판정에 쓸 수 없고, "제품 결함 1건 major 로 남았고 환불 부분취소는 격리 중이며 수동으로 대체 확인함"은 판정에 쓸 수 있다.

추적 가능성 사슬 — 양방향으로 물어본다

추적 가능성이 작동하는지 확인하는 방법은 사슬을 양쪽에서 걸어 보는 것이다.

앞에서 뒤로 (변경 → 검증)
요구사항 / 티켓
  → 그 티켓을 구현한 커밋
  → 그 커밋을 담은 빌드 아티팩트(digest)
  → 그 아티팩트를 검증한 실행(run_id)과 환경 좌표
  → 그 실행에서 관련 테스트의 결과
  → 그 결과를 담은 릴리스 증거
 
뒤에서 앞으로 (사고 → 원인)
운영 장애 발생
  → 지금 돌고 있는 아티팩트 digest
  → 그 digest 를 검증한 릴리스 증거
  → 그 증거의 coverage_gap — 이 장애 경로가 공백에 있었는가
  → 있었다면 왜 공백이었는지, 없었다면 왜 못 잡았는지

두 번째 방향이 사고 회고에서 가장 쓸모 있다. "왜 테스트가 못 잡았나"라는 질문에 감정 없이 답할 수 있게 된다 — 공백 목록에 적혀 있었다면 알고 감수한 위험이고, 적혀 있지 않았다면 자동화의 맹점을 새로 발견한 것이다. 이 구분이 없으면 회고는 "테스트를 더 잘 했어야 한다"로 끝나고 아무것도 바뀌지 않는다.

규제 산업이나 외부 감사가 있는 조직에서는 이 사슬 자체가 요구사항이 된다. 적용되는 표준과 그 요구 범위는 산업별로 다르므로 품질보증·규제 담당과 원문 확인이 필요하다. 다만 기술적 준비는 같다 — 다섯 축이 한 레코드에서 서로를 가리키면 대부분의 추적 요구는 충족된다.

아티팩트 보관 정책 — 재현을 위한 최소치

보관 정책이 없는 조직은 두 극단 중 하나에 있다. 전부 영구 보관해 저장소 비용이 커지거나, 짧은 기본값을 그대로 둬서 조사할 때 아무것도 없다. 층으로 나누는 것이 답이다.

층무엇권장 보관근거
실행 레코드테스트별 상태·소요·분류 (구조화 한 줄)1~3년용량이 작고 추세·감사에 쓰인다
결과 리포트실행 1회의 요약 리포트6~12개월과거 실행을 사람이 다시 읽는 상한
실패 아티팩트실패 건의 스크린샷·트레이스·로그30~90일조사 수요가 급격히 떨어지는 구간
통과 아티팩트통과 건의 비디오·트레이스수집하지 않거나 수일거의 열리지 않는다 (레슨 2 감축 1번)
릴리스 증거품질 증거 요약 JSON릴리스와 동일 수명사고 조사·감사의 출발점
빌드 아티팩트배포 산출물 자체릴리스 수명 + 롤백 기간같은 digest 로 재배포·재현

정책에 반드시 붙여야 하는 예외 규칙이 하나 있다. 보관 기간을 연장하는 트리거 다. 특정 실행이 사고 조사나 릴리스 판정의 근거가 되면, 그 실행에 보존 표시를 붙여 정리 작업에서 제외한다. 표시 없이는 30일 뒤 조사 근거가 사라지고, 이 사고는 되돌릴 수 없다.

보존 표시(hold)를 붙이는 경우
- 릴리스 증거로 참조된 실행
- 사고·장애 회고에서 인용된 실행
- 열린 결함 티켓이 가리키는 실패 건
- 감사·외부 보고에 제출된 실행
→ 티켓이 닫히거나 릴리스 수명이 끝날 때 해제

실무에서 왜 필요한가

연계가 없을 때 드는 비용은 사고가 날 때 한꺼번에 나타난다. 운영 장애가 터지고, 지금 돌고 있는 버전이 무엇인지 확인하는 데 30분, 그 버전이 어떤 테스트를 통과했는지 확인하는 데 몇 시간이 든다. 그 사이 롤백 판단이 지연되고, 지연된 만큼 사용자가 겪는다. 다섯 축이 묶여 있으면 이 조회는 몇 분이다.

평상시의 효용은 다른 데 있다. 릴리스 판정 회의의 성격이 바뀐다. 증거 요약이 자동 생성되면 회의는 "테스트 돌았나요"를 확인하는 자리가 아니라 남은 위험을 받아들일지 결정하는 자리 가 된다. QA 의 발언도 달라진다 — "조금 불안합니다"가 아니라 "제품 결함 1건이 major 로 남아 있고, 환불 부분취소 경로는 격리 중이라 수동 확인으로 대체했습니다"가 된다. 후자는 반박할 수도, 받아들일 수도 있는 정보다.

보관 정책은 조용한 비용을 양쪽에서 막는다. 과도한 보관은 저장소 비용으로, 짧은 보관은 조사 불가로 나타난다. 층을 나누고 보존 표시를 두는 설계는 둘 다 피하면서, 무엇보다 "그때 자료가 없어서 원인을 못 찾았다"는 결말을 막는다.

실습 과제

과제 1 — 다섯 축 연결 점검 (30분)

가장 최근의 릴리스 하나를 골라 다섯 축을 실제로 추적해 본다.

  1. 릴리스에 올라간 아티팩트의 다이제스트를 찾는다. 찾을 수 없으면 거기가 첫 공백이다
  2. 그 다이제스트를 검증한 테스트 실행의 run_id 를 찾는다
  3. 그 실행의 환경 좌표를 찾고, 운영 환경과 다른 축이 무엇인지 적는다
  4. 환경마다 재빌드하는지 확인한다 — 재빌드한다면 스테이징 결과가 운영 배포의 근거로 쓰일 수 없는 이유를 한 문단으로 정리한다

각 단계에 걸린 시간을 기록한다. 합계가 10분을 넘으면 연계가 없는 것이다.

과제 2 — 품질 증거 요약 만들어 보기 (30분)

본문의 JSON 형식으로 최근 릴리스의 증거 요약을 손으로 채운다. 자동 생성이 아직 없어도 수동으로 한 번 채워 보는 것이 설계의 출발점이다.

  • quarantined 와 coverage_gap 을 채울 수 있는가 — 레슨 4의 장부가 없으면 못 채운다
  • open_failures 의 triage 값을 채울 수 있는가 — 레슨 7의 분류가 필요하다
  • 채운 요약을 릴리스 판정자에게 보여주고, 추가로 알고 싶은 항목이 있는지 묻는다
  • 자동 생성하려면 어느 데이터가 추가로 필요한지 목록으로 만든다

과제 3 — 보관 정책과 보존 표시 설계 (20분)

본문의 층별 표를 우리 상황으로 고쳐 쓴다. 각 층의 현재 보관 기간을 먼저 확인하고 (기본값을 그대로 쓰고 있는 층이 있을 것이다), 목표 기간과 근거를 적는다.

이어서 보존 표시를 설계한다 — 무엇이 트리거이고, 어떻게 표시하고, 누가 해제하는가. 마지막으로 "열린 결함 티켓이 가리키는 아티팩트가 이미 삭제된 사례"가 지난 6개월에 있었는지 확인한다.

자가 체크리스트

  • m55 레슨 3의 상관 ID(실패 진단용)와 이 레슨의 상관 식별자(버전 추적용)를 구분해 쓴다
  • 다섯 축(소스·빌드·환경·실행·결과)이 한 레코드에서 서로를 가리킨다
  • 빌드 아티팩트의 다이제스트를 기록하고, 검증한 산출물과 배포한 산출물이 같은지 확인할 수 있다
  • 환경마다 재빌드하지 않고 같은 아티팩트를 승격하는 구조인지 확인했다
  • run_id 형식이 정해져 있고, 실행 레코드·테스트 데이터·아티팩트 경로·알림에 같은 값이 쓰인다
  • 릴리스 판정에 붙는 품질 증거 요약 형식이 정해져 있다
  • 증거 요약에 격리 건수와 커버리지 공백이 들어가 통과율만으로 판정하지 않는다
  • 추적 사슬을 양방향(변경→검증, 사고→원인)으로 한 번 걸어 봤다
  • 아티팩트 보관 정책이 층별로 다르게 정해져 있다
  • 보존 표시(hold) 규칙이 있어 조사·릴리스 근거가 된 실행이 자동 삭제되지 않는다

흔한 실수

커밋만 기록하고 다이제스트를 안 남긴다. 같은 커밋을 재빌드한 다른 산출물을 구분할 수 없다. "테스트한 빌드와 배포한 빌드가 다르다"는 사고가 이 틈에서 나온다.

환경마다 재빌드하면서 앞 환경의 결과를 근거로 쓴다. 논리적으로 성립하지 않지만 실무에서 매우 흔하다. 재빌드 구조라면 그 사실을 증거 요약에 명시해야 한다.

run_id 를 테스트 데이터에 심지 않는다. 실행과 데이터의 연결이 끊어져 고아 데이터 탐지(레슨 3)와 사고 조사가 둘 다 어려워진다.

증거 요약에 격리와 공백을 안 쓴다. 통과율만 올라간 요약은 판정자를 오도한다. 검증되지 않은 범위를 명시하는 것이 요약의 핵심 기능이다.

보관 기간을 도구 기본값에 맡긴다. 기본값은 비용 최적화로 정해져 있고, 조사 수요를 고려하지 않는다. 열린 티켓이 가리키는 아티팩트가 사라진 뒤에야 알게 된다.

보존 표시 없이 정리 작업을 돌린다. 사고 조사 중인 실행의 트레이스가 야간 정리에 지워지는 일이 실제로 일어난다. 되돌릴 수 없는 종류의 실수다.

참고 자료

  • Jez Humble, David Farley, Continuous Delivery — 불변 아티팩트와 환경 승격, 배포 파이프라인의 추적 가능성 장
  • ISTQB Foundation Level v4.0 실러버스 — 형상 관리와 추적 가능성 절
  • ISTQB CTAL-TAE v2.0 실러버스 — 테스트 자동화와 형상 관리·보고 장. 장 번호는 원문 확인
  • SLSA(Supply-chain Levels for Software Artifacts) 문서 — 빌드 출처(provenance)와 아티팩트 다이제스트 기록 규약. 레벨 정의와 요구사항은 원문 확인
  • OCI 이미지 명세 — 컨테이너 이미지 다이제스트의 의미와 태그와의 차이. 원문 확인
  • 규제 산업의 추적 가능성 요구(의료기기·금융 등) — 적용 범위는 품질보증·규제 담당과 원문 확인
  • 이 커리큘럼 안의 관련 레슨: 모듈 14 레슨 1·4(파이프라인·품질 게이트), 모듈 55 레슨 3(진단용 상관 ID), 이 모듈 레슨 3(환경 좌표)·레슨 4(격리 장부)·레슨 5(실행 레코드)·레슨 7(트리아지 라벨)

낯선 말 풀이

이 레슨에 나온 약어입니다. 본문에서도 처음 나올 때 점선으로 표시해 두었습니다 (제목과 링크 글자는 그대로 둡니다).

ID — 식별자
무엇 하나를 다른 것과 구별하기 위해 붙인 이름이나 번호.
JSON — 제이슨
프로그램끼리 값을 주고받을 때 쓰는 글자 형식. 괄호와 따옴표로 이름과 값을 짝지어 적는다.
QA — 품질보증
만든 것이 쓸 만한지 확인하고, 애초에 덜 망가지게 만드는 방법을 정하는 일. "테스트"는 그 일의 일부다.
ISTQB — 국제 소프트웨어 테스팅 자격 위원회
테스터 자격 시험을 만들고 용어를 정리하는 국제 단체.
CTAL-TAE — 상급 자격 · 테스트 자동화
상급 테스터 자격 중 자동화 설계를 다루는 과목.
컨테이너 — 컨테이너
이 커리큘럼에서 두 가지로 쓰인다. ① 프로그램과 그것을 돌리는 데 필요한 것들을 한 상자에 담아, 어느 컴퓨터에서나 똑같이 돌아가게 만든 것 ② 화면에서 여러 요소를 하나로 묶는 바깥 틀. 서버·배포 이야기면 ①, 화면·접근성 이야기면 ②다.

용어집 전체 보기