← Rust 강의 목록으로
🦀
소유권
소유권 · 선수: 06강 · 소유권 · 선수: 07강

07–08. 참조와 빌림 · 슬라이스 — &str 와 &[T]

빌림(borrowing) 은 소유권을 옮기지 않고 값을 잠깐 빌려 쓰는 방법입니다. & 로 불변 참조, &mut 로 가변 참조. 핵심 규칙은 한 시점에 (불변 참조 N개) OR (가변 참조 1개), 둘이 섞일 수 없습니다. 이 규칙이 어떻게 데이터 경합을 컴파일 타임에 막아 주는지 살펴봅니다. 슬라이스는 컬렉션의 일부를 빌려서 보는 참조형 뷰입니다. &str 은 String 의 일부 또는 문자열 리터럴 그 자체이고, &[T] 는 Vec<T> 나 배열의 일부에 대한 참조입니다. 슬라이스가 어떻게 함수 인자의 유연성을 폭발적으로 늘려 주는지, 그리고 왜 함수 인자에 String 보다 &str 을 받는 것이 idiomatic 인지 살펴봅니다.

Rustborrow참조&T&mut T빌림 검사기slice&str&[T]String
소요 시간
⏱ 약 1.5시간 + 약 1시간
난이도
📊 중급
선수 조건
🎯 06강
결과물
빌림(borrowing) 은 소유권을 옮기지 않고 값을 잠깐 빌려 쓰는 방법입니다. & 로 불변 참조, &mut 로 가변 참조. 핵심 규칙은 한 시점에 (불변 참조 N개) OR (가변 참조 1개), 둘이 섞일 수 없습니다. 이 규칙이 어떻게 데이터 경합을 컴파일 타임에 막아 주는지 살펴봅니다. / 슬라이스는 컬렉션의 일부를 빌려서 보는 참조형 뷰입니다. &str 은 String 의 일부 또는 문자열 리터럴 그 자체이고, &[T] 는 Vec<T> 나 배열의 일부에 대한 참조입니다. 슬라이스가 어떻게 함수 인자의 유연성을 폭발적으로 늘려 주는지, 그리고 왜 함수 인자에 String 보다 &str 을 받는 것이 idiomatic 인지 살펴봅니다.

이 강의에서 배우는 것

  • 1& 와 &mut 로 참조를 만들고 dereference 한다
  • 2빌림 규칙(공유 N or 독점 1) 을 설명한다
  • 3함수 인자로 참조를 받아 소유권 이동을 피한다
  • 4borrow checker 의 에러 메시지를 읽고 해결한다
  • 5참조의 스코프(NLL) 가 어떻게 동작하는지 안다
  • 6&str 과 &[T] 슬라이스를 만들고 인덱싱한다
  • 7함수 인자로 &str 을 받아 호출자 유연성을 높인다
  • 8슬라이스가 (ptr, len) 으로 표현되는 fat pointer 임을 안다
  • 9문자열 리터럴이 &'static str 임을 안다
  • 10슬라이스 범위를 잘못 잡았을 때의 panic 위치를 안다

07. 참조와 빌림 (&T, &mut T, 빌림 규칙)

빌림(borrowing) 은 소유권을 옮기지 않고 값을 잠깐 빌려 쓰는 방법입니다. & 로 불변 참조, &mut 로 가변 참조. 핵심 규칙은 한 시점에 (불변 참조 N개) OR (가변 참조 1개), 둘이 섞일 수 없습니다. 이 규칙이 어떻게 데이터 경합을 컴파일 타임에 막아 주는지 살펴봅니다.

소개

이전 강의의 move 만으로는 함수에 값을 넘기고 나면 호출자에서 못 쓰게 됩니다. 매번 반환으로 돌려받으면 코드가 지저분해지죠. 빌림은 이 문제를 해결하면서도 **데이터 경합을 컴파일 타임에 막는** 핵심 메커니즘입니다.

핵심 개념

1) 두 종류의 참조

  • **&T** — 불변 참조. 읽기만 가능. 한 시점에 여러 개 OK
  • **&mut T** — 가변 참조. 쓰기 가능. 한 시점에 단 하나만

2) 빌림 규칙 (한 줄 요약)

**같은 데이터에 대해 한 시점에는 (불변 참조 N개) 또는 (가변 참조 1개) — 둘 중 하나만 존재 가능**.

3) 왜? — 데이터 경합 방지

한 스레드가 읽고 있는데 다른 스레드가 쓰면 데이터 경합입니다. Rust 는 이걸 **단일 스레드 코드에서조차** 막아 동시성 안전을 단순화합니다. 가변 참조 하나만 허용한다는 규칙이 멀티스레드까지 그대로 확장됩니다.

4) NLL — Non-Lexical Lifetimes

참조의 유효 범위는 **마지막 사용 지점까지** 입니다. 스코프 끝까지가 아닙니다. 이 덕에 같은 변수에 대해 불변 → 가변 → 불변 식의 sequential 사용이 자연스럽게 됩니다.

조합OK?
&x 만 여러 개✓
&mut x 한 개✓
&x + &x✓
&x + &mut x✗
&mut x + &mut x✗

핵심 예제

함수에 참조로 넘기기 — 소유권 이동 없음:

rust
fn print_len(s: &String) {
    println!("len={}", s.len());
}

fn main() {
    let s = String::from("hello");
    print_len(&s);          // 빌려줌
    println!("{}", s);     // OK — s 는 여전히 살아있음
}

가변 참조로 안쪽 값 변경:

rust
fn add_world(s: &mut String) {
    s.push_str(" world");
}

fn main() {
    let mut s = String::from("hello");
    add_world(&mut s);
    println!("{}", s); // hello world
}

동시 가변 참조는 컴파일 에러:

rust
let mut x = 5;
let r1 = &mut x;
let r2 = &mut x; // error[E0499]: cannot borrow `x` as mutable more than once at a time
println!("{} {}", r1, r2);

NLL — 사용 지점 끝나면 새 가변 빌림 가능:

rust
let mut x = 5;
let r1 = &x;
let r2 = &x;
println!("{} {}", r1, r2);  // r1, r2 의 마지막 사용
let r3 = &mut x;             // OK — 위 둘은 더 안 쓰이므로
*r3 += 1;
println!("{}", r3);

자주 하는 실수

Q. 함수 인자에 &mut 를 받는데 호출 시 또 &mut 를 써야 하나요?

A. 네, 호출자도 `add_world(&mut s)` 처럼 명시적으로 가변 참조를 만들어 넘겨야 합니다. 코드만 읽어도 어디서 변경이 일어나는지 보입니다.

Q. 참조에 `.` 를 쓰면 자동으로 dereference 되나요?

A. 네, `s.len()` 처럼 메서드 호출 시 컴파일러가 알아서 deref 합니다. 원시 dereference 는 `*r` 으로 명시.

Q. 빌림 검사기가 너무 빡빡해요. 어떻게 해야 하나요?

A. 에러 메시지를 천천히 읽으세요. Rust 컴파일러는 거의 항상 **수정 제안** 까지 같이 알려줍니다. 그리고 보통 코드 구조를 약간 바꿔(스코프 좁히기, 같은 함수에서 가변·불변 동시 안 쓰기) 풀립니다.

정리

  • 빌림은 소유권 이동 없이 잠깐 빌리는 것 — & 와 &mut
  • 한 시점에 (불변 N) 또는 (가변 1) — 동시에 섞일 수 없음
  • 데이터 경합 방지가 단일 스레드 코드에서부터 강제됨
  • NLL 덕에 마지막 사용 후 새 빌림 가능

과제

  1. 문자열을 받아 길이를 출력하는 함수를 참조 버전과 소유권 버전으로 둘 다 작성 후 호출자에서 차이 확인
  2. Vec<i32> 의 모든 원소에 1 을 더하는 함수를 &mut Vec<i32> 로 작성
  3. 동일 변수에 &mut 를 두 번 만드는 코드를 작성하고 컴파일 에러 메시지를 그대로 옮겨 적기

08. 슬라이스 — &str 와 &[T]

슬라이스는 컬렉션의 일부를 빌려서 보는 참조형 뷰입니다. &str 은 String 의 일부 또는 문자열 리터럴 그 자체이고, &[T] 는 Vec<T> 나 배열의 일부에 대한 참조입니다. 슬라이스가 어떻게 함수 인자의 유연성을 폭발적으로 늘려 주는지, 그리고 왜 함수 인자에 String 보다 &str 을 받는 것이 idiomatic 인지 살펴봅니다.

소개

함수가 String 을 인자로 받으면 호출자는 매번 새 String 을 만들어야 합니다. 문자열 리터럴 `"hello"` 도 직접 못 넘기죠. 슬라이스가 이 문제를 풉니다 — &str 으로 받으면 String 도 리터럴도 모두 통과.

핵심 개념

1) 슬라이스 = 참조형 뷰

슬라이스는 **데이터 + 길이** 를 가리키는 fat pointer (16바이트 — 포인터 8 + 길이 8) 입니다. 데이터를 소유하지 않고 빌려 봅니다.

2) &str — 문자열 슬라이스

  • 문자열 리터럴 `"hello"` 의 타입이 `&'static str` — 프로그램 전체 수명
  • `&s[0..5]` 처럼 String 의 일부도 슬라이스로 만들 수 있음
  • **UTF-8 문자 경계** 가 아닌 인덱스로 자르면 런타임 panic

3) &[T] — 배열·Vec 슬라이스

  • `&v[1..4]` 처럼 부분 슬라이스
  • 함수 인자로 `fn sum(v: &[i32])` 식으로 받으면 배열·Vec 모두 통과

4) 함수 시그니처 idiom

라이브러리 함수를 만들 때 **String 보다 &str, Vec<T> 보다 &[T]** 를 받는 것이 idiomatic. 호출자가 더 다양한 입력을 넘길 수 있고, 불필요한 클론을 피합니다.

핵심 예제

String 과 리터럴 모두 받는 함수 — &str:

rust
fn shout(s: &str) {
    println!("{}!", s.to_uppercase());
}

fn main() {
    let owned = String::from("hello");
    shout(&owned);   // String → &str (Deref)
    shout("world"); // 리터럴 그대로
}

슬라이스 만들기·인덱싱:

rust
fn main() {
    let s = String::from("hello world");
    let hello: &str = &s[0..5];
    let world: &str = &s[6..11];
    println!("{} / {}", hello, world);

    let arr = [10, 20, 30, 40, 50];
    let mid: &[i32] = &arr[1..4]; // [20, 30, 40]
    println!("{:?}", mid);
}

Vec 슬라이스를 받는 일반 함수:

rust
fn sum(v: &[i32]) -> i32 {
    let mut s = 0;
    for x in v { s += x; }
    s
}

fn main() {
    let v = vec![1, 2, 3, 4, 5];
    println!("{}", sum(&v));      // Vec → &[i32]
    println!("{}", sum(&[10, 20])); // 배열 → &[i32]
    println!("{}", sum(&v[1..4]));   // 슬라이스
}

자주 하는 실수

Q. 한글 문자열을 슬라이스하면 panic 나요

A. UTF-8 에서 한글 한 글자는 3바이트입니다. `&"안녕"[0..1]` 은 글자 중간을 자르므로 panic. 문자 단위로 다루려면 `.chars()` 이터레이터를 쓰세요.

Q. String 을 인자로 받으면 안 되나요?

A. 됩니다만, &str 이 더 일반적입니다. 함수가 소유권을 가져야 하는 경우(저장해 둘 때 등) 만 String 으로 받으세요.

Q. 슬라이스의 길이가 컴파일 타임에 정해지나요?

A. 아닙니다. 런타임에 결정되므로 fat pointer 가 길이를 함께 갖고 다닙니다. 그래서 `usize` 한 워드의 길이 정보가 포함돼 일반 참조보다 두 배 크기.

정리

  • 슬라이스는 (포인터, 길이) 의 참조형 뷰
  • &str / &[T] — 문자열 / 임의 타입
  • 함수 시그니처에 &str·&[T] 를 쓰면 더 많은 호출이 자연스럽게 통과
  • UTF-8 경계가 아닌 위치에서 자르면 런타임 panic

과제

  1. 문장에서 첫 단어만 잘라 반환하는 fn first_word(s: &str) -> &str 작성
  2. Vec<i32> 의 최대값을 찾는 함수를 &[i32] 인자로 작성
  3. 한글 문자열에서 .chars().count() 와 .len() 의 차이를 출력해 비교
예제 코드 / 강의 자료

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

GitHub에서 보기 ↗

댓글 0

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

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

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