Rust에서 사용하는 기호와 연산자

Rust의 기호(=>, ::, ., -> 등)와 연산자(산술, 비교, 사칙연산자, 논리 연산자, 패턴 매치 연산자 등) 는 종류도 많고 다른 언어와 다른 것도 있어 많이 헷갈리므로 이것에 대해 전체적으로 알아보겠습니다.

Ⅰ. 기호

1. =>

match 문에서 패턴 매핑 결과를 지정할 때 사용합니다.

let num = 2;
match num {
1 => println!("one"),
2 => println!("two"),
_ => println!("other"),
}

위에서 2 => println!(“two”)는 num이 2일 때 실행됩니다.

2. ::

경로(네임스페이스) 구분자. 모듈, 구조체, 열거형, 연관함수, 상수 등에 접근할 때 사용합니다.

let color = Color::Red;
let s = String::from("hello");

Color 열거형의 Red variant, String 타입의 from 연관 함수에 접근합니다.

3. .

필드 접근하거나 메서드를 호출할 때 사용합니다.

let s = String::from("hi");
let len = s.len(); // 메서드 호출
let point = (3, 4);
let x = point.0; // 튜플의 첫 번째 요소

s.len()은 s 객체의 len 메서드를, point.0은 튜플의 첫번째 값을 뜻합니다.

4. ->

함수 또는 클로저의 반환 타입을 지정할 때 사용합니다.

fn add_one(x: i32) -> i32 {
x + 1
}

이 함수는 매개변수 x를 받아 i32 타입으로 결과를 반환합니다.

5. :

변수의 타입을 명시하거나 패턴 매칭에서 사용됩니다.

let score: i32 = 100;

변수 score의 타입이 i32임을 명시합니다.

아래는 패턴 매칭에서 :이 사용된 예입니다.
Point { x, y: 0 }에서 y: 0은 y 필드가 정확히 0일 때 매칭됨을 의미합니다.

struct Point { x: i32, y: i32 }

fn main() {
let p = Point { x: 0, y: 7 };
match p {
Point { x, y: 0 } => println!("On the x axis at {x}"),
Point { x: 0, y } => println!("On the y axis at {y}"),
Point { x, y } => println!("On neither axis: ({x}, {y})"),
}
}

6. ;

구문(문장)의 끝을 표시합니다.

let x = 5;
println!("{}", x);

7. ,

목록(튜플, 인수 등)을 구분할 때 사용합니다.

rustlet point = (3, 4);
fn add(x: i32, y: i32) -> i32 { x + y }

8. ..

범위를 표현할 때 사용합니다.

for i in 1..5 {
println!("{}", i); // 1, 2, 3, 4 출력 (5는 포함X)
}

..는 시작값 이상, 끝값 미만의 범위를 의미하며, 끝값을 포함할 때는 ..=으로 사용합니다.

9. &

참조(Reference)를 의미합니다.

let x = 3;
let y = &x;

y는 x의 참조를 가집니다 (메모리 주소 공유).

10. *

역참조(Dereference)를 의미합니다.

let x = 5;
let y = &x;
println!("{}", *y); // y가 참조하는 실제 값(x) 출력

*y는 y가 가리키는 값을 가져옵니다.

11. @

의미: 패턴 매칭에서 값 바인딩

let v = Some(10);
if let id @ Some(x) = v {
println!("id: {:?}", id);
}

id @ Some(x)는 Some(10) 전체를 id에 바인딩합니다.

Ⅱ. 연산자

1. 산술 연산자

연산자설명예시결과 설명
+덧셈let a = 10 + 5;a는 15
뺄셈let b = 10 – 5;b는 5
*곱셈let c = 10 * 5;c는 50
/나눗셈let d = 10 / 2d는 5
%나머지let e = 10 % 3e는 1

2. 비교(관계) 연산자

연산자설명예시결과
==같다a == btrue 또는 false
!=같지 않다a != btrue 또는 false
>크다a > btrue 또는 false
<작다a < btrue 또는 false
>=크거나 같다a >= btrue 또는 false
<=작거나 같다a <= btrue 또는 false

3. 논리 연산자

연산자설명예시결과
&&논리 AND(a > 1 ) && (b < 5)둘 다 true면 true
<code>||</code>논리 OR`(a == 1)
!논리 NOT!is_validtrue->false, false->true

4. 비트 연산자

연산자설명예시결과
&비트 ANDa & b각 비트 AND
<code>|</code>비트 OR`ab`
^비트 XORa ^ b각 비트 XOR
!비트 NOT!a각 비트 반전
<<왼쪽 시프트a << 2비트를 왼쪽 이동
>>오른쪽 시프트a >> 1비트를 오른쪽 이동

5. 복합 할당 연산자

연산자설명예시
+=더해서 할당a +=1;
-=빼서 할당b -=2;
*=곱해서 할당c *= 3;
/=나눠서 할당d /= 2;
%=나머지를 할당e %= 4;

※ Rust는 ++와 –(증가/감소 연산자)를 지원하지 않습니다.

6. 기타 연산자

가. as: 타입 변환

let x: f32 = 10 as f32;

나. 단항 부정 연산자

-a : a의 음수
!a : a의 반전

Ⅲ. 패턴 매칭 관련 연산자 및 문법

match는 여러 패턴에 따라 코드를 분기할 수 있는 핵심 문법으로, C 계열의 switch보다 다양하고 강력한 매칭을 제공합니다.

let value = Some(7);
match value {
Some(x) if x > 5 => println!("greater than five: {}", x),
Some(x) => println!("got: {}", x),
None => println!("no value"),
}

1. | (or 패턴 연산자)

여러 패턴을 한 번에 처리할 수 있습니다.

let x = 1;
match x {
1 | 2 => println!("one or two"),
3 => println!("three"),
_ => println!("other"),
}
  • 이 예시에서 1 | 2는 x가 1 또는 2일 때 모두 해당 arm을 실행합니다.

2. _ (와일드카드/전부수용 패턴)

모든 값을 의미하며, 사용하지 않을 값을 무시할 때 씁니다.

match some_value {
1 => println!("one"),
_ => println!("other"),
}

3. @ (패턴 바인딩 연산자)

패턴과 동시에 값을 바인딩할 때 사용합니다.

let v = Some(42);
match v {
id @ Some(n) if n > 40 => println!("big! {:?}", id),
_ => println!("other"),
}
  • id @ Some(n)은 Some(42) 전체를 id에 바인딩하면서 n 값도 패턴 매칭합니다.

4. if let

특정 패턴만 처리하고 나머지는 무시하고 싶을 때 간결하게 쓸 수 있는 문법입니다.

if let Some(x) = option {
println!("have value: {}", x);
}

5. while let

while let은Rust에서 반복문과 패턴 매칭을 결합해, 어떤 값이 특정 패턴에 계속 일치하는 동안만 루프를 실행하는 구문입니다. 보통 Option, Result, Iterator 등에서 값을 꺼내거나 처리할 때 매우 자주 사용됩니다.

가. Stack처럼 값 꺼내기

let mut stack = vec![1, 2, 3];

while let Some(top) = stack.pop() {
println!("스택에서 꺼냄: {}", top);
}
  • 벡터에서 값을 꺼내는 동작이지만, 동시에 벡터를 스택(LIFO)처럼 쓰는 대표적인 패턴이라 “스택처럼 값 꺼내기”라고 표현한 것입니다.
  • stack.pop()이 벡터의 마지막 요소를 꺼내서(Some) 반환합니다. 벡터가 비어 있다면 None을 반환합니다. Vec은 동적 배열이지만, pop은 이를 스택처럼 사용하게 해 줍니다.
  • 값이 없을 때 None이 반환되고 루프가 끝납니다.

나. Option을 이용한 카운팅

let mut optional = Some(0);

while let Some(i) = optional {
if i > 9 {
println!("9보다 커서 종료!");
optional = None;
} else {
println!("현재 값: {}", i);
optional = Some(i + 1);
}
}
  • optional이 Some(i)에 매칭되는 동안 루프 실행.

다. Result 타입, Iterator 등에도 활용

let mut results = vec![Ok(1), Err("Error"), Ok(2)];

while let Some(res) = results.pop() {
match res {
Ok(v) => println!("ok: {}", v),
Err(e) => println!("err: {}", e),
}
}
  • Result값을 반복적으로 처리하다가 벡터가 비면 자동 종료.

라. while let, if let, match 비교

구문실용 상황특징와 차이점
while let값이 반복적으로 패턴에 매칭될 때패턴 매칭이 실패하면 자동으로 루프 종료
if let단일 조건만 한 번 검사할 때한 번만 검사, 반복X
match모든 경우의 수를 분기 처리할 때모든 possibility처리, 반복X

라이프타임(Lifetime)

Rust는 메모리 안전성을 보장하기 위해 소유권과 함께 라이프타임이라는 개념을 도입합니다. Rust는 라이프타임을 자동으로 추론하지만, 필요한 경우 참조가 유효한 범위인 라이프타임을 컴파일러에게 명시적으로 알려주어 댕글링 참조(dangling reference)를 방지합니다.

댕글링 참조는 메모리가 해제된 이후에도 해당 메모리 위치를 가리키는 참조를 말합니다. 즉, 참조자가 가리키는 메모리가 더 이상 유효하지 않은 상태를 의미합니다. 이러한 댕글링 참조는 프로그램 충돌이나 데이터 손상을 유발할 수 있는 잠재적인 위험을 가지고 있습니다.

1. 라이프타임이 필요한 이유

다음 코드를 보세요.

fn main() {
    let r;
    {
        let x = 5;
        r = &x; // ❌ x는 여기서 소멸됨
    }
    println!("{}", r); // 컴파일 에러!
}
  • x는 내부 블록에서 생성되어 그 블록이 끝나면 해제되는데,
  • r은 그보다 오래 살아 남으므로 유효하지 않은 x를 참조가 됩니다. 이것을 댕글링 참조라고 합니다.
  • Rust는 댕글링 참조를 컴파일 타임에 잡아냅니다.
댕글링 참조(dangling reference)

2. 기본 라이프타임 추론

위와 같이 대부분의 경우, Rust는 라이프타임을 자동으로 추론합니다. 그러나 두 개 이상의 참조가 관련되면 명시적 표기가 필요합니다.

fn longest(x: &str, y: &str) -> &str {
    if x.len() > y.len() { x } else { y }
}

fn main() {
    let s1 = String::from("hello");
    let s2 = String::from("world");
    let result = longest(&s1, &s2);
    println!("The longest string is: {}", result);
}

이 함수는 x, y 두 개 중 어떤 것이 수명이 긴지 알 수가 없어서 컴파일 에러 발생 가능성이 있으므로 라이프타임 명시가 필요합니다.

lifetime 에러

3. 라이프타임 명시하기

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}
  • ‘a는 라이프타임 매개변수입니다.
  • x, y, 반환값에 모두 ‘a를 붙여 “동일한 수명을 가진다”는 것을 명시해야 합니다.
  • 입력 참조가 없어진 상태에서 반환값이 살아 있으면, 다시 말해 반환값이 입력 참조보다 수명이 길면 오류가 발생하기 때문입니다.

4. 구조체에서의 라이프타임

아래와 같이 구조체 필드의 형식이 String인 경우는 Lifetime을 명시하지 않아도 되나,

struct Excerpt {
    part: String,
}

fn main() {
    let text = String::from("Rust는 안전하다.");
    let excerpt = Excerpt { part: text };
    println!("{}", excerpt.part);
}

구조체 필드의 타입이 참조인 경우, 여기서는 &str(문자열 슬라이스), 반드시 라이프타임을 명시해야 합니다.

라이프타임 지정시 구조체명 오른쪽의 <> 안에 라이프타임 매개변수를 ‘a라고 명시했고, 필드의 형식에도 라이프타임 매개변수 ‘a를 추가했습니다.

struct Excerpt<'a> {
    part: &'a str,
}

fn main() {
    let text = String::from("Rust는 안전하다.");
    let excerpt = Excerpt { part: &text[0..4] };
    println!("{}", excerpt.part);
}
  • Excerpt<‘a>는 part 필드가 ‘a 동안 유효함을 의미합니다. 이 말은 결국, “Excerpt 구조체는 자신이 가리키는 문자열(&str)보다 더 오래 살아있을 수 없다”는 제약을 의미하며, “Excerpt 구조체 내의 part 필드는 text가 살아 있는 동안만 유효하다”는 의미도 됩니다.
  • 출력 결과는 Rust입니다.

5. 함수 내 수명 충돌 예시

가. 문제

fn return_ref<'a>(x: &'a str, y: &str) -> &'a str {
    // x // ✔ OK
    return y; // ❌ y는 'a보다 짧은 수명일 수 있음
}
fn main() {
    let s1 = String::from("hello");
    let s2 = String::from("world");
    let r = return_ref(&s1, &s2);
    println!("r = {}", r);
}
  • 함수의 반환값이 ‘a 수명을 가진 참조여야 하므로,
    라이프타임 매개변수 ‘a를 갖지 않은 y는 반환할 수 없습니다.

나. 해결

fn return_ref<'a>(x: &'a str, y: &'a str) -> &'a str {
    // x // ✔ OK
    return y; // ❌ y는 'a보다 짧은 수명일 수 있음
}
  • y를 출력하고자 하는 경우는 y: &str을 y: &’a str로 수정해야 합니다.
  • x를 출력하려고 하는 경우는 x 앞의 주석을 제거하고, return y;를 주석 처리하면 됩니다.

6. ‘static 라이프타임

가. 반환 형식이 String인 경우 문제 없음

아래와 같이 반환 타입이 String인 경우는 문제가 없는데, 출력이 잘 됩니다.

fn get_static_str() -> String {
    String::from("나는 프로그램 전체에서 살아있다!")
}

fn main() {
    let msg = get_static_str();
    println!("{}", msg);
}

나. 반환 형식이 &str인 경우 문제 있음

아래와 같이 반환 형식이 &str인 경우는 실행 시

fn get_static_str() -> &str {
    "나는 프로그램 전체에서 살아있다!"
}

fn main() {
    let msg = get_static_str();
    println!("{}", msg);
}

“라이프타임 매개변수 지정이 기대된다”고 하면서 빌려올 값이 없으므로 ‘static을 추가하거나, 반환 형식을 String으로 하라고 제안합니다.

'static 라이프타임 에러
  • 'static프로그램 전체 수명을 의미
  • 문자열 리터럴 등 컴파일 타임 상수에 주로 사용

다. 수정 코드

&str을 &’static str로 수정하면 문제 없이 출력됩니다.

fn get_static_str() -> &'static str {
    "나는 프로그램 전체에서 살아있다!"
}

fn main() {
    let msg = get_static_str();
    println!("{}", msg);
}
'static을 추가하여 정상적으로 출력된 화면

7. 요약 정리

개념설명
라이프타임 ‘a참조가 얼마나 오래 유효한지 표시하는 표기
함수 수명 명시여러 참조 중 어느 것이 반환되는지(라이프타임)를 명확히 지정
구조체 + 참조참조 필드가 있으면 명시적 라이프타임 필요
‘static프로그램 전체 수명 (전역 문자열 등)
오류 예방 목적댕글링 참조를 컴파일 타임에 방지함

8. 결론

라이프타임은 Rust의 가장 강력하면서도 헷갈릴 수 있는 개념입니다. 하지만 “누가 누구보다 오래 살아야 하는가”를 생각하면서 설계하면, 오히려 런타임 오류 없이 안전한 코드를 보장받을 수 있습니다.

벡터(Vector), 문자열(String), 슬라이스(Slice)

Rust에서는 데이터를 저장하고 조작하기 위해 다양한 컬렉션을 제공합니다. 컬렉션에는 벡터, 문자열, 슬라이스와 해시맵이 있는데, 오늘은 그중 자주 쓰이는 벡터, 문자열, 그리고 슬라이스에 대해 알아보고 다음 편에 해시맵(HashMap)에 대해 알아보겠습니다. 문자열은 Rust의 특이한 요소 중 하나입니다.


1. 벡터 (Vector)

Vec는 가변 길이의 배열로, 가장 자주 쓰이는 컬렉션 중 하나입니다.

fn main() {
    let mut v: Vec<i32> = Vec::new();
    v.push(1);
    v.push(2);
    v.push(3);

    println!("{:?}", v); // [1, 2, 3]
}
  • 고정된 길이의 array와 대비되고, 같은 데이터 타입이어야 하는 것은 동일합니다.
    데이터 형식으로 Vec안에 i32라고 하나만 지정되어 있어서 여러가지 형식 입력이 가능한 tuple과 다릅니다.
  • Vec::new()로 생성하고 push()로 요소 추가. pop()으로 마지막 요소 삭제
    v.pop(); // 마지막 요소 제거
    println!(“{:?}”, v); // [1, 2]
  • 벡터 v는 mut로 가변 변수로 선언해야 데이터 추가, 삭제, 수정 가능
  • println!(“{:?}”, v)로 Debug 포맷으로 벡터 출력.

가. 벡터 초기화

let v = vec![10, 20, 30];
  • vec!를 이용해 여러 요소를 한꺼번에 입력할 수 있습니다. vec!에서 !는 매크로라고 읽습니다.

나. 벡터 접근

fn main() {
    let v = vec![10, 20, 30];
    let third = v[2];
    println!("세 번째 값: {}", third);
    // println!("세 번째 값: {}", v[3]); // panic 발생

    let maybe = v.get(1); // Option 타입 반환

    if let Some(val) = maybe {
        println!("값: {}", val);
    } else {
        println!("없음"); // None일 때 실행
    }
}
  • 벡터의 값을 추출할 때 변수명 다음에 대괄호를 입력하고 그 안에 인덱스를 입력할 수도 있고, .get을 이용할 때는 소괄호를 이용하는데, 둘의 차이점은 대괄호를 이용할 때는 인덱스가 존재하지 않으면 패닉이 발생하나, get을 이용하면 None이 반환됩니다.
  • 위 코드를 실행하면 println!(“세 번째 값: {}”, third);은 실행되는데,
    println!(“세 번째 값: {}”, v[3]);에서 패닉이 발생하므로 이후 코드는 실행되지 않습니다.
  • 따라서, println!(“세 번째 값: {}”, v[3]);을 Ctrl + /를 눌러 주석처리한 다음 실행하면 뒷 부분 get으로 구한 값까지 표시됩니다.
  • get 다음에 index로 범위를 벗어난 5를 입력하고 실행하면 None이 되므로 else문이 실행되어 “없음”이 표시됩니다.

2. 문자열 (String)

가. 문자열의 정의

fn main() {
    let s1 = String::from("Hello");
    let s2 = "World!".to_string();

    println!("{s1}, {s2}");
}
  • String은 가변 문자열 타입으로 Heap에 저장되며,
  • 일반적인 프로그래밍 언어는 큰 따옴표안에 문자열을 입력하는데, Rust는 ① String::from 다음의 괄호안에 큰 따옴표를 이용해 문자열을 입력하거나, ②큰 따옴표 안에 문자열을 입력한 후 .to_string을 추가해서 입력합니다.
  • String::from없이 큰 따옴표 안에 문자열을 넣으면 String이 아니라 다음에 설명하는 문자열 슬라이스가 되어 성격이 다릅니다.
  • 위 코드를 실행하면

나. 문자열 연결

    let s2 = "World!".to_string();
    let s3 = s1 + ", " + &s2;
    println!("{s3}");
    // println!("{s1}");
  • 문자열 연결은 + 연산자를 사용합니다.
  • let s3 = s1 + “, ” + &s2;에서 s2는 빌림(&)을 사용해서 + 후에도 존재하나, s1은 + 후에 s3로 move되었으므로 더 이상 사용할 수 없습니다.

다. 슬라이스 (Slice)

슬라이스는 컬렉션의 일부를 참조하는 타입입니다.

fn main() {
    let s = String::from("hello world");
    let hello = &s[0..5];
    let world = &s[6..11];

    println!("{}, {}", hello, world);
}
  • &s[a..b]는 a부터 b-1까지의 부분 문자열을 참조합니다. 범위 설정과 마찬가지로 b앞에 =을 추가하면 b가 포함됩니다.
  • 슬라이스는 원본이 유효한 동안만 유효합니다.

3. 문자열 리터럴(&str)과 String 비교

Rust에서 &str과 String은 모두 문자열을 나타내는 데 사용되지만, 그 역할과 특징이 다릅니다. &str은 문자열 슬라이스로, 고정 길이이고 값을 직접 소유하지 않습니다. 반면, String은 힙에 할당되어 동적으로 길이를 변경할 수 있으며 값을 소유합니다.

구분&str(문자열 리터럴 )String
저장프로그램 실행 시 정적 메모리(static memory)에 저장됩니다.힙(heap)에 할당되어 동적으로 크기가 변할 수 있습니다.
소유권소유하지 않고 참조만 합니다.데이터를 소유합니다.
가변성변경할 수 없습니다.문자열 내용을 추가, 수정, 삭제할 수 있습니다.
표현&str 또는 “문자열 리터럴” 형태로 표현됩니다. 
예1) let s = “hello world”;
예2) let s = String::from(“hello world”);
let hello = &s[0..5];
String::from(“문자열”) 또는 to_string()과 같은 메서드를 통해 생성합니다. 
예) let s = String::from(“hello world”);

간단하게 말하자면 “hello world”는 문자열 리터럴이고, type은 &str인데, String::from(“hello world”)은 type이 String입니다.
그런데, &str은 &str의 예2처럼 String을 참조하기도 합니다.

Rust의 String은 UTF-8로 인코딩됩니다.

📌 &str과 String 비교 예제 코드

fn main() {
    let s = String::from("hello world");
    let first_word = first_word(&s);
    print!("첫 번째 단어: {}", first_word);
}

fn first_word(s: &str) -> &str {
    let bytes = s.as_bytes();

    for (i, &b) in bytes.iter().enumerate() {
        if b == b' ' {
            return &s[0..i];
        }
    }

    &s[..]
}
  • let s = String::from(“hello world”); : s란 변수에 hello world란 String을 저장합니다.
  • let first_word = first_word(&s); : 변수 s를 참조 형식으로 받아 first_word 함수의 인수로 전달하고, 반환 값을 다시 first_word란 변수에 저장합니다.
  • print!(“첫 번째 단어: {}”, first_word); : 위에서 구한 first_world를 화면에 출력합니다.
  • fn first_word(s: &str) -> &str { : first_word 함수는 인수 s를 &str(String 참조) 타입으로 받고, &str 형식으로 반환합니다.
  • let bytes = s.as_bytes(); : &str인 s를 string slice를 byte slice로 바꿉니다.
  • for (i, &b) in bytes.iter().enumerate() { : 위에서 구한 bytes를 하나씩 옮겨가면서 처리하는데(iter), 인덱스를 같이 반환하도록 enumerate를 같이 사용합니다.
  • if b == b’ ‘ { : b가 b’ ‘, 다시 말해 byte literal ‘ ‘와 같은 경우, 다시 말해 공백을 만나게 되면
  • return &s[0..i]; : 공백 전까지의 글자를 반환합니다.
  • &s[..] : &s가 공백 전까지의 글자이므로 이 글자 전체를 반환합니다. 세미콜론이 없으므로 표현식(expression)이고 반환값입니다.
  • 따라서, 위 코드를 실행하면 hello가 반환됩니다.

🧠 요약

타입설명
Vec<T>가변크기 배열, push, get, pop 지원
StringUTF-8로 인코딩된 힙 문자열
&str슬라이스 타입, 컬렉션 일부 참조
슬라이스소유권 없이 일부분만 안전하게 사용 가능

참조와 빌림 (References & Borrowing)

Rust의 핵심 개념 중 하나는 메모리 안전성을 컴파일 타임에 보장하는 것으로서 소유권 개념을 기반으로 한 참조(reference)와 빌림(borrowing)을 통해 변수의 소유권을 넘기지 않고도 다른 함수나 변수에서 데이터를 읽거나 수정할 수 있게 해줍니다.

Rust의 참조와 빌림은 처음엔 까다로워 보일 수 있습니다. 하지만 이 시스템 덕분에 Rust는 런타임 오류 없이 안전한 코드 작성이 가능합니다. 메모리 안전성을 수동으로 관리할 필요 없이, 컴파일러가 우리의 실수를 미리 잡아줍니다.


1. 불변 참조 (Immutable Reference)

가장 기본적인 형태는 불변 참조입니다. 값의 소유권을 넘기지 않으면서 읽기 권한만 주는 방식입니다.

fn main() {
    let s = String::from("hello");
    print_str(&s);
    println!("main: {}", s);
}

fn print_str(s: &String) {
    println!("func: {}", s);
}

(소유권이 이동(move)된 경우)
위 글에서 살펴본 바와 같이 main 함수의 print_str의 인수를 &s가 아니라 s로 바꾸고, fn print_str의 인수 s를 &String이 아니라 String으로 바꾸고 실행하면 s의 소유권이 함수로 넘어갔고, 함수 종료와 함께 메모리에서 해제됐기때문에 main함수의 println!문에서 에러가 발생합니다.

(참조한(빌린) 경우)
그래서 s를 함수 실행이후에도 사용하려면 &s라고 표현해서 s를 참조(borrow)해야 합니다. print_str 함수는 s의 복사본(clone)을 받는 것이 아니라 참조된 주소값을 받아 사용합니다. 중요한 점은 빌렸기때문에 print_str 함수 실행이 끝난 뒤에도 s는 여전히 main 함수에서 유효하게 사용 가능하다는 점입니다.

주의해야 할 점은 print_str함수로 인수를 전달할 때도 &s라고 &를 붙이고, print_str함수에도 인수의 타입을 지정할 때도 &String이라고 &표시를 해야한다는 것입니다.

핵심 포인트:
불변 참조는 소유권을 넘기지 않으며, 데이터를 읽기만 할 수 있다.

Rust는 동시에 여러 개의 불변 참조를 허용합니다. 읽기만 하는 작업이므로 충돌이 없기 때문입니다.

fn main() {
    let s = String::from("hello");
    let r1 = &s;
    let r2 = &s;
    println!("{}, {}", r1, r2); // OK
}

위 코드를 실행하면 hello, hello라고 화면에 표시됩니다.


2. 가변 참조 (Mutable Reference)

값을 변경하고 싶다면, 가변 참조(mutable reference)를 사용해야 합니다. 불변 참조와 달리, 가변 참조는 값을 수정할 수 있는 권한을 갖습니다.

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

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

이 코드에서 &mut s는 가변 참조입니다. change 함수는 s의 실제 데이터를 수정할 수 있습니다. s를 변경할 수 있어야 하기때문에 let으로 s 변수를 선언할 때도 mut를 붙였습니다.

s.push_str(” world”);는 s라는 변수에 ” world”를 추가하는 것입니다.
따라서, 위 코드를 실행하면 화면에 changed: hello world라고 표시됩니다.

신기한 것은 change 함수에서 s의 참조값을 변경하고 값을 반환하지 않았는데도 main 함수의 s값이 변했다는 것입니다. 가변 참조라 main 함수의 s에 영향을 줍니다.

하지만 Rust는 여기서 아주 엄격한 규칙을 적용합니다:

  • 가변 참조는 동시에 하나만 존재 가능
  • 가변 참조와 불변 참조는 동시에 존재할 수 없음
fn main() {
    let mut s = String::from("hello");
    let r1 = &mut s;
    let r2 = &mut s; // 오류: 두 개의 가변 참조!
    println!("{}, {}", r1, r2);
}

이 규칙은 데이터 경쟁(data race)을 방지하기 위함입니다. 여러 스레드나 함수에서 동시에 하나의 값을 변경하게 되면 런타임 오류가 발생할 수 있는데, Rust는 컴파일 타임에 이를 미리 막아줍니다.


3. 불변과 가변 참조는 함께 못 쓴다

다음 코드를 보겠습니다.

fn main() {
    let mut s = String::from("hello");
    let r1 = &s;
    let r2 = &mut s; // 오류: 두 개의 가변 참조!
    println!("{}, {}", r1, r2);
}

불변 참조가 존재하는 동안에는 어떤 가변 참조도 허용되지 않습니다. 이는 데이터 일관성을 지키기 위한 Rust의 철칙입니다.

위 코드를 실행하면 아래와 같이 “불변 참조로 빌렸기때문에 가변 참조로 빌릴 수 없다”는 에러메시지가 표시됩니다.


4. 스코프와 참조

참조가 유효한 범위(scope)는 매우 중요합니다.

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

    {
        let r1 = &mut s;
        r1.push_str("!");
        println!("r1: {r1}"); // r1의 유효 범위 안에서 사용
    } // r1의 유효 범위가 끝났으므로...


    let r2 = &mut s; // 가능!
    r2.push_str(" again");
    println!("r2: {r2}"); // r2의 유효 범위 안에서 사용
}

Rust는 스코프가 겹치지 않는 한, 가변 참조를 연속적으로 허용합니다. 위 예제를 보면 r1의 유효 범위가 끝났기 때문에 가변 참조 r2가 생성될 수 있습니다.

위 코드를 실행하면 r1.push_str(“!”);으로 s 변수에 !가 추가되고, r2.push_str(” again”);로 s에 ” again”이 추가되어
r1: hello!
r2: hello! again
이 출력됩니다.


5. 참조는 기본적으로 일시적

참조는 값의 복사본이 아니라 원본을 일시적으로 사용하는 방식입니다. 다시 말해 원본보다 오래 살 수 없습니다.

fn main() {
    let reference_to_nothing = dangle();
}

fn dangle() -> &String {
    let s = String::from("hello");
    &s
}

dangle 함수는 컴파일 에러가 발생합니다. 왜냐하면 dangle 함수 실행결과 &s를 반환하는데, 함수가 끝나면서 s가 메모리에서 해제되기 때문에 참조할 변수 s가 없기 때문입니다. Rust는 이런 dangling reference를 허용하지 않습니다.

따라서, 위 코드를 s를 직접 반환하도록 아래와 같이 dangle 함수를 수정해야 합니다.

fn dangle() -> String {
    let s = String::from("hello");
    s
}

반환 값의 형식에서 &를 제거하고, 반환값에서도 &를 제거하는 것입니다.


6. 요약: 빌림 규칙 정리

  • 불변 참조 &T: 여러 개 가능, 읽기 전용
  • 가변 참조 &mut T: 한번에 하나만 가능, 읽기 + 쓰기
  • 불변 참조와 가변 참조는 동시에 사용할 수 없음
  • 참조는 항상 원본(소유자)보다 먼저 소멸해야 함(dangling reference)