19–21. 모듈 시스템·Cargo·crates.io · 단위 테스트·통합 테스트·doctest · 스레드·채널·Arc/Mutex
Rust 프로젝트가 커지면 코드를 모듈로 나누고 외부 크레잇을 끌어다 씁니다. 이 강의에서 mod 키워드와 파일 분할 규칙, pub 가시성, use 경로, Cargo.toml 의 의존성 명시, 그리고 crates.io 에서 패키지를 찾고 추가하는 흐름까지 한 번에 정리합니다. Rust 는 테스트 러너가 cargo 에 내장돼 있습니다. #[test] 어트리뷰트를 함수에 붙이면 cargo test 가 자동으로 모아 실행. 단위 테스트(같은 파일의 mod tests), 통합 테스트(tests/ 폴더), 그리고 문서 안의 코드 블록을 자동 실행하는 doctest 까지 한 번에 정리합니다. Rust 가 fearless concurrency 를 표방하는 이유는 빌림 검사기가 멀티스레드 데이터 경합까지 컴파일 타임에 막기 때문입니다. 이 강의에서 std::thread, channel(mpsc) 로 스레드 간 메시지, 그리고 공유 상태가 필요할 때의 Arc<Mutex<T>> 패턴까지 한 번에 정리합니다.
이 강의에서 배우는 것
- 1mod 로 코드를 모듈로 분할한다
- 2pub 키워드로 가시성을 제어한다
- 3use 로 경로를 짧게 줄인다
- 4Cargo.toml 의 [dependencies] 에 외부 크레잇을 추가한다
- 5cargo add / cargo search / cargo doc 으로 패키지 작업을 한다
- 6#[test] 함수를 작성하고 cargo test 로 실행한다
- 7단위 테스트의 mod tests 관례를 따른다
- 8통합 테스트를 tests/ 폴더에 분리해 작성한다
- 9#[should_panic] 으로 패닉 검증 테스트를 작성한다
- 10/// 주석 안의 예제 코드를 doctest 로 실행한다
- 11std::thread::spawn 으로 스레드를 시작하고 .join 으로 기다린다
- 12move 클로저로 변수를 스레드에 넘긴다
- 13mpsc 채널로 스레드 간 메시지를 주고받는다
- 14Arc<Mutex<T>> 패턴으로 공유 상태를 안전하게 변경한다
- 15Send / Sync 트레잇의 역할을 안다
19. 모듈 시스템·Cargo·crates.io
Rust 프로젝트가 커지면 코드를 모듈로 나누고 외부 크레잇을 끌어다 씁니다. 이 강의에서 mod 키워드와 파일 분할 규칙, pub 가시성, use 경로, Cargo.toml 의 의존성 명시, 그리고 crates.io 에서 패키지를 찾고 추가하는 흐름까지 한 번에 정리합니다.
소개
main.rs 한 파일에 모든 코드를 몰아 두면 100줄을 넘기는 순간 가독성이 빠르게 떨어집니다. Rust 는 한 파일 = 한 모듈 식의 간단한 규칙으로 코드 분할을 강제 없이 자연스럽게 풀어줍니다.
핵심 개념
1) crate / module 구분
- **crate** — Cargo.toml 한 개 단위 (== 패키지 = 빌드 단위)
- **module** — 한 crate 안의 네임스페이스 단위
- binary crate 의 root = `src/main.rs`, library crate 의 root = `src/lib.rs`
2) mod 와 파일 분할
// src/main.rs
mod math; // math.rs 또는 math/mod.rs 를 찾음
fn main() {
println!("{}", math::add(2, 3));
}
// src/math.rs
pub fn add(a: i32, b: i32) -> i32 { a + b }3) pub — 가시성 제어
- 기본은 **private** (같은 모듈 안에서만)
- `pub` — 외부 공개
- `pub(crate)` — 같은 crate 안에서만 공개
- `pub(super)` — 부모 모듈에만
4) use — 경로 단축
use std::collections::HashMap;
use std::io::{self, Read}; // self = io 자체, Read = trait
use crate::math::add as plus; // 별칭5) Cargo.toml 의존성
[dependencies]
serde = { version = "1", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
thiserror = "1"핵심 예제
디렉토리 구조 예:
src/
├── main.rs
├── config.rs
└── api/
├── mod.rs (또는 src/api.rs)
├── user.rs
└── post.rs// src/main.rs
mod config;
mod api;
fn main() {
let cfg = config::load();
api::user::create(&cfg);
}
// src/api/mod.rs (또는 src/api.rs + src/api/ 폴더)
pub mod user;
pub mod post;
// src/api/user.rs
use crate::config::Config;
pub fn create(cfg: &Config) { /* ... */ }외부 크레잇 추가·사용:
cargo add serde --features derive
cargo add chrono
cargo search regex # crates.io 검색use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug)]
struct User { name: String, age: u32 }
fn main() {
let u = User { name: "홍길동".into(), age: 30 };
let json = serde_json::to_string(&u).unwrap();
println!("{}", json);
}자주 하는 실수
Q. mod 와 use 의 차이가 뭐죠?
A. **mod** 는 모듈을 "존재시키는" 선언, **use** 는 이미 존재하는 경로를 "짧게 가져오는" 선언입니다. mod foo; 를 안 쓰면 use crate::foo 도 못 합니다.
Q. pub 안 붙이면 같은 파일에서도 못 쓰나요?
A. 같은 모듈(=같은 파일·같은 mod 블록) 안에서는 pub 없이도 접근 가능. **외부 모듈에서 접근하려면 pub 필수**.
Q. Cargo.lock 은 git 에 올리나요?
A. **바이너리 crate 는 올리고, 라이브러리 crate 는 올리지 않습니다.** 바이너리는 재현 가능한 빌드가 중요하고, 라이브러리는 사용자의 lockfile 이 별도라 의미가 없습니다.
정리
- crate = 빌드 단위, module = 네임스페이스 단위
- mod 로 선언, 파일/폴더 규칙으로 자동 매핑
- pub / pub(crate) / pub(super) 로 가시성 제어
- use 로 경로 단축, Cargo.toml 에 의존성, cargo add 로 추가
과제
- main.rs + math.rs 로 분할하고 main 에서 math::add 호출
- src/api/ 폴더에 user.rs / post.rs 모듈을 만들고 main 에서 사용
- cargo add chrono 로 chrono 추가 후 현재 시각을 출력
20. 단위 테스트·통합 테스트·doctest
Rust 는 테스트 러너가 cargo 에 내장돼 있습니다. #[test] 어트리뷰트를 함수에 붙이면 cargo test 가 자동으로 모아 실행. 단위 테스트(같은 파일의 mod tests), 통합 테스트(tests/ 폴더), 그리고 문서 안의 코드 블록을 자동 실행하는 doctest 까지 한 번에 정리합니다.
소개
테스트가 별도 도구 없이 cargo 안에 들어 있다는 것은 큰 이점입니다. "테스트 프레임워크 뭘 쓰지" 같은 결정을 할 필요가 없고, 새 프로젝트도 처음부터 테스트를 자연스럽게 작성할 수 있습니다.
핵심 개념
1) 단위 테스트 — 같은 파일 안 mod tests
pub fn add(a: i32, b: i32) -> i32 { a + b }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn add_works() {
assert_eq!(add(2, 3), 5);
}
#[test]
fn add_negative() {
assert_eq!(add(-1, 1), 0);
}
}- `#[cfg(test)]` — 테스트 빌드에서만 컴파일
- `use super::*` — 부모 모듈의 private 항목까지 접근 가능
- `#[test]` 가 붙은 fn 만 cargo test 가 실행
2) 통합 테스트 — tests/ 폴더
라이브러리의 공개 API 만 사용해 전체 동작을 검증. 외부 사용자가 쓰는 것과 같은 경로.
// tests/api.rs
use my_crate::add;
#[test]
fn integration_add() {
assert_eq!(add(10, 20), 30);
}3) 자주 쓰는 assert 매크로
| 매크로 | 용도 |
|---|---|
| assert!(expr) | expr 이 true 여야 |
| assert_eq!(a, b) | a == b |
| assert_ne!(a, b) | a != b |
| #[should_panic] | 테스트 함수가 panic 해야 통과 |
| #[ignore] | 기본 실행에서 제외 (cargo test -- --ignored) |
4) doctest — /// 안의 예제 코드 실행
/// 두 수를 더합니다.
///
/// # 예제
///
/// ```
/// assert_eq!(my_crate::add(2, 3), 5);
/// ```
pub fn add(a: i32, b: i32) -> i32 { a + b }핵심 예제
Result 반환 테스트와 should_panic:
pub fn divide(a: i32, b: i32) -> i32 {
if b == 0 { panic!("div by zero"); }
a / b
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ok_case() -> Result<(), String> {
if divide(10, 2) == 5 { Ok(()) } else { Err("expected 5".into()) }
}
#[test]
#[should_panic(expected = "div by zero")]
fn panic_case() { divide(10, 0); }
}실행 명령:
cargo test # 전체
cargo test add_ # 이름에 add_ 가 포함된 것만
cargo test -- --nocapture # println! 출력 보기
cargo test --doc # doctest 만
cargo test -- --ignored # ignore 된 것 실행자주 하는 실수
Q. cargo test 출력에 println! 이 안 보입니다
A. 기본 동작입니다 — 성공한 테스트의 출력은 가려집니다. `cargo test -- --nocapture` 로 켤 수 있습니다.
Q. tests/ 폴더와 mod tests 의 차이는?
A. **mod tests** = 같은 crate 내부 private 까지 접근 가능. **tests/** = 외부 사용자 시점에서 public API 만 호출. 둘을 함께 쓰는 것이 일반적.
Q. 테스트가 너무 느립니다
A. cargo test 는 기본적으로 병렬 실행합니다. 직렬이 필요하면 `cargo test -- --test-threads=1`. 특정 테스트만 `--ignored` 로 분리해 평소 실행에서 빼는 것도 방법.
정리
- cargo test 가 #[test] 함수를 자동 수집·실행
- mod tests + #[cfg(test)] = 단위 테스트 관례
- tests/ 폴더 = 통합 테스트 (public API 만)
- /// 안 코드 블록 = doctest 자동 실행
- should_panic·ignore·assert_eq 매크로 활용
과제
- 기존 라이브러리 함수에 mod tests 를 붙이고 3개 이상 단위 테스트 작성
- tests/api.rs 에 통합 테스트 하나 추가
- /// 주석 예제 코드를 추가하고 cargo test --doc 으로 통과 확인
21. 스레드·채널·Arc/Mutex
Rust 가 fearless concurrency 를 표방하는 이유는 빌림 검사기가 멀티스레드 데이터 경합까지 컴파일 타임에 막기 때문입니다. 이 강의에서 std::thread, channel(mpsc) 로 스레드 간 메시지, 그리고 공유 상태가 필요할 때의 Arc<Mutex<T>> 패턴까지 한 번에 정리합니다.
소개
Rust 의 동시성은 "실수해도 안전" 이 핵심 약속입니다. 빌림 규칙이 단일 스레드에서 막던 것(가변 참조 1개) 이 멀티스레드까지 그대로 확장되어, 데이터 경합이 **컴파일 타임에** 차단됩니다. 그 결과 락을 빠뜨려서 생기는 데이터 경합 디버깅 시간이 극적으로 줄어듭니다.
핵심 개념
1) 스레드 생성
use std::thread;
use std::time::Duration;
fn main() {
let h = thread::spawn(|| {
for i in 1..=3 {
println!("child {}", i);
thread::sleep(Duration::from_millis(50));
}
});
for i in 1..=3 {
println!("main {}", i);
thread::sleep(Duration::from_millis(50));
}
h.join().unwrap();
}2) move 클로저로 변수 이동
클로저가 외부 변수를 빌리면 스레드 수명을 컴파일러가 보장 못 합니다. `move` 키워드로 소유권 자체를 이동시켜 해결.
let v = vec![1, 2, 3];
let h = thread::spawn(move || println!("{:?}", v));
// 여기서 v 는 이미 move 됨
h.join().unwrap();3) mpsc 채널 — 메시지 패싱
multi-producer single-consumer 표준 채널. 송신자(Sender) 가 여러 개, 수신자(Receiver) 가 하나.
4) Arc<Mutex<T>> — 공유 가변 상태
- **Arc<T>** — 여러 스레드에서 공유 가능한 reference count (atomic)
- **Mutex<T>** — 한 시점에 하나만 가변 접근 (런타임 락)
- **조합 Arc<Mutex<T>>** — 여러 스레드가 공유하면서 가변 접근
5) Send / Sync 트레잇
| 트레잇 | 의미 |
|---|---|
| Send | 스레드 간 소유권 이전 안전 |
| Sync | 여러 스레드에서 동시 참조 안전 |
거의 모든 표준 타입에 자동 derive 됩니다. Rc 와 RefCell 은 Send/Sync 가 아니라 스레드 간 전달 시 컴파일 에러 — 그 자리에 Arc/Mutex 를 쓰면 됩니다.
핵심 예제
채널로 1~10 을 워커 스레드에서 보내고 메인에서 받기:
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel::<i32>();
thread::spawn(move || {
for i in 1..=10 { tx.send(i).unwrap(); }
});
let sum: i32 = rx.iter().sum();
println!("sum = {}", sum); // 55
}여러 스레드가 카운터를 안전하게 증가:
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let counter = Arc::new(Mutex::new(0));
let mut hs = vec![];
for _ in 0..10 {
let c = Arc::clone(&counter);
hs.push(thread::spawn(move || {
let mut n = c.lock().unwrap();
*n += 1;
}));
}
for h in hs { h.join().unwrap(); }
println!("counter = {}", *counter.lock().unwrap()); // 10
}자주 하는 실수
Q. Rc 를 스레드 사이에 넘기면 안 되나요?
A. 안 됩니다 — Rc 는 non-atomic ref count 라 멀티스레드에서 경합 발생. Arc 로 바꾸면 됩니다 (atomic operation). 컴파일러가 Send 가 아니라고 막아 줍니다.
Q. Mutex 가 락 풀리는 시점은?
A. `lock()` 으로 얻은 가드(MutexGuard) 가 **drop 되는 시점**. 보통 스코프 끝. 명시적으로 빨리 풀려면 `drop(guard)` 또는 별도 블록으로 묶어 스코프를 좁힙니다.
Q. async/await 가 대신 쓸 수 있나요?
A. CPU 작업은 스레드, IO 중심은 async 가 일반적. 다음 강의에서 async/await + tokio 로 IO 비동기를 다룹니다.
정리
- thread::spawn + move 클로저로 스레드 생성
- mpsc 채널로 메시지 패싱
- 공유 가변 상태 = Arc<Mutex<T>>
- Send/Sync 트레잇으로 컴파일 타임 데이터 경합 검사
과제
- 네 개 워커 스레드를 만들고 각각 1~25, 26~50, 51~75, 76~100 합을 mpsc 로 메인에 전달
- Arc<Mutex<Vec<i32>>> 를 만들어 여러 스레드에서 push 가 안전한지 확인
- Rc 를 thread::spawn 에 넘기는 코드를 일부러 작성해 컴파일 에러 메시지 확인
댓글 0
아직 댓글이 없습니다. 첫 댓글을 남겨 주세요.
댓글은 회원만 쓸 수 있습니다.
광고·욕설·비방·개인정보·불법 내용이 담긴 댓글은 숨기거나 지웁니다. 댓글은 닉네임과 함께 공개됩니다. 개인정보처리방침