러스트 만의 메모리 관리 규칙 이라고 보면 된다.
다른 언어들은
1. 가비지 컬렉터를 사용하여 메모리를 관리
2. 프로그래머가 직접 메모리를 할당하고 해제하는 것을 관리
rust는 이에 대하여 1, 2번의 방식이 아닌 소유권이라는 시스템을 이용하여 메모리를 관리한다.
rust는 소유권이 지켜진 상태가 아니라면 컴파일 되지 않는다.
소유권은 다음의 규칙을 가진다.
- 러스트에서, 각각의 값은 소유자 (owner) 가 정해져 있습니다.
- 한 값의 소유자는 동시에 여럿 존재할 수 없습니다.
- 소유자가 스코프 밖으로 벗어날 때, 값은 버려집니다 (dropped).
이 규칙을 통해 메모리의 할당과 해제를 수행한다.
만약 프로그래머가 java를 사용하여 객체를 만들면 가비지컬렉터가 메모리를 해제하고 C를 사용하여 메모리를 할당하면 직접 해제해줘야 한다.
하지만 rust는 위의 규칙을 통해 생성 및 사용하는 객체가 사용되는 스코프를 넘어가면 자동으로 해당 객체에 대하여 drop이라는 메모리 할당 해제 함수를 호출한다. (drop은 객체를 구현하는 프로그래머가 직접 작성 가능하다.)
이러한 규칙을 통한 방식은 만약 객체의 소유권을 가진 포인터, 변수가 2개 이상 존제할 경우 double free 취약점을 야기할 수 있다.
이에 대하여 rust는 한 객체에 대하여 얕은 복사가 아닌 이동(move)연산을 기본적인 연산으로 규정한다.
이동연산을 통해 값의 소유권, 데이터를 넘긴 이후에 기존 변수를 사용하려고하면 컴파일러는 에러를 내보낸다.
만약 값을 복사하여 사용할 경우에는 깊은 복사를 지원하는 함수를 따로 호출해 사용하는 구조인 것이다.
함수의 매개변수로 전달하는 경우에도 이동연산이 이뤄지고 소유권이 넘어간다.
위의 얘기들은 모두 힙 메모리 영역의 메모리 할당 및 해제에 대한 얘기이며 만약 스택 메모리에 저장하는 타입들인 i32, bool 같은 기본타입들을 사용하게 되면 새롭게 변수를 복사하여도 기존 변수를 사용가능하다.
- 모든 정수형 타입 (예: u32)
- true, false 값을 갖는 논리 자료형 bool
- 모든 부동 소수점 타입 (예: f64)
- 문자 타입 char
- Copy 가능한 타입만으로 구성된 튜플 (예를 들어, (i32, i32)는 Copy 가능하지만 (i32, String)은 불가능합니다)
예시 코드
fn main() {
// 1. 이동 (Move) - String (힙 데이터)
let s1 = String::from("hello");
let s2 = s1;
// println!("{}", s1); // 에러! s1의 소유권이 s2로 넘어갔습니다.
println!("Move: s2 = {}", s2);
// 2. 복사 (Copy) - i32 (스택 데이터)
let x = 10;
let y = x;
println!("Copy: x = {}, y = {}", x, y); // 둘 다 사용 가능
// 3. 클론 (Clone) - 명시적 깊은 복사
let s3 = String::from("world");
let s4 = s3.clone();
println!("Clone: s3 = {}, s4 = {}", s3, s4); // 둘 다 사용 가능 (새 메모리 할당)
// 4. 참조 (Reference) - 빌리기 (포인터만 복사)
let s5 = String::from("rust");
let s6 = &s5;
println!("Ref: s5 = {}, s6 = {}", s5, s6); // 둘 다 사용 가능 (소유권은 여전히 s5에게 있음)
}
위 코드의 마지막을 보면 참조 (Reference) 를 볼 수 있다.
위 글에서는 double free 때문에 이동연산을 기본으로 본다고 했는데 저렇게 두개의 변수가 하나의 메모리를 가리키게하면 똑같이 double free 위협이 존재하는 것 아닌가라고 생각할 수 있다.
참조는 이동 연산과 다르게 동작한다. rust는 메모리에 소유권 시스템을 사용한다.
이는 각 메모리에 특정 변수가 소유권을 가지는 것을 의미하며 해당 소유권을 가지는 소유자가 스코프를 벗어날 때 메모리를 해제함을 의미한다.
참조는 이 소유권을 넘기지 않는 연산이다.
소유권을 넘겨받지 않기에 참조하는 변수는 스코프를 벗어나더라도 해당 메모리를 해제하지 않는다.
mut 변수에 대하여 참조자를 만들고 참조자로 데이터를 수정하기 위해서는 &mut 키워드를 사용하여 가변 참조자 변수로 선언해야 한다.
그냥 참조자는 여러개 만들고 사용 가능하다.
이 한 객체의 가변 참조자는 스코프 내에서 하나만 존재 가능하다. (현제의 rust는 컴파일러의 계산으로 가변참조자의 역할이 종료하면 다른 가변 참조자를 같은 스코프내에서 선언 및 사용가능하도록 하고 있다)
그리고 불변 참조자가 존재할 때 가변참조자를 만드는 것도 불가능하다.
위의 가변참조자에 대한 규약은 스택타입 i32, bool같은 타입들도 동일하게 적용된다.
이러한 가변 참조자를 하나만 사용하도록 하는 제어는 다음과 같은 data race 조건을 없도록 규제한다.
- 둘 이상의 포인터가 동시에 같은 데이터에 접근
- 포인터 중 하나 이상이 데이터에 쓰기 작업을 시행
- 데이터 접근 동기화 메커니즘이 없음
공부하다 생긴 의문점
데이터의 mutable immutable에 대해서는 생성시가 아닌 let 뒤의 mut가 존재하는지 안하는지인데 이는 데이터, 객체의 생성에 무조건 let이 붙어서 mut여부를 제어해야하나?
이에 대한 AI 답변
러스트의 가변성(mut)은 데이터 자체의 속성이라기보다는 **"그 데이터를 담고 있는 변수(바인딩)가 데이터를 수정할 권한이 있는가"**의 문제입니다.
질문하신 내용에 대해 핵심적인 답변을 드리자면, **"모든 데이터는 let 바인딩을 통해 가변성이 결정되지만, 소유권이 이동(Move)할 때 가변성을 새로 정의할 수 있다"**는 점이 중요합니다.
러스트에는 python과 같은 슬라이스가 사용가능하다.
arr[0..9], arr[..=10], arr[..] 이런 식으로 사용가능
슬라이스를 이용하여 참조 변수를 만드는 것은 가능하나 이동연산은 구조적으로 불가능하다.
String을 슬라이스 하면 &str 타입이 된다. &str은 문자열에 대한 슬라이스 참조 타입 즉, 전체 문자열의 서브스트링 타입이라고 보면 된다.
'Rust' 카테고리의 다른 글
| 열거형 (enumerate) (0) | 2026.01.06 |
|---|---|
| 구조체 (struct), 약간의 메크로 얘기 (0) | 2026.01.05 |
| 조건, 반복문 (0) | 2026.01.04 |
| 데이터 타입, 함수 (0) | 2026.01.03 |
| Crate와 Library (0) | 2026.01.02 |