CI가 30분이면 AI 도구를 붙여도 소용없습니다
AI 테스트 생성 도구 데모를 보고 나면 팀 채널이 잠깐 시끄러워집니다. 화면 녹화 안에서는 자연어 한 줄이 Playwright 스펙 파일로 바뀌고, 셀렉터가 깨져도 알아서 고쳐집니다. 그런데 두 달 뒤 릴리스 주기를 다시 재보면 아무것도 달라져 있지 않습니다. PR은 여전히 이틀 걸려 머지되고, QA는 여전히 금요일 저녁에 리그레션을 돌립니다. 도구를 잘못 골랐나 싶어 다른 벤더 데모를 또 잡습니다.
여기서 갈래는 셋입니다. 첫째, 모델이나 도구를 더 좋은 것으로 바꿉니다. 둘째, 자동화 커버리지를 더 올립니다. 셋째, 도구 검토를 멈추고 파이프라인이 실제로 몇 분 걸리는지부터 잽니다. 앞의 둘은 예산과 사람이 필요하고 셋째는 오늘 오후에 시작할 수 있습니다. 그런데 대부분의 팀이 앞의 둘부터 손을 댑니다.
결론을 먼저 말씀드리면, 셋째부터 하시는 편이 맞습니다. 파이프라인이 30분이면 그 안에서 사람이든 에이전트든 하루에 시도할 수 있는 횟수가 물리적으로 제한됩니다. 도구를 바꿔서 코드 작성 시간을 절반으로 줄여도 검증 대기 시간이 그대로면 전체 리드타임은 거의 움직이지 않습니다. 이건 정성적인 훈계가 아니라 8월 24일에 GitLab이 숫자와 함께 내놓은 주장이고, 저는 우리 팀 상황에 그대로 적용된다고 봅니다.
1. 8월 24일 GitLab이 내놓은 문장 하나
1-1. "코드는 더 이상 병목이 아니다"
GitLab 공식 블로그에 2026-08-24 올라온 CEO Bill Staples의 글 "When code is abundant"는 "Code is no longer the bottleneck."이라는 문장으로 시작합니다. 여기까지는 흔한 수사입니다. 이 글이 인용할 만해지는 지점은 그다음입니다.
같은 글에 이런 문장이 있습니다. "a thirty-minute CI pipeline defeats every model you point at it." 30분짜리 CI 파이프라인은 당신이 무엇을 겨누든 그 모델을 이긴다는 뜻입니다. 그리고 실행 권고를 아주 짧게 붙여 놓았습니다. "Time your CI. If a full pipeline takes much longer than five minutes, improving that may matter more than switching models."
5분이라는 숫자가 우리 현실과 얼마나 동떨어져 있는지는 각자 아실 겁니다. 저희도 모바일 리그레션은 시간 단위입니다. 중요한 건 5분을 당장 맞추는 게 아니라, 기준선이 존재한다는 사실을 팀 안에서 공유하는 것입니다. 기준선이 없으면 "우리 CI 좀 느리죠"는 영원히 불평으로만 남습니다.
1-2. cost per accepted change — 단위를 바꾸는 제안
같은 글에서 지표에 대한 제안이 하나 나옵니다. "The economic unit I think matters most in this transition is not cost per line of code. It is cost per accepted change." 코드 한 줄당 비용이 아니라, 받아들여진 변경 하나당 비용을 세라는 것입니다.
QA 관점에서 이 제안이 유용한 이유는 분모가 바뀌기 때문입니다. 우리가 습관적으로 보고하는 지표는 대체로 산출량 쪽에 붙어 있습니다.
| 습관적으로 보고하는 지표 | 이 지표가 숨기는 것 | cost per accepted change 관점의 대체 지표 |
|---|---|---|
| 자동화 테스트 케이스 수 | 그 케이스가 실제로 실패를 잡았는지 | 케이스당 실제 결함 검출 건수 |
| 자동화 커버리지 % | 커버된 경로가 릴리스에 쓰이는지 | 릴리스 차단 결함 중 자동화가 먼저 잡은 비율 |
| CI 실행 성공률 | 실패했을 때 원인 규명에 든 시간 | 실패 1건당 평균 트리아지 시간 |
| AI 도구 라이선스 사용률 | 산출물 교정에 든 시간 | 수용된 변경 1건당 총 대기 시간 |
제 해석으로는, 이 표의 오른쪽 열로 옮겨가는 것 자체가 QA 조직의 발언권을 바꿉니다. 왼쪽 열은 QA가 얼마나 일했는지를 말하고, 오른쪽 열은 조직이 얼마나 빨리 안전하게 움직이는지를 말합니다. 예산 회의에서 후자가 훨씬 강합니다.
2. Amplitude 사례에서 실제로 움직인 숫자
2-1. 표로 본 before / after
같은 글이 인용한 Amplitude 사례의 수치는 이렇습니다. 원문 표기를 그대로 옮깁니다.
| 항목 | 원문 수치 | 기간 |
|---|---|---|
| 출하한 PR 수 | "tripled the number of pull requests it shipped" | over six months |
| 월간 보고 버그 | "715 to 319" | 같은 기간 |
| PR 사이클 타임 | "5.2 hours to 44 minutes" | 같은 기간 |
| 프론트엔드 CI | "roughly thirty minutes to three or four" (분) | 같은 기간 |
여기서 눈여겨볼 조합은 첫 줄과 둘째 줄입니다. PR 수가 3배가 되었는데 버그는 715에서 319로 줄었습니다. 보통 처리량이 3배가 되면 결함도 늘어납니다. 늘지 않았다는 건 처리량 증가가 검증을 우회해서 얻은 것이 아니라는 뜻입니다.
한 가지는 분명히 해 두겠습니다. 이 수치는 GitLab이 자사 제품 맥락에서 인용한 고객 사례이고, 외부 검증은 없습니다. 저는 이 숫자를 목표치로 쓰지 않고, "처리량과 결함이 같은 방향으로 움직일 필요는 없다"는 방향성의 근거로만 씁니다.
2-2. 인과 방향을 뒤집어 읽기
기사를 그냥 읽으면 "AI 에이전트를 도입해서 PR이 3배가 되었다"로 읽힙니다. 그런데 프론트엔드 CI가 30분에서 3~4분으로 줄었다는 줄이 같이 있습니다.
제 해석으로는 순서가 반대입니다. CI가 30분일 때 개발자는 하루에 검증 사이클을 열 번 이상 돌리지 않습니다. 심리적으로도 그렇고 물리적으로도 그렇습니다. 그래서 변경을 크게 묶어서 한 번에 보냅니다. 변경이 크면 리뷰가 느려지고, 리뷰가 느려지면 사이클 타임이 5.2시간이 됩니다. CI가 3분이 되면 이 사슬이 반대로 풀립니다. 변경을 작게 쪼갤 수 있고, 작으면 리뷰가 빠르고, 실패해도 원인 범위가 좁습니다.
즉 CI 시간 단축이 결과가 아니라 선행 조건이었을 가능성이 높습니다. 그렇다면 우리가 먼저 손댈 곳도 정해집니다.
2-3. 다른 소식과 겹쳐 읽기
같은 날 InfoQ에 올라온 Roblox의 QCon 발표 트랜스크립트(InfoQ, 2026-08-24)에도 비슷한 결이 보입니다. AI 코드리뷰 제안 수용률이 "almost 68%, 70%"까지 올라갔고, 비교 대상으로 제시된 사람 리뷰 수용률은 "even only 55% of human code reviews are accepted"였습니다. 여기서도 수용률을 끌어올린 수단은 모델 교체가 아니라 "code review exemplars", 즉 팀의 리뷰 판단 기준을 문서화해 주입하는 작업이었습니다.
같은 발표에서 테스트에 대한 지적도 하나 나옵니다. "It might be a really crappy unit test that's testing something that's not that important." AI가 생성한 테스트의 문제는 개수가 아니라 중요도라는 말입니다. 커버리지 숫자만 보는 지표 체계에서는 이 문제가 보이지 않습니다.
3. "5분"을 우리 팀 기준으로 번역하기
3-1. 먼저 재기
권고는 "Time your CI." 한 줄인데, 실제로 재려고 하면 평균값 하나로는 아무 판단이 안 됩니다. 필요한 건 분포입니다. GitHub Actions를 쓰는 경우 아래 정도로 시작할 수 있습니다.
#!/usr/bin/env bash
# 최근 200개 워크플로 실행의 소요시간 분포를 뽑습니다.
# 목적: 평균이 아니라 p50 / p90 / p95 를 봅니다.
set -euo pipefail
REPO="${1:?사용법: ./ci-time.sh owner/repo [workflow-file]}"
WF="${2:-ci.yml}"
gh api "repos/${REPO}/actions/workflows/${WF}/runs?per_page=100&status=completed" \
--paginate --jq '.workflow_runs[]
| select(.run_started_at != null and .updated_at != null)
| {
br: .head_branch,
con: .conclusion,
# 초 단위 소요시간
sec: ((.updated_at | fromdateiso8601) - (.run_started_at | fromdateiso8601))
}' \
| jq -s '
# 브랜치 구분 없이 전체 분포부터 봅니다.
(map(.sec) | sort) as $s
| {
n: ($s | length),
p50_min: (($s[($s|length)/2|floor] // 0) / 60 | .*10|round|./10),
p90_min: (($s[($s|length)*0.9|floor] // 0) / 60 | .*10|round|./10),
p95_min: (($s[($s|length)*0.95|floor] // 0) / 60 | .*10|round|./10),
max_min: (($s[-1] // 0) / 60 | .*10|round|./10),
# 실패 비율은 flaky 판단의 출발점입니다.
fail_rate: ((map(select(.con != "success")) | length) / ($s|length) * 100 | round)
}'
이 스크립트가 뱉는 다섯 숫자를 팀 위키 맨 위에 붙여 두고 주 1회 갱신하십시오. 저는 이걸 해 보고 나서야 우리 파이프라인이 "평균 11분, p95 34분"이라는 걸 알았습니다. 체감이 나빴던 이유는 평균이 아니라 p95였습니다. 그리고 p95를 만든 것은 테스트가 아니라 의존성 설치 캐시 미스였습니다.
3-2. 무엇이 30분을 만드는가
측정해 보면 시간을 먹는 항목은 대체로 다음 다섯 가지 안에 들어갑니다.
| 구간 | 전형적 비중 | 단축 난이도 | 먼저 손댈 가치 |
|---|---|---|---|
| 의존성 설치·빌드 (캐시 미스 포함) | 30~50% | 낮음 | 매우 높음 |
| 컨테이너·에뮬레이터 부팅 | 10~20% | 중간 | 높음 |
| E2E 테스트 실행 | 20~40% | 높음 | 중간 |
| 순차 실행되는 잡 사이의 대기 | 5~15% | 낮음 | 높음 |
| 러너 큐 대기 | 편차 큼 | 중간 | 중간 |
경험상 E2E 실행 시간부터 손대려는 유혹이 큽니다. 눈에 가장 잘 보이니까요. 그런데 비중은 앞의 두 줄이 더 큽니다. 캐시 키를 제대로 잡고 잡 의존 그래프를 병렬로 펴는 작업은 며칠이면 끝나고, E2E 최적화는 몇 달이 걸립니다.
4. 이 길에서 만나는 함정 셋
4-1. 첫 번째 함정: 평균만 보고 p95를 놓친다
증상은 이렇습니다. 대시보드에는 "평균 8분"이 찍혀 있는데 개발자들은 계속 CI가 느리다고 말합니다. 원인은 분포의 꼬리입니다. 열 번 중 한 번이 35분이면 사람은 그 한 번을 기억합니다. 대응은 단순합니다. 보고 지표를 p95로 바꾸고, 평균은 참고로만 두십시오.
4-2. 두 번째 함정: 테스트를 지워서 시간을 줄인다
증상은 파이프라인이 갑자기 빨라졌는데 3주 뒤 운영에서 결함이 터지는 것입니다. 원인은 "느린 테스트"와 "가치 없는 테스트"를 구분하지 않고 실행 시간 기준으로만 잘라낸 것입니다.
대응은 잘라내기 전에 각 테스트가 최근 6개월 동안 실제로 실패를 잡은 횟수를 세는 것입니다.
# 목적: 실행시간과 "실제 결함 검출 이력"을 함께 놓고 삭제 후보를 고릅니다.
# 입력: CI가 남긴 JUnit XML 아카이브 (최근 6개월치)
import glob, collections
import xml.etree.ElementTree as ET
stats = collections.defaultdict(lambda: {"runs": 0, "fails": 0, "sec": 0.0})
for path in glob.glob("junit-archive/**/*.xml", recursive=True):
for tc in ET.parse(path).getroot().iter("testcase"):
key = f'{tc.get("classname")}::{tc.get("name")}'
s = stats[key]
s["runs"] += 1
s["sec"] += float(tc.get("time") or 0)
# failure 와 error 만 셉니다. skipped 는 신호가 아닙니다.
if tc.find("failure") is not None or tc.find("error") is not None:
s["fails"] += 1
rows = []
for key, s in stats.items():
avg = s["sec"] / max(s["runs"], 1)
rows.append((key, round(avg, 2), s["fails"], s["runs"]))
# 느리면서 6개월간 한 번도 실패를 잡지 못한 케이스 = 1순위 검토 대상
print("== 느리고(2초 초과) 검출 이력 0인 케이스 ==")
for key, avg, fails, runs in sorted(rows, key=lambda r: -r[1]):
if avg > 2.0 and fails == 0:
print(f"{avg:7.2f}s runs={runs:4d} {key}")
# 반대편도 봅니다. 자주 실패하지만 원인이 제품이 아닌 케이스 = flaky 후보
print("\n== 실패율 5~60% 구간 (flaky 의심) ==")
for key, avg, fails, runs in rows:
if runs >= 20 and 0.05 <= fails / runs <= 0.60:
print(f"fail={fails/runs:5.1%} runs={runs:4d} {key}")
"검출 이력 0"이 곧 "가치 없음"은 아닙니다. 회귀를 막고 있어서 실패가 안 나는 경우도 있습니다. 그래서 이 목록은 삭제 명단이 아니라 검토 명단입니다. 이 구분을 흐리면 두 번째 함정에 그대로 빠집니다.
4-3. 가장 비싼 함정: 병렬화로 flaky를 증폭시킨다
증상이 아주 특징적입니다. 잡을 4분할해서 파이프라인이 24분에서 8분이 되었는데, 재실행 버튼을 누르는 횟수가 두 배로 늘어납니다. 결과적으로 머지까지의 실제 대기 시간은 줄지 않거나 오히려 늘어납니다.
원인은 병렬 실행이 테스트 간 공유 상태를 드러내기 때문입니다. 같은 계정, 같은 시드 데이터, 같은 포트를 쓰던 테스트들이 순차 실행에서는 우연히 통과하고 있었던 것입니다. 이건 병렬화가 만든 문제가 아니라 병렬화가 찾아낸 문제인데, 팀 안에서는 "병렬화 때문에 불안정해졌다"로 기록되고 롤백됩니다. 가장 비싼 함정이라고 부르는 이유는, 롤백하면 문제는 남고 정보만 사라지기 때문입니다.
대응은 병렬화 도입 시점에 재실행 횟수를 함께 지표로 잡는 것입니다. 그리고 flaky로 판정된 케이스는 고칠 때까지 격리 잡으로 빼되, 격리된 케이스 수를 매주 공개하십시오. 안 보이는 격리 목록은 반드시 자랍니다.
| 함정 | 증상 | 원인 | 대응 |
|---|---|---|---|
| 평균만 본다 | 지표는 좋은데 불만이 계속됨 | 분포 꼬리 무시 | 보고 지표를 p95로 교체 |
| 테스트를 지운다 | 3주 뒤 운영 결함 | 실행시간 기준 일괄 삭제 | 검출 이력과 함께 검토 명단 운영 |
| 병렬화로 flaky 증폭 | 재실행 횟수 2배 | 테스트 간 공유 상태 노출 | 재실행 횟수 지표화 + 격리 목록 주간 공개 |
5. 도입 평가 — 우리 규모에서 무엇을 먼저 할 것인가
QA 4~6명, 자동화 초기 단계라는 조건에서 위 항목들을 평가하면 이렇습니다.
| 작업 | 도입 난이도 | 유지보수 비용 | 팀 학습곡선 | 판단 |
|---|---|---|---|---|
| CI 시간 p50/p95 측정·주간 공개 | 낮음 (0.5일) | 낮음 (스크립트 1개) | 낮음 | 즉시 |
| 의존성 캐시 키 정비 | 낮음 (2~3일) | 낮음 | 낮음 | 즉시 |
| 잡 의존 그래프 병렬화 | 중간 (1주) | 중간 (flaky 관리 동반) | 중간 | 측정 후 |
| 검출 이력 기반 테스트 검토 명단 | 중간 (1주) | 중간 (분기 1회 갱신) | 중간 | 2순위 |
| AI 테스트 생성 도구 도입 | 높음 | 높음 (생성물 소유권 문제) | 높음 | 위 4개 이후 |
| cost per accepted change 지표 전환 | 중간 | 낮음 | 높음 (합의 필요) | 병행 |
마지막 줄의 학습곡선을 높게 잡은 이유는 기술이 아니라 합의 때문입니다. 지표를 바꾸면 그동안 좋게 보였던 숫자가 나빠 보입니다. 이 대화를 준비 없이 시작하면 지표 전환은 실패합니다.
6. 권할 만한 경우 / 권하지 않는 경우
권할 만한 경우
- 파이프라인 전체 시간을 아무도 정확히 모르는 상태 — 측정만으로도 논의의 질이 바뀝니다
- AI 도구 도입 검토를 요청받았고, 근거가 필요한 상황 — 측정 결과가 가장 좋은 근거입니다
- 자동화 케이스는 늘고 있는데 릴리스 속도는 그대로인 팀
- 재실행 버튼을 누르는 것이 일상화된 팀 — flaky 비용이 이미 지불되고 있습니다
권하지 않는 경우
- 파이프라인이 이미 5분 이내인 팀 — 여기서 더 줄이는 건 투자 대비 효과가 낮습니다. 결함 검출 쪽으로 옮기십시오
- 릴리스 주기가 분기 단위인 조직 — 병목이 CI가 아니라 승인 프로세스에 있습니다
- CI 설정 변경 권한이 QA 조직에 전혀 없는 경우 — 측정까지만 하고 권한 협의를 먼저 하십시오
- 다음 2주 안에 대형 릴리스가 있는 경우 — 병렬화는 릴리스 직후에 시작하십시오
다음 주 월요일에 그대로 쓸 체크리스트
[ 1주차 — 측정만 한다 ]
□ 주력 워크플로 1개의 최근 200회 실행에서 p50 / p90 / p95 / max 를 뽑는다
□ 같은 기간 재실행(rerun) 횟수와 실패율을 함께 기록한다
□ 파이프라인을 5개 구간(설치·부팅·단위·E2E·대기)으로 쪼개 구간별 소요시간을 잰다
□ 다섯 숫자를 팀 위키 최상단에 게시한다. 개선안은 아직 쓰지 않는다
[ 2주차 — 가장 싼 것부터 ]
□ 구간별 표에서 비중 1위 구간을 지목한다
□ 그 구간이 의존성 설치라면 캐시 키 적중률을 먼저 확인한다
□ 개선 후 같은 스크립트로 재측정해 before/after 를 한 줄로 남긴다
[ 3주차 — 지표 대화 ]
□ 보고 지표에서 "자동화 케이스 수"를 빼고 "p95 파이프라인 시간"을 넣는다
□ 릴리스 차단 결함 중 자동화가 먼저 잡은 비율을 한 번 계산해 본다
□ AI 도구 도입 논의가 있다면, 위 숫자를 먼저 테이블에 올린다
첫 주에 개선안을 쓰지 말라는 항목이 제일 중요합니다. 측정과 개선을 같은 주에 하면 측정이 개선안을 정당화하는 방향으로 휘어집니다. 일주일 참으면 숫자가 스스로 순서를 정해 줍니다.
댓글
댓글 쓰기