AI 에이전트로 Rust 쓰기 — 컴파일러·테스트·clippy 를 되먹임으로 삼는다
AI 코딩 에이전트(Claude Code·Cursor·GitHub Copilot 같은 것)에게 Rust 를 맡길 때 가장 큰 무기는 엄격한 컴파일러다. 테스트를 먼저 정하고, cargo check·clippy·test 를 에이전트가 스스로 돌려 통과할 때까지 고치게 한다. 이 편에서는 정렬을 빠뜨린 중앙값 함수가 테스트와 clippy 에 차례로 걸리는 과정을 실제로 돌려 본다.
01 왜 Rust 는 AI 와 잘 맞나
에이전트는 그럴듯한 코드를 빨리 쓰지만, 그것이 맞는지는 스스로 확인해야 안다. 확인 수단이 많을수록 사람이 볼 일이 준다.
- 컴파일러 — 타입·소유권·빌림이 틀리면 실행하기 전에 막는다. 그것도 무엇·어디·고치는 법(입문 2편)을 글로 주니, 에이전트가 읽고 고치기 좋다.
- clippy — 컴파일은 되지만 어색한 코드를 짚는다(9편).
- cargo test — 뜻(동작)이 맞는지 확인한다(입문 42편).
Claude Code 공식 문서의 모범 사례 장도 첫머리에서 "Claude 가 볼 수 없는 것을 알려 주고, 돌려 볼 검사를 주고, 예외가 없어야 할 규칙은 강제하라" 고 정리한다. Rust 는 그 "돌려 볼 검사" 가 언어와 도구에 기본으로 들어 있다.
02 되먹임 고리 — 테스트 먼저
에이전트에게 "중앙값 함수를 만들어 줘" 라고만 하면, 맞았는지 판정할 기준이 없다. 테스트를 먼저 적어 준다. 빈 입력, 정렬되지 않은 홀수 개, 짝수 개(가운데 둘의 평균)다.
아래는 에이전트가 흔히 내놓는 첫 판을 흉내 낸 것이다. 정렬을 빠뜨렸고 짝수 개를 생각하지 않았다.
/// Returns the median of the numbers. Empty input gives None.
pub fn median(numbers: &[i32]) -> Option<f64> {
if numbers.is_empty() {
return None;
}
let mid = numbers.len() / 2;
Some(numbers[mid] as f64)
}
fn main() {
println!("{:?}", median(&[3, 1, 2]));
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_is_none() {
assert_eq!(median(&[]), None);
}
#[test]
fn odd_count_unsorted() {
assert_eq!(median(&[3, 1, 2]), Some(2.0));
}
#[test]
fn even_count_takes_average() {
assert_eq!(median(&[4, 1, 3, 2]), Some(2.5));
}
}running 3 tests
test tests::empty_is_none ... ok
test tests::odd_count_unsorted ... FAILED
test tests::even_count_takes_average ... FAILED
failures:
---- tests::odd_count_unsorted stdout ----
thread 'tests::odd_count_unsorted' (213394076) panicked at src/main.rs:25:9:
assertion `left == right` failed
left: Some(1.0)
right: Some(2.0)
---- tests::even_count_takes_average stdout ----
thread 'tests::even_count_takes_average' (213394075) panicked at src/main.rs:30:9:
assertion `left == right` failed
left: Some(3.0)
right: Some(2.5)
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
tests::even_count_takes_average
tests::odd_count_unsorted
test result: FAILED. 1 passed; 2 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s테스트 셋 중 둘이 실패했다. left(실제)와 right(기대)가 나란히 찍히니, 에이전트가 이 출력만 읽고도 "정렬을 안 했다", "짝수 개는 평균이어야 한다" 를 알아챈다. 사람에게 묻지 않고 고치는 단계로 갈 수 있다.
03 테스트를 통과해도 끝이 아니다
정렬하고 짝수 개를 처리한 판은 테스트를 통과한다.
running 3 tests
test tests::even_count_takes_average ... ok
test tests::empty_is_none ... ok
test tests::odd_count_unsorted ... ok
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s여기서 끝내면 안 된다. 9편의 두 줄 가운데 clippy 를 -D warnings 로 돌리면 이렇게 막힌다.
cargo clippy --all-targets -- -D warningserror: manual implementation of `.is_multiple_of()`
--> src/main.rs:9:8
|
9 | if sorted.len() % 2 == 0 {
| ^^^^^^^^^^^^^^^^^^^^^ help: replace with: `sorted.len().is_multiple_of(2)`
|
= help: for further information visit https://rust-lang.github.io/rust-clippy/rust-1.95.0/index.html#manual_is_multiple_of
= note: `-D clippy::manual-is-multiple-of` implied by `-D warnings`
= help: to override `-D warnings` add `#[allow(clippy::manual_is_multiple_of)]`len() % 2 == 0 대신 새 메서드 is_multiple_of(2) 를 쓰라는 것이다. 에이전트는 학습한 시점의 코드 습관을 따르기 쉬워서, 이렇게 최근 Rust 에 들어온 쓰는 법을 모를 수 있다. clippy 는 설치된 툴체인의 최신 습관으로 판정하니, 사람이 일일이 알 필요가 없다. help 대로 고친 판이 둘 다 통과한다.
/// Returns the median of the numbers. Empty input gives None.
pub fn median(numbers: &[i32]) -> Option<f64> {
if numbers.is_empty() {
return None;
}
let mut sorted = numbers.to_vec();
sorted.sort();
let mid = sorted.len() / 2;
if sorted.len().is_multiple_of(2) {
Some((sorted[mid - 1] + sorted[mid]) as f64 / 2.0)
} else {
Some(sorted[mid] as f64)
}
}
fn main() {
println!("{:?}", median(&[3, 1, 2]));
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_is_none() {
assert_eq!(median(&[]), None);
}
#[test]
fn odd_count_unsorted() {
assert_eq!(median(&[3, 1, 2]), Some(2.0));
}
#[test]
fn even_count_takes_average() {
assert_eq!(median(&[4, 1, 3, 2]), Some(2.5));
}
} Checking stats v0.1.0 (~/projects/stats)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.11s
Compiling stats v0.1.0 (~/projects/stats)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.20s
Running unittests src/main.rs (target/debug/deps/stats-d66f33ab1a38efd6)
running 3 tests
test tests::empty_is_none ... ok
test tests::odd_count_unsorted ... ok
test tests::even_count_takes_average ... ok
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s04 실습 — 프로젝트 규칙 파일
에이전트는 세션마다 프로젝트를 새로 읽는다. 매번 말할 규칙은 저장소의 규칙 파일(Claude Code 는 CLAUDE.md, 여러 도구가 함께 읽는 AGENTS.md 등)에 적어 둔다. Rust 프로젝트라면 이런 내용이다.
# Rust 규칙
- edition 2024, 툴체인은 rust-toolchain.toml 을 따른다
- 다 됐다고 말하기 전에 셋을 모두 통과시킨다:
cargo fmt --check
cargo clippy --all-targets -- -D warnings
cargo test
- 라이브러리 코드에서 unwrap()·expect()·unsafe 를 쓰지 않는다. 쓰려면 먼저 묻는다
- 새 크레이트를 더하지 않는다. 필요하면 이름·버전·이유를 먼저 말한다
- 테스트를 지우거나 기대값을 바꿔 통과시키지 않는다마지막 두 줄이 중요하다. 에이전트는 테스트를 통과시키려고 테스트 쪽을 고치거나, 일을 쉽게 하려고 크레이트를 늘리는 쪽으로 가기 쉽다. 사람이 리뷰할 때도 이 둘(테스트 변경·Cargo.toml 변경)부터 본다. 다음 편은 이 고리로도 잡히지 않는 실수, 바이브 코딩의 주의점이다.