Skip to main content

Command Palette

Search for a command to run...

Rust Basic[변수~라이프타임 입문하기]

Updated
14 min read
Rust Basic[변수~라이프타임 입문하기]

일반 프로그래밍

변수

러스트에서 변수는 기본적으로 불변이다. 따라서 let x = 5;와 같이 선언하면 이는 기본적으로 불변이다. 따라서 이 값을 바꾸고자 한다면 컴파일이 불변성 에러를 뱉을 것이다.

따라서 변수를 가변으로 만들고자 한다면 let mut x = 5;처럼 반드시 mut 키워드를 붙여야 한다.

상수

사실 불변 변수라면 그냥 상수와 구분되지 않아도 되는 것이 아닌가 싶지만 상수는 let 대신 const 키워드로 선언하고 mut와 함께 사용할 수 없다.

그리고 기본적으로 불변 변수는 런타임에 바인딩되지만 상수는 컴파일 타임에 인라인된다. 불변 변수와 상수는 비슷한 것이라기보단 변수에 가변 옵션을 꺼놓은 것과 비슷한다. 유효한 범위도 불변 변수와 상수는 다르다.

그리고 변수는 타입 생략이 가능하지만 상수는 언제나 선언해주어야 한다.

const THREE_HOURS_IN_SECONDS: u32 = 60 * 60 * 3

+) 섀도잉 러스트에서 같은 이름으로 변수를 다시 선언할 수 있으나 이건 mut 키워드로 가변 변수의 값을 바꿔주는 것과 다르다.

같은 이름으로 선언하는 것은 변수를 다시 만드는 것이고 값을 바꿔주는 것과는 다르다.

따라서 기본적으로 타입 간 변경이 불가능한 가변 변수에서

let mut spaces = " ";
spaces = spaces.len();

로 서로 다른 타입으로 값을 바꾸고자 하면 컴파일 에러가 일어나지만

let spaces = " ";
let spaces = spaces.len();

는 에러를 일으키지 않는다는 것이다.

함수

러스트에서 함수는 fn 키워드로 생성하고 -> 뒤에 반환값의 타입을 선언한다. 그리고 세미콜론 없이 값을 적어줘서 반환값을 지정할 수 있는데

fn five() -> i32 {
    5
}

이러한 식이다. 반환값의 타입이 일치하지 않으면 이 또한 컴파일 에러를 일으킨다.

조건문

딱히 특별할 건 없고

fn main() {
    let number = 6;

    if number % 4 == 0 {
        println!("number is divisible by 4");
    } else if number % 3 == 0 {
        println!("number is divisible by 3");
    } else if number % 2 == 0 {
        println!("number is divisible by 2");
    } else {
        println!("number is not divisible by 4, 3, or 2");
    }
}

이런 식으로 쓰면 되겠다.

반복문

loop 키워드는 코드를 명시적으로 그만두라고 하기 전까지 반복문이 계속 돌아간다. 이걸 어디 쓰냐 하면 어떤 스레드가 실행 완료되었는지 검사하는 데에 쓸 수 있다. 대체로 실패할지도 모르는 연산을 재시도하는 경우로 보면 되겠다. break 이후에 값을 반환할 수도 있는데 그것은

fn main() {
    let mut counter = 0;

    let result = loop {
        counter += 1;

        if counter == 10 {
            break counter * 2;
        }
    };

    println!("The result is {result}");
}

이런 식으로 써주면 되겠다.

그리고 라벨을 지정해서 특정한 루프에 적용되도록 할 수 있는데

fn main() {
    let mut count = 0;
    'counting_up: loop {
        println!("count = {count}");
        let mut remaining = 10;

        loop {
            println!("remaining = {remaining}");
            if remaining == 9 {
                break;
            }
            if count == 2 {
                break 'counting_up;
            }
            remaining -= 1;
        }

        count += 1;
    }
    println!("End count = {count}");
}

이런 식으로.

while이나 for 문도 다른 프로그래밍 언어랑 크게 다를 것은 없다.

fn main() {
    let a = [10, 20, 30, 40, 50];
    let mut index = 0;

    while index < 5 {
        println!("the value is: {}", a[index]);

        index += 1;
    }
}
fn main() {
    let a = [10, 20, 30, 40, 50];

    for element in a {
        println!("the value is: {element}");
    }
}

이런 식으로도 쓸 수 있다.

fn main() {
    for number in (1..4).rev() {
        println!("{number}!");
    }
    println!("LIFTOFF!!!");
}

구조체 & 열거형

구조체

구조체 정의하기

러스트에서 구조체도 C에서와 크게 다르지 않다.

struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
}

이렇게 써주면 되겠다.

사용도

fn main() {
    let user1 = User {
        active: true,
        username: String::from("username123"),
        email: String::from("email@email.com"),
        sign_int_count: 1,
    };
}

이런 식으로 써주면 되겠다.

똑같이 mut 키워드로 가변성을 지정해줄 수 있는데

fn main() {
    let mut user1 = User {
        active: true,
        username: String::from("username");
        email: String::from("email.com");
        sign_int_count: 1,
    };
    user1.email = String::from("other.com");
}

가변성은 인스턴스 전체가 지니게 되어서 모든 필드가 가변적이게 된다. 특정 필드만 가변 나머지는 불변 이렇게 만드는 것은 불가능하다.

다음 코드를 보자.

fn build_user(email: String, username: String) -> User {
    User {
        active: true,
        username: username,
        email: email,
        sign_in_count: 1,
    }
}

여기서 변수명과 필드명이 같아서 username: username과 같이 써주었는데 이걸 생략할 수 있다. 즉

fn build_user(email: String, username: String) -> User {
    User {
        active: true,
        username,
        email,
        sign_in_count: 1,
    }
}

이런 식으로 쓸 수 있다.

구조체 업데이트 문법

또 기존 인스턴스를 이용해 새 인스턴스를 만들 때 축약된 구조체 업데이트 문법을 사용할 수 있는데

fn main() {
    // --생략--

    let user2 = User {
        active: user1.active,
        username: user1.username,
        email: String::from("another@example.com"),
        sign_in_count: user1.sign_in_count,
    };
}

가 있다고 할 때 이것을

fn main() {
    // --생략--

    let user2 = User {
        email: String::from("anotheremail@example.com"),
        ..user1
    };
}

이런 식으로 쓸 수 있다.

튜플 구조체

러스트는 튜플같은 구조체도 지원하는데 이를 튜플 구조체라고 부른다. 튜플 구조체는 이름은 있지만 필드가 없고, 튜플처럼 인덱스로 접근하는 구조체이다.

struct Color(i32, i32, i32);
struct Point(i32, i32, i32);

fn main() {
    let black = Color(0, 0, 0);
    let origin = Point(0, 0, 0);

    println!("Black: ({}, {}, {})", black.0, black.1, black.2);
    println!("Origin: ({}, {}, {})", origin.0, origin.1, origin.2);
}

이런 식으로 쓸 수 있다.

유닛 구조체

필드가 없는 구조체도 있는데 이를 유닛 구조체라고 부른다.

struct AlwaysEqual; // 필드가 없다!
fn main() {
    let subject = AlwaysEqual; 
    let subject2 = AlwaysEqual;

    if subject == subject2 {
        println!("They are equal!");
    }
}

구조체에 디버깅 출력

러스트는 구조체에 디버깅 출력을 할 수 있는 Debug 트레이트를 제공한다. 그런데 구조체에 해당 기능을 적용하려면 명시적인 동의가 필요하다. #[derive(Debug)]를 구조체 정의 위에 붙여주면 된다.

그러면

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let rect = Rectangle {
        width: 30,
        height: 50,
    };

    println!("rect is {:#?}", rect); // {:#?}는 pretty print
    println!("rect is {:?}", rect); // {:?}는 일반 print
 }

이런 식으로 구조체를 출력할 수 있다.

메서드

메서드는 구조체에 정의된 함수로, 구조체의 인스턴스에 대해 호출할 수 있다.

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn area (&self) -> u32 {
        self.width * self.height
    }
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };

    println!("The area of the rectangle is {} square pixels.", rect1.area());
    println!("rect1 is {:#?}", rect1);
}
    }
}

로 쓸 수 있는데

Rectangle 의 컨택스트에 impl 블록을 사용하여 메서드를 정의한다.

연관 함수

impl 블록 내에 구현된 모든 함수를 연관 함수라고 부른다. 이건 impl 뒤에 나오는 타입과 모두 연관되어 있기 때문에 이런 이름이 붙었다.

메서드가 아닌 연관 함수도 존재하는데 이건 구조체의 새 인스턴스를 반환하는 생성자로 자주 활용된다.

여러개의 impl 블록

각 구조체는 여러 개의 impl 블록을 가질 수 있다. 예컨대

impl Rectangle {
    fn new(width: u32, height: u32) -> Rectangle {
        Rectangle { width, height }
    }
}

impl Rectangle {
    fn is_square(&self) -> bool {
        self.width == self.height
    }
}

이런 식으로 여러 개의 impl 블록을 정의할 수 있다. 다만 여기서 이렇게 쓰는 게 유리하지는 않다.

열거형

열거형은 어떤 값이 여러 개의 가능한 값의 집합 중 하나라는 것을 나타내는 방법을 제공한다. 열거형으로 IPv4와 IPv6 주소를 표현할 수 있다.

enum IpAddrKine {
    v4,
    v6,
}

let four = IpAddrKind::v4;
let six = IpAddrKind::v6;

이런 식으로 열거형을 정의할 수 있다. 이러면 이제 IpAddrKind는 코드 어디에서나 쓸 수 있는 커스텀 데이터 타입이 된다.

또 다른 예시로 qiskit이라는 양자 컴퓨팅 프레임워크에서 양자 게이트 관련 상태들이 이런 식으로 열거형으로 정의되어 있다.

열거형을 사용하면 코드를 간결하게 만들 수 있는데

enum IpAddr {
    V4(String),
    V6(String),
}

let home = IpAddr::V4(String::from("127.0.0.1"));
let loopback = IpAddr::V6(String::from("::1"));

이 코드를 구조체로 해결하려고 하면 꽤나 길어진다.

Option 열거형

Option 열거형은 값이 있거나 없을 수도 있는 상황을 나타낸다. 널로 써도 될 것 같지만, 널은 런타임에 오류를 발생시킬 수 있기 때문에 Option 열거형을 사용한다.

요약하자면 널 값을 널이 아닌 것처럼 사용하려고 하면 여러 종류 에러가 나타날 수 있다는 것인데, 이것은 개념적인 문제에서 나타난다기보다 구현적인 문제에 의해 나타난다.

러스트에는 널이 없다. 다만 값의 존재 또는 부재를 나타내는 Option 열거형이 있다. Option<T>로 나타낸다.

enum Option<T> {
    Some(T),
    None,
}

와 같이 정의되어있다. 다시 구현할 필요는 없다! 여기서 T는 제네릭 타입 매개변수로, 구체적인 T의 타입을 집어넣는 것이 Option이 어떤 타입의 값을 가질 수 있는지 나타낸다.T 자체에는 어떤 타입이든 들어갈 수 있다.


소유권

러스트 소유권 이해하기 를 참고할 수 있다.

소유권

러스트의 소유권 시스템은 메모리 안전성을 보장하는 핵심 개념이다. 소유권 규칙이라는 것이 존재하는데, 이는 다음과 같다:

  1. 러스트에서 각각의 값은 소유자가 정해져 있다.

  2. 한 값의 소유자는 동시에 여럿 존재할 수 없다.

  3. 소유자가 스코프 밖으로 벗어날 때, 값은 버려진다.

여기서 3번에 스코프라는 것에 대해 나오는데, 스코프란 프로그램 내에서 아이템이 유효한 범위를 말한다. 러스트에서는 중괄호 {}로 묶인 블록이 스코프를 나타낸다. 중괄호가 끝나는 시점에서 러스트는 drop이라는 함수를 호출하여 해당 변수의 메모리를 해제한다. 즉, 스코프가 끝나면 해당 변수는 더 이상 유효하지 않게 된다.

예제로 보자면

{ // s가 선언되기 전이다. 아직 유효하지 않다
    let s = "Hello"; // s가 선언되어 여기서부터 유효하다
    // s로 어떠한 작업을 한다.
} // 스코프가 종료되어 s가 유효하지 않다.

이런 식으로 스코프가 종료되면 해당 변수는 더 이상 유효하지 않게 된다.

String

바로 위에서 "Hello"라는 문자열을 사용했는데, 이는 문자열 리터럴로, 컴파일 타임에 결정되는 불변 문자열이다. 하지만 러스트에서는 동적으로 크기가 변할 수 있는 문자열을 다루기 위해 String 타입을 제공한다.

let s = String::from("Hello");

로 쓰면 가변적인 문자열을 만들 수 있다. 이 문자열은 힙에 저장되며, 런타임에 크기가 결정된다. 이런 이야기가 왜 나오나 싶지만 뒤의 소유권 개념에서 다시 나온다.

소유권 이동

let s1 = String::from("Hello");
let s2 = s1;

와 같은 코드를 작성했다고 하자. 그런데 s2s1을 할당하면 스택에 있는 데이터가 복사되지 포인터가 가리키는 힙 영역의 데이터까지 복사되지는 않는다.

그래서 s1이 여전히 살아있다면 s1s2가 같은 힙 메모리를 가리키게 된다. 이런 경우 중복 해제 문제가 발생할 수 있다.

그래서 러스트에서는 소유권 이동이라는 개념을 제공하는데, s1이 더 이상 유효하지 않고, s2만 유효하다고 판단하는 것이다.

위에서 소유권 규칙을 설명했는데, 한 값의 소유자는 동시에 여럿 존재할 수 없다는 규칙이 바로 이 소유권 이동을 통해 지켜진다. 즉, s1은 더 이상 유효하지 않게 되고, s2s1의 소유권을 가지게 된다.

이것은 복사와는 다른 개념이다. 스택 데이터만 복사되는 것을 얕은 복사, 힙 데이터까지 복사되는 것을 깊은 복사라고 하는데, 얕은 복사에서는 위와 같이 중복 해제 문제가 발생할 수 있다고 했다. 그러나 깊은 복사에서 힙 데이터까지 복사하는 것은 상당히 비효율적이 될 수 있다. 따라서 러스트에서는 소유권 이동을 통해 소유권을 이전하는 방식을 채택하고 있다.

힙 데이터까지 깊은 복사를 하려면 clone 메서드를 사용해야 한다.

let s1 = String::from("Hello");
let s2 = s1.clone(); // 깊은 복사

이런식으로.

그런데

let x = 1;
let y = x; // x의 값을 y에 복사
println!("x: {}, y: {}", x, y); // x와 y 모두 1

를 실행하면 여전히 x가 유효하다. 아까 우리가 살펴본 소유권 개념으로는 그래서는 안 될 것 같은데, 왜 그럴까?

그건 소유권 개념은 힙 메모리 관리에 사용되는 개념이므로 저렇게 컴파일 타임에 크기를 알 수 있는 고정 크기 값들은 스택에 저장되기 때문이다. 일반적인 스칼라 타입이나 튜플은 이런 식의 복사가 가능하다.


fn main() {
    let s1 = gives_ownership(); // gives_ownership이 반환 값을 s1으로
    let s2 = String::from("Hello"); // s2가 스코프 안으로

    let s3 = takes_and_gives_back(s2); // s2가 함수로 값을 이동
    // 함수도 값을 s3으로 이동
} // s3이 스코프 밖으로 벗어나면서 drop
// s1도 스코프 밖으로 벗어나고 버려진다

fn gives_ownership() -> String {
    // gives_ownership은 자신의 반환 값을 자신의 호출자 함수로
    // 이동시키게 된다
    let some_string = String::from("yours"); //some_string이 스코프 안으로

    some_string // some_string이 반환되고 호출자 함수로 이동
}

fn takes_and_gives_back(a_string: String) -> String {
    // a_string이 스코프 안으로
    a_string // a_string이 반환되고 호출자 함수로 이동
}

위 예제를 통해 소유권을 더 깊이 이해할 수 있다.

참조와 대여

만약 함수에 값을 넘겨주고 싶지만 소유권을 넘겨주고 싶지 않다면, 참조를 사용해야 한다. 참조는 & 기호를 사용하여 생성한다.

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

    let (s2, len) = calculate_length(s1);

    println!("The length of '{}' is {}.", s2, len);
}

fn calculate_length(s: String) -> (String, usize) {
    let length = s.len(); // len()은 String의 길이를 반환합니다

    (s, length)
}

이걸 어떻게 바꿀 수 있을까?

fn main() {
    let s1 = String::from("hello");
    let len = calculate_length(&s1); // s1의 참조를 넘겨줍니다
    println!("The length of '{}' is {}.", s1, len);
}

fn calculate_length(s: &String) -> usize {
    s.len() // s의 길이를 반환합니다
}

이런 식으로 &를 사용하여 참조를 넘겨줄 수 있다. 이 경우 소유권은 이동하지 않고, 함수 내에서 s의 값을 읽을 수만 있다.

가변 참조자

참조자도 mut 키워드를 사용하여 가변 참조자를 만들 수 있다. 가변 참조자는 값을 변경할 수 있는 참조자이다.

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

    change(&mut s); // 가변 참조자를 넘겨줍니다
    println!("Changed string: {}", s);
}

fn change(s: &mut String) {
    s.push_str(", world!"); // s의 값을 변경합니다
}

이런 식으로 가변 참조자를 사용하여 값을 변경할 수 있다. 그런데 가변 참조자는 큰 제약사항이 있는데, 어떤 값에 대해 가변 참조자가 있다면 그 값에 대한 참조자는 더 이상 만들 수 없다. 즉, 가변 참조자는 동시에 하나만 존재할 수 있다. 이는 데이터 경합을 방지하기 위한 것이다.

댕글링 참조

댕글링 참조는 참조자가 유효하지 않은 메모리를 가리키는 경우를 말한다. 러스트에서는 댕글링 참조를 방지하기 위해 컴파일 타임에 참조의 유효성을 검사한다.

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

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

    &s
}

이런 코드는 컴파일 에러를 발생시킨다. 왜냐하면 s는 함수가 끝나면 스코프 밖으로 벗어나면서 메모리가 해제되기 때문이다.

이걸 유효하게 하려면

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

    s // 소유권을 반환합니다
}

이런 식으로 소유권을 반환해야 한다. 즉, 참조를 반환하는 것이 아니라 소유권을 반환하는 것이다. 이렇게 하면 s가 스코프 밖으로 벗어나면서 메모리가 해제되지 않고, no_dangle 함수의 호출자가 s의 소유권을 가지게 된다.

슬라이스

슬라이스는 배열이나 문자열의 전체를 참조하는 것이 아니라 그 일부를 참조하는 것을 말한다.

문자열 슬라이스의 예제를 살펴보자.

let s = String::from("hello world");
let hello = &s[0..5]; // "hello" 부분 문자열을 슬라이스로 참조
let world = &s[6..11]; // "world" 부분 문자열을 슬라이스로 참조

이런 식으로 문자열의 일부를 슬라이스로 참조할 수 있다.


제네릭 & 라이프타임

제네릭

제네릭을 사용하면 함수 시그니처나 구조체 아이템에 다양한 데이터 타입을 사용할 수 있다. 제네릭을 사용하여 아래 예제의 두 함수를 합칠 수 있다.

fn largest_i32(list: &[i32]) -> &i32 {
    let mut largest = &list[0];

    for item in list {
        if item > largest {
            largest = item;
        }
    }

    largest
}

fn largest_char(list: &[char]) -> &char {
    let mut largest = &list[0];

    for item in list {
        if item > largest {
            largest = item;
        }
    }

    largest
}

이 두 함수를

fn largest<T> (list: &[T]) -> &T {
    let mut largest = &list[0];
    T:PartialOrd; // T가 PartialOrd 트레이트를 구현해야 한다.
    for item in list {
        if item > largest {
            largest = item;
        }
    }

    largest
}

이런 식으로 제네릭을 사용하여 하나의 함수로 합칠 수 있다. 여기서 T는 제네릭 타입 매개변수로, 함수가 호출될 때 구체적인 타입으로 대체된다. 다만 TPartialOrd 트레이트를 구현해야 한다는 제약 조건을 추가해야 한다. 이는 > 연산자를 사용할 수 있도록 하기 위함이다. i32char는 모두 PartialOrd를 구현하고 이므로 이 함수는 두 타입 모두에 대해 작동한다.

제네릭 구조체

struct Point<T> {
    x: T,
    y: T,
}

fn main() {
    let integer_point = Point { x: 5, y: 10 };
    let float_point = Point { x: 1.0, y: 4.0 };

    println!("Integer Point: ({}, {})", integer_point.x, integer_point.y);
    println!("Float Point: ({}, {})", float_point.x, float_point.y);
}

이런식으로 제네릭 구조체를 정의할 수 있는데 여기서 x, y는 모두 같은 타입이어야만 컴파일 에러가 발생하지 않는다. 만약 서로 다른 타입을 사용하고 싶다면

struct Point<T, U> {
    x: T,
    y: U,
}

fn main() {
    let integer_point = Point { x: 5, y: 10.0 };
    let float_point = Point { x: 1.0, y: 4.0 };

    println!("Integer Point: ({}, {})", integer_point.x, integer_point.y);
    println!("Float Point: ({}, {})", float_point.x, float_point.y);
}

이런 식으로 제네릭 타입 매개변수를 두 개 사용하여 서로 다른 타입을 사용할 수 있다.

제네릭 열거형

제네릭 열거형도 마찬가지로 정의할 수 있다. 이전에 Option 열거형을 살펴봤는데, 이 또한 제네릭 열거형이다.

enum Option<T>{
    Some(T),
    None,
}

다른 예제로

enum Result<T, E> {
    Ok(T),
    Err(E),
}

도 존재한다. 이 열거형은 성공적인 결과와 실패한 결과를 나타내는 데 사용된다. T는 성공적인 결과의 타입, E는 실패한 결과의 타입을 나타낸다.

제네릭 메서드

제네릭 메서드도 정의할 수 있다. 예를 들어

struct Point<T> {
    x: T,
    y: T,
}

impl<T> Point<T> {
    fn new(x: T, y: T) -> Point<T> {
        Point { x, y }
    }

    fn x(&self) -> &T {
        &self.x
    }

    fn y(&self) -> &T {
        &self.y
    }
}

fn main() {
    let p = Point::new(5, 10);
    println!("Point: ({}, {})", p.x(), p.y());
}

트레이트

트레이트는 특정한 타입이 가지고 있으면서 다른 타입과 공유할 수 있는 기능을 정의한다. 이전에 Debug도 트레이트의 일종이다.

아래 코드는 candleqwen3.rs에서 가져온 예제이다. 이런 식으로 기본 트레이트인 DebugClone같은 것은 꽤 자주 쓰이는 편이다.

#[derive(Debug, Clone)]
pub(crate) struct Qwen3RotaryEmbedding {
    sin: Tensor,
    cos: Tensor,
}

impl Qwen3RotaryEmbedding {
    pub(crate) fn new(dtype: DType, cfg: &Config, dev: &Device) -> Result<Self> {
        let dim = cfg.head_dim;
        let max_seq_len = cfg.max_position_embeddings;
        let inv_freq: Vec<_> = (0..dim)
            .step_by(2)
            .map(|i| 1f32 / cfg.rope_theta.powf(i as f64 / dim as f64) as f32)
            .collect();
        let inv_freq_len = inv_freq.len();
        let inv_freq = Tensor::from_vec(inv_freq, (1, inv_freq_len), dev)?.to_dtype(dtype)?;
        let t = Tensor::arange(0u32, max_seq_len as u32, dev)?
            .to_dtype(dtype)?
            .reshape((max_seq_len, 1))?;
        let freqs = t.matmul(&inv_freq)?;
        Ok(Self {
            sin: freqs.sin()?,
            cos: freqs.cos()?,
        })
    }

    /// Apply RoPE (q, k shape: B x H x L x D)
    pub(crate) fn apply(&self, q: &Tensor, k: &Tensor, offset: usize) -> Result<(Tensor, Tensor)> {
        let (_, _, seq_len, _) = q.dims4()?;
        let cos = self.cos.narrow(0, offset, seq_len)?;
        let sin = self.sin.narrow(0, offset, seq_len)?;
        let q_embed = candle_nn::rotary_emb::rope(&q.contiguous()?, &cos, &sin)?;
        let k_embed = candle_nn::rotary_emb::rope(&k.contiguous()?, &cos, &sin)?;
        Ok((q_embed, k_embed))
    }
}

다시 돌아와서 트레이트를 정의하는 방법을 알아보자.

트레이트는 trait 키워드를 사용하여 정의한다. 예를 들어

pub trait Summary {
    fn summarize(&self) -> String {
        String::from("(Read more...)")
    }
}

이런 식으로 정의하고

let article = NewsArticle {
    headline: String::from("Breaking News"),
    location: String::from("New York"),
    author: String::from("John Doe"),
    content: String::from("Lorem ipsum dolor sit amet..."),
};

println!("New article available! {}", article.summarize());

이런 식으로 사용할 수 있다.

라이프타임으로 참조자의 유효성 검증하기

라이프타임은 어떤 참조자가 필요한 기간동안 유효함을 보장하도록 하는 일종의 제네릭이다.

라이프타임의 주 목적은 댕글링 참조 방지다. 예를 들어

fn main() {
    let r;
{
        let x = 5;
        r = &x; // x는 이 블록 안에서만 유효하다
    }
    println!("r: {}", r); // r은 x를 참조하지만 x는 이미 스코프 밖으로 벗어났다
}

이런 코드는 컴파일 에러를 발생시킨다. 그런데 이때 컴파일러의 에러 메시지를 살펴보면 'x' does not live long enough라는 메시지가 나타난다.

대여 검사기(borrow checker)

러스트 컴파일러는 대여 검사기라는 기능을 통해 참조자의 유효성을 검사한다. 대여 검사기는 참조자가 유효한지, 댕글링 참조가 발생하지 않는지 등을 검사한다.

fn main() {
    let r;                // ---------+-- 'a
                          //          |
    {                     //          |
        let x = 5;        // -+-- 'b  |
        r = &x;           //  |       |
    }                     // -+       |
                          //          |
    println!("r: {}", r); //          |
}                         // ---------+

위 예제의 라이프타임을 시각적으로 나타내면 위와 같다. 여기서 'ar의 라이프타임, 'bx의 라이프타임을 나타낸다. rx를 참조하지만, x는 스코프 밖으로 벗어나면서 더 이상 유효하지 않게 된다. 따라서 r은 댕글링 참조가 되어 컴파일 에러가 발생한다. 그러니까 참조 대상이 참조자보다 오래 살지 못해서 러스트 컴파일러가 이 프로그램을 허용하지 않는 것이다.

76 views

More from this blog

락프리 데이터 구조와 알고리즘

여기서는 락프리 데이터 구조를 설명한다. 락프리(lock-free) 란 배타락을 이용하지 않고 처리를 수행하는 데이터 구조 및 그에 대한 조작 알고리즘을 총칭한다. 왜 락프리인가? 전통적인 동시성 제어 방법인 뮤텍스나 세마포어는 여러 문제점을 가지고 있다: 성능 저하: 락 경합(lock contention)으로 인한 대기 시간 데드락: 여러 스레드가 서로의 락을 기다리는 상황 우선순위 역전: 낮은 우선순위 스레드가 높은 우선순위 스레드를 ...

Jul 27, 20257 min read126

소프트웨어 트랜잭셔널 메모리

소프트웨어 트랜잭셔널 메모리 동시성 프로그래밍에서 공유 자원에 대한 안전한 접근은 항상 중요한 과제다. 전통적으로 뮤텍스 락과 같은 비관적 락(Negative Lock) 방식을 사용해왔다. 이 방식은 크리티컬 섹션에 진입하기 전에 반드시 락을 획득해야 하며, 락을 얻지 못하면 코드 실행 자체가 블록된다. 하지만 이와는 다른 접근 방식이 있다. 바로 낙관적 락(Optimistic Lock) 방식인데, 이는 "일단 실행하고 나중에 검증하자"는 철학...

Jul 20, 202517 min read263

공평한 배타 제어

공평한 배타 제어 여기서는 공평한 배타 제어에 대해 설명한다. 먼저 컨텐션(contention) 이라는 개념을 이해할 필요가 있다. 컨텐션이란 여러 스레드가 동시에 같은 락을 획득하려고 경쟁하는 상황을 말한다. 컨텐션이 높을수록 스레드들이 락을 기다리는 시간이 길어지고 성능이 저하된다. 이러한 컨텐션 상황은 시스템 아키텍처에 따라 더욱 복잡해질 수 있다. 특히 비균일 메모리 접근(Non-Uniform Memory Access, NUMA) 와 같...

Jul 13, 20259 min read21

KernelSnitch[논문 리뷰]

Paper 1. Intro 이 글은 NDSS 2025에서 발표된 KernelSnitch 논문을 소개이다. 이 연구는 커널의 평범한 데이터 구조체들이 가진 본질적인 특성이 어떻게 심각한 보안 취약점이 되는지를 보여준다. 핵심은 이러하다: "데이터 구조체의 크기에 따른 접근 시간 차이를 이용해 커널의 비밀 정보를 유출할 수 있다" 여기서는 커널 힙 포인터 유출에 집중해서 설명한다. 이 공격이 성공하면 KASLR을 우회하고 더 심각한 커널 익스플로...

Jul 11, 20257 min read131

멀티태스크와 액터 모델

멀티태스크 협조적/비협조적 멀티태스크 선점: 프로세스와의 협조 없이 수행하는 컨택스트 스위칭이라고는 하나, 결국 뺏어오는 게 가능하냐의 문제다. 협조적 멀티태스크(비선점형, cooperative): 각각의 프로세스가 자발적으로 컨택스트 스위칭을 수행하는 멀티태스크 방식. 장점: 멀티태스크 매커니즘을 구현하기 쉽다. 단점: 프로세스가 자발적으로 컨텍스트 스위칭을 해야하는데, 만약 버그가 발생하여 프로세스가 무한 루프에 빠지거나 정지하게 되면 그 ...

Jul 6, 20252 min read25
M

MaxLog

35 posts