2024년 12월 31일 화요일

rust : lifetime

Rust 라이프타임(Lifetime) 쉽게 이해하기

Rust 라이프타임(Lifetime) 쉽게 이해하기

Rust에서 라이프타임(Lifetime)이란, “참조가 실제로 유효한 기간”을 의미합니다. 이 개념이 있는 이유는, Rust가 메모리 안전성을 보장하기 위해서에요. “값이 살아있는 동안만, 그 값을 참조할 수 있다”는 조건을 엄격하게 지키기 위해서는, “참조가 언제까지 유효한가?”를 Rust 컴파일러가 추적할 수 있어야 합니다.

라이프타임 없이 생기는 혼란

간단한 함수나 로직에서는 라이프타임 표기를 자주 쓰지 않아도 됩니다. 하지만 여러 개의 참조를 인자로 받고, 그 중 하나를 반환하는 함수는 이야기가 달라져요.

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

이 함수가 최종적으로 내보내는 참조가 언제까지 살아 있을지를 컴파일러는 명확히 알 수 없습니다. 특히 xy의 라이프타임(“참조가 유효한 기간”)이 서로 다르거나, 함수 호출 이후에 어떤 식으로 사용되는지에 따라 달라지면 컴파일러로서는 “어느 쪽 라이프타임에 맞춰야 하지?”라는 문제가 생기거든요.

좀 더 구체적으로 왜 혼란이 생길까?

  1. 컴파일러의 역할
    Rust 컴파일러는 참조가 유효한지(= 값이 아직 안 죽었는지) 항상 검사해야 해요. 값(owners)과 빌려 쓰는 참조(borrowers)가 있는데, 빌려 쓰는 동안 원본 값이 사라져버리면 안 되니까요.
  2. 함수 반환 시, 참조의 라이프타임 판단
    우리가 x 또는 y 중 하나를 반환한다고 했을 때, 컴파일러 입장에서는 “반환된 참조는 x의 라이프타임에 따라야 하나, y의 라이프타임에 따라야 하나?”라고 고민을 해야 해요.
    • 만약 x의 라이프타임이 y보다 짧은데 함수가 y를 반환한다면?
    • 그 반대 상황이라면?
    이처럼 어느 한 쪽으로도 결정하기 애매해서, 컴파일러는 에러를 낼 수밖에 없습니다.

명시적 라이프타임으로 해결

그래서 우리가 직접 함수 선언에 <'a>를 붙이고, 파라미터와 반환값에 'a를 지정해 주면 문제가 해결돼요.

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

이렇게 “둘 다 'a라는 동일한 기간(또는 그 이하)만큼만 유효해!”라고 컴파일러에게 확실히 알려주면, 컴파일러 입장에서는 “어떤 값을 반환하든, 'a라는 라이프타임 범위 내에서만 참조가 사용되는구나!” 하고 안심할 수 있게 됩니다. 결과적으로 컴파일러가 “라이프타임이 모호하다”는 이유로 에러를 내지 않는 것이죠.

정리하자면

  • 라이프타임은 참조(예: &str)가 언제까지 유효한가를 나타내는 개념입니다.
  • 함수 반환 시 여러 참조 중 어느 것을 반환하는 로직이 들어가면, 컴파일러가 정확한 라이프타임을 추론하기 어려워집니다.
  • 명시적 라이프타임(<'a>)을 써서 “함수의 파라미터와 반환값 사이의 라이프타임 관계”를 직접 명시해 주면, 컴파일러가 모호함 없이 판단할 수 있습니다.
  • 결국 무효 참조(Dangling Pointer)를 방지하고, Rust가 메모리 안전성을 컴파일 타임에 보장하도록 돕는 핵심 장치가 라이프타임이에요.

라이프타임은 Rust가 제공하는 강력한 메모리 안전 장치이지만, 처음에는 생소해서 어렵게 느껴질 수 있어요. 그래도 위 예시처럼 “왜 필요한지, 컴파일러가 어떤 혼란을 느끼는지”를 이해하면, 보다 쉽게 다가갈 수 있을 것입니다!