Rust 와 보안 2 — 웹 API 를 지키는 설정: 크기·시간 제한과 SQL 주입
Rust 웹 API 도 설정으로 지킨다. 큰 요청은 DefaultBodyLimit 로 413, 느린 요청은 TimeoutLayer 로 408 로 끊는다. SQL 을 format! 으로 이어 붙이면 Rust 도 주입에 뚫린다는 것을 실행하고 매개변수로 고친다.
Rust 웹 API 도 설정으로 지킨다. 큰 요청은 DefaultBodyLimit 로 413, 느린 요청은 TimeoutLayer 로 408 로 끊는다. SQL 을 format! 으로 이어 붙이면 Rust 도 주입에 뚫린다는 것을 실행하고 매개변수로 고친다.
Rust 는 메모리 오류를 컴파일러로 막지만 release 의 정수 넘침, 권한 같은 로직 실수, unsafe, 의존성은 막지 못한다. 배열 밖 패닉, 조용히 틀리는 70000×70000, 테스트에만 걸리는 권한 버그를 실행해 본다.
axum 으로 REST API 를 실무 모양으로 짠다. AppError 에 IntoResponse 를 구현해 에러를 알맞은 상태 코드와 JSON 으로 바꾸고, 입력을 검증하고, Router 를 서버 없이 바로 불러 테스트한다.
Rust 를 쓰는 곳 가운데 가장 많이 꼽히는 것이 서버 백엔드·웹·네트워크 서비스다. 다만 웹 서비스 전체를 Rust 로 짓기보다, 느린 부분만 떼어 내거나 지연이 튀면 안 되는 서비스·프록시·서버리스 함수에 쓰는 경우가 많다. 공식 사례로 다섯 가지 방식을 정리한다.
AI 가 쓴 Rust 코드를 읽지 않고 받아들일 때 걸리는 함정을 실제 오류로 본다. 없는 메서드, 2024 판에서 unsafe 가 된 set_var, 폐기된 이름, 없는 버전, 그리고 컴파일러가 못 잡는 공급망 위험이다.
AI 코딩 에이전트에게 Rust 를 맡길 때 가장 큰 무기는 엄격한 컴파일러다. 테스트를 먼저 정하고 check·clippy·test 를 에이전트가 스스로 돌려 통과할 때까지 고치게 한다. 그 과정을 실행해 본다.
컴파일러를 달래려고 clone 과 unwrap 을 붙이는 습관이 Rust 를 어렵게 만든다. 값이 필요 없으면 빌려서 받고, 실패할 수 있으면 어디서 왜 실패했는지 담은 Result 로 돌려주고, bool 여러 개 대신 enum 으로 상태를 적는다. 세 가지를 실행해 비교한다.
clippy 는 컴파일은 되지만 Rust 답지 않거나 실수하기 쉬운 코드를 짚어 주는 린터이고, rustfmt 는 코드 모양을 한 가지로 맞추는 포매터다. 다른 언어 습관으로 쓴 함수 하나에서 clippy 경고 다섯 개를 받고, help 대로 고쳐 깨끗하게 만든다.
Rust 실무의 에러 처리는 둘로 나눈다. 라이브러리는 thiserror 로 경우를 나눈 에러 타입을, 앱은 anyhow 로 어떤 에러든 받아 설명(context)을 붙인다. 원인 사슬과 종료 코드 1 을 실행해 본다.
axum 은 tokio 위의 Rust 웹 프레임워크다. Router 에 경로와 핸들러를 묶고 Json·Path·State 추출기로 요청을 받는다. 메모 API 로 201·200·404·422 응답을 127.0.0.1 에서 확인한다.
clap 은 Rust 명령줄 도구의 옵션을 읽는 크레이트다. 구조체에 #[derive(Parser)] 를 붙이면 칸이 인자·옵션이 되고, 문서 주석이 --help 문장이 된다. 잘못된 값은 clap 이 막고 종료 코드 2 로 끝낸다. wordfreq 에 --top 을 단다.
serde 는 Rust 구조체와 JSON 을 오가는 사실상의 표준이다. derive 한 줄이면 되고, 잘못된 JSON 은 어느 칸이 왜 틀렸는지 알려 준다. rename_all·default·skip_serializing_if 로 모양을 맞춘다.
Rust 실무에서 자주 쓰는 크레이트를 분야별로 정리한다(serde·clap·tokio·axum·thiserror·anyhow). 직접 적은 4개가 실제로는 25개를 끌어온다는 것을 cargo tree 로 확인하고, 고르는 기준을 본다.
Rust 는 메모리 오류가 사고로 이어지고 성능도 놓칠 수 없는 곳에 쓰인다. 리눅스·윈도우 커널과 안드로이드, 크롬의 글꼴 처리, 클라우드플레어 프록시와 AWS Lambda 의 가상 머신, 파이썬 패키지 관리자 uv 까지 공식 발표로 정리한다.
Rust 를 고르면 컴파일 시간·디스크·배우는 시간을 치른다. axum 웹 서버 하나가 처음 빌드에 크레이트 48개·11초·target 173MB 를 쓰고, 1.0 이 안 된 크레이트도 많다. 언제 피하는 편이 나은지 기준을 정리한다.
Rust 를 고르면 메모리 안전을 컴파일할 때 얻고, 가비지 컬렉터 없이 빠르게 돌고, 런타임 없는 실행 파일 하나로 배포한다. release 빌드 7배 속도·반복자와 손 루프의 같은 속도·hello 431KB 를 직접 재 본다.
34편의 wordfreq 를 다시 만든다. 세는 부분을 lib.rs 로 빼고 반복자로 줄이며, 파일마다 스레드로 세고, 테스트 넷으로 고정한다. 같은 횟수는 알파벳 순이다.
Rust 의 async fn 은 .await 해야 실행되고, 빠뜨리면 경고만 남는다. tokio 의 join! 으로 1초짜리 둘을 1초에 끝내고, 127.0.0.1 에코 서버로 동시 접속을 본다.
Rust 에서 여러 스레드가 한 값을 가지려면 Arc, 함께 고치려면 Mutex 를 쓴다. Rc 를 스레드에 넘기면 E0277 이 난다. 스레드 넷이 1000번씩 더하면 늘 4000 이다.
Rust 채널은 보내는 쪽 tx 와 받는 쪽 rx 를 만든다. 스레드가 send 로 보내고 받는 쪽은 for 로 받는다. 보낸 값을 다시 쓰면 E0382 가 나고, tx.clone() 으로 여럿이 보낸다.
Rust 의 thread::spawn 은 클로저를 새 스레드에서 돌리고, join 으로 끝나기를 기다려 결과를 받는다. 스레드는 만든 함수보다 오래 살 수 있어서, 바깥 변수를 빌리기만 하면 E0373 이 난다. move 로 변수를 스레드 안으로 옮기면 된다.
Rust 테스트는 #[test] 함수이고 cargo test 가 돌린다. assert_eq! 가 틀리면 left·right 두 값을 찍고 실패한다. 패닉이 맞는 동작은 #[should_panic] 으로 시험한다.
Rust 외부 라이브러리를 크레이트라 한다. Cargo.toml 에 정확한 버전을 적으면 cargo 가 받아 컴파일하고, 안 적고 use 하면 E0432 가 난다. 쓴 버전은 Cargo.lock 에 남는다.
Rust 모듈은 mod 이름 { } 으로 코드를 묶는 단위다. 모듈 안의 것은 기본이 비공개라서, 밖에서 쓰려면 pub 을 붙여야 하고 아니면 E0603 이 난다. use 로 긴 경로를 줄이고, mod 이름; 한 줄로 내용을 src/이름.rs 파일로 옮긴다.
Rust 의 Rc 는 주인이 여럿인 값을 만든다. Rc 안은 고칠 수 없어 E0596 이 나고, RefCell 로 감싸 borrow_mut 로 고친다. 빌림 규칙을 어기면 실행 중에 패닉한다.
Rust 의 Box<T> 는 값을 힙에 두고, 그 자리를 가리키는 크기가 정해진 포인터다. 자기 자신을 칸으로 갖는 재귀 타입은 크기가 끝없이 커져서 E0072 로 막히는데, 그 칸을 Box 로 감싸면 크기가 정해져 컴파일된다.
Rust 반복자는 값을 하나씩 내준다. map·filter 로 바꾸고 거른 뒤 collect·sum 으로 마무리한다. 마무리 없이 map 만 쓰면 아무 일도 하지 않고 경고가 난다.
Rust 클로저는 |x| 식 으로 쓰는 이름 없는 함수이고 주변 변수를 잡아 쓴다. 타입은 처음 부른 값으로 정해져 다른 타입으로 부르면 E0308 이 난다.
Rust 수명은 참조가 얼마나 유효한지 컴파일러에 알리는 표시다. 참조를 돌려주는 함수는 E0106 이 나서 <'a> 로 적고, 먼저 사라지는 값을 빌리면 E0597 이 난다.
입문 구간에서 배운 것만으로 명령줄 도구를 만든다. 파일 이름을 인자로 받아(env::args) 파일을 읽고(fs::read_to_string), HashMap 으로 단어를 세서 많이 나온 10개를 찍는다. 인자가 없거나 파일이 없으면 이유를 찍고 종료 코드 2·1 로 끝낸다.
Rust 트레이트는 '이 메서드가 있다' 는 약속이다. trait 으로 정하고 impl 트레이트 for 타입 으로 채우며, 빠뜨리면 E0046 이 난다. 기본 구현과 &impl 매개변수도 본다.
Rust 제네릭은 타입 자리에 T 를 두고 함수·구조체 하나를 여러 타입에 쓴다. T 로 무엇을 할지는 T: PartialOrd 같은 경계로 약속해야 해서, 빠뜨리면 E0369 가 난다.
Rust 의 ? 는 Ok 면 값을 꺼내고 Err 면 그 오류를 들고 함수를 끝낸다. Result 를 돌려주는 함수에서만 써서 보통의 main 에서는 E0277 이 난다.
Rust 는 고칠 수 없는 실패는 panic! 으로 멈추고, 다룰 수 있는 실패는 Ok·Err 인 Result 로 돌려준다. parse 가 그 예이고, Result 를 값 자리에 쓰면 E0308 이 난다.
Rust 의 HashMap 은 키로 값을 찾는 표다. use 로 불러오지 않으면 E0433 이 난다. insert 로 넣고 get 은 Option 을 주며, entry().or_insert(0) 으로 단어를 센다.
Rust 의 Vec 은 같은 타입 값을 담는, 길이가 바뀌는 목록이다. Vec::new() 나 vec![…] 로 만들고 push 로 더한다. v[n] 처럼 범위 밖 번호를 쓰면 실행 중에 패닉하고, get(n) 은 Option 을 돌려줘서 없는 경우를 안전하게 다룬다.
Rust 의 if let 은 한 경우만 보는 match 이고, let else 는 맞지 않으면 바로 빠져나간다. let else 의 else 가 빠져나가지 않으면 E0308 이 난다.
Rust 에는 null 이 없고, 값이 없을 수 있으면 Some(값)·None 인 Option 을 쓴다. Option<i32> 를 i32 자리에 쓰면 E0308 이 나고, match 나 unwrap_or 로 꺼낸다.
Rust 의 match 는 값이 어느 경우인지 위에서부터 맞춰 보고 맞는 갈래를 실행한다. 열거형의 경우를 하나라도 빠뜨리면 E0004 로 컴파일이 멈춘다. 경우에 붙은 데이터는 갈래에서 이름을 붙여 꺼내고, 나머지를 한꺼번에 받을 때는 _ 를 쓴다.
Rust 열거형(enum)은 신호등의 빨강·노랑·초록처럼 정해진 몇 가지 중 하나인 값을 타입으로 만든다. Light::Red 처럼 타입 이름과 :: 로 고르고, 없는 이름을 쓰면 E0599 가 난다. 경우마다 다른 데이터를 붙일 수도 있다.
Rust 메서드는 impl 블록 안의 함수다. &self 면 읽고 &mut self 면 고치며, 고치는 메서드를 mut 아닌 변수에 부르면 E0596 이 난다. self 없는 함수는 Rect::new 처럼 부른다.
Rust 구조체(struct)는 관련된 값 여러 개를 칸마다 이름을 붙여 하나로 묶은 타입이다. struct 로 정의하고, 만들 때는 모든 칸을 채워야 해서 하나라도 빠지면 E0063 이 난다. #[derive(Debug)] 를 붙이면 {:?} 로 통째로 찍을 수 있다.
Rust 슬라이스는 &scores[1..4] 처럼 배열이나 문자열의 일부만 빌린 참조다. 문자열 슬라이스의 번호는 글자가 아니라 바이트라서, 한 글자가 3바이트인 한글을 글자 중간에서 자르면 실행 중에 패닉한다. 글자 단위로 다룰 때는 chars() 를 쓴다.
Rust 문자열은 둘이다. String 은 고칠 수 있는 가진 문자열, &str 은 빌려 보는 문자열이다. 큰따옴표 글자는 &str 이라 String 자리에 넣으면 E0308 이 난다.
Rust 에서 빌린 값을 고치려면 &mut 로 빌린다. 읽기 참조(&)는 여럿, 가변 참조(&mut)는 하나만 동시에 둘 수 있고 섞을 수 없다. 어기면 E0499·E0502 가 난다.
Rust 에서 &name 처럼 & 를 붙이면 값의 주인은 그대로 두고 참조만 넘긴다. 이것을 빌림(borrowing)이라 한다. 빌린 쪽은 값을 읽을 수만 있고, 고치려 하면 E0596 이 난다. 읽기만 하는 참조는 여러 개를 동시에 만들어도 된다.
Rust 에서 String 을 함수에 넘기면 주인이 함수의 매개변수로 옮겨 간다. 그 뒤에 원래 변수를 쓰면 E0382 가 난다. 함수가 값을 돌려주면 주인도 다시 돌아온다. 정수 같은 Copy 타입은 복사되어 넘어가니 상관없다.
Rust 에서 String 을 두 변수가 함께 쓰려면 .clone() 으로 값을 복사한다. 정수·실수·bool·char 처럼 작은 값은 Copy 타입이라 let y = x; 해도 이동하지 않고 저절로 복사된다. String 이 하나라도 든 튜플은 Copy 가 아니어서 이동한다.
Rust 의 값에는 주인(변수)이 하나뿐이다. let b = a; 처럼 String 을 다른 변수에 넣으면 주인이 b 로 옮겨 가고(이동), 그 뒤에 a 를 쓰면 E0382 로 컴파일이 멈춘다. 주인이 블록을 벗어나면 값은 저절로 치워진다.
Rust 의 for 는 배열이나 범위의 값을 하나씩 꺼내 되풀이한다. 0..3 은 0·1·2, 1..=3 은 1·2·3 이고 .rev() 로 거꾸로 돈다. 번호를 직접 세지 않으니 범위를 벗어날 일이 없어서, 정해진 만큼 돌 때는 for 가 기본이다.
Rust 의 loop 는 break 를 만날 때까지 되풀이하고, break 값 으로 반복을 끝내며 값을 돌려줄 수 있다. while 은 조건이 참인 동안 되풀이한다. continue 는 이번 차례의 나머지를 건너뛰고 다음 차례로 간다.
Rust 의 if 는 조건이 반드시 bool 이어야 해서, 숫자를 그대로 넣으면 E0308 이 난다. else if 로 갈래를 늘리고, if 가 값을 내는 식이라 let label = if … 처럼 결과를 바로 담는다. 이때 갈래마다 같은 타입을 내야 한다.
Rust 함수는 -> 타입 으로 돌려줄 값의 타입을 적고, 세미콜론 없는 마지막 줄(식)의 값을 돌려준다. 그 줄에 습관대로 세미콜론을 붙이면 아무 값도 돌려주지 않게 되어 E0308 이 난다. 값을 둘 이상 돌려줄 때는 튜플을 쓴다.
Rust 함수는 fn 이름(매개변수: 타입) { … } 으로 만든다. 매개변수에는 타입을 반드시 적고, 부를 때는 그 개수와 타입대로 값을 넘긴다. 개수가 모자라면 E0061 로 컴파일이 멈추고, 빠진 자리를 컴파일러가 짚어 준다.
Rust 배열은 같은 타입의 값을 정해진 개수만큼 대괄호로 담는다. scores[0] 처럼 0 부터 센 번호로 꺼내고, len() 으로 길이를 본다. 범위 밖 번호는 컴파일러가 미리 막거나, 실행 중이면 패닉으로 멈춘다.
Rust 튜플은 타입이 다른 값 몇 개를 괄호로 묶은 하나의 값이다. .0·.1 처럼 번호로 꺼내고, let (a, b) = … 로 한 번에 나눠 담는다. 없는 번호를 쓰면 E0609 로 컴파일이 멈춘다.
Rust 의 bool 은 true 와 false 두 값뿐이고, 비교 연산(>=, ==)의 결과가 bool 이다. char 는 글자 하나를 작은따옴표로 담는다. 큰따옴표로 쓰면 문자열이 되어 E0308 이 난다. 한글과 이모지도 char 하나다.
Rust 실수는 소수점이 있는 숫자이고 타입을 안 적으면 f64 다. 정수와 실수를 섞어 계산하면 E0277 로 컴파일이 멈춘다. 9 대신 9.0 처럼 실수로 쓰거나, as 로 타입을 바꿔서 맞춘다.
Rust 정수는 크기와 부호에 따라 i8~i128, u8~u128 로 나뉘고, 타입을 안 적으면 i32 다. 타입마다 담을 수 있는 범위가 정해져 있어서, 범위를 넘는 값은 컴파일러가 미리 막고 실행 중에 넘치면 디버그 빌드에서 멈춘다.
Rust 에서 같은 이름으로 let 을 다시 쓰면 앞 변수를 가리는 새 변수가 생긴다. 이것을 가림(shadowing)이라 한다. mut 와 달리 타입이 다른 값도 담을 수 있고, 중괄호 블록 안에서 가리면 블록이 끝날 때 원래 값으로 돌아온다.
Rust 에서 let 으로 만든 변수는 값을 바꿀 수 없고, 다시 대입하면 E0384 로 컴파일이 멈춘다. 바꿀 변수에만 mut 를 붙이고, 프로그램 내내 그대로인 값은 const 로 둔다.
Rust 컴파일 오류는 길어 보여도 세 군데만 읽으면 된다. error 줄이 무엇이 틀렸는지, --> 줄이 어디인지, help 줄이 고치는 법이다. 느낌표·세미콜론·따옴표를 빠뜨린 오류를 실제 출력으로 읽어 본다.
rustup 명령 한 줄이면 컴파일러 rustc 와 빌드 도구 cargo 가 함께 깔린다. cargo new 로 프로젝트를 만들고 cargo run 으로 첫 프로그램을 돌려 본다.