간헐적 실패
고친 사람 github-actions[bot]
간헐적 실패는 같은 코드를 똑같이 돌려도 나타났다 사라졌다 하는 실패입니다. 다시 돌리면 대개 사라져서 원인을 붙잡기 어렵습니다. 테스트가 가끔 떨어질 때도 서비스 요청이 가끔 실패할 때도 이 이름으로 부릅니다. 원인은 테스트 코드, 제품 코드, 네트워크가 잠깐 끊기는 것 같은 실행 환경 중 한 곳에 숨습니다.
쉽고 빠른 이해
간헐적 실패는 똑같이 돌렸는데 어떤 때만 나는 실패입니다. 테스트를 백 번 돌리면 아흔아홉 번 통과하고 한 번 떨어지는 식입니다.
늘 나는 실패는 다시 돌려 보면서 원인을 찾으면 됩니다. 간헐적 실패는 다시 돌리면 대개 통과해서 원인을 볼 기회가 드뭅니다. 그래서 「어쩌다 한 번」으로 넘기기 쉽습니다.
어떻게 생기나:
- 결과가 코드와 입력 말고 다른 것에 기댑니다. 스레드가 도는 순서나 네트워크가 그렇습니다
- 그것이 돌릴 때마다 조금씩 달라집니다
- 어쩌다 한 번 그 차이가 결과를 바꿀 만큼 커지면 실패합니다
무엇이 나빠지나 — 다시 돌려 통과하는 것만 보고 넘기면 제품 코드에 있던 진짜 결함까지 같이 넘어갑니다. 고쳤는지 확인할 때도 몇 번 돌려서는 알 수 없습니다.
테스트에서도 운영 중인 서비스에서도 쓰는 말입니다. 실패가 가끔만 나는 것을 본 순간부터 이렇게 부를 수 있습니다. 다만 원인을 가려내기 전에는 테스트 자체가 흔들린다는 뜻의 「불안정한 테스트」로 부르지 않습니다. 다시 돌려 통과시키는 것으로 덮지도 않습니다.
상세
테스트는 코드를 돌려 보고 결과가 기대와 같은지 확인하는 코드입니다. 믿을 만한 테스트는 같은 코드에 언제나 같은 판정을 냅니다. 같은 입력에 늘 같은 결과를 내는 성질을 결정성이라 합니다.
간헐적 실패는 이 결정성이 깨졌을 때 보이는 실패입니다. 코드도 입력도 그대로인데 어떤 실행은 통과하고 어떤 실행은 실패합니다. 영어로는 intermittent failure 라 부릅니다.
이 절은 재고를 하나 빼는 코드 하나로 간헐적 실패를 따라갑니다. 원인이 테스트가 아니라 제품 코드에 숨어 있는 경우입니다. 그다음 원인이 숨는 곳을 셋으로 나누고, 원인을 좁히는 법과 고쳤는지 확인하는 법을 봅니다.
늘 나는 실패와의 차이
늘 나는 실패는 다루기 쉽습니다. 같은 테스트를 다시 돌리면 같은 곳에서 또 실패합니다. 실패하는 순간을 원하는 만큼 다시 볼 수 있습니다.
버그를 찾아 고치는 일을 디버깅이라 합니다. 디버깅은 대개 실패를 다시 만드는 데서 시작합니다. 다시 만들 수 있어야 변수 값을 찍어 보고 한 줄씩 따라가 볼 수 있기 때문입니다.
간헐적 실패는 이 첫걸음이 막힙니다. 다시 돌리면 대개 통과합니다. 실패한 그 실행이 남긴 흔적만 들고 원인을 짐작해야 합니다.
고쳤는지 확인하는 일도 어렵습니다. 원래 드물게 나던 실패라면 고친 뒤 열 번 다 통과해도 고쳐졌다고 말할 수 없습니다. 몇 번을 돌려야 하는지는 뒤의 「고쳤는지 확인하는 횟수」에서 셉니다.
가끔 실패하는 재고 차감
쇼핑몰에서 주문이 들어오면 재고를 하나 빼는 코드가 있다고 합시다. 재고가 남아 있을 때만 뺍니다.
스레드는 한 프로그램 안에서 따로 나아가는 실행 흐름입니다. 서버는 요청마다 스레드 하나를 맡겨 여러 요청을 동시에 처리합니다. 마지막 재고 하나에 주문 요청 A 와 B 가 거의 동시에 들어와 서로 다른 스레드에서 이 코드를 돈다고 합시다.
if (stock > 0) { // 남아 있으면
stock = stock - 1; // 하나 뺀다
}
테스트는 재고를 1로 두고 두 스레드가 동시에 이 코드를 부르게 합니다. 그리고 재고가 0 인지 확인합니다.
대부분의 실행에서는 A 가 다 끝난 뒤에 B 가 재고를 봅니다. B 는 재고 0 을 보고 아무것도 안 합니다. 테스트는 통과합니다.
드물게 두 스레드가 거의 같은 순간에 조건을 봅니다.
sequenceDiagram
participant A as 요청 A
participant S as 재고
participant B as 요청 B
A->>S: 재고를 본다 · 1
B->>S: 재고를 본다 · 1
Note over A,B: 둘 다 남아 있다고 판단한다
A->>S: 하나 빼서 0 을 쓴다
B->>S: 하나 빼서 -1 을 쓴다
A 가 재고를 보고 빼기 전에 B 도 재고를 봅니다. 둘 다 재고가 1 이라고 판단하고 둘 다 뺍니다. 재고는 -1 이 되고 테스트는 실패합니다.
결과가 스레드들이 도는 순서에 따라 달라지는 상태를 경쟁 상태라 합니다. 이 실패는 테스트가 틀려서 난 것이 아닙니다. 마지막 재고 하나에 주문 둘이 동시에 오면 운영 중인 서비스에서도 똑같이 재고가 -1 이 됩니다.
가끔만 나는 까닭
간헐적 실패는 세 조건이 모두 갖춰질 때 생깁니다.
- 결과가 코드와 입력 말고 다른 요인에 기댑니다
- 그 요인을 코드가 정하지 않아서 실행마다 달라집니다
- 어쩌다 한 번 그 차이가 결과를 바꿀 만큼 커집니다
재고 차감에 대 보면 이렇습니다. 결과는 두 스레드가 도는 순서에 기댑니다. 그 순서는 코드가 아니라 운영체제가 그때그때 정합니다. 차이가 결과를 바꾸는 것은 A 가 조건을 보고 빼기까지의 짧은 틈에 B 가 끼어들 때입니다.
그 틈은 아주 짧습니다. 그래서 대부분의 실행에서 B 는 틈을 비껴갑니다. B 가 그 틈에 끼어드는 실행은 가끔만 있습니다. 개발자가 몇 번 돌려 봐서는 실패를 못 보는 까닭이 이것입니다.
실패는 대개 지속적 통합 서버에서 먼저 드러납니다. 지속적 통합(CI, Continuous Integration)은 커밋이 올라올 때마다 빌드와 테스트를 자동으로 돌리는 방식입니다. 여러 사람이 하루에도 여러 번 돌리니 드문 실행도 결국 한 번은 나옵니다.
원인이 숨는 세 곳
간헐적 실패를 보면 먼저 원인이 어디 있는지를 가려야 합니다. 원인은 대개 세 곳 중 하나에 있습니다.
| 원인이 있는 곳 | 예 | 붙는 이름 |
|---|---|---|
| 테스트 코드 | 백그라운드 작업이 끝나기를 고정된 시간만 기다리고 확인한다 | 불안정한 테스트 |
| 제품 코드 | 두 스레드가 같은 값을 동시에 고친다 | 경쟁 상태 |
| 실행 환경 | 네트워크가 잠깐 끊긴다 · 디스크가 잠깐 가득 찬다 | 일시적 장애 |
원인이 테스트 코드에 있으면 그 테스트를 불안정한 테스트라 부릅니다. 제품 코드는 멀쩡합니다. 테스트를 고치면 실패도 사라집니다.
원인이 제품 코드에 있으면 이야기가 다릅니다. 재고 차감처럼 테스트는 제 일을 했습니다. 드물게 나는 결함을 드물게 잡았을 뿐입니다. 이 결함은 사용자에게도 드물게 나타납니다.
여러 실행 흐름이 같은 때에 도는 것을 동시성이라 합니다. 경쟁 상태는 동시성 코드에서 나는 대표 결함입니다. 흐름끼리 도는 순서가 실행마다 달라서 이런 결함은 대개 가끔만 드러납니다.
원인이 실행 환경에 있으면 코드는 테스트도 제품도 멀쩡할 수 있습니다. 바깥 세상이 잠깐 흔들린 것입니다. 이렇게 잠깐 났다가 저절로 사라지는 장애를 일시적 장애라 부릅니다.
불안정한 테스트와의 경계
간헐적 실패와 불안정한 테스트는 자주 같은 말처럼 쓰입니다. 둘은 가리키는 것이 다릅니다. 간헐적 실패는 눈에 보이는 증상입니다. 불안정한 테스트는 그 원인이 테스트에 있다는 판정입니다.
| 무엇을 말하나 | 언제 쓸 수 있나 | |
|---|---|---|
| 간헐적 실패 | 실패가 가끔만 난다 | 실패를 본 순간 |
| 불안정한 테스트 | 테스트 자체가 흔들린다 | 원인이 테스트에 있다고 가려낸 뒤 |
이 구분이 무너지면 위험해집니다. 가끔 떨어지는 테스트를 모두 불안정한 테스트로 부르면 원인을 찾지 않고 넘기게 됩니다. 재고 차감 같은 제품 결함도 그렇게 함께 넘어갑니다.
운영 중인 서비스에서
운영 중인 서비스에서도 간헐적 실패라는 말을 씁니다. 요청 천 개 중 몇 개만 실패하고 나머지는 멀쩡한 경우입니다. 사용자는 「다시 누르니 됐다」고 말합니다.
원인은 테스트 코드를 빼면 테스트 때와 같은 두 곳입니다. 제품 코드의 결함일 수도 있고 실행 환경이 잠깐 흔들린 것일 수도 있습니다. 재고 차감의 경쟁 상태도 운영에서는 「가끔 재고가 -1 이 된다」는 간헐적 실패로 나타납니다.
서버에서 흔한 환경 원인은 몇 가지로 모입니다.
| 원인 | 실패가 가끔만 나는 까닭 |
|---|---|
| 데이터베이스 연결을 돌려 쓰는 커넥션 풀이 잠깐 바닥난다 | 요청이 몰리는 순간에만 빈 연결이 없다 |
| 안 쓰는 메모리를 거두는 가비지 컬렉션이 프로그램을 잠깐 멈춘다 | 멈춘 순간에 걸린 요청만 늦어진다 |
| 네트워크가 잠깐 끊긴다 | 끊긴 순간에 오간 요청만 잃는다 |
| 상대 서버가 늦게 답한다 | 정해 둔 시간을 넘긴 응답만 타임아웃으로 실패한다 |
넷 모두 평소에는 넉넉하다가 어느 순간에만 모자라거나 늦어집니다. 앞에서 본 셋째 조건이 서버에서도 성립합니다. 어쩌다 한 번 차이가 결과를 바꿀 만큼 커진다는 조건입니다.
원인을 좁히는 법
간헐적 실패를 좁히는 일은 「드문 실행을 자주 나게 만들기」에서 시작합니다. 자주 나야 원인을 볼 수 있기 때문입니다.
먼저 같은 코드를 여러 번 돌려 얼마나 자주 실패하는지 셉니다. 백 번에 한 번인지 열 번에 한 번인지를 알아야 뒤에서 고쳤는지 판단할 수 있습니다.
실패한 실행의 흔적을 모읍니다. 그 실행의 로그, 돌린 기계, 시각, 같이 돈 테스트가 무엇이었는지를 남겨 둡니다. 실패한 실행끼리 겹치는 것이 원인을 가리킵니다.
흔드는 요인을 하나씩 바꿔 봅니다. 먼저 함께 도는 테스트를 바꿉니다. 혼자 돌리면 통과하는데 다른 테스트와 함께 돌리면 실패한다면 앞 테스트가 남긴 데이터를 의심합니다. 이렇게 결과가 앞서 돈 테스트에 따라 달라지는 것을 테스트 순서 의존성이라 부릅니다.
다음으로 기계에 부하를 겁니다. 기계를 일부러 붐비게 해서 실패가 늘면 걸린 시간이나 스레드 순서를 의심합니다. 기계가 붐빌수록 스레드가 도는 순서와 걸리는 시간이 더 크게 흔들리기 때문입니다.
틈을 일부러 넓히기도 합니다. 재고 차감이라면 조건을 본 줄과 빼는 줄 사이에 잠깐 멈추는 코드를 넣어 봅니다. 백 번에 한 번 나던 실패가 매번 나면 원인을 찾은 것입니다.
거꾸로 들여다보려고 넣은 코드가 실패를 감추기도 합니다. 로그 한 줄을 찍는 데도 시간이 듭니다. 그 시간이 스레드 순서를 바꿔 실패가 사라집니다. 이렇게 들여다보면 사라지는 버그를 하이젠버그라 부릅니다.
고쳤는지 확인하는 횟수
고친 뒤에는 실패가 정말 사라졌는지 확인해야 합니다. 드문 실패는 몇 번 통과한 것으로는 확인이 안 됩니다.
백 번에 한 번 실패하던 테스트가 있다고 합시다. 한 번 돌려 통과할 확률은 0.99 입니다. 못 고쳤는데도 N번을 연달아 통과할 확률은 0.99를 N번 곱한 값입니다.
| 연달아 돌린 횟수 N | 못 고쳤는데도 전부 통과할 확률 |
|---|---|
| 10 | 약 0.90 |
| 100 | 약 0.37 |
| 300 | 약 0.05 |
못 고친 채로 열 번을 돌려도 열에 아홉은 전부 통과합니다. 열 번 통과로는 고친 것과 못 고친 것이 거의 안 갈립니다. 못 고친 테스트가 전부 통과해 버리는 일을 스무 번에 한 번 꼴로 줄이려면 삼백 번은 돌려야 합니다.
실패가 드물수록 확인에 드는 횟수는 더 늘어납니다. 그래서 앞에서 실패가 얼마나 자주 나는지를 먼저 세어 두는 것입니다.
재시도가 가리는 결함
간헐적 실패를 다루는 가장 손쉬운 방법은 실패하면 다시 돌리는 것입니다. 실패한 작업을 자동으로 다시 시도하는 장치를 재시도라 합니다.
원인이 일시적 장애라면 재시도는 맞는 처방입니다. 네트워크가 잠깐 끊겼다면 조금 뒤에 다시 보내면 됩니다. 이때는 다시 시도할 때마다 기다리는 시간을 늘려 상대가 회복할 틈을 줍니다. 이 방식을 지수 백오프라 합니다.
원인이 코드 결함이라면 재시도는 결함을 가리기만 합니다. 재고 차감 테스트를 재시도로 통과시키면 테스트 기록은 깨끗해집니다. 재고가 -1 이 되는 결함은 그대로 남아 운영으로 나갑니다.
그래서 재시도를 걸기 전에 원인이 어디 있는지를 먼저 가립니다. 재시도는 원인이 실행 환경에 있다고 가려낸 실패에만 겁니다.
관련 항목
간헐적 실패와 헷갈리는 이웃
불안정한 테스트 · 깨지기 쉬운 테스트 · 비결정적 동작 · 일시적 장애
간헐적 실패 뒤에 숨는 결함
경쟁 상태 · 데이터 경쟁 · 데드락 · 라이브락 · 테스트 순서 의존성 · 하이젠버그 · 메모리 누수
간헐적 실패를 일으키는 실행 환경 요인
스레드 · 네트워크 · 타임아웃 · 커넥션 풀 · 가비지 컬렉션 · 벽시계 · 난수 생성기
간헐적 실패가 드러나는 테스트 종류
단위 테스트 · 통합 테스트 · 종단 간 테스트 · 스트레스 테스트 · 테스트 스위트 · 지속적 통합
간헐적 실패를 좁히고 다루는 수단
재시도 · 지수 백오프 · 로그 · 분산 추적 · 시드 · 테스트 격리 · 락 · 원자성 · 카오스 엔지니어링
간헐적 실패와 맞세워지는 성질
간헐적 실패가 속하는 상위 분류
다른 이름: intermittent failure · intermittent test failure · 간헐적 장애 · 간헐 실패