Rust 라이프타임 깊이 알아보기: Borrow 검사 및 메모리 관리
Emily Parker
Product Engineer · Leapcell

라이프타임이란 무엇인가?
라이프타임의 정의
Rust에서 모든 참조는 라이프타임을 가지며, 이는 참조가 가리키는 값이 메모리에서 존재하는 기간을 나타냅니다(참조가 유효한 코드 라인 범위로 간주할 수도 있습니다). 라이프타임은 참조가 전체 라이프타임 동안 유효하도록 보장합니다. 이는 참조의 유효성을 보장하기 위해 존재합니다.
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y } }
위 코드에서 함수 longest
는 두 개의 입력 파라미터를 가지며, 둘 다 문자열 슬라이스에 대한 참조입니다. 또한 문자열 슬라이스에 대한 참조인 반환 값도 가집니다. Rust는 메모리 안전에 매우 집중하므로 참조의 유효성을 보장하기 위해 라이프타임이 도입되었습니다. 반환된 참조가 유효한지 확인하려면 먼저 해당 라이프타임을 결정해야 합니다. 하지만 어떻게 결정할 수 있을까요?
Rust는 함수 파라미터와 반환 값의 라이프타임을 자동으로 추론할 수 있으며, 이는 나중에 자세히 설명합니다. 그러나 이 추론은 보편적이지 않습니다. Rust는 세 가지 특정 시나리오에서만 라이프타임을 추론할 수 있습니다. 위의 코드는 이러한 경우 중 하나에 해당하지 않습니다. 이러한 상황에서는 라이프타임을 수동으로 주석 처리해야 합니다. 명시적 주석이 없으면 Rust의 빌림 검사기가 반환 값의 라이프타임을 결정할 수 없으므로 참조의 유효성을 확인할 수 없습니다.
코드를 다시 살펴보면 반환 값은 파라미터에서 비롯됩니다. 반환 값이 파라미터와 동일한 라이프타임을 갖도록 하는 것으로 충분할까요? 적어도 함수 호출 범위 내에서는 참조가 유효하게 유지됩니다. 그러나 두 개의 파라미터가 있으므로 해당 라이프타임이 다를 수 있습니다. 반환 값을 어느 파라미터와 연결해야 할까요? 해결책은 간단합니다. 반환 값은 가장 짧은 라이프타임을 가진 파라미터와 동일한 라이프타임을 가져야 합니다. 이렇게 하면 반환 값은 두 파라미터가 유효한 한 최소한 유효하게 유지됩니다. 따라서 위 코드에서 'a
주석은 반환 값의 라이프타임이 두 'a
파라미터 라이프타임의 교집합임을 의미합니다. 이를 통해 반환 값의 라이프타임이 잘 정의되어 Rust에서 해당 참조가 유효한지 확인할 수 있습니다.
라이프타임 및 메모리 관리
Rust는 라이프타임을 사용하여 메모리를 관리합니다. 변수가 범위를 벗어나면 해당 변수가 사용하는 메모리가 해제됩니다. 참조가 이미 해제된 메모리를 가리키면 이는 매달린 참조가 되며, 이를 사용하려고 시도하면 컴파일 오류가 발생합니다.
fn main() { let r; { let x = 5; r = &x; } println!("r: {}", r); }
위 코드에서 변수 x
는 범위를 벗어나면 할당 해제되지만 변수 r
은 여전히 해당 변수에 대한 참조를 보유하고 있습니다. 이는 매달린 참조를 만듭니다. Rust 컴파일러는 이 문제를 감지하고 오류 메시지를 제공합니다.
왜 라이프타임이 필요한가?
매달린 참조 방지 및 메모리 안전 보장
앞서 언급했듯이 Rust는 라이프타임을 사용하여 매달린 참조를 방지합니다. 컴파일러는 코드에 있는 모든 참조의 라이프타임을 검사하여 전체 라이프타임 동안 유효하게 유지되도록 합니다.
fn main() { let string1 = String::from("abcd"); let string2 = "xyz"; let result = longest(string1.as_str(), string2); println!("The longest string is {}", result); } fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y } }
위 코드에서 함수 longest
는 문자열 슬라이스에 대한 참조를 반환합니다. 컴파일러는 반환 값의 라이프타임이 유효한지 확인합니다. 반환 값이 매달린 참조인 경우 컴파일러는 오류를 생성합니다.
다음은 Rust가 라이프타임을 통해 메모리 안전을 보장하는 방법을 보여주는 또 다른 예입니다.
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y } } fn main() { let string1 = String::from("long string is long"); let result; { let string2 = String::from("xyz"); result = longest(string1.as_str(), string2.as_str()); } println!("The longest string is {}", result); }
이 코드에서는 두 개의 문자열 슬라이스를 파라미터로 사용하고 문자열 슬라이스를 반환하는 longest
라는 함수를 정의합니다. 이 함수는 라이프타임 파라미터 'a
를 사용하여 입력 파라미터와 반환 값의 라이프타임 간의 관계를 지정합니다.
main
함수에서는 두 개의 문자열 변수 string1
과 string2
를 만들고 해당 슬라이스를 longest
에 전달합니다. longest
는 입력 파라미터와 반환 값이 동일한 라이프타임을 갖도록 요구하므로 컴파일러는 슬라이스가 이 요구 사항을 충족하는지 확인합니다. 여기서 string2
는 string1
보다 짧은 라이프타임을 가지므로 컴파일러는 오류를 보고 반환 값에 매달린 참조가 포함될 수 있다고 경고합니다. 이 메커니즘은 메모리 안전을 보장합니다.
라이프타임 구문
라이프타임 주석
함수를 정의할 때 꺾쇠 괄호를 사용하여 라이프타임 파라미터를 주석 처리할 수 있습니다. 라이프타임 파라미터 이름은 작은따옴표로 시작해야 합니다(예: 'a
).
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y } }
위 코드에서 함수 longest
는 두 개의 입력 파라미터를 가지며, 둘 다 문자열 슬라이스에 대한 참조입니다. 이러한 참조는 동일한 라이프타임을 가져야 함을 나타내는 라이프타임 파라미터 'a
를 가집니다. 반환 값도 라이프타임 파라미터 'a
를 가지며, 이는 해당 라이프타임이 입력 파라미터의 라이프타임과 일치함을 의미합니다.
라이프타임 생략 규칙
대부분의 경우 Rust 컴파일러는 참조 라이프타임을 자동으로 추론할 수 있으므로 라이프타임 주석을 생략할 수 있습니다.
fn longest(x: &str, y: &str) -> &str { if x.len() > y.len() { x } else { y } }
이 경우 컴파일러는 파라미터와 반환 값의 라이프타임을 결정할 수 없습니다. 반환 값은 두 파라미터를 비교하는 데 따라 달라지므로 컴파일러는 어떤 파라미터의 라이프타임을 사용해야 하는지 추론할 수 없습니다.
컴파일러가 함수의 반환 값 라이프타임을 결정할 수 없으면 오류를 발생시켜 개발자가 라이프타임 파라미터를 명시적으로 지정하도록 요구합니다. 예를 들어 longest
함수를 다음과 같이 수정할 수 있습니다.
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y } }
여기서 라이프타임 파라미터 'a
는 입력 파라미터와 반환 값이 동일한 라이프타임을 가져야 함을 지정합니다. 이를 통해 컴파일러는 함수 인수가 라이프타임 제약 조건을 충족하는지 확인하고 반환 값에 매달린 참조가 포함되어 있지 않도록 할 수 있습니다.
그러나 많은 경우에 Rust 컴파일러는 라이프타임을 자동으로 추론할 수 있습니다. Rust는 올바른 라이프타임을 추론하기 위해 라이프타임 생략 규칙 집합을 적용합니다. 이러한 규칙은 다음과 같습니다.
- 각 참조 파라미터는 고유한 라이프타임 파라미터를 얻습니다. 예를 들어
fn foo(x: &i32)
는fn foo<'a>(x: &'a i32)
로 변환됩니다. - 함수에 단일 입력 라이프타임 파라미터가 있는 경우 해당 라이프타임은 모든 출력 라이프타임 파라미터에 할당됩니다. 예를 들어
fn foo<'a>(x: &'a i32) -> &i32
는fn foo<'a>(x: &'a i32) -> &'a i32
로 변환됩니다. - 함수에 여러 입력 라이프타임 파라미터가 있지만 그 중 하나가
&self
또는&mut self
인 경우 반환 값은self
의 라이프타임을 받습니다. 예를 들어fn foo(&self, x: &i32) -> &i32
는fn foo<'a, 'b>(&'a self, x: &'b i32) -> &'a i32
로 변환됩니다.
이러한 규칙을 통해 Rust 컴파일러는 많은 경우에 라이프타임을 자동으로 추론할 수 있습니다. 그러나 복잡한 시나리오에서는 컴파일러에서 명시적인 라이프타임 주석을 요구할 수도 있습니다.
라이프타임의 사용 사례
함수 파라미터 및 반환 값
함수의 입력 파라미터 또는 반환 값에 참조가 포함된 경우 해당 참조의 유효성을 보장하기 위해 라이프타임을 사용해야 합니다.
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y } }
위 코드에서 함수 longest
는 두 개의 입력 파라미터를 가지며, 둘 다 문자열 슬라이스에 대한 참조입니다. 이러한 참조는 동일한 라이프타임을 공유해야 함을 의미하는 라이프타임 파라미터 'a
를 가집니다. 함수의 반환 값도 라이프타임 파라미터 'a
를 가지며, 이는 해당 라이프타임이 입력 파라미터와 일치함을 나타냅니다.
구조체 정의
구조체에 참조가 포함된 경우 참조 유효성을 보장하기 위해 라이프타임을 사용해야 합니다.
struct ImportantExcerpt<'a> { part: &'a str, } fn main() { let novel = String::from("Call me Ishmael. Some years ago..."); let first_sentence = novel.split('.').next().expect("Could not find a '.'"); let i = ImportantExcerpt { part: first_sentence }; }
위 코드에서 구조체 ImportantExcerpt
에는 문자열 슬라이스에 대한 참조가 포함되어 있습니다. 이 참조는 잘 정의된 라이프타임을 가져야 함을 나타내는 라이프타임 파라미터 'a
를 가집니다. 매달린 참조를 방지하기 위해 문자열 슬라이스는 구조체와 동일한 라이프타임을 가져야 구조체가 유효한 한 문자열 슬라이스도 유효합니다.
라이프타임의 고급 사용법
라이프타임 하위 유형 및 다형성
Rust는 라이프타임 하위 유형 및 다형성을 지원합니다. 라이프타임 하위 유형은 하나의 라이프타임이 다른 라이프타임의 하위 집합일 수 있음을 의미합니다.
fn longest<'a>(x: &'a str, y: &str) -> &'a str { x }
이 예에서 첫 번째 입력 파라미터는 라이프타임 'a
를 갖는 반면 두 번째 입력 파라미터는 명시적 라이프타임 주석을 갖지 않습니다. 즉, 두 번째 입력 파라미터는 모든 라이프타임을 가질 수 있으며 반환 값에 영향을 미치지 않습니다.
정적 라이프타임
Rust에는 프로그램의 전체 기간 동안 참조가 유효함을 나타내는 'static
이라는 특수 라이프타임이 있습니다.
let s: &'static str = "I have a static lifetime.";
이 예에서 변수 s
는 정적 라이프타임을 가진 문자열 슬라이스에 대한 참조입니다. 즉, 전체 프로그램 실행 동안 유효하게 유지됩니다.
라이프타임 및 빌림 검사기
빌림 검사기의 역할
Rust 컴파일러에는 모든 참조가 빌림 규칙을 준수하는지 확인하는 빌림 검사기가 포함되어 있습니다. 규칙을 위반하면 컴파일러에서 오류가 발생합니다.
fn main() { let mut s = String::from("hello"); let r1 = &s; let r2 = &s; let r3 = &mut s; println!("{}, {}, and {}", r1, r2, r3); }
이 코드에서 변수 s
는 동일한 범위 내에서 불변 참조(r1
및 r2
)와 가변 참조(r3
)를 모두 갖습니다. 이는 Rust의 빌림 규칙을 위반합니다. 컴파일러는 이 문제를 감지하고 오류를 생성합니다.
라이프타임 검사는 참조가 전체 존재 기간 동안 유효하게 유지되도록 합니다. 그러나 라이프타임이 동일하다고 해서 반드시 빌림이 허용되는 것은 아닙니다. Rust의 빌림 규칙은 라이프타임 유효성 과 가변성 제약 조건을 모두 고려합니다.
위 코드에서 r1
, r2
및 r3
가 동일한 라이프타임을 갖더라도 동일한 범위 내에서 동일한 변수에 대한 불변 참조와 가변 참조를 모두 만들려고 시도하므로 Rust의 빌림 규칙을 위반합니다. Rust의 빌림 규칙에 따르면 다음과 같습니다.
- 동시에 변수에 대한 여러 개의 불변 참조를 가질 수 있습니다.
- 하나의 가변 참조를 가질 수 있지만 동시에 다른 참조(가변 또는 불변)는 가질 수 없습니다.
이를 통해 메모리 안전을 보장하고 데이터 경합을 방지합니다.
라이프타임의 제한 사항
Rust가 라이프타임을 사용하여 메모리를 관리하고 안전을 보장하지만 라이프타임에는 몇 가지 제한 사항도 있습니다. 예를 들어 컴파일러가 올바른 라이프타임을 자동으로 추론할 수 없는 경우가 있어 프로그래머가 명시적 주석을 추가해야 합니다. 이로 인해 개발자의 부담이 가중되고 코드 가독성이 저하될 수 있습니다.
Rust 프로젝트 호스팅을 위한 최고의 선택, Leapcell입니다.
Leapcell은 웹 호스팅, 비동기 작업 및 Redis를 위한 차세대 서버리스 플랫폼입니다.
다국어 지원
- Node.js, Python, Go 또는 Rust로 개발하십시오.
무제한 프로젝트를 무료로 배포
- 사용량에 대해서만 지불하십시오. 요청이 없으면 요금이 부과되지 않습니다.
탁월한 비용 효율성
- 유휴 요금 없이 종량제로 지불하십시오.
- 예: $25는 평균 응답 시간 60ms에서 694만 건의 요청을 지원합니다.
능률적인 개발자 경험
- 간편한 설정을 위한 직관적인 UI.
- 완전 자동화된 CI/CD 파이프라인 및 GitOps 통합.
- 실행 가능한 통찰력을 위한 실시간 메트릭 및 로깅.
손쉬운 확장성 및 고성능
- 고도의 동시성을 쉽게 처리할 수 있도록 자동 확장합니다.
- 운영 오버헤드 제로 — 구축에만 집중하십시오.
설명서에서 자세히 알아보십시오!
X에서 팔로우하세요: @LeapcellHQ