Rust 기초 강좌 #9 수명과 모듈: lifetime 표기가 필요한 순간, 모듈과 크레이트까지

5 분 소요

마지막 편입니다. 남은 약속은 하나, 4편에서 “참조가 얼마나 오래 유효한지를 다루는 검사"라고만 말해 둔 수명(lifetime)입니다. 수명을 정리한 뒤, 한 파일을 벗어나 프로젝트를 구성하는 모듈과 외부 크레이트까지 다루고 강좌를 마무리하겠습니다.

수명: 컴파일러는 이미 계산하고 있었다 #

4편에서 지역 변수의 참조를 반환하면 컴파일이 거부된다고 했습니다. 그 판정의 근거가 수명입니다. 컴파일러는 모든 참조에 대해 “가리키는 값이 살아 있는 구간"과 “참조가 사용되는 구간"을 계산하고, 후자가 전자를 벗어나면 거부합니다. 지금까지 쓴 모든 참조에서 이 계산은 이미 돌고 있었고, 우리가 표기한 적이 없을 뿐입니다.

표기가 필요해지는 대표적인 순간은 참조를 여러 개 받아 참조를 반환하는 함수입니다.

src/main.rs
fn longer(a: &str, b: &str) -> &str {
    if a.len() > b.len() { a } else { b }
}
컴파일 에러
error[E0106]: missing lifetime specifier
  |
1 | fn longer(a: &str, b: &str) -> &str {
  |              ----     ----     ^ expected named lifetime parameter
  |
  = help: this function's return type contains a borrowed value, but the
          signature does not say whether it is borrowed from `a` or `b`

에러 메시지가 이유를 정확히 말해 줍니다. 반환되는 참조가 a에서 온 것인지 b에서 온 것인지 시그니처만으로는 알 수 없다는 것입니다. 호출한 쪽에서 반환값을 얼마나 오래 써도 되는지 판정하려면 그 정보가 필요합니다. 답은 수명 매개변수입니다.

src/main.rs
fn longer<'a>(a: &'a str, b: &'a str) -> &'a str {
    if a.len() > b.len() { a } else { b }
}

'a를 처음 보면 위압적이지만, 뜻은 한 문장입니다. “반환 참조는 a와 b 중 짧게 사는 쪽보다 오래 쓸 수 없다"는 관계 선언입니다. 수명 표기는 값을 더 오래 살게 만드는 마법이 아니라, 이미 존재하는 관계를 컴파일러와 호출자에게 알리는 문서입니다. 이 관계 덕분에 다음 코드가 정확히 거부됩니다.

src/main.rs
let result;
{
    let b = String::from("잠깐 사는 문자열");
    result = longer("오래 사는 쪽", &b);
} // b가 여기서 해제된다
println!("{result}"); // 컴파일 에러: b보다 오래 쓰려고 했다

다행히 실무에서 수명 표기를 직접 쓰는 일은 생각보다 드뭅니다. 참조를 하나만 받아 참조를 반환하는 함수처럼 관계가 자명한 경우는 컴파일러가 생략 규칙(elision)으로 채워 줍니다. 4편의 count_chars(text: &str)에 표기가 없었던 이유입니다. 구조체에 참조를 담을 때도 수명 표기가 필요해지는데, 기초 단계의 실용 지침은 단순합니다. 구조체 필드는 참조(&str)보다 소유 타입(String)을 기본값으로 하고, 수명 표기가 번지기 시작하면 설계를 다시 보라는 신호로 읽는 것입니다.

모듈: 한 파일을 벗어나기 #

강좌 내내 main.rs 한 파일이었지만, 실제 프로젝트는 코드를 나눕니다. Rust의 단위는 모듈입니다.

src/billing.rs
// src/billing.rs
pub struct Invoice { pub amount: u32 }

pub fn issue(amount: u32) -> Invoice {
    log_issue(amount);
    Invoice { amount }
}

fn log_issue(amount: u32) { /* 모듈 내부 전용 */ }
src/main.rs
// src/main.rs
mod billing; // src/billing.rs를 모듈로 포함

use billing::issue;

fn main() {
    let invoice = issue(50_000);
    println!("발행 금액: {}", invoice.amount);
}

규칙은 세 개면 충분합니다. mod billing;이 파일을 모듈로 연결하고, 모든 항목은 기본 비공개라서 밖에서 쓸 것에만 pub을 붙이며, use로 경로를 줄입니다. 변수가 불변 기본이었듯 가시성도 비공개가 기본입니다. 공개 API가 명시적 선택이 되도록 언어가 같은 방향으로 설계되어 있습니다.

외부 크레이트: cargo add 한 줄 #

Rust의 패키지 단위를 크레이트(crate)라고 부르고, 공용 저장소는 crates.io입니다. 1편에서 만든 Cargo.toml의 의존성 관리도 cargo가 담당합니다.

설치
cargo add rand
src/main.rs
use rand::Rng;

fn main() {
    let dice = rand::rng().random_range(1..=6);
    println!("주사위: {dice}");
}

cargo addCargo.toml에 의존성을 기록하고, 다음 빌드에서 내려받아 컴파일합니다. 정확한 버전은 Cargo.lock에 고정되어 팀 전체가 같은 빌드를 재현합니다. 파이썬의 requirements/lock, 자바스크립트의 package.json/lock과 같은 구도인데, 언어에 처음부터 내장되어 도구가 갈라져 있지 않다는 것이 1편에서 말한 “cargo가 전부"의 의미였습니다. 어떤 크레이트가 표준처럼 쓰이는지는 직렬화의 serde, HTTP 클라이언트의 reqwest, 비동기 런타임의 tokio 정도를 이름만이라도 알아 두면 검색이 쉬워집니다.

강좌를 마치며: 다음 단계 #

9편의 여정을 요약하면 이렇습니다. Rust는 소유권(3편)과 빌림(4편)으로 GC 없이 메모리 안전을 얻고, 열거형과 Option·Result(5·6편)로 “없음"과 “실패"를 타입으로 강제하며, 트레이트(8편)로 추상화하고 수명(이번 편)으로 참조의 유효 범위를 증명합니다. 전부 하나의 원칙, “런타임 사고를 컴파일 에러로 앞당긴다"의 변주입니다.

다음 학습 경로로는 세 가지를 권합니다. 공식 문서인 The Rust Programming Language(통칭 The Book)로 이 강좌가 덜 판 곳(클로저 심화, 스마트 포인터, 동시성)을 채우고, 연습 문제 모음인 rustlings로 컴파일러와의 대화량을 늘리고, 그다음 작은 CLI 도구를 하나 직접 만들어 보는 것입니다. Rust는 읽어서 아는 것과 빌림 검사기를 통과시켜 본 것의 차이가 유난히 큰 언어입니다.

정리 #

  • 수명은 모든 참조에 대해 컴파일러가 항상 계산하는 “유효 구간"입니다. 표기가 필요한 것은 관계가 모호해지는 지점, 대표적으로 참조 여러 개를 받아 참조를 반환하는 함수뿐입니다.
  • 'a는 수명을 늘리는 장치가 아니라 “반환 참조는 입력 중 짧게 사는 쪽을 넘지 못한다"는 관계 선언입니다. 대부분은 생략 규칙이 채워 줍니다.
  • 구조체 필드는 소유 타입이 기본값입니다. 수명 표기가 번지면 설계를 다시 보라는 신호입니다.
  • 모듈은 mod로 연결하고 기본 비공개, 공개할 것에만 pub을 붙입니다. 외부 크레이트는 cargo add 한 줄이고 버전은 Cargo.lock이 고정합니다.
  • 다음 단계는 The Book으로 빈 곳 채우기, rustlings로 연습량 쌓기, 그리고 작은 CLI 도구 하나를 끝까지 만들어 보기입니다. 기초 강좌는 여기까지입니다.
X