[CS300 #298] 소프트웨어 장애의 사회적 영향 사례 — 작은 버그가 사람에게 닿을 때
컴퓨터공학 300 주제 시리즈의 298번째 글이다. 전체 지도는 여기.
한 줄 요약
소프트웨어 장애의 원인은 대개 평범하다. 숫자 변환, 경계 검사 누락, 재사용한 가정, 부족한 시험. 하지만 그 소프트웨어가 로켓, 의료기기, 공항, 사법 절차에 쓰이면 결과는 평범하지 않다. 사례에서 배울 것은 “누가 실수했나”가 아니라 “어떤 구조가 실수를 사고로 키웠나”다.
왜 필요한가
개발자가 작성한 코드는 결국 사람의 삶에 닿는다. 서비스가 멈추면 누군가는 비행기를 놓치고, 병원 진료가 밀리고, 월급이 늦게 들어온다. 회계 시스템이 틀린 숫자를 내면 누군가는 횡령범이 된다.
대표 사례들을 기술적 원인, 사회적 영향, 구조적 교훈으로 나눠 보면 반복되는 패턴이 보인다. 그 패턴은 내일 내가 짜는 코드와 운영하는 시스템에도 그대로 적용된다.
핵심 개념
사례 1: 아리안 5 501편 (1996)
- 무슨 일: 1996년 6월 4일, 유럽 아리안 5 로켓의 첫 비행이 이륙 약 40초 만에 경로를 벗어나 파괴됐다. ESA 와 CNES 의 조사위원회는 관성 기준 장치(SRI) 소프트웨어의 명세·설계 오류로 유도·자세 정보가 완전히 사라진 것을 원인으로 결론 내렸다.
- 기술적 원인: 조사 보고서에 따르면, 아리안 4 에서 가져온 정렬(alignment) 기능이 이륙 후에도 동작하며 64비트 부동소수점 값을 16비트 부호 정수로 변환했다. 아리안 5 의 더 큰 수평 속도에서 값이 범위를 넘었고, 예외 처리 없이 프로세서가 정지했다. 예비 장치도 같은 소프트웨어라 같은 방식으로 멈췄다.
- 구조적 교훈: 이전 시스템에서 검증된 코드라도 새 환경에서는 가정이 깨진다. 이중화가 같은 결함을 공유하면 이중화가 아니다. 그리고 이륙 후 필요 없는 기능이 계속 돌고 있었다.
사례 2: Therac-25 (1985–1987)
- 무슨 일: 방사선 치료기 Therac-25 가 여러 환자에게 과다 방사선을 조사해 사망과 중상을 일으켰다.
- 기술적 원인: Leveson 과 Turner 의 분석에 따르면, 이전 모델의 하드웨어 안전 연동 장치를 소프트웨어 검사로 대체했고, 운영자가 빠르게 입력을 수정할 때 생기는 경쟁 조건(race condition)과 카운터 오버플로 같은 결함이 있었다. 오류 메시지는 모호했고 운영자는 이를 무시하고 진행할 수 있었다.
- 구조적 교훈: 안전을 소프트웨어 하나에만 맡기지 않는다. 사용자 인터페이스의 모호한 오류 메시지는 안전 문제다. 사고 보고가 들어왔을 때 제조사가 “불가능하다”고 대응한 것이 피해를 키웠다.
사례 3: 패트리어트 미사일 시계 오차 (1991)
- 무슨 일: 걸프전 중 사우디아라비아 다란에서 패트리어트 방공 시스템이 날아오는 스커드 미사일을 요격하지 못했고, 미군 병사들이 숨졌다.
- 기술적 원인: 미국 회계감사원(GAO) 보고서 IMTEC-92-26 에 따르면, 시스템은 0.1초 단위 시간을 24비트 고정소수점으로 다뤘고 0.1 을 이진수로 정확히 표현할 수 없어 작은 오차가 생겼다. 시스템이 오래 가동될수록 오차가 누적되어 추적 범위 계산이 어긋났다. 장시간 가동 시 재시작하라는 권고와 수정 소프트웨어가 있었지만 현장에 제때 반영되지 않았다.
- 구조적 교훈: 설계 시 가정한 운용 조건(짧은 가동 시간)을 실제 운용이 벗어날 수 있다. 수정은 배포되어야 수정이다.
사례 4: 나이트 캐피털 (2012)
- 무슨 일: 미국 증권사 나이트 캐피털이 새 주문 소프트웨어 배포 후 약 45분 동안 의도하지 않은 대량 주문을 냈고, 4억 6천만 달러 이상의 손실을 입었다. 미국 증권거래위원회(SEC)의 2013년 행정 명령(Release No. 34-70694)에 경위가 정리되어 있다.
- 기술적 원인: 8대 서버 중 1대에 새 코드가 배포되지 않았다. 새 기능이 재사용한 플래그가 그 서버에서는 오래전 사용 중단된 기능을 깨웠다. 배포 검증 절차와 이상 상황에서의 즉시 차단 장치가 부족했다.
- 구조적 교훈: 수동 배포는 언젠가 한 대를 빠뜨린다. 죽은 코드는 지운다. 플래그 이름을 재사용하지 않는다. 자동화된 비상 정지가 필요하다.
사례 5: 영국 우체국 Horizon (1999년경부터)
- 무슨 일: 영국 우체국의 회계 시스템 Horizon 이 지점 장부에 잔액 부족을 표시했고, 우체국은 이를 근거로 많은 지점 운영자를 횡령 등으로 기소했다. 영국 고등법원은 2019년 Bates v Post Office 판결에서 Horizon 에 계좌 불일치를 일으킬 수 있는 버그와 오류가 있었다고 판단했다. 이후 공적 조사(Post Office Horizon IT Inquiry)가 진행됐다.
- 기술적 원인: 시스템 결함 자체보다 더 큰 문제는 “컴퓨터 기록은 맞다”는 전제였다. 운영자는 시스템 데이터에 접근해 반박할 수단이 없었다.
- 구조적 교훈: 소프트웨어 출력이 사람에게 불리한 결정의 증거로 쓰이면, 그 출력에 이의를 제기하고 검증할 수 있는 경로가 있어야 한다. 이것은 AI 시스템의 책임성 논의와 같은 문제다.
사례 6: CrowdStrike 채널 파일 291 (2024)
- 무슨 일: 2024년 7월 19일, 보안 제품 CrowdStrike Falcon 의 콘텐츠 업데이트가 수많은 Windows 시스템을 충돌시켜 항공, 병원, 방송, 금융 등에서 대규모 장애가 났다.
- 기술적 원인: CrowdStrike 의 근본 원인 분석(RCA)에 따르면, 새 IPC 템플릿 유형은 21개 입력 필드를 정의했지만 센서의 통합 코드는 20개만 제공했다. 21번째 필드를 실제로 쓰는 템플릿 인스턴스가 배포되자 콘텐츠 해석기가 범위 밖 메모리를 읽었다. 검증기도 이 불일치를 잡지 못했고, 해당 콘텐츠는 단계적 배포 없이 전체에 동시 배포됐다.
- 구조적 교훈: 코드가 아닌 “설정·데이터 업데이트”도 코드만큼 위험하다. 커널 수준에서 도는 소프트웨어는 경계 검사가 필수다. 카나리 배포와 단계적 확산은 모든 변경에 적용한다. CrowdStrike 도 이후 콘텐츠의 단계적 배포를 도입하겠다고 밝혔다.
공통 패턴
| 패턴 | 사례 |
|---|---|
| 재사용 코드의 숨은 가정 | 아리안 5, 나이트 캐피털 |
| 수치 표현·경계 문제 | 아리안 5(변환), 패트리어트(누적 오차), CrowdStrike(범위 밖 읽기) |
| 안전장치 제거·부재 | Therac-25(하드웨어 연동), 나이트 캐피털(비상 정지) |
| 일괄 배포 | CrowdStrike, 나이트 캐피털 |
| 사람의 이의 제기 차단 | Horizon, Therac-25 |
Nancy Leveson 은 사고를 개별 부품의 고장이 아니라 시스템의 제어 구조가 안전 제약을 강제하지 못한 결과로 보아야 한다고 주장한다. 버그를 고치는 것만으로는 다음 사고를 막지 못한다. 그 버그가 사고로 이어지게 한 구조를 고쳐야 한다.
직접 해 보기
두 사례의 수치 문제를 Python 으로 재현한다. 실제 시스템을 단순화한 모델이다.
import math, struct
# 1) 패트리어트 유형: 0.1 초를 이진 고정소수점(소수부 23비트)으로 잘라 저장
FRAC_BITS = 23
tick = math.floor(0.1 * 2**FRAC_BITS) / 2**FRAC_BITS
err_per_tick = 0.1 - tick
print(f"저장된 0.1 = {tick:.10f}, 틱당 오차 = {err_per_tick:.2e} 초")
for hours in (1, 8, 20, 100):
ticks = hours * 3600 * 10
print(f"{hours:4d}시간 가동 후 누적 오차: {ticks * err_per_tick:.4f} 초")
# 2) 아리안 5 유형: 64비트 실수를 16비트 부호 정수로 변환
def to_int16(x):
return struct.pack(">h", int(x)) # 범위(-32768~32767)를 넘으면 예외
for value in (1234.5, 32767.0, 40000.0):
try:
to_int16(value)
print(f"{value:>9}: 변환 성공")
except struct.error as e:
print(f"{value:>9}: 변환 실패 -> {e}")
실행 결과다.
저장된 0.1 = 0.0999999046, 틱당 오차 = 9.54e-08 초
1시간 가동 후 누적 오차: 0.0034 초
8시간 가동 후 누적 오차: 0.0275 초
20시간 가동 후 누적 오차: 0.0687 초
100시간 가동 후 누적 오차: 0.3433 초
1234.5: 변환 성공
32767.0: 변환 성공
40000.0: 변환 실패 -> 'h' format requires -32768 <= number <= 32767
틱당 1억분의 1초 수준의 오차가 100시간이면 0.34초가 된다. 이 값은 GAO 보고서가 제시한 약 0.34초와 같은 크기다. 빠르게 움직이는 물체를 추적하는 시스템에서는 큰 거리 차이가 된다.
16비트 변환은 범위를 넘으면 Python 에서는 예외로 드러난다. 언어와 설정에 따라 조용히 값이 감기거나(wrap around), 아리안 5 처럼 처리되지 않은 예외로 프로세서가 멈춘다. 어느 쪽이든 “이 값은 이 범위를 넘지 않는다”는 가정이 코드 어딘가에 숨어 있다는 뜻이다.
현업에서는
- 설정·규칙·피처 플래그 변경도 코드처럼 리뷰하고, 카나리로 일부에 먼저 적용하고, 지표를 보고 확산한다. 쿠버네티스의 롤링 업데이트와
maxUnavailable설정은 “한 번에 전부 바꾸지 않는다”를 구현한 것이다. 홈랩 k3s 처럼 작은 클러스터에서도 모든 노드를 동시에 업그레이드하지 않는 습관이 같은 원리다. - 경계 검사와 입력 검증을 신뢰 경계마다 둔다. “상위 시스템이 이미 검증했다”는 가정이 CrowdStrike 사례의 핵심이었다.
- 장시간 가동 시 누적되는 문제(시계 오차, 메모리 누수, 카운터 오버플로)는 짧은 테스트로는 드러나지 않는다. 장시간 시험(soak test)을 둔다.
- 시스템 출력으로 사람을 판단하는 기능에는 감사 로그, 원천 데이터 접근, 이의 제기 절차를 함께 설계한다.
- 회고는 비난 없이 쓰고, “누가”보다 “어떤 구조가”를 묻는다.
확인 문제
- 아리안 5 사례에서 이중화가 사고를 막지 못한 이유는?
- 패트리어트 사례에서 오차가 가동 시간에 비례해 커진 이유는?
- 나이트 캐피털 사례가 주는 배포 관련 교훈 두 가지는?
- Horizon 사례가 기술적 결함 외에 보여 주는 문제는?
- CrowdStrike 사례에서 “데이터 업데이트”가 위험했던 이유는?
풀이
- 주 장치와 예비 장치가 같은 소프트웨어를 써서 같은 입력에 같은 방식으로 실패했기 때문이다.
- 0.1 의 이진 표현 오차가 0.1초마다 한 번씩 더해져 누적되었기 때문이다.
- 수동 배포는 일부 서버를 빠뜨릴 수 있으니 자동화·검증하라. 죽은 코드와 플래그 재사용은 예상 못 한 동작을 깨운다. 이상 시 즉시 멈출 장치가 필요하다(중 둘).
- 시스템 출력이 무조건 옳다는 전제로 사람을 기소했고, 당사자가 데이터를 검증하거나 이의를 제기할 수단이 없었다.
- 코드보다 가볍게 취급되어 단계적 배포 없이 전체에 동시 배포됐고, 커널 수준 구성요소가 그 데이터를 경계 검사 없이 읽었다.
더 읽을거리 (References)
- ESA, Ariane 501 - Presentation of Inquiry Board report, 1996.
- CrowdStrike, Channel File 291 Incident Root Cause Analysis, 2024.
- Post Office Horizon IT Inquiry
- N. G. Leveson, C. S. Turner, “An Investigation of the Therac-25 Accidents,” IEEE Computer, 26(7), 18–41, 1993. / U.S. GAO, Patriot Missile Defense: Software Problem Led to System Failure at Dhahran, Saudi Arabia, IMTEC-92-26, 1992. / SEC, In the Matter of Knight Capital Americas LLC, Release No. 34-70694, 2013.