LabHub
배우기 러닝패스 코스

CI/CD 파이프라인 · 테스트 분할 · 이론

나누면 빨라지지만 결과가 달라질 수 있다

LabHub 에서 이어서 보기

한 줄 요약

시험을 여러 조각으로 나눠 동시에 돌리면 되먹임이 짧아진다. 대신 나눔이 결정적이지 않거나 조각 사이에 숨은 순서 의존성이 있으면 총 판정이 달라진다. 분할은 속도를 사는 대신 새로운 실패 모드를 들여오는 거래다.

왜 이게 필요했나

되먹임이 느려지면 사람이 파이프라인을 보지 않는다. 결과를 기다리는 대신 다른 일을 하러 가고, 돌아왔을 때는 이미 다른 커밋이 얹혀 있어 무엇이 깨뜨렸는지 흐려진다. 마틴 파울러는 익스트림 프로그래밍의 지침을 빌려 10분 빌드를 대부분의 프로젝트에 합리적인 목표로 제시하고, 빌드 시간에서 깎아 낸 1분은 모든 개발자가 커밋할 때마다 아끼는 1분이라고 적는다. 시험이 40분이면 사람들은 하루에 한 번만 통합하게 되고, 그러면 CI 가 주는 이득 자체가 사라진다.

분할은 그 40분을 줄이는 가장 직접적인 수단이다. 서버를 더 빠르게 만들거나 시험을 지우는 것과 달리, 시험을 그대로 두고 벽시계 시간만 줄인다.

어떻게 나누나

두 가지 방식이 있고, 둘 다 쓸 자리가 있다.

파일 단위로 균등하게. 시험 파일 목록을 정렬해 조각 수로 나눈다. 구현이 단순하고 추가 정보가 필요 없다. 대신 파일마다 걸리는 시간이 제각각이라 가장 느린 조각이 전체 시간을 결정한다. 파일 하나가 10분이면 조각을 아무리 늘려도 10분 밑으로 내려가지 않는다.

기록된 소요 시간으로 균형 있게. 지난 실행에서 각 시험이 얼마나 걸렸는지를 저장해 두고, 그 값을 기준으로 합이 비슷해지도록 나눈다. 조각들이 거의 동시에 끝나서 낭비가 적다. 대신 소요 시간 기록 자체가 입력이 되므로, 그 파일을 어디에 두고 어떻게 갱신할지 정해야 한다. 기록이 없는 새 시험에는 기본값을 주고 다음 실행에서 채운다.

# 균등 분할: 느린 파일 하나가 전체를 붙잡는다조각0 [====                ] 2분조각1 [==================  ] 9분   <- 전체 9분조각2 [=====               ] 3분# 시간 기반 분할: 합이 비슷해진다조각0 [========            ] 5분조각1 [========            ] 5분   <- 전체 5분조각2 [=======             ] 4분

나눔은 결정적이어야 한다

여기가 핵심이다. 같은 커밋이면 언제 돌려도 같은 배분이 나와야 한다. 배분이 실행마다 달라지면 세 가지가 무너진다.

첫째, 실패를 재현할 수 없다. "조각 2에서 실패" 라는 기록이 다음 실행에서는 다른 시험 묶음을 가리킨다. 둘째, 재시도가 의미를 잃는다. 조각 하나만 다시 돌리려 해도 그 조각이 같은 시험을 담는다는 보장이 없다. 셋째, 순서 의존성 문제가 들쭉날쭉 나타나 "가끔 실패하는 시험" 으로 오인된다.

그래서 배분은 실행 시각이나 난수가 아니라 커밋에서 결정되는 값 으로만 계산한다. 목록을 정렬하고, 소요 시간 기록도 저장소에 커밋된 파일에서 읽는다. 해시로 조각을 정할 때도 시험 이름 같은 안정된 문자열을 쓴다.

나눴을 때 드러나는 순서 의존성

분할이 가장 자주 들춰내는 것은 혼자 돌면 통과하고 같이 돌면 실패하는 시험, 그리고 그 반대다. 원인은 대개 공유 상태다. 앞 시험이 만들어 둔 데이터베이스 행, 전역 변수, 임시 파일, 등록해 둔 전역 설정에 뒤 시험이 기대고 있는 것이다.

이런 시험은 원래 깨져 있었는데 우연히 늘 같은 순서로 돌아서 숨어 있었다. 분할은 그 순서를 흔들어 문제를 드러낸 것뿐이다. 그래서 분할 도입 직후의 실패는 분할의 버그가 아니라 대개 이미 있던 결함이라고 보고 시작하는 편이 맞다. 확인하는 방법도 간단하다. 실패한 시험을 혼자 돌려 본다. 혼자서는 통과한다면 의존하고 있는 상태가 있는 것이다.

합쳐야 판정이 나온다

조각별 결과는 각각 부분 정보다. 조각들이 모두 끝나고 결과를 합친 뒤에야 판정이 나온다. 그래서 파이프라인에는 조각들을 기다렸다가 결과 파일을 모아 세는 단계가 반드시 있어야 한다. 각 조각은 자신의 보고서를 산출물로 올리고, 합치는 단계가 그것을 내려받아 합산한다. GitLab 문서가 산출물을 "단계 사이로 중간 결과를 넘기는 데 쓴다" 고 적는 것이 바로 이 용도다.

합산 단계가 확인해야 할 것은 합격 수만이 아니다. 조각이 하나도 빠지지 않았는지, 총 시험 수가 기대한 값인지를 같이 본다. 조각 하나가 통째로 죽었는데 나머지가 다 초록이면, 합산을 대충 하는 파이프라인은 초록으로 판정한다. 시험이 0개 돌고 성공한 조각도 마찬가지다.

그리고 판정의 불변식이 하나 있다. 조각 수를 3에서 5로 바꿔도 총 판정은 같아야 한다. 이것이 분할이 올바른지 검사하는 가장 좋은 방법이다. 같은 커밋을 조각 수만 바꿔 두 번 돌려 총 시험 수와 실패 목록을 대조한다.

현장에서 만나는 모습

참고

다음 실습에서 할 것

시험 러너와 분할기를 셸과 python3 로 직접 만든다. 먼저 파일 단위 균등 분할로 나눠 조각별 시간이 얼마나 벌어지는지 재고, 소요 시간 기록을 읽어 합이 비슷해지도록 다시 나눈다. 같은 커밋으로 두 번 나눠 배분이 글자 하나까지 같은지 대조하고, 일부러 난수를 섞은 분할기와 비교해 무엇이 무너지는지 본다. 그다음 공유 상태에 기대는 시험을 넣어 순서 의존성을 재현하고, 조각별 보고서를 합산해 조각 하나가 빠졌을 때 합산 단계가 잡아내는지까지 확인한다. 마지막은 조각 수를 바꿔도 총 판정이 같은지 검사하는 단계다.