다음에 올 것을 만들어가는 기술에 대한 심층 기사.

서버를 재부팅할 수 없을 때의 소프트웨어 엔지니어링

버그 하나가 수십억 달러 임무를 날리고 재부팅에 몇 시간이 걸리는 우주 소프트웨어, 그 환경에서 코드를 짜는 방법을 살펴봅니다.

지구에서 멀리 떨어진 붉은 행성 궤도를 도는 회로로 뒤덮인 외로운 우주선

오후 3시에 웹 서버가 죽었다고 해봅시다. Kubernetes가 서버를 다시 띄우고, 사용자는 잠깐 에러 페이지를 봅니다. 아무도 잘리지 않죠. 이제 서버가 화성 궤도를 돌고 있다고 상상해 보세요. 재부팅에 45분이 걸리는데, 그 시간 동안 자세 제어도, 열 관리도, 통신도 없습니다. '사용자'는 25억 달러짜리 우주선이고, Kubernetes도 없습니다. 있는 것은 여러분의 코드와 그 코드가 돌아가는 방사선 내성 CPU뿐입니다.

우주 소프트웨어는 일반적인 소프트웨어 엔지니어링이 오히려 느긋해 보일 만큼 빡빡한 제약 아래에서 돌아갑니다. 핫픽스를 배포할 수 없고, SSH로 접속해 로그를 확인할 수도 없으며, 부하가 늘었다고 서버를 추가할 수도 없습니다. 발사 전에 모든 코드가 완벽해야 합니다. 발사 후에는 소프트웨어가 몇 년, 길게는 수십 년 동안 혼자 돌아가야 하고, 방사선에 서서히 손상되는 하드웨어 위에서 수 분의 지연이 있고 초당 킬로비트 수준의 대역폭만 있는 통신 링크로 버텨야 하니까요.

모든 것을 결정하는 제약들

방사선. 우주에서는 고에너지 입자가 전자 장비를 끊임없이 두드립니다. 입자 하나가 메모리의 비트를 뒤집거나(단일 사건 upset, SEU), 레지스터를 망가뜨리거나, 프로세서를 멈춰버릴 수 있습니다. 드문 일이 아닙니다. 저궤도 위성은 하루에도 수천 번씩 비트 플립을 겪습니다. 그래서 우주용 하드웨어는 방사선 내성 부품을 쓰는데, 이 부품들은 느리고 비싸며 소비자용 하드웨어보다 몇 세대나 뒤처져 있습니다. 화성 탐사 로버 퍼서비어런스의 프로세서는 RAD750인데, 대략 200MHz로 돌아가는 1998년형 PowerPC 정도라고 보면 됩니다.

통신 지연. 전파로 화성까지는 궤도 위치에 따라 4~24분이 걸리고, 목성까지는 33~54분이 걸립니다. 단순한 지연 문제가 아닙니다. 지상 관제가 문제를 알아채고 명령을 보내기도 전에, 우주선은 최소한 왕복 시간 동안 스스로 모든 문제를 처리해야 한다는 뜻입니다. 보이저 탐사선이 지구에서 22광시간 이상 떨어진 곳에서 이상을 만났을 때, 소프트웨어는 거의 이틀 동안 스스로 판단해야 했습니다.

물리적 접근 불가. 고장 난 부품을 갈아 끼울 수도, RAM을 추가할 수도, 하드디스크를 교체할 수도 없습니다. 메인 컴퓨터가 죽었는데 백업도 작동하지 않으면 임무는 끝입니다. 모든 실패 모드를 발사 전에 소프트웨어로 예측해 둬야 합니다.

우주 코드는 어떻게 다른가

우주 소프트웨어는 일반적인 소프트웨어 개발에서는 말도 안 될 만큼 과잉 설계로 보일 법한 기법들을 사용합니다.

삼중 모듈러 중복(TMR). 중요한 연산을 서로 독립된 프로세서 세 개에서 세 번 돌립니다. 투표기(voter)가 세 결과를 비교해 다수결 답을 채택합니다. 프로세서 하나가 방사선 때문에 틀린 답을 내도 나머지 두 개가 그 값을 뒤집어 버립니다. 더 확실히 하려고 다섯 벌을 돌리는 시스템(펜타플 중복)도 있습니다.

메모리 스크러빙. 백그라운드 프로세스가 메모리를 계속 읽으면서 오류 정정 코드(ECC)로 검사하고, 단일 비트 오류가 정정 불가능한 다중 비트 오류로 쌓이기 전에 고칩니다. 이 작업은 쉬지 않고 돌아갑니다. 메모리의 모든 바이트가 초당 여러 번 검사되고 정정됩니다.

워치독 타이머. 소프트웨어가 주기적으로 리셋해 줘야 하는 하드웨어 타이머입니다. 소프트웨어가 멈추면(방사선 때문에 락업이 걸렸을 수도 있죠) 타이머가 만료되고 하드웨어 리셋이 걸립니다. 따라서 소프트웨어는 실행의 어느 지점에서든 예기치 않은 재부팅을 견디도록 설계돼야 합니다. 어떤 계산이든 중단되고 다시 시작될 수 있다고 가정해야 하니까요.

// Simplified space software patterns
// Watchdog — must be kicked regularly or hardware resets
void main_loop(void) {
while (1) {
kick_watchdog();  // Reset the timer — we're alive
// All operations must complete within watchdog timeout
read_sensors();
kick_watchdog();
run_attitude_control();
kick_watchdog();
check_thermal_limits();
kick_watchdog();
process_ground_commands();
kick_watchdog();
}
}
// Critical value stored with redundancy
typedef struct {
int32_t value_a;  // Primary copy
int32_t value_b;  // Redundant copy
int32_t value_c;  // Third copy for voting
} RedundantInt;
int32_t read_redundant(RedundantInt *r) {
// Majority vote — tolerates one corrupted copy
if (r->value_a == r->value_b) return r->value_a;
if (r->value_a == r->value_c) return r->value_a;
if (r->value_b == r->value_c) return r->value_b;
// All three differ — flag anomaly
raise_anomaly(MEMORY_CORRUPTION);
return r->value_a;  // Best guess
}

테스트가 곧 제품이다

NASA 제트추진연구소는 우주 소프트웨어 테스트가 전체 개발 노력의 60~80%를 차지한다고 추정합니다. 시간의 60%가 아니라 전체 비용의 60%입니다. 일반 소프트웨어 테스트가 근접하지도 못하는 수준으로 정확성을 증명해야 하기 때문에, 테스트가 개발보다 더 비쌉니다.

모든 코드 경로를 테스트해야 합니다. 웹 개발자들이 말하는 '높은 코드 커버리지'가 아니라, 말 그대로 모든 함수의 모든 경로를 다룹니다. 에러 경로, 타임아웃 경로, 하드웨어 장애를 처리하는 경로까지 포함해서요. 분기 커버리지 100%는 목표가 아니라 출발점입니다.

단위 테스트 너머에서 우주 소프트웨어는 하드웨어 인 더 루프(HIL) 테스트를 거칩니다. 실제 비행 하드웨어에서 실제 소프트웨어를 돌리며 우주 환경을 시뮬레이션하는 방식입니다. 또 몇 달씩 돌리면서 타이밍에 따라 드러나는 버그를 찾는 장기 스트레스 테스트를 하고, 메모리를 일부러 훼손하고, 프로세서를 죽이고, 통신 링크를 끊어서 소프트웨어가 복구하는지 확인하는 결함 주입 테스트도 합니다.

가장 중요한 컴포넌트에는 형식 검증(formal verification)도 점점 더 많이 쓰입니다. 특정 입력에서 코드가 동작하는지 테스트하는 대신, 형식 검증은 가능한 모든 입력에 대해 코드가 동작한다는 것을 수학적으로 증명합니다. 느리고 비싸지만, 우주선의 자세(방향) 제어나 추진을 담당하는 코드라면 그 비용을 감수할 만합니다.

그래도 실패는 일어난다

이렇게 철저한데도 우주 소프트웨어는 실패합니다. 이 실패들은 아무리 신중한 엔지니어링에도 한계가 있다는 것을 보여주기 때문에 배울 점이 많습니다.

  • 화성 기후 궤도선(1999)은 한 팀은 야드파운드법을, 다른 팀은 미터법을 써서 추락했습니다. 소프트웨어는 맞았습니다. 틀린 것은 요구사항이었습니다. 잘못된 명세는 어떤 테스트로도 잡을 수 없습니다.
  • 아리안 5 501 비행(1996)은 64비트 부동소수점이 16비트 정수로 변환되면서 오버플로가 나서 폭발했습니다. 코드는 아리안 4에서 재사용한 것이었고, 아리안 4에서는 그 값이 16비트 범위를 넘은 적이 없었습니다. 아리안 4에서는 맞았던 코드가 아리안 5에서는 치명적으로 틀렸던 겁니다.
  • 화성 극지 착륙선(1999)은 다리를 펼칠 때 발생한 센서 진동을 지면 접촉으로 잘못 해석해서, 고도 40미터에서 엔진을 꺼버린 것으로 보입니다. 타이밍에 의존하는 센서 해석 문제였는데, 테스트가 진동 특성을 완벽하게 재현하지 못해서 이를 잡아내지 못했습니다.
  • 허블 망원경의 초기 거울 결함(1990)은 소프트웨어 버그가 아니었습니다. 잘못 보정된 검사 장비 때문에 거울이 잘못된 모양으로 연마됐습니다. 검사 도구 자체에 버그가 있었던 겁니다.

공통점은 코드가 명세에 비추어 보면 정확했지만, 명세가 현실과 맞지 않았다는 것입니다. 이런 종류의 버그는 모델과 실제 세계 사이의 틈에 존재하기 때문에 막기가 가장 어렵습니다.

지구의 소프트웨어가 배울 점

대부분은 우주 소프트웨어를 만들지 않습니다. 하지만 이 중 일부 관행은 지구에서 신뢰성 높은 시스템을 만들 때 그대로 적용할 수 있습니다.

재시작을 전제로 설계하기. 우주 소프트웨어는 언제든 재부팅될 수 있다고 가정하고, 알려진 정상 상태로 복구되어야 합니다. 웹 서비스도 같은 성질을 가져야 합니다. 서버가 죽었다가 다시 떠도 사람이 개입하지 않고 복구되는가? 멈췄던 지점부터 처리를 재개하는가, 아니면 작업을 잃어버리는가? 우주 엔지니어들에게는 필수인 방어적 엔지니어링 패턴들이 웹 개발에서는 흔히 선택 사항으로 취급되지만, 그럴 이유는 없습니다.

해피 패스만이 아니라 실패 모드를 테스트하기. 우주 소프트웨어 테스트는 실패를 구체적으로 주입합니다. 프로세스를 죽이고, 메모리를 망가뜨리고, 네트워크 연결을 끊고, 모든 시스템 콜에서 에러 코드를 돌려줍니다. 대부분의 웹 애플리케이션 테스트는 기능이 동작하는지에 초점을 맞추지, 시스템이 실패를 우아하게 처리하는지는 잘 보지 않습니다.

중요한 데이터에는 이중화를. 애플리케이션이 다시 만들기 비싼 데이터, 예를 들어 금융 기록, 사용자 콘텐츠, 설정 상태 등을 저장한다면 여러 곳에 중복 저장하고 주기적으로 검증하세요. 데이터베이스 복제가 가장 분명한 예지만, 애플리케이션 차원의 이중화(체크섬, 유효성 검증, 주기적인 무결성 검사)는 복제가 그대로 퍼뜨리는 손상을 잡아냅니다.

중요한 프로세스에는 워치독을. 절대 멈추면 안 되는 프로세스라면 모니터링하세요. '프로세스가 실행 중인가'만 볼 게 아니라 '프로세스가 진행되고 있는가'를 봐야 합니다. 요청을 처리하고, 상태를 갱신하고, 마감 시한을 지키는지 확인하는 헬스 체크는 프로세스 단위 모니터링이 놓치는 실패를 잡아냅니다.

새로운 우주 소프트웨어

상업 우주 산업(SpaceX, Rocket Lab, Planet)은 이런 전통 중 일부에 도전하고 있습니다. SpaceX는 Falcon 9 비행 컴퓨터에 Linux와 C++를 사용하는데, 전통적인 항공우주 기준으로는 이단에 가까운 일입니다. Planet Labs는 수백 기의 소형 위성을 운용하며, 이를 맞춤형 하드웨어라기보다 분산 시스템처럼 다룹니다. 위성 하나가 고장 나면 군집 전체가 그 빈자리를 메우는 방식입니다.

이 변화는 더 넓은 질문을 던집니다. 정말 얼마나 높은 신뢰성이 필요한가? 25억 달러짜리 화성 로버는 5년짜리 테스트를 정당화합니다. 수백 기 중 하나인 50만 달러짜리 통신 위성은 그만큼 필요하지 않습니다. 엔지니어링 관행은 위험 프로필에 맞춰야지, 다른 시대를 위해 만들어진 전통을 맹목적으로 따를 필요는 없습니다.

하지만 예산과 무관하게 핵심 교훈은 남습니다. 배포 후 물리적으로 접근할 수 없는 소프트웨어는, 접근할 수 있는 소프트웨어보다 더 신뢰할 수 있어야 합니다. 우주선을 쏘아 올리든, IoT 기기 군을 배포하든, 엣지 컴퓨팅 네트워크를 운영하든 원칙은 같습니다. SSH로 들어가서 고칠 수 없다면, 소프트웨어가 스스로 감당해야 합니다. 이 태도를 적절히 적용하면 모든 소프트웨어가 더 나아집니다.