← Rust 강의 목록으로
🦀
모던 Rust
모던 · 선수: 에러 처리 1~4강 · 모던 · 선수: 19강 · 모던 · 선수: 20강

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>> 패턴까지 한 번에 정리합니다.

RustmoduleCargocratecrates.iopubusetestcargo testdoctestassert_eqTDDthreadconcurrencyArcMutexchannelmpsc
소요 시간
⏱ 약 1.5시간 + 약 1.5시간 + 약 2시간
난이도
📊 중급
선수 조건
🎯 에러 처리 1~4강
결과물
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 와 파일 분할

rust
// 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 — 경로 단축

rust
use std::collections::HashMap;
use std::io::{self, Read};       // self = io 자체, Read = trait
use crate::math::add as plus;     // 별칭

5) Cargo.toml 의존성

toml
[dependencies]
serde = { version = "1", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
thiserror = "1"

핵심 예제

디렉토리 구조 예:

bash
src/
├── main.rs
├── config.rs
└── api/
    ├── mod.rs       (또는 src/api.rs)
    ├── user.rs
    └── post.rs
rust
// 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) { /* ... */ }

외부 크레잇 추가·사용:

bash
cargo add serde --features derive
cargo add chrono
cargo search regex      # crates.io 검색
rust
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 로 추가

과제

  1. main.rs + math.rs 로 분할하고 main 에서 math::add 호출
  2. src/api/ 폴더에 user.rs / post.rs 모듈을 만들고 main 에서 사용
  3. cargo add chrono 로 chrono 추가 후 현재 시각을 출력

20. 단위 테스트·통합 테스트·doctest

Rust 는 테스트 러너가 cargo 에 내장돼 있습니다. #[test] 어트리뷰트를 함수에 붙이면 cargo test 가 자동으로 모아 실행. 단위 테스트(같은 파일의 mod tests), 통합 테스트(tests/ 폴더), 그리고 문서 안의 코드 블록을 자동 실행하는 doctest 까지 한 번에 정리합니다.

소개

테스트가 별도 도구 없이 cargo 안에 들어 있다는 것은 큰 이점입니다. "테스트 프레임워크 뭘 쓰지" 같은 결정을 할 필요가 없고, 새 프로젝트도 처음부터 테스트를 자연스럽게 작성할 수 있습니다.

핵심 개념

1) 단위 테스트 — 같은 파일 안 mod tests

rust
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 만 사용해 전체 동작을 검증. 외부 사용자가 쓰는 것과 같은 경로.

rust
// 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 — /// 안의 예제 코드 실행

rust
/// 두 수를 더합니다.
///
/// # 예제
///
/// ```
/// assert_eq!(my_crate::add(2, 3), 5);
/// ```
pub fn add(a: i32, b: i32) -> i32 { a + b }

핵심 예제

Result 반환 테스트와 should_panic:

rust
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); }
}

실행 명령:

bash
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 매크로 활용

과제

  1. 기존 라이브러리 함수에 mod tests 를 붙이고 3개 이상 단위 테스트 작성
  2. tests/api.rs 에 통합 테스트 하나 추가
  3. /// 주석 예제 코드를 추가하고 cargo test --doc 으로 통과 확인

21. 스레드·채널·Arc/Mutex

Rust 가 fearless concurrency 를 표방하는 이유는 빌림 검사기가 멀티스레드 데이터 경합까지 컴파일 타임에 막기 때문입니다. 이 강의에서 std::thread, channel(mpsc) 로 스레드 간 메시지, 그리고 공유 상태가 필요할 때의 Arc<Mutex<T>> 패턴까지 한 번에 정리합니다.

소개

Rust 의 동시성은 "실수해도 안전" 이 핵심 약속입니다. 빌림 규칙이 단일 스레드에서 막던 것(가변 참조 1개) 이 멀티스레드까지 그대로 확장되어, 데이터 경합이 **컴파일 타임에** 차단됩니다. 그 결과 락을 빠뜨려서 생기는 데이터 경합 디버깅 시간이 극적으로 줄어듭니다.

핵심 개념

1) 스레드 생성

rust
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` 키워드로 소유권 자체를 이동시켜 해결.

rust
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 을 워커 스레드에서 보내고 메인에서 받기:

rust
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
}

여러 스레드가 카운터를 안전하게 증가:

rust
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. 네 개 워커 스레드를 만들고 각각 1~25, 26~50, 51~75, 76~100 합을 mpsc 로 메인에 전달
  2. Arc<Mutex<Vec<i32>>> 를 만들어 여러 스레드에서 push 가 안전한지 확인
  3. Rc 를 thread::spawn 에 넘기는 코드를 일부러 작성해 컴파일 에러 메시지 확인
예제 코드 / 강의 자료

전체 강의 자료와 예제 코드는 GitHub에서 자유롭게 받아볼 수 있습니다.

GitHub에서 보기 ↗

댓글 0

아직 댓글이 없습니다. 첫 댓글을 남겨 주세요.

댓글은 회원만 쓸 수 있습니다.

광고·욕설·비방·개인정보·불법 내용이 담긴 댓글은 숨기거나 지웁니다. 댓글은 닉네임과 함께 공개됩니다. 개인정보처리방침