더 안전한 프로그램을 위한 제안: no panic(runtime exception)

안태우·2023년 9월 17일

한때 주로 안드로이드를 Java로 프로그래밍하던 시절이 있었습니다. 말만 해도 강산이 변했을 법한 시간이 흘렀음이 느껴지네요. Java에서 Apache의 HttpClient로 API를 콜하고 데이터를 받아오는 것을 짜고 그랬습니다. 이런 코드를 짤 때 여러 Exception(예외)을 만나볼 수 있습니다. IOException이라든가 MalformedURLException 등의 구체적인 에러를 보는 경우가 많았습니다. 아마 기억상으로는 컴파일러가 필수적으로 예외 처리 코드를 짜라고 강요했던 거 같은데, 투덜대면서 e.prineStackTrace()만 대충 넣어줬던 기억이 나네요. 또, 특히 Array/List쪽을 다룰 때에는 툭하면 IndexOutOfBoundsException을 보기도 했고, 잠깐 멍 때리고 코드를 짜면 ArithmeticException를 산술 연산에서 보기도 하였죠.

위의 에러들은 어떤 공통점과 차이점을 가질까요? 공통점으로는 모두 다 Exception으로, try-catch 구문을 통하여 에러를 처리할 수 있다는 점입니다. 차이점으로는 뒤의 IndexOutOfBoundsExceptionArithmeticExceptionRuntimeException으로, 앞의 예외들과 달리 예측할 수 없는 에러라는 것입니다. 각 분류가 checked exception(명시적으로 처리해줘야 하는 에러), unchecked exception(예측할 수 없어 처리를 꼭 요구하지는 않는 에러)라고 볼 수 있습니다.

이전 글에서 밝힌 것처럼 저는 정말로 발생 가능한 모든 에러는 다 처리할 수 있어야 한다고 생각합니다. 그런 방법 중의 하나가 산술 연산 등을 실행할 때 check하여 실행할 수 있는 명령어였고요. 이번 글에서는 이렇게 check하는 명령어를 강요하여, 프로그램 자체가 안전함을 보장할 수 있도록 하는 프로그래밍 방법을 간단하게 제안해보고자 합니다. 사실, 제안이라기보다 이미 있는 방식을 좀 더 도입하고자 주장하는 것에 가깝습니다.

checked operation을 사용하는 것은 좋습니다. 하지만, 내가 짠 코드 부분 말고 다른 부분(타인이 짠 코드나 라이브러리 등)에서는 정말 잘 체크하여 사용하고 있는지 알 방법이 없습니다. 간단하게 이 말로 요약할 수 있습니다: "내가 호출하고자 하는 함수는 panic(runtime exception)이 발생하지 않음을 확인하거나 보장할 수 있을까?"

no panic(runtime exception)

Rust에는 이런 게 있습니다: no-panic. README에도 있는 내용이지만 단 한 줄로 설명 가능합니다. #[no_panic] attribute를 부여함으로써, linking 타임에 이 함수가 절대로 panic을 일으키지 않음을 증명하여 줍니다. 지난 글에서 다뤘던 우박수 예제에 적용해봅시다.

use no_panic::no_panic;

#[no_panic]
fn hailstone(mut n: u32) -> u32 {
    let mut iter = 0;

    loop {
        if n == 1 {
            return iter;
        }

        if n % 2 == 0 {
            n /= 2;
        } else {
            n = n * 3 + 1;
        }

        iter += 1;
    }
}

#[no_panic]
fn checked_hailstone(mut n: u32) -> Option<u32> {
    let mut iter = 0;

    loop {
        if n == 1 {
            return Some(iter);
        }

        if n.checked_rem(2)? == 0 {
            n = n.checked_div(2)?;
        } else {
            n = n.checked_mul(3)?.checked_add(1)?;
        }

        iter = iter.checked_add(1)?;
    }
}

fn main() {
    let n = 837799;
    println!("{} {:?}", hailstone(n), checked_hailstone(n));
}

debug 모드로 빌드하면 다른 데에서도 debug_assert가 엄청나게 많이 들어가있는지 false positive는 아니더라도, 저희가 원하는 결과를 얻을 수 없으므로 아래와 같이 Cargo.toml에 release 모드에서도 integer overflow를 체크할 수 있도록 하고 cargo build --release로 확인해봅시다.

[profile.release]
overflow-checks = true
$ cargo build --release
   Compiling ...
error: linking with `cc` failed: exit status: 1
  |
  = note: ...
  = note: Undefined symbols for architecture arm64:
            "_
          
          ERROR[no-panic]: detected panic in function `hailstone`
          ", referenced from:
                core::ptr::drop_in_place$LT$rust_playground..hailstone..__NoPanic$GT$::h71f76d8b495c149a in rust_playground-774837751d2d30da.rust_playground.ce4fbfa9-cgu.1.rcgu.o
          ld: symbol(s) not found for architecture arm64
          clang: error: linker command failed with exit code 1 (use -v to see invocation)
          

error: could not compile ... due to previous error

이런 요상한 에러가 나면서(너무 번잡스럽지만 linker 레벨에서 체크하게 하는 것이라 어쩔 수 없는 거 같긴 합니다) hailstone 함수가 panic이 일어날 수 있다고 보여줍니다. 반면에, checked operation으로 도배한 checked_hailstone은 에러가 나지 않음을 확인할 수 있습니다. 이렇게 컴파일 타임에서 이 함수는 내부에서 unsafe한 시도를 하지 않으면 절대로 프로그램이 panic으로 죽는 일이 없음을 보일 수 있습니다.

좀 더 개선된 no panic 활용

위와 같이 단순히 #[no_panic]을 막 붙여서 사용할 수 있을 것입니다. 다만, panic이 있는지를 체크하는데에도 비용이 들어가기 때문에 no-panic Github issue에서는 다음과 같이 wrapper 함수를 따로 정의하여 쓰는 것을 추천하기도 합니다

#[inline(never)]
#[no_panic]
#[cfg(test)]
fn wrapper() {
    ...
}

절대 inline하지 않아서 함수의 정보가 날라가는 불상사를 막아주고, test할 때만 체크하도록 하여 낭비를 줄여줍니다.

실제로 Rust의 libm에서도 비슷한 방식으로 사용하고 있는 것으로 보입니다. https://github.com/rust-lang/libm/blob/master/src/math/acos.rs#L62

정리

코드에 열심히 check operation을 사용하여 panic을 막아볼 수 있습니다. 그러나 타인이 작성한 코드나 라이브러리에서는 그것이 정말 panic을 안 일으키는지 확인할 방법이 없을 뿐더러, 코드를 짜는 자신이 실수할 수도 있기 때문에 이럴 때 활용할 수 있는 no-panic을 간단하게 소개해봤습니다. 이러한 것에 일일히 신경 쓰는 것이 곧 생산성 저하나 과도한 집착으로 보여질 수도 있으나, 믿을 것 없는 세상에서 내 코드라도 믿어보려면 이런 노력을 해보는 게 좋지 않을까 싶습니다.

profile
Rust 삽질러 / 동시성 프로그래밍을 주로 공부합니다.

0개의 댓글