Rust에서 소유권이란?
글또(글쓰는 또라이) 활동 중, 유데미 강의를 골라 들을 수 있는 기회가 있었는데요. 강의 중 평상시 관심이 있던 Rust 강의가 있어, 강의를 들으면서 다른 언어에서 보지 못했던 소유권 개념을 정리해봤습니다.
메모리 관리…
컴퓨터 프로그램이 사용하는 자원은 여러가지가 있지만, 그 중 가장 흔하게 볼 수 있는 것이 CPU와 메모리입니다. 하지만 개인적으로 두 자원 중 관리가 되지 않을 경우 큰 문제가 되는 것은 메모리라고 생각합니다. CPU 사용량이 높은 경우, OS는 전체 CPU사용 시간을 쪼개 각 프로그램 별 사용량을 관리합니다. 따라서 전체 시스템이나 프로그램이 느려집니다. 하지만 메모리 사용량의 경우, 대부분의 OS나 메모리 관리 주체(K8s - Pod의 메모리 사용량)은 OOM(Out Of Memory)에러를 발생하며 해당 작업을 종료합니다.
CPU사용량이 많아지는 것은 그래도 작업이 어느정도 진행될 수 있는 여지가 있지만, 메모리를 과하게 사용한다거나 Leak이 발생한다면 작업이 비정상적으로 종료될 가능성이 높아 메모리 관리를 잘 해야 합니다.
메모리를 개발자가 직접 관리해야 하는 C/C++을 사용해 과제를 진행할 때나 토이프로젝트를 진행할 때, malloc()과 free()를 어떻게든 잘 넣어 Leak이나 Runtime에 발생하는 과도한 메모리 사용량을 없애려고 실제 서비스 로직을 구성하는것 만큼 노력했던 기억이 있습니다…
Rust 에서의 메모리 관리 - 소유권
시스템 프로그래밍 언어로 시작한 Rust는, 메모리를 직접적으로 관리할일이 많을 것이라는 것을 상정해 메모리 관리(메모리 안전)을 언어의 설계에서부터 신경쓴것이 느껴졌습니다.
메모리 소유권 이라는 개념을 도입해 메모리(변수)의 할당 - 사용 - 해제로 이어지는 모든 라이프사이클을 관리하는데요, 이 점이 제가 봤던 다른 언어들과는 달라 신선하게 다가왔습니다.
소유권 규칙
소유권에는 3가지 원칙이 있습니다.
- Rust내 모든 값은 “소유자 - Owner”가 있습니다.
- 한 값을 동시에 여러개의 소유자가 소유할 수 없습니다.
- 변수가 생성된 후, 스코프 밖으로 나가면(해당 스코프 코드가 끝나면) 할당 해제됩니다.
이동
스코프 간 소유권을 넘겨주는 방법에는 이동과 대여가 있습니다. 아래 스니펫은 이동 예시 입니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
enum Light {
Bright,
Dull,
}
fn display_light(light: Light) {
match light {
Light::Bright => println!("bright");
Light::Dull => println!("dull");
}
} # 3. 첫 display_light() 실행 종료시, 소유권 옮겨받은 dull 변수 할당 해제
fn main() {
let dull = Light::Dull; #1. 변수 생성
display_light(dull); #2. display_light 함수로 소유권 이동
display_light(dull); #4. dull 변수가 위 코드 실행시 해제되어 사용할 수 없음
}
main() 함수 내 dull 변수를 생성하였고(1), 그 뒤 display_light() 함수에 인자로 넣어 함수를 실행하게 되면 소유권이 display_light() 함수로 옮겨갑니다.(2) 그 뒤, 함수의 코드가 전부 실행이 끝나고 스코프를 벗어나게 되면 소유권을 넘겨받은 변수도 해제됩니다.(3) main() 함수에서는 해제된 변수를 사용할 수 없어 에러가 발생합니다.
이렇게 되면 컴파일러가 에러를 발생시켜 메모리 사용에 문제가 있음을 알려줍니다.
대여
소유권을 이동하는 방법 외에는 대여 가 있는데요, 아래 스니펫은 대여 예시입니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
enum Light {
Bright,
Dull,
}
fn display_light(light: &Light) {
match light {
Light::Bright => println!("bright");
Light::Dull => println!("dull");
}
} # 3. 첫 display_light() 실행 종료시, 소유권 옮겨받은 dull 변수 할당 하지 않음
fn main() {
let dull = Light::Dull; #1. 변수 생성
display_light(&dull); #2. display_light 함수로 소유권 대여
display_light(&dull); #4. dull 변수를 사용해 다시 실행 가능
}
위 이동 예시와 유사하지만, 함수 시그니처와 호출 시 & 기호를 추가한 것이 다릅니다.
이 경우, 함수를 실행할 때 함수의 소유권(메모리 영역 자체)을 넘기는 것이 아니라 함수의 레퍼런스(메모리 영역의 위치)를 넘깁니다. 따라서 함수는 메모리 영역을 직접 소유하지 않고, 참조를 통해 메모리 영역에 접근하고 사용할 수 있습니다.
스니펫 예시를 보면, display_light() 함수는 Light enum 타입의 레퍼런스를 받도록 변경되었고 main() 에서는 dull 변수의 레퍼런스(&dull)를 전달하고 있습니다. 이 경우, display_light() 함수는 소유권을 받지 않고도 변수를 접근해 사용할 수 있고 main()함수는 이후 함수 호출을 문제 없이 진행할 수 있습니다.
소유권 이동 시 복사?
메모리 구역의 소유권이 이동되는 경우, 함수의 타입에 따라 함수 값의 복사가 일어납니다.
작은 값(Copy Trait 구현 값)의 경우, 소유권을 이동할 때 메모리 복사가 일어납니다. 작은 값의 경우, 일반적으로 메모리 공간에서 복사 비용이 저렴하고 성능에 큰 영향을 미치지 않기에 값의 안전한 복사를 보장하는 방향으로 설계되었습니다. 작은 값은 아래와 같습니다.(스택에 들어갈 수 있는 - 컴파일 타임에 크기를 알 수 있는 primitive 한 값)
- 작은 정수 값 (i8, u8, i16, u16, i32, u32, f32, char 등), 논리 값 (bool)
- 문자열 (String)
- 구조체 타입 (단순히 필드를 가진 경우)
큰 값(Copy Trait 미구현 값)의 경우, 소유권을 이동할 때 메모리 복사가 이루어지지 않습니다. 이동되는 값의 소유권이 새로운 소유자에게 전달되고, 원래 소유자는 해당 값에 대한 책임을 가지지 않습니다.
큰 값을 이동하는 것은 비용이 많이 들고 성능 저하가 발생할 수 있기에 소유권 이동으로 해당 메모리 구역을 관리하면서 성능 오버헤드를 줄일 수 있도록 설계되었습니다. 큰 값은 아래와 같습니다.
- 벡터(Vec), 해시맵(HashMap)과 같은 동적 데이터 구조
- 큰 구조체(복잡한 필드나 참조를 가진 경우)
강의를 들으면서, 생소한 개념이 나와 정리해봤습니다. 업무에서 직접 Rust를 사용할거같지는 않은데, 메모리 관리형 시스템 프로그래밍 언어를 사용하라는 백악관의 권고를 봐 한번 강의를 들어봤습니다.
강의는 간단한 hello world 에서부터 loop ~ 어려운 개념까지 넓게 커버하고 있습니다. 생각보다 쉬운 부분부터 시작해 다른 언어를 접해본 분이라면 입문용으로 쉽게 접근할 수 있을 것 같습니다. 무엇보다 한글 자막이 된다는 점!
Github에서 Rust 프로젝트를 좀 뜯어보면 좋겠다는 생각을 하고 있는데, 일단 강의의 내용을 몇 번 더 정리해보고 나면 코드를 편하게 읽는데 큰 도움이 될 것 같네요.