Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

2.2 참조와 빌림

참조가 왜 필요한가?

앞 챕터에서 소유권을 함수에 전달하면 원래 변수를 사용할 수 없게 된다는 걸 봤습니다:

fn calculate_length(s: String) -> usize {
    s.len()
}

fn main() {
    let s = String::from("hello");
    let len = calculate_length(s);  // s의 소유권이 이동!
    println!("The length of '???' is {}.", len);
    // println!("{}", s);  // 에러! s는 이미 이동됨
}

이 문제를 해결하는 방법이 **참조(reference)**입니다. 참조를 사용하면 소유권을 넘기지 않고 값을 빌려 사용할 수 있습니다.


불변 참조 &T

fn calculate_length(s: &String) -> usize {  // &String: String의 참조를 받음
    s.len()
}

fn main() {
    let s = String::from("hello");
    let len = calculate_length(&s);  // &s: s의 참조를 전달
    println!("The length of '{}' is {}.", s, len);  // s는 여전히 유효!
}

&s는 “s를 참조하지만 소유하지는 않는다“는 의미입니다. 소유권이 이동하지 않으므로, 참조가 스코프를 벗어나도 s는 해제되지 않습니다.

메모리 구조

 스택               힙
┌─────────┐        ┌─────────────┐
│   s     │        │             │
│  ptr ───┼───────►│  "hello"    │
│  len: 5 │        │             │
│  cap: 5 │        └─────────────┘
└─────────┘               ▲
     ▲                    │
┌─────────┐               │
│   r (&s)│               │
│  ptr ───┼───────────────┘
└─────────┘
  (s를 가리키는 참조)

참조 자체는 스택에 있고, s의 스택 데이터(포인터, 길이, 용량)를 가리킵니다.

참조는 불변이 기본

#![allow(unused)]
fn main() {
fn try_to_change(s: &String) {
    s.push_str(", world");  // 컴파일 에러!
    // error[E0596]: cannot borrow `*s` as mutable, as it is behind a `&` reference
}
}

참조를 통해서는 기본적으로 값을 변경할 수 없습니다. 변경하려면 가변 참조를 써야 합니다.


가변 참조 &mut T

fn change(s: &mut String) {
    s.push_str(", world");  // OK! 가변 참조로 변경 가능
}

fn main() {
    let mut s = String::from("hello");  // mut 키워드 필요!
    change(&mut s);  // &mut s: 가변 참조 전달
    println!("{}", s);  // "hello, world"
}

가변 참조를 사용하려면:

  1. 변수 자체가 mut으로 선언되어야 함
  2. 참조를 &mut으로 만들어야 함
  3. 함수 인자 타입이 &mut T여야 함

TypeScript와 비교:

// TypeScript: 객체는 기본적으로 가변
function change(s: string[]): void {
    s.push("world");  // 그냥 변경 가능
}

const arr = ["hello"];
change(arr);
console.log(arr);  // ["hello", "world"]

// readonly로 불변 강제
function readOnly(s: readonly string[]): void {
    s.push("world");  // 타입 에러! (컴파일 타임)
}

빌림 규칙 (Borrowing Rules)

Rust의 빌림 규칙은 참조가 항상 유효함을 보장합니다:

규칙 1: 가변 참조는 한 번에 하나만

fn main() {
    let mut s = String::from("hello");

    let r1 = &mut s;
    let r2 = &mut s;  // 컴파일 에러!
    // error[E0499]: cannot borrow `s` as mutable more than once at a time

    println!("{}, {}", r1, r2);
}

왜? 두 개의 가변 참조가 동시에 존재하면 데이터 레이스(data race)가 발생할 수 있습니다.

데이터 레이스: 두 포인터가 같은 데이터를 동시에 접근하고, 적어도 하나가 쓰기 작업을 하며, 접근을 동기화하는 메커니즘이 없는 상황.

Node.js는 싱글 스레드여서 이 문제를 신경 쓸 필요가 없었습니다. Rust는 멀티스레드 환경을 기본으로 고려합니다.

// 해결법 1: 스코프 분리
fn main() {
    let mut s = String::from("hello");

    {
        let r1 = &mut s;
        println!("{}", r1);
    }   // r1의 스코프 끝

    let r2 = &mut s;  // OK! r1은 이미 스코프를 벗어남
    println!("{}", r2);
}

// 해결법 2: NLL (Non-Lexical Lifetimes) — 마지막 사용 후 참조 종료
fn main() {
    let mut s = String::from("hello");
    let r1 = &mut s;
    println!("{}", r1);  // r1의 마지막 사용
    // r1은 여기서 더 이상 사용되지 않으므로 유효 범위 종료

    let r2 = &mut s;  // OK!
    println!("{}", r2);
}

규칙 2: 불변 참조와 가변 참조의 공존 불가

fn main() {
    let mut s = String::from("hello");

    let r1 = &s;     // 불변 참조
    let r2 = &s;     // 불변 참조 (여러 개 OK)
    let r3 = &mut s; // 컴파일 에러!
    // error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable

    println!("{}, {}, {}", r1, r2, r3);
}

왜? 불변 참조를 사용하는 코드는 값이 변경되지 않을 거라고 기대합니다. 가변 참조가 동시에 존재하면 이 기대가 깨집니다.

// NLL 덕분에 이건 OK
fn main() {
    let mut s = String::from("hello");

    let r1 = &s;
    let r2 = &s;
    println!("{} and {}", r1, r2);  // r1, r2의 마지막 사용

    // r1, r2가 더 이상 사용되지 않으므로
    let r3 = &mut s;  // OK!
    println!("{}", r3);
}

규칙 정리

허용 여부상황
✅ 허용불변 참조 여러 개 동시에
✅ 허용가변 참조 하나만 (불변 참조 없을 때)
❌ 불허가변 참조 여러 개 동시에
❌ 불허불변 참조 + 가변 참조 동시에

댕글링 참조 방지

Rust의 컴파일러는 댕글링 참조(dangling reference)를 방지합니다:

#![allow(unused)]
fn main() {
fn dangle() -> &String {  // 컴파일 에러!
    let s = String::from("hello");
    &s  // s의 참조를 반환하려고 시도
}   // s는 여기서 drop됨! — 반환된 참조가 가리키는 메모리가 해제됨

// error[E0106]: missing lifetime specifier
// help: this function's return type contains a borrowed value,
//       but there is no value for it to be borrowed from
}

해결책: 소유권을 반환

#![allow(unused)]
fn main() {
fn no_dangle() -> String {
    let s = String::from("hello");
    s   // 소유권을 반환 — 메모리가 해제되지 않음
}
}

실용적인 참조 패턴

패턴 1: 읽기만 할 때 &T

struct Block {
    index: u64,
    hash: String,
    data: String,
}

impl Block {
    // self를 참조로 받음 — 소유권 이동 없음
    fn get_hash(&self) -> &str {
        &self.hash
    }

    fn get_data(&self) -> &str {
        &self.data
    }

    fn is_valid(&self) -> bool {
        !self.hash.is_empty()
    }
}

fn print_block(block: &Block) {  // 참조로 받음
    println!("Block {}: {}", block.index, block.hash);
}

fn main() {
    let block = Block {
        index: 0,
        hash: String::from("abc123"),
        data: String::from("genesis"),
    };

    print_block(&block);  // 소유권 이동 없음
    println!("Hash: {}", block.get_hash());
    println!("Valid: {}", block.is_valid());
    // block은 여전히 유효
}

패턴 2: 수정이 필요할 때 &mut T

struct Blockchain {
    blocks: Vec<Block>,
}

impl Blockchain {
    // &mut self: 자신을 가변으로 빌림
    fn add_block(&mut self, block: Block) {
        self.blocks.push(block);
    }

    fn get_last_block(&self) -> Option<&Block> {
        self.blocks.last()
    }

    fn len(&self) -> usize {
        self.blocks.len()
    }
}

fn main() {
    let mut chain = Blockchain { blocks: vec![] };

    chain.add_block(Block {
        index: 0,
        hash: String::from("000abc"),
        data: String::from("genesis"),
    });

    if let Some(last) = chain.get_last_block() {
        println!("Last block: {}", last.index);
    }

    println!("Chain length: {}", chain.len());
}

패턴 3: 여러 필드를 동시에 가변 참조

struct Point {
    x: f64,
    y: f64,
}

fn main() {
    let mut p = Point { x: 1.0, y: 2.0 };

    // 구조체의 서로 다른 필드를 동시에 가변 참조 — OK
    let rx = &mut p.x;
    let ry = &mut p.y;
    *rx += 1.0;
    *ry += 1.0;
    println!("({}, {})", p.x, p.y);
}

역참조 연산자 *

참조를 통해 실제 값에 접근하려면 *(역참조)를 씁니다:

fn main() {
    let x = 5;
    let r = &x;       // r은 x의 참조

    println!("{}", r);   // 자동 역참조 — 5 출력
    println!("{}", *r);  // 명시적 역참조 — 5 출력

    let mut y = 10;
    let ry = &mut y;
    *ry += 1;            // 역참조 후 수정
    println!("{}", y);   // 11
}

대부분의 경우 Rust가 자동으로 역참조합니다(deref coercion). . 연산자는 자동으로 역참조합니다:

fn main() {
    let s = String::from("hello");
    let r = &s;

    // 다음 두 줄은 동일
    println!("{}", r.len());    // 자동 역참조
    println!("{}", (*r).len()); // 명시적 역참조
}

함수에서 참조 반환 시 주의사항

// OK: 입력 참조의 수명을 그대로 반환
fn first_word(s: &str) -> &str {
    let bytes = s.as_bytes();
    for (i, &byte) in bytes.iter().enumerate() {
        if byte == b' ' {
            return &s[..i];
        }
    }
    s
}

fn main() {
    let sentence = String::from("hello world");
    let word = first_word(&sentence);
    // sentence.clear();  // 에러! word가 sentence를 참조 중
    println!("First word: {}", word);
}

반환되는 참조의 수명은 입력 참조의 수명과 연결됩니다. sentence가 살아있는 동안만 word를 사용할 수 있습니다.


TypeScript/Node.js 개발자를 위한 핵심 차이점

// TypeScript: 참조는 항상 공유 가능
class Block {
    data: string;
    constructor(data: string) { this.data = data; }
}

const block = new Block("genesis");
const ref1 = block;  // 같은 객체를 가리킴
const ref2 = block;  // 같은 객체를 가리킴
ref1.data = "modified";
console.log(ref2.data);  // "modified" — 같은 객체!
// Rust: 불변 참조는 여러 개, 가변 참조는 하나만
let mut block = Block { data: String::from("genesis") };

let ref1 = &block;        // 불변 참조
let ref2 = &block;        // 불변 참조 — OK
// let ref3 = &mut block; // 에러! 불변 참조가 살아있는 동안 불가

// 불변 참조를 다 쓴 후
let ref3 = &mut block;    // OK
ref3.data = String::from("modified");

이 규칙이 멀티스레드 데이터 레이스를 컴파일 타임에 방지합니다.


요약

  • &T: 불변 참조 — 소유권 이동 없이 읽기만
  • &mut T: 가변 참조 — 소유권 이동 없이 읽기/쓰기
  • 빌림 규칙: 불변 참조 여러 개 OR 가변 참조 하나 (동시에 둘 다 안 됨)
  • 댕글링 참조: 컴파일러가 방지
  • *: 역참조 연산자 (. 연산자는 자동 역참조)
  • 함수 인자는 가능하면 &T&str로 받아서 소유권 이동 방지

다음 챕터에서는 슬라이스(slice)를 배웁니다.