Skip to main content

11. 테스트, 디버깅, Clippy로 좋은 코드 만들기

Rust를 배우다 보면 어느 순간 이런 생각이 듭니다. "와, 드디어 컴파일된다!" 그리고 바로 다음 순간, 더 중요한 질문이 따라옵니다. "그래서 이 코드, 정말 믿어도 될까?"

Rust 컴파일러는 아주 강력합니다. 타입 오류를 잡아주고, 소유권 실수를 막아주고, 위험한 코드를 일찍 멈춰 세웁니다. 하지만 컴파일러가 모든 것을 대신해주지는 않습니다. 내가 의도한 동작이 맞는지, 수정 후에도 기존 기능이 안 깨지는지, 코드가 읽기 좋은지, 더 Rust답게 쓸 수 있는지는 결국 개발 습관의 영역입니다.1

이 장에서는 그 습관을 만드는 핵심 도구들을 배웁니다. 단위 테스트와 통합 테스트로 동작을 검증하고, assert! 계열 매크로로 기대값을 분명히 적고, dbg!와 백트레이스로 문제를 추적하고, rustfmt와 Clippy로 코드의 모양과 품질을 다듬는 흐름입니다.

핵심 메시지는 단순합니다. Rust는 강한 언어이지만, 좋은 개발 습관까지 자동으로 대신해주지는 않는다.

테스트, 디버깅, Clippy로 좋은 코드 만들기

테스트는 보험이 아니라 설계 도구다

초보자는 테스트를 나중에 붙이는 추가 작업처럼 느끼기 쉽습니다. 하지만 실제로 테스트는 디버깅 비용을 줄여주는 가장 싼 도구입니다. 함수 하나를 만들고 테스트 하나를 붙이면, 그 함수가 무엇을 보장해야 하는지가 또렷해집니다.

예를 들어 parse_age()라는 함수가 있다고 해봅시다. 테스트가 없다면 "나이를 파싱하겠지" 정도로만 이해할 수 있습니다. 하지만 테스트가 있으면 빈 문자열은 오류인지, 공백은 허용하는지, 음수는 어떻게 처리하는지가 명확해집니다. 즉, 테스트는 버그를 찾는 도구이면서 동시에 함수의 사용 설명서 역할도 합니다.

Rust에서는 테스트를 크게 두 가지로 나눠 생각합니다. 단위 테스트는 작은 부품 하나를 가까이에서 검사하고, 통합 테스트는 공개 API를 통해 여러 부품이 함께 잘 작동하는지 확인합니다.12

단위 테스트: 구현 가까이에서 빠르게 확인하기

단위 테스트는 보통 구현 코드와 같은 파일 안에 둡니다. Rust에서는 #[cfg(test)] 모듈 안에 #[test] 함수를 넣는 방식이 관례입니다.1

pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn add_two_numbers() {
        assert_eq!(add(2, 3), 5);
    }
}

여기서 #[cfg(test)]는 테스트를 실행할 때만 이 모듈을 컴파일하라는 뜻입니다. 그래서 실제 프로그램 빌드에는 테스트 코드가 포함되지 않습니다.1 use super::*;는 부모 모듈의 함수들을 테스트에서 쉽게 쓰기 위한 구문입니다.

단위 테스트의 가장 큰 장점은 빠르다는 점입니다. 함수 옆에 테스트가 있으니 작성도 쉽고, 문제가 생겨도 어디를 봐야 할지 금방 알 수 있습니다. 작은 함수일수록 작은 테스트를 붙이는 습관이 큰 힘을 발휘합니다.

assert!, assert_eq!, assert_ne!를 정확히 구분하기

Rust 테스트에서 가장 자주 쓰는 도구는 단연 assert! 계열 매크로입니다. 세 가지를 먼저 익혀두면 테스트의 절반은 배운 셈입니다.56

#[test]
fn basic_assertions() {
    let value = 10;

    assert!(value > 0);
    assert_eq!(value, 10);
    assert_ne!(value, 5);
}
  • assert!(조건)은 조건이 참인지 확인합니다.

  • assert_eq!(왼쪽, 오른쪽)은 두 값이 같은지 확인합니다.

  • assert_ne!(왼쪽, 오른쪽)은 두 값이 다른지 확인합니다.

실무에서는 대부분 assert_eq!가 가장 유용합니다. 실패했을 때 양쪽 값을 함께 보여주기 때문에 무엇이 예상과 달랐는지 쉽게 알 수 있기 때문입니다.

또 하나 중요한 점이 있습니다. 테스트는 "성공해야 한다"만 적는 것이 아니라, "실패해야 한다"도 적을 수 있습니다. #[should_panic]을 쓰면 특정 상황에서 패닉이 나는지 확인할 수 있습니다.1

#[test]
#[should_panic]
fn divide_by_zero_panics() {
    let _ = 1 / 0;
}

물론 이런 테스트는 정말로 그런 상황을 의도했을 때만 씁니다. 무턱대고 패닉을 기대하는 테스트를 늘리면 오히려 코드가 불안정해질 수 있습니다.

통합 테스트: 공개 API를 실제처럼 확인하기

단위 테스트가 함수 하나를 검사한다면, 통합 테스트는 여러 모듈이 합쳐진 결과를 봅니다. Rust에서는 보통 tests/ 디렉터리에 별도의 테스트 파일을 둡니다.2

예를 들어 src/lib.rs에 공개 함수가 있다면, tests/integration_test.rs에서 그것을 가져와 확인할 수 있습니다.

// tests/integration_test.rs
use my_crate::add;

#[test]
fn integration_add() {
    assert_eq!(add(4, 6), 10);
}

통합 테스트는 사용자 관점에 가깝습니다. 라이브러리를 쓰는 입장에서 API가 실제로 잘 동작하는지 확인할 수 있기 때문에, 공개 인터페이스를 바꿀 때 특히 중요합니다.

실패를 빠르게 읽는 법: 테스트 이름과 구조

테스트가 많아질수록 테스트 이름이 중요해집니다. test1, test2 같은 이름은 아무 정보도 주지 않습니다. 대신 parse_age_rejects_empty_input처럼 의도를 드러내면, 실패했을 때도 어떤 상황이 깨졌는지 바로 알 수 있습니다.

좋은 테스트는 보통 한 가지 상황만 확인합니다. 입력, 실행, 검증의 흐름이 뚜렷해야 유지보수가 쉽습니다. 한 테스트에 너무 많은 조건을 몰아넣으면, 실패했을 때 원인을 좁히기 어려워집니다.

Result를 반환하는 테스트도 가능하다

테스트 함수는 꼭 패닉 기반일 필요가 없습니다. Result<(), E>를 반환하게 해서 ?를 쓸 수도 있습니다.1

#[test]
fn can_open_file() -> Result<(), std::io::Error> {
    let _file = std::fs::File::open("Cargo.toml")?;
    Ok(())
}

이 방식은 오류를 더 자연스럽게 전달하고 싶을 때 유용합니다. 특히 파일, 네트워크, 파싱처럼 Result를 많이 다루는 코드에서 깔끔합니다.

디버깅: dbg!와 백트레이스로 문제를 좁히기

컴파일은 되는데 결과가 이상할 때가 있습니다. 그럴 때 가장 먼저 떠올릴 도구 중 하나가 dbg!입니다. println!보다 빠르게 변수 값과 위치를 확인할 수 있어, 초보자에게 특히 유용합니다.3

let x = 3;
let y = dbg!(x + 1);

dbg!는 표현식의 값뿐 아니라 파일과 줄 번호도 함께 보여줍니다. 그리고 표현식의 결과를 그대로 반환하므로, 잠깐 끼워 넣어 확인하기 좋습니다.

또 하나 중요한 디버깅 도구는 백트레이스입니다. 패닉이 났을 때 RUST_BACKTRACE=1 또는 RUST_BACKTRACE=full을 설정하면, 어디서 문제가 시작됐는지 호출 경로를 볼 수 있습니다.4

예를 들어:

RUST_BACKTRACE=1 cargo test

패닉 메시지와 함께 함수 호출 흐름이 보이면, "어디서 터졌는지"를 훨씬 빨리 찾을 수 있습니다. Rust를 막 배우는 단계에서는 백트레이스를 읽는 습관이 큰 도움이 됩니다.

panic!은 오류 처리가 아니라 마지막 안전장치

초보자가 자주 헷갈리는 부분이 panic!입니다. panic!은 보통 정상적인 오류 처리 수단이 아닙니다. 프로그램이 더 진행되면 안 되는, 정말 비정상적인 상태를 만났을 때 쓰는 마지막 안전장치에 가깝습니다.4

가능하면 예측 가능한 실패는 Result로 돌려주고, 호출자가 처리하게 만드는 편이 더 좋습니다. 테스트에서도 이 차이를 이해하고 있어야 합니다. "오류가 날 수 있다"는 사실과 "패닉이 나도 된다"는 사실은 전혀 다릅니다.

rustfmt: 코드 스타일은 고민하지 말고 자동화하기

코드 품질은 기능만이 아닙니다. 들여쓰기, 줄바꿈, 괄호 배치, 긴 체인의 정렬 같은 스타일도 협업에서 중요합니다. Rust에서는 이 역할을 rustfmt가 맡습니다.7

보통은 다음처럼 사용합니다.

cargo fmt

이 명령은 프로젝트 전체의 Rust 코드를 표준 스타일로 정리해 줍니다. 좋은 점은, 사람이 "이 줄은 2칸 들여쓸까 4칸 들여쓸까"를 매번 고민하지 않아도 된다는 것입니다. 스타일 논쟁을 줄이고, 리뷰는 내용에 집중할 수 있게 됩니다.

Clippy: 컴파일은 되지만 더 나은 코드가 있는지 묻기

rustfmt가 코드의 모양을 다듬는 도구라면, Clippy는 코드의 습관을 점검하는 도구입니다.8 Rust에는 "동작은 하지만 더 좋은 방식이 있는" 코드가 꽤 자주 생기는데, Clippy가 그런 부분을 잘 찾아줍니다.

보통은 다음처럼 실행합니다.

cargo clippy

Clippy는 다음과 같은 도움을 줍니다.

  • 불필요한 복사나 clone을 줄이도록 제안

  • 더 간단한 조건문 작성 방법 제안

  • 패턴 매칭이나 반복문의 더 관용적인 표현 제안

  • 잠재적인 실수나 애매한 코드를 경고

중요한 점은, Clippy의 제안을 무조건 맹신할 필요는 없다는 것입니다. 때로는 의도적으로 더 명시적인 코드를 쓰는 편이 좋을 수 있습니다. 하지만 초보 단계에서는 Clippy가 "왜 이런 식이 더 Rust스럽지?"를 배우는 훌륭한 교사 역할을 합니다.

테스트, 디버깅, 정리의 흐름을 한 번에 묶기

좋은 Rust 개발 흐름은 대체로 이렇게 정리됩니다.

  1. 작은 함수로 나눈다.

  2. 단위 테스트를 붙인다.

  3. cargo test로 빠르게 확인한다.

  4. 문제가 있으면 dbg!와 백트레이스로 추적한다.

  5. cargo fmt로 스타일을 정리한다.

  6. cargo clippy로 개선점을 찾는다.

이 흐름을 습관으로 만들면, 코드를 고칠 때마다 마음이 덜 불안해집니다. "어딘가 망가뜨렸을지도 모른다"는 걱정보다, "테스트가 말해줄 것"이라는 믿음을 갖게 되기 때문입니다.

자주 하는 실수

초보자가 자주 빠지는 함정도 몇 가지 있습니다.

  • 테스트를 너무 늦게 작성한다.

  • 테스트 이름이 모호하다.

  • 한 테스트에 너무 많은 내용을 넣는다.

  • 실패를 panic!로만 처리하려 한다.

  • dbg!를 지우지 않고 방치한다.

  • rustfmt와 Clippy를 "나중에" 하겠다고 미룬다.

이 중 특히 마지막 두 개는 습관과 관련이 큽니다. 개발 도구는 한 번 써보고 끝내는 것이 아니라, 자주 돌릴수록 효과가 커집니다.

정리

이 장에서 배운 핵심은 다음과 같습니다.

  • 테스트는 버그를 찾는 도구이면서 설계 문서이기도 하다.

  • 단위 테스트는 구현 가까이에서 빠르게 검증한다.

  • 통합 테스트는 공개 API 중심으로 전체 동작을 확인한다.

  • assert!, assert_eq!, assert_ne!는 가장 기본적인 검증 도구다.

  • dbg!와 백트레이스는 문제를 좁히는 데 유용하다.

  • rustfmt는 스타일을 자동으로 통일한다.

  • Clippy는 더 Rust다운 코드를 쓰도록 도와준다.

Rust를 잘한다는 것은 단지 컴파일을 통과시키는 것이 아닙니다. 믿을 수 있는 코드를 만들고, 고치기 쉬운 구조를 유지하고, 도구를 제대로 활용하는 습관까지 포함합니다. 결국 좋은 Rust 코드는 언어 기능만으로 만들어지지 않고, 좋은 개발 습관과 함께 만들어집니다.

1Rust 공식 문서의 testing 관련 자료.

2Cargo의 integration test 관례.

3dbg! 매크로는 값과 위치를 출력한다.

4panic!과 백트레이스 관련 Rust 표준 동작.

5assert! 매크로의 기본 용법.

6assert_eq!, assert_ne! 매크로는 비교 실패 시 더 풍부한 정보를 제공한다.

7rustfmt는 Rust 코드 포맷터다.

8Clippy는 Rust 코드의 lint 도구다.

책 목록

rust 학습

전체 목차 보기

이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.

틸노트는 사용자가 AI를 활용해 노트를 쉽게 작성하거나 편집할 수 있는 서비스입니다.

AI 스퀘어 단톡방에 함께해요

AI 활용과 틸노트 사용 팁을 나누는 오픈채팅방입니다. 입장 비밀번호는 tilnote입니다.

참여하기