무슨 일이 있었나요?
Rust는 '안전한 언어'로 유명하지만, 그렇다고 프로그램이 절대 죽지 않는다는 뜻은 아니에요. 메모리 안전성은 보장해 줘도 실행 중에 panic이 터지면 프로그램은 그 자리에서 멈추거든요. 네덜란드의 Rust 전문 회사 Tweede Golf가 블로그에서 호출 그래프(call graph) 분석으로 코드 속에 숨은 panic 경로를 찾는 방법을 소개했어요. 임베디드나 시스템 소프트웨어처럼 '절대 멈추면 안 되는' 코드를 다루는 분이라면 꼭 눈여겨볼 주제예요.
Tweede Golf는 ntpd-rs, sudo-rs처럼 C로 작성된 핵심 시스템 소프트웨어를 Rust로 다시 만드는 프로젝트에 참여해 왔어요. 이 문제를 현장에서 직접 겪어 온 팀인 거죠. 도구의 구체적인 구현 방식과 사용법은 원문에서 확인하세요. 여기서는 이게 왜 어려운 문제인지, 호출 그래프 분석이 어떻게 도움이 되는지 쉽게 풀어볼게요.
panic이 뭐길래?
panic은 Rust가 '더 진행하면 위험한 상황'을 만났을 때 프로그램을 멈추는 비상 브레이크예요. 예를 들면 이런 코드요.
fn get_third(v: &[u8]) -> u8 {
v[2] // 길이가 3보다 짧으면 panic!
}
배열 범위를 벗어난 접근, None에 unwrap() 호출, 0으로 나누기, 디버그 빌드에서의 정수 오버플로까지 전부 panic을 일으켜요. 일반 웹 서버라면 요청 하나 실패하고 끝날 수도 있어요. 하지만 펌웨어나 OS 컴포넌트, 다른 언어에서 불러 쓰는 라이브러리라면 얘기가 완전히 달라요. 심장박동기 펌웨어가 panic으로 멈추면 안 되잖아요.
더 큰 문제는 panic이 눈에 잘 안 띈다는 거예요. 내가 직접 unwrap()을 안 썼어도, 내가 호출한 표준 라이브러리 함수나 외부 크레이트 깊숙한 곳에 panic 가능성이 숨어 있을 수 있거든요.
호출 그래프 분석이란?
호출 그래프는 '어떤 함수가 어떤 함수를 부르는지'를 화살표로 이어 그린 지도예요. 지하철 노선도를 떠올리면 쉬워요. 각 역이 함수고, 노선이 호출 관계인 거죠.
이 지도에서 panic을 찾는 아이디어는 단순해요. 프로그램의 시작점(main이나 공개 API 함수)을 출발역으로, Rust의 panic 처리 함수들을 도착역으로 잡아요. 그리고 출발역에서 도착역까지 가는 길이 있는지 탐색하는 거예요. 길이 있으면 '이 함수에서 시작해 이런 경로를 거치면 panic이 날 수 있다'는 증거가 돼요. 개발자는 그 경로를 따라가며 원인을 찾으면 되고요. 'panic 가능성 있음'이라고 경고만 하는 게 아니라 경로를 보여준다는 게 핵심이에요.
왜 그냥 검색으로는 안 될까?
소스 코드에서 unwrap이나 인덱싱을 검색하면 되지 않냐고 생각할 수도 있어요. 그런데 두 가지 문제가 있어요.
첫째, 너무 많이 걸려요. 아래 코드를 보세요.
fn sum_first_four(v: &[u32]) -> u32 {
if v.len() < 4 { return 0; }
v[0] + v[1] + v[2] + v[3]
}
앞에서 길이를 확인했으니 인덱싱이 실패할 일은 없어요. 컴파일러도 최적화 단계에서 이런 경계 검사를 지워버리는 경우가 많고요. 소스만 보면 panic 후보가 여러 개인데, 최종 바이너리에는 하나도 안 남을 수도 있는 거죠.
둘째, 너무 적게 걸려요. 의존성 깊숙이 숨은 panic은 내 소스만 봐서는 보이지 않거든요.
그래서 분석을 어느 단계에서 하느냐가 중요해요. 소스 코드나 컴파일러 중간 표현을 분석하면 원래 코드와 연결하기는 쉽지만 거짓 경보가 많아요. 최적화가 끝난 결과물을 분석하면 실제로 남은 panic만 정확히 잡지만, 원래 소스와 짝지어 보기가 까다롭고요. 트레이트 객체나 함수 포인터처럼 실행 시점에야 누구를 부를지 정해지는 호출은 그래프로 그리기 어렵다는 점도 이 분야의 오랜 숙제예요.
기존 도구들과 비교
이 문제를 다루는 도구는 이전에도 있었어요.
- clippy 린트:
clippy::unwrap_used,clippy::indexing_slicing,clippy::panic같은 린트를 켜면 소스에서 panic 후보를 경고해 줘요. 가장 쉽게 시작할 수 있지만 내 코드 표면만 봐요. - no-panic 크레이트: 함수에
#[no_panic]을 붙이면, 컴파일러가 panic 가능성을 제거하지 못했을 때 링크 에러를 내요. 강력하지만 panic이 '어디서' 오는지는 알려주지 않고, 최적화 수준에 따라 결과가 달라지기도 해요. - cargo-call-stack: 임베디드 쪽에서 스택 사용량을 분석할 때 쓰는 도구예요. 호출 그래프를 뽑아준다는 점에서 비슷한 계열이에요.
한국 개발자에게 주는 시사점
국내에서도 Rust를 임베디드, 자동차 전장, 블록체인 노드, 고성능 백엔드에 도입하는 곳이 늘고 있어요. 특히 기능 안전 인증이 필요한 분야에서는 'panic이 발생하지 않는다'는 걸 근거와 함께 보여주는 능력이 점점 중요해질 거예요.
당장 해볼 수 있는 건 이래요.
1. 핵심 모듈에 #![deny(clippy::unwrap_used, clippy::expect_used)]를 걸어서 습관부터 바꾸기
2. 인덱싱 대신 .get()처럼 Option을 돌려주는 API 쓰기
3. 정말 중요한 함수에는 no-panic이나 호출 그래프 분석 도구로 검증 단계 추가하기
마무리
한 줄 정리: Rust의 안전함은 메모리까지예요. '죽지 않는 코드'는 panic 경로를 직접 찾아 없앨 때 완성돼요.
여러분은 프로덕션 Rust 코드에서 unwrap()을 어디까지 허용하시나요? 팀만의 panic 관리 규칙이 있다면 공유해 주세요.
🔗 출처: Hacker News