문제 설정
let rect = Rectangle { ... }로 만든 instance는 stack(또는 Box라면 heap)에 살고, 스코프가 끝나면 사라진다. Java/C++의 static field처럼 "프로그램 전체에서 하나만 존재하고 인스턴스에 묶이지 않은 값"을 두려면 어떻게 해야 하는가.
답: Rust에는 static 키워드와 const 키워드가 있다. 의미가 비슷해 보이지만 메모리 모델이 다르다.
static — data segment에 박히는 단 하나의 인스턴스
static RECT: Rectangle = Rectangle { width: 30, height: 50 };
- 컴파일된 바이너리의 data segment(RODATA) 에 그 값의 비트 패턴이 미리 들어간다.
- 프로그램 시작 시 OS가 페이지를 매핑하면 즉시 사용 가능. 별도 초기화 코드가 돌지 않는다.
- lifetime은
'static— 프로그램 시작부터 종료까지. - 모든 참조가 같은 메모리 주소를 가리킨다.
- 기본 immutable.
Java의 static final Rectangle SHARED = ..., C의 전역 static const Rectangle r = {...}와 의미적으로 매칭된다.
const — 메모리 자리가 없는 inline 상수
const RECT_C: Rectangle = Rectangle { width: 30, height: 50 };
- 사용 시점마다 컴파일러가 값을 그 자리에 inline 한다. 별도의 메모리 자리가 없다.
&RECT_C로 주소를 가져오면 컴파일러가 임시 위치를 만들어주는 식 — 매번 다른 주소일 수 있다.- 따라서 lifetime이라는 개념이 의미가 없다.
큰 데이터는 static이 적절하다 (inline되면 바이너리가 부풀고 캐시 효율이 떨어진다). 작고 자주 쓰이는 상수는 const가 적합하다.
제약 — compile-time에 결정 가능한 값만
static과 const 모두 초기값이 const fn으로 표현 가능해야 한다.
static NOW: Instant = Instant::now(); // ❌ 런타임 호출 필요
이유는 단순하다. 컴파일러가 바이너리에 비트 패턴을 미리 박아넣어야 하는데, 런타임에야 알 수 있는 값은 박을 수 없다.
런타임 초기화 — OnceLock / LazyLock
전역에서 단 하나, 하지만 첫 접근 시 초기화하는 lazy singleton 패턴은 표준 라이브러리에 안정화되어 있다.
use std::sync::OnceLock;
static CONFIG: OnceLock<Config> = OnceLock::new();
fn config() -> &'static Config {
CONFIG.get_or_init(|| Config::load_from_disk())
}
OnceLock::new()는 const fn이라 빈 상태로 직접 static 초기화에 쓸 수 있다. 첫 호출 시 클로저로 진짜 값을 채우고, 이후 호출은 그 값을 빌려준다. thread-safe.
LazyLock은 첫 접근에서 자동으로 초기화하는 syntactic sugar. 외부 crate lazy_static!, once_cell::sync::Lazy도 같은 목적이며 표준화 이전부터 쓰이던 사실상 표준이었다.
이는 Java의 class Holder { static Config CONFIG = Config.load(); }가 클래스 로딩 시점에 초기화되는 패턴의 Rust 안전 구현이다.
가변 전역 상태 — static mut을 피하라
static mut COUNTER: u32 = 0;
unsafe { COUNTER += 1; } // 모든 접근이 unsafe
static mut은 데이터 레이스를 컴파일러가 막을 수 없어 unsafe 블록 안에서만 접근 가능. Rust ownership 보장의 핵심이 깨지는 영역이라 실무에서 거의 쓰지 않는다.
권장 패턴은 Mutex/RwLock을 static에 박는 것:
use std::sync::Mutex;
static COUNTER: Mutex<u32> = Mutex::new(0);
fn increment() {
*COUNTER.lock().unwrap() += 1;
}
Mutex::new가 const fn이라 직접 static 초기화에 사용 가능하다 (최근 안정화). 더 복잡한 초기화가 필요하면 OnceLock<Mutex<T>> 조합으로 간다.
타입에 묶인 상수 — associated constant
impl Rectangle {
const UNIT: Rectangle = Rectangle { width: 1, height: 1 };
const ZERO: Rectangle = Rectangle { width: 0, height: 0 };
}
let r = Rectangle::UNIT;
impl 블록 안에 const를 두면 그 타입 namespace에 상수가 묶인다. f64::EPSILON, i32::MAX가 이 메커니즘이다. 메모리 모델은 일반 const와 동일 — inline. namespacing만 타입에 붙는다.
Java의 static final 필드와 가장 가까운 매칭. 단, *associated static*은 Rust 문법이 받지 않는다. "타입에 묶인 메모리 한 자리"가 필요하면 모듈 스코프에 static을 두고 모듈로 namespacing한다 — Rust에서 module과 type은 namespacing 측면에서 동등하다.
메모리 위치 한눈에
| 선언 | 메모리 위치 | lifetime | 가변 |
|---|---|---|---|
let x = T { ... } | stack | 스코프 | mut 키워드 |
Box::new(T { ... }) | heap | Box drop까지 | Box<T> mut |
static X: T = ... | data segment (RODATA) | 'static | 불가 |
static mut X: T = ... | data segment (BSS) | 'static | unsafe로 가능 |
const X: T = ... | 자리 없음, 사용처 inline | 해당 없음 | 불가 |
impl T { const X: ... } | 동일 (inline, 타입 namespace) | 해당 없음 | 불가 |
다른 언어와의 매핑
| Java/C++/C | Rust |
|---|---|
static final 필드 | static 또는 impl T { const } |
lazy static (Holder pattern) | static X: OnceLock<T> = OnceLock::new(); |
static 가변 필드 | static X: Mutex<T> = Mutex::new(...); |
static 메서드 | self 없는 associated function (what-are-methods-and-associated-functions) |
결론
static 키워드는 인스턴스를 stack/heap이 아닌 data segment에 박아 프로그램 전체에서 하나의 자리를 차지하게 한다. 컴파일 타임에 결정 가능한 값만 직접 가능하며, 런타임 초기화는 OnceLock/LazyLock으로, 가변 전역 상태는 Mutex를 static에 넣는 패턴으로 해결한다. const는 inline 상수라서 메모리 자리가 없다는 점에서 static과 본질이 다르다 — 큰 데이터엔 static, 작은 상수엔 const가 적절하다.