모두의 코드
Rust 의 static 키워드

작성일 : 2021-06-10 이 글은 1130 번 읽혔습니다.

요즘에 떠오르는 언어인 Rust 를 계속 사용해보고 있는데 굉장히 좋은 언어인것 같습니다. Rust 를 공부하면서 새롭게 배운 내용들을 조금씩 적어보고자 합니다. 아래 내용은 이 글 을 참고해서 작성하였습니다.

static 키워드

러스트의 가장 특이한 점으로 라이프타임(Lifetime) 을 따로 표시해줘야 한다는 점입니다. 물론 대부분의 경우 러스트의 Borrow checker 가 알아서 유추를 해주지만, 레퍼런스들을 사용하는 경우 라이프타임을 사용자가 명시적으로 표기해야 할 일들이 종종 있습니다. 왜 라이프타임이 필요한지는 나중에 라이프타임에 대해서 다룰 때 자세히 정리해보도록 하겠습니다.

라이프타임은 일반적으로 사용자가 원하는 이름으로 표시 가능하지만 (그냥 'a 이렇게 말이죠), 라이프타임중에서 러스트에 예약되어 있는 것이 하나 있는데 바로 'static 입니다.

static 은 다른 언어들을 다루어보신 분들은 한 번쯤 보았을 키워드 입니다. 예를 들어서 C++ 에서 static 키워드는 두 가지 경우에 사용할 수 있습니다. 클래스 내부에서 사용할 경우 어떠한 객체의 저장 기간(Storage duration)과, 링크 방식(Linkage)을 지정하는데, 저장 기간의 경우 프로그램의 시작 때 부터 프로그램이 끝날 때 까지이고 링크 방식은 해당 TU 에서만 접근 가능한 내부 링크 방식 (Internal linkage) 으로 지정합니다. 반면에 클래스 내부에서 사용할 경우 해당 클래스 객체 마다 정의된 것이 아니라, 해당 클래스 자체 안에 정의된 객체라는 의미를 가집니다.

러스트의 static 키워드도 비슷한 의미를 가집니다. C++ 의 경우 처럼 러스트 에서도 static 키워드가 아래와 같이 두 가지 형태로 사용될 수 있습니다.

// 'static 라이프타임을 가지는 레퍼런스 만들기.
let s: &'static str = "hello world";

// Trait bound 에서 'static 사용하기.
fn generic<T>(x: T) where T: 'static {}

레퍼런스의 라이프타임

만약에 어떠한 레퍼런스의 라이프타임이 'static 으로 명시되 있다면 해당 레퍼런스는 프로그램의 전체 실행 시간 동안 존재하는 데이터를 레퍼런스 한다는 의미가 됩니다. 당연히도 'static 는 다른 어떠한 라이프타임으로도 변환이 가능합니다. 왜냐하면 'static 보다 긴 라이프타임은 있을 수 없기 때문이죠.

러스트에서 어떠한 변수를 'static 라이프타임을 가지게 하는 방법은 두 가지가 있다고 하였습니다.

  • static 으로 정의된 상수 만들기

  • 문자열 리터럴 (&'static str 의 타입을 가지게 됩니다)

// static 으로 정의된 상수 만들기
static CONST: i64 = 10;

// 참고로 T 가 ?Sized 인 것은 str 이 크기가 정의가 되지 않은 타입이기 때문.
// Rust 의 모든 타입은 따로 명시되지 않으면 암묵적으로 Sized 라 생각된다.
fn takes_static<T: ?Sized>(v: &'static T) -> &'static T {
    v
}

fn main() {
    println!("Takes static : {}", takes_static(&CONST));
    println!("Takes static : {}", takes_static("some literal"));
}

위 두 경우 모두

실행 결과

Takes static : 10
Takes static : some literal

이 잘 출력됩니다.

Trait bound 에서의 static

만약에 trait bound 로써 static 을 사용한다면, 이 말은 해당 타입이 static 이 아닌 레퍼런스를 포함하지 않는다 라는 의미 입니다. 트레잇 객체의 타입에 레퍼런스가 포함되는 것을 막고 싶은 경우 유용하게 사용할 수 있습니다. 예를 들어서 아래를 살펴봅시다.

struct NoRef {
    data: i64,
}

struct HasRef<'a> {
    data: &'a i64,
}

trait DoSth {
    fn do_sth(&self);
}

impl DoSth for NoRef {
    fn do_sth(&self) {
        println!("data : {}", self.data);
    }
}

impl<'a> DoSth for HasRef<'a> {
    fn do_sth(&self) {
        println!("data : {}", self.data);
    }
}

NoRef 의 경우 struct 안에 i64 값을 소유하는 필드를 가지고 있고, 반면에 HasRef 의 경우 i64 의 레퍼런스를 가지는 필드가 하나 들어 있습니다.

그리고 두 struct 에 공통으로 적용되는 트레잇인 DoSth 을 간단히 만들었습니다.

fn do_sth(sth: &impl DoSth) {
    sth.do_sth();
}

그렇다면 위 do_sth 함수는 NoRefHasRef 를 모두 받을 수 있을까요?

fn main() {
    let no_ref = NoRef { data: 1234 };
    let data = 12345;
    let has_ref = HasRef { data: &data };

    do_sth(&no_ref); // Ok
    do_sth(&has_ref); // Ok

실행해보면

실행 결과

data : 1234
data : 12345

와 같이 예상대로 잘 나옵니다. 하지만 만약에 인자로 받는 impl DoSth 객체에 'static 을 강제하면 어떨까요?

fn do_sth_static(sth: &(impl DoSth + 'static)) {
    sth.do_sth();
}

fn main() {
    let no_ref = NoRef { data: 1234 };
    let data = 12345;
    let has_ref = HasRef { data: &data };

    do_sth_static(&no_ref); // Ok
    do_sth_static(&has_ref); // Error!
}

컴파일 해보면

컴파일 오류

error[E0597]: `data` does not live long enough
  --> src/main.rs:36:34
   |
36 |     let has_ref = HasRef { data: &data };
   |                                  ^^^^^ borrowed value does not live long enough
...
39 |     do_sth_static(&has_ref); // Error!
   |     ----------------------- argument requires that `data` is borrowed for `'static`
40 | }
   | - `data` dropped here while still borrowed

error: aborting due to previous error

For more information about this error, try `rustc --explain E0597`.
error: could not compile `chap3`

와 같이 오류가 발생합니다. 왜냐하면 HasRef 필드안에 레퍼런스인 필드 data 가 있기 때문에 'static 으로 명시된 impl DoSth 트레잇이 받을 수 없기 때문이죠. 그렇기에, data does not live long enough 와 같은 오류가 발생하게 됩니다.

저의 경우 이 오류를 언제 처음 접해보았냐면 임의의 함수를 받는 함수 컨테이너 객체를 만들 때였습니다.

enum FuncContainer {
    F1(Box<dyn Fn(Obj) -> Obj>),
}

struct Obj {}

fn create_func_container<T: From<Obj>, V: Into<Obj>>(func: fn(T) -> V) -> FuncContainer {
    FuncContainer::F1(Box::new(move |v: Obj| -> Obj { func(v.into()).into() }))
}

FuncContainer::F1Obj 를 인자로 받아서 Obj 를 리턴하는 함수 인데, create_func_container 를 통해서 임의의 함수를 FuncContainer::F1 으로 변환해주는 코드를 짜고자 하였습니다.

이 경우 Obj 를 임의의 함수의 인자로 바꿀 수 있고, 또 해당 함수의 리턴값을 Obj 로 변환할 수 있다면 해당 함수를 FuncContainer::F1 으로 감쌀 수 있겠죠.

문제는 위 코드를 컴파일 하면;

컴파일 오류

error[E0310]: the parameter type `V` may not live long enough
 --> src/main.rs:8:23
  |
7 | fn create_func_container<T: From<Obj>, V: Into<Obj>>(func: fn(T) -> V) -> FuncContainer {
  |                                        -- help: consider adding an explicit lifetime bound...: `V: 'static +`
8 |     FuncContainer::F1(Box::new(move |v: Obj| -> Obj { func(v.into()).into() }))
  |                       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ...so that the type `[closure@src/main.rs:8:32: 8:78]` will meet its required lifetime bounds

error[E0310]: the parameter type `T` may not live long enough
 --> src/main.rs:8:23
  |
7 | fn create_func_container<T: From<Obj>, V: Into<Obj>>(func: fn(T) -> V) -> FuncContainer {
  |                          -- help: consider adding an explicit lifetime bound...: `T: 'static +`
8 |     FuncContainer::F1(Box::new(move |v: Obj| -> Obj { func(v.into()).into() }))
  |                       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ...so that the type `[closure@src/main.rs:8:32: 8:78]` will meet its required lifetime bounds

위와 가은 오류가 발생한다는 점 이였습니다. 여기서 러스트 컴파일러가 말하는 것이 바로 create_func_container 의 인자로 받는 TV 의 라이프타임이 리턴되는 FuncContainer::F1 보다 길다는 것이 보장이 되지 않는다는 것입니다.

처음에 이 오류를 보았을 뭐가 문제인지 한참을 고민했는데, 생각해보니 T 와 V 는 임의의 타입이기 때문에 만일 레퍼런스를 필드로 가지는 타입 이라면 문제가 될 수 있는 것이였습니다. 따라서 이를 해결하기 위해서는, 위에 친절히 컴파일러가 알려주는 대로 TV'static 으로 trait bound 를 지정해주면 됩니다.

실제로 아래 코드는 깔끔히 컴파일 됩니다.

enum FuncContainer {
    F1(Box<dyn Fn(Obj) -> Obj>),
}

struct Obj {}

fn create_func_container<T: From<Obj> + 'static, V: Into<Obj> + 'static>(
    func: fn(T) -> V,
) -> FuncContainer {
    FuncContainer::F1(Box::new(move |v: Obj| -> Obj { func(v.into()).into() }))
}

Rust 의 Borrow Checker 가 때로는 예상치 못한 곳에서 오류를 발생시키는 경우들이 있는데 정말 곰곰히 생각해보면 말이 되는 걱정인걸 깨닫게 됩니다. 그래도 C++ 의 템플릿 오류들과 씨름할 때 보다는 Rust 의 컴파일러는 오류를 조금 더 친절하게 나마 가르쳐주니 정말 좋은 것 같습니다.

아직은 회사에서 C++ 을 주력으로 사용하고 있지만, Fuchsia 라는 구글 내부에서 새로 개발하고 있는 운영 체제의 일부분으로써 Rust 를 도입한 것을 보면 조만간 업무에서도 사용할 날이 멀지 않은 것 같습니다.

모두의 코드 서버 역시도 현재 Node JS 로 작성되어 있지만, 조만간 Rust 로 완전히 백엔드 서버를 바꿀 계획을 하고 있습니다. 참고로 서버 템플릿 엔진으로 EJS 를 사용하고 있는데 러스트 기반의 대체제가 마땅히 없어서 Dojang (여러분이 생각하는 도장 맞습니다) 이라는 러스트 기반의 템플릿 엔진을 제작하고 있습니다. 혹시라도 관심있으신 분들은 한 번 들려주시기를 바랍니다.

첫 댓글을 달아주세요!
프로필 사진 없음
강좌에 관련 없이 궁금한 내용은 여기를 사용해주세요