병렬 실행
고친 사람 github-actions[bot]
병렬 실행은 여러 일을 차례로 돌리지 않고 같은 순간에 나란히 돌려 끝나는 시간을 줄입니다. 넓게는 계산 여러 개를 나란히 돌리는 일을 모두 이렇게 부릅니다. 테스트에서는 테스트 여러 개를 한꺼번에 돌려 전체를 빨리 끝내는 일을 가리킵니다. 대가로 테스트가 도는 순서가 사라집니다.
쉽고 빠른 이해
병렬 실행은 테스트를 한 줄로 세우지 않고 여러 줄로 나눠 동시에 돌립니다. 테스트 400개를 네 줄로 나누면 한 줄이 100개씩만 맡습니다.
테스트는 코드를 고칠 때마다 돌립니다. 개수가 늘면 한 줄로 돌리는 시간도 그만큼 길어집니다. 기다리는 시간이 길면 개발자는 테스트를 덜 돌리게 됩니다.
도는 순서는 이렇습니다.
- 테스트를 돌려 주는 프로그램이 테스트를 여러 묶음으로 나눕니다
- 따로 도는 실행 단위 여럿이 묶음을 하나씩 맡아 동시에 돌립니다
- 다 끝나면 결과를 한데 모아 보여 줍니다
대가는 순서입니다. 어느 테스트가 먼저 돌지 정해지지 않습니다. 같은 데이터를 함께 쓰는 테스트끼리는 서로 밟아 가끔씩만 실패합니다.
테스트가 몇십 개뿐이라 몇 초면 끝나면 나누는 비용이 더 큽니다. 그럴 때는 한 줄로 돌립니다.
상세
마트 계산대와 닮았습니다. 계산대가 하나면 손님 마흔 명이 한 줄로 서서 차례를 기다립니다. 계산대를 넷 열면 줄이 넷으로 갈립니다. 마지막 손님이 훨씬 일찍 나갑니다.
병렬 실행은 일 여러 개를 나눠 받아 같은 순간에 나란히 진행하는 것입니다. 한 줄로 하나씩 차례로 돌리는 것은 직렬 실행이라고 부릅니다. 둘은 어느 한 순간을 잘라 보면 갈립니다. 그 순간에 도는 일이 둘 이상이면 병렬 실행입니다.
테스트 400개를 모은 테스트 스위트가 있다고 해 봅시다. 스위트는 한 번에 돌리려고 모은 테스트 묶음입니다. 테스트 하나가 0.5초씩 걸리면 직렬로는 200초가 걸립니다. 넷으로 나눠 나란히 돌리면 한 줄이 100개만 맡으니 50초 남짓에 끝납니다.
이 시간이 왜 중요한지는 테스트를 언제 돌리는지에서 나옵니다. 테스트는 코드를 고칠 때마다, 올릴 때마다 돌립니다. 한 번 도는 데 삼십 분이 걸리면 개발자는 결과를 기다리다 다른 일로 넘어갑니다. 병렬 실행은 테스트를 하나도 빼지 않고 기다리는 시간만 줄입니다.
넓은 뜻과 테스트에서의 뜻
같은 낱말이 두 맥락에서 쓰입니다. 뜻의 뼈대는 같고 나누는 대상이 다릅니다.
넓은 뜻에서 일을 나눠 받는 쪽은 코어입니다. 코어는 CPU(Central Processing Unit, 중앙 처리 장치) 안에서 명령을 실행하는 단위입니다. 코어가 여럿이면 명령을 여러 줄기로 같은 순간에 실행할 수 있습니다.
넓은 뜻의 병렬 실행은 계산 하나를 조각내 여러 코어에 나눠 맡기는 일입니다. 사진 만 장에 같은 필터를 거는 일을 코어 여덟 개가 나눠 맡는 식입니다.
병렬 실행은 동시성과 자주 섞여 쓰입니다. 동시성은 여러 일을 겹친 시간 구간 안에서 함께 다루는 성질입니다. 코어 하나가 일 사이를 잘게 오가며 번갈아 돌려도 동시성은 성립합니다. 병렬 실행은 같은 순간에 둘 이상이 진행되어야 성립합니다.
테스트에서의 병렬 실행은 나누는 대상이 테스트입니다. 테스트 하나하나는 서로 따로 부를 수 있는 작은 프로그램이라 처음부터 조각이 나 있습니다. 이 문서의 나머지는 이 뜻을 다룹니다.
테스트를 나눠 받는 워커
병렬 실행에는 테스트를 나눠 주는 쪽과 받아서 돌리는 쪽이 있습니다.
나눠 주는 쪽은 테스트 러너입니다. 러너는 짜 둔 테스트를 찾아 돌리고 결과를 모으는 프로그램입니다. 병렬 실행에서 러너는 테스트를 직접 돌리지 않습니다. 돌릴 목록을 만들어 나눠 주는 일을 맡습니다.
받아서 돌리는 쪽은 워커입니다. 워커는 러너에게서 테스트를 받아 돌리고 결과를 돌려주는 실행 단위입니다. 앞의 예에서 100개씩 맡은 한 줄이 워커 하나입니다. 워커가 넷이면 같은 순간에 테스트 넷이 돕니다.
flowchart TD
R["러너 · 테스트 목록을 만들어 나눠 준다"]
R --> W1["워커 1"]
R --> W2["워커 2"]
R --> W3["워커 3"]
R --> W4["워커 4"]
W1 --> M["러너 · 결과를 모아 보고한다"]
W2 --> M
W3 --> M
W4 --> M
그림에서 러너는 위아래에 두 번 나옵니다. 나누는 일과 모으는 일은 러너 혼자 합니다. 가운데 줄의 워커들만 나란히 돕니다.
러너는 처음에 목록을 워커 수만큼 잘라 미리 나눠 줄 수 있습니다. 목록을 한 줄에 두고 손이 빈 워커가 다음 테스트를 가져가게 할 수도 있습니다.
미리 자르면 오래 걸리는 테스트가 한 몫에 몰릴 수 있습니다. 그러면 그 워커만 늦게 끝납니다. 나머지 워커는 먼저 끝나 기다립니다.
그때그때 가져가게 하면 손이 빈 워커가 남은 테스트를 덜어 갑니다. 그래서 워커들이 끝나는 때가 고르게 맞습니다.
프로세스 워커와 스레드 워커
워커를 무엇으로 만드느냐에 따라 테스트끼리 함께 쓰는 것이 달라집니다. 고를 수 있는 것은 프로세스와 스레드 둘입니다.
프로세스는 운영체제가 띄운 프로그램 하나입니다. 프로세스마다 메모리를 따로 받습니다. 그래서 한 프로세스가 메모리에 쓴 값을 다른 프로세스는 볼 수 없습니다.
스레드는 한 프로세스 안에서 따로 도는 실행 흐름입니다. 같은 프로세스의 스레드끼리는 메모리를 함께 씁니다. 한 스레드가 바꾼 값을 다른 스레드가 바로 읽습니다.
이 차이는 전역 변수에서 드러납니다. 전역 변수는 프로그램 어디서나 읽고 고칠 수 있는 값입니다. 시간대 설정처럼 테스트가 잠깐 바꿨다가 되돌리는 값이 흔히 여기에 삽니다.
| 프로세스 워커 | 스레드 워커 | |
|---|---|---|
| 메모리 | 워커마다 따로 받습니다 | 워커끼리 함께 씁니다 |
| 띄우는 비용 | 큽니다. 프로그램을 새로 올립니다 | 작습니다 |
| 전역 변수 | 워커마다 제 것이 있어 안 부딪힙니다 | 한 테스트가 바꾼 값을 다른 테스트가 읽습니다 |
표의 마지막 줄이 고르는 기준입니다. 전역 값을 바꿨다 되돌리는 테스트가 많으면 스레드 워커에서 서로 부딪힙니다. 그럴 때는 띄우는 비용을 치르고 프로세스 워커를 씁니다.
프로세스 워커도 메모리 밖의 것은 함께 씁니다. 데이터베이스와 파일과 네트워크 포트는 기계 하나에 하나씩입니다. 다음 소절의 문제는 워커 종류와 상관없이 생깁니다.
사라지는 순서
병렬 실행은 테스트에 없던 조건을 하나 새로 붙입니다. 순서에 기대지 않아야 합니다. 주문 테스트 한 쌍과 가입 테스트 한 쌍으로 이 조건을 봅니다.
직렬로 돌리면 테스트는 늘 정해진 순서로 돕니다. 병렬로 돌리면 어느 테스트가 먼저 끝날지, 어느 둘이 같은 순간에 돌지가 실행마다 바뀝니다.
주문을 만드는 테스트가 넣은 주문을 취소 테스트가 꺼내 쓰는 경우가 그렇습니다. 둘이 다른 워커로 가면 취소 테스트가 주문 테스트보다 먼저 돌 수 있습니다. 그러면 취소 테스트는 주문이 없는 상태로 시작해 실패합니다. 다른 테스트가 먼저 돌았는지에 결과가 달린 이런 상태를 테스트 순서 의존성이라고 합니다.
병렬에서는 순서보다 더한 것이 생깁니다. 두 테스트가 같은 순간에 같은 데이터를 건드립니다. 가입 테스트 A와 B가 둘 다 [email protected] 으로 가입하고 끝에 그 회원을 지운다고 해 봅시다.
sequenceDiagram
participant A as 가입 테스트 A
participant 디비 as 데이터베이스
participant B as 가입 테스트 B
A->>디비: [email protected] 으로 가입
B->>디비: [email protected] 으로 가입
디비-->>A: 성공
디비-->>B: 실패 · 이미 있는 이메일
A->>디비: 끝나고 회원을 지운다
직렬이었다면 A가 회원을 지운 뒤에 B가 가입하므로 둘 다 통과합니다. 병렬에서는 A가 지우기 전에 B가 가입을 시도해 B만 실패합니다.
결과가 누가 먼저 닿느냐에 달리는 이런 상황을 경쟁 상태라고 합니다. 경쟁 상태는 두 테스트의 타이밍이 겹칠 때만 터집니다.
그래서 코드를 안 바꿨는데 어떤 실행에서는 통과하고 어떤 실행에서는 실패하는 테스트가 생깁니다. 이런 테스트를 불안정한 테스트라고 부릅니다. 실패를 봐도 코드 탓인지 타이밍 탓인지 가려내기 어렵습니다.
함께 쓰는 것을 나누는 방법
해법의 원칙은 같은 순간에 도는 테스트끼리 아무것도 함께 쓰지 않게 하는 것입니다. 테스트가 서로에게 영향을 주지 않게 떼어 놓는 이 원칙을 테스트 격리라고 부릅니다.
아래 표는 테스트가 흔히 함께 쓰는 것과 그것을 나누는 방법을 모았습니다.
| 함께 쓰는 것 | 부딪히는 모습 | 나누는 법 |
|---|---|---|
| 데이터베이스 | 같은 행을 넣고 지우다 겹칩니다 | 워커마다 데이터베이스를 따로 둡니다 |
| 테스트 데이터 | 같은 이메일로 가입합니다 | 테스트마다 겹치지 않는 값을 만들어 씁니다 |
| 파일 | 같은 경로에 쓰고 지웁니다 | 테스트마다 임시 디렉터리를 받습니다 |
| 네트워크 포트 | 같은 포트로 서버를 띄웁니다 | 운영체제가 빈 포트를 골라 주게 합니다 |
| 전역 변수 | 한 테스트가 바꾼 설정을 다른 테스트가 읽습니다 | 프로세스 워커를 씁니다 |
표의 방법은 모두 함께 쓰던 것을 워커나 테스트 수만큼 늘려 각자 제 것을 갖게 합니다.
데이터베이스에는 트랜잭션을 쓰는 방법이 하나 더 있습니다. 트랜잭션은 여러 쓰기를 한 덩어리로 묶는 단위입니다. 묶인 쓰기는 한꺼번에 반영되거나 한꺼번에 취소됩니다.
이 한꺼번에 취소하는 일을 롤백이라고 부릅니다. 테스트를 트랜잭션 하나로 감쌉니다. 테스트가 끝날 때 롤백하면 넣은 데이터가 다음 테스트에 남지 않습니다.
이 방법이 막는 것은 앞 테스트가 남긴 데이터입니다. 같은 순간에 도는 두 테스트가 같은 값을 넣는 충돌은 앞의 표처럼 값이나 데이터베이스를 나눠서 막습니다.
끝내 나눌 수 없는 것도 있습니다. 외부 회사가 하나만 열어 준 시험용 서버가 그런 예입니다. 이런 것을 쓰는 테스트는 병렬 실행에서 빼고 따로 한 줄로 돌리도록 표시해 둡니다.
빨라지는 폭의 한계
워커를 넷으로 늘려도 시간이 꼭 넷으로 나뉘지는 않습니다. 앞의 테스트 400개로 까닭을 봅니다.
전체는 가장 늦게 끝나는 워커가 끝날 때 끝납니다. 400개 중 하나가 혼자 60초 걸리면 워커를 백 개로 늘려도 스위트는 60초 아래로 못 내려갑니다. 그 테스트 하나는 나눌 수 없기 때문입니다.
나눌 수 없는 부분이 단축 폭에 천장을 만든다는 원리를 암달의 법칙이라고 부릅니다. 천장은 테스트 말고도 여러 곳에서 생깁니다.
| 막히는 곳 | 왜 안 줄어드나 | 대처 |
|---|---|---|
| 가장 긴 테스트 | 그 테스트 하나는 나눌 수 없습니다 | 오래 걸리는 테스트를 먼저 나눠 줍니다 |
| 워커 준비 | 워커마다 띄우고 준비하는 시간이 따로 듭니다 | 테스트가 적으면 워커 수를 줄입니다 |
| 코어 수 | 워커가 코어보다 많으면 코어를 번갈아 씁니다 | 워커 수를 코어 수 안으로 둡니다 |
| 함께 쓰는 서버 | 모든 워커가 한 데이터베이스 서버에 몰립니다 | 워커마다 서버를 따로 띄웁니다 |
표의 둘째 줄은 테스트 픽스처와 얽힙니다. 픽스처는 테스트가 돌기 전에 갖춰 둘 값이나 환경입니다. 테스트용 데이터베이스를 띄워 두는 것이 흔한 예입니다.
직렬에서는 이 데이터베이스를 한 번만 띄웁니다. 워커마다 데이터베이스를 따로 두면 워커 수만큼 띄웁니다. 테스트가 수십 개뿐이면 이 비용이 줄인 시간보다 클 수 있습니다.
병렬 실행과 테스트 샤딩
테스트 샤딩도 테스트를 나눠 돌립니다. 둘은 나누는 범위가 다릅니다.
병렬 실행은 기계 한 대 안에서 워커 여럿에 나눕니다. 테스트 샤딩은 스위트를 조각으로 잘라 기계 여러 대에 보냅니다.
코드를 올릴 때마다 서버가 빌드와 테스트를 돌리는 방식을 지속적 통합(CI, Continuous Integration)이라고 부릅니다. 샤딩은 흔히 이 CI 서버에서 씁니다. 서버가 기계 넷을 띄워 조각을 하나씩 맡기는 식입니다.
둘은 겹쳐 쓸 수 있습니다. 기계마다 받은 조각을 다시 워커 여럿에 나눠 돌립니다.
쓸 때와 안 쓸 때
병렬 실행이 이득인지는 스위트의 크기와 테스트끼리의 독립성이 정합니다.
| 상황 | 병렬 실행 |
|---|---|
| 스위트가 몇 분 넘게 걸리고 테스트가 서로 독립적입니다 | 씁니다 |
| 테스트가 수십 개라 몇 초면 끝납니다 | 워커를 띄우는 비용이 더 커서 안 씁니다 |
| 하나뿐인 외부 자원을 쓰는 테스트가 섞였습니다 | 그 테스트만 빼서 한 줄로 돌립니다 |
| 가끔씩만 실패하는 테스트의 원인을 좇습니다 | 한 줄로 돌려 순서를 고정하고 봅니다 |
마지막 줄은 병렬 실행을 끄는 것이 진단 도구도 된다는 뜻입니다. 한 줄로 돌리면 늘 통과하는 테스트가 병렬에서만 실패할 때가 있습니다. 그 테스트는 다른 테스트와 무언가를 함께 쓰고 있습니다.
관련 항목
병렬 실행이 속하는 상위 분류
병렬성 · 병렬 컴퓨팅 · 테스트 자동화
병렬 실행과 맞세워지는 실행 방식
직렬 실행 · 동시성 · 비동기 프로그래밍
병렬 실행을 맡는 도구와 실행 단위
테스트 러너 · 워커 · 프로세스 · 스레드 · 스레드 풀 · 코어 · CPU
병렬 실행이 나눠 돌리는 테스트 묶음
테스트 스위트 · 테스트 케이스 · 단위 테스트 · 통합 테스트 · 종단 간 테스트 · 테스트 피라미드
병렬 실행에서 자주 나는 실패
테스트 순서 의존성 · 경쟁 상태 · 불안정한 테스트 · 공유 상태 · 데드락
병렬 실행을 받쳐 주는 격리 수단
테스트 격리 · 격리 · 테스트 픽스처 · 트랜잭션 · 롤백 · 테스트 더블 · 임시 디렉터리 · 결정성
병렬 실행과 함께 테스트 시간을 줄이는 수단
테스트 샤딩 · 테스트 선택 · 회귀 테스트 선택 · 테스트 영향 분석 · 지속적 통합
워커에 일을 고르게 나누는 원리
다른 이름: parallel execution · 테스트 병렬 실행 · parallel test execution · 병렬 테스트