[0x02] Understanding Onwership

41343 단어 RustOwnershipOwnership

1. What is Ownership?

  • OwnershipRust 프로그램의 메모리 관리 방법에 대한 규칙이다.
  • 모든 프로그램들은 실행되고 있을 때, 메모리를 관리해야 한다.
    1. Java같은 언어들은, garbage collector를 통해 낭비되는 메모리를 최소화하는 방법을 사용한다.
    2. C 같은 언어들은, 메모리의 할당과 해제를 명시적으로 해주어야 한다.
    3. Rust는 새로운 접근법으로 메모리를 관리하는데, 바로 컴파일러가 체크하는 Ownership의 개념이다.
      컴파일러가 체크하는 만큼, 프로그램의 실행 시 퍼포먼스를 저해하는 일은 없다.

1.1 Ownership Rules

  • Ownership에 관한 규칙들을 우선 살펴보자.
    1. 모든 들은 owner라 불리는 변수를 갖는다.
    2. 하나의 에는 하나의 owner만 있을 수 있다.
    3. ownerscope밖으로 나가게 되면, drop된다.

1.2 Variable Scope

  • 변수의 scope는 다른 언어에서의 개념과 동일하다.
fn main() {
						// s 가 아직 선언되지 않음, 아직 유효하지 않음
  let s = "Hello";		// s 가 선언됨, 유효함
  println!("{}", s);	// s 를 이용해 프로그램이 동작함
}						// scope가 끝났기 떄문에, s 는 더이상 유효하지 않음

1.3 The String Type

  • Rust에서의 Stringliteral한 스트링과, 자료형으로써의 스트링이 나뉜다.
  • 여기서 살펴 볼 String Type은 힙 영역에 저장되며, 가변적인 크기를 가지는 자료형이다.

1.3.1 Memory Allocation

  • String이 가변적인 크기를 갖는다는 것은 아래 두 과정을 수행한다는 것이다.
    • 런타임 중, 메모리 할당자에 의해 메모리가 할당(allocating)되어야 한다.
    • 사용한 메모리를 메모리 할당자에게 반환(returning)하는 방법이 있어야 한다.
  • 메모리를 할당(allocate)하는 것은 String::from 메소드를 호출할 때 일어난다.
    • 메소드 내부에서 메모리 할당자에게 메모리를 요청한다.
  • 메모리를 반환(return)하는 것은 해당 메모리에 대한 ownerscope를 벗어날 경우 자동으로 일어난다.
    • Rustscope가 끝나는 curly bracket(})에 대해 drop()함수를 자동으로 호출한다.
    • drop 함수가 메모리를 반환하는 것이다.

1.4 Ways Variables and Data Interact

1.4.1 [_] Move

  • 여기서는 RustOwnership과 관련하여, 메모리 단에서 어떤 일이 일어나는지 살펴본다.
  • 아래의 코드를 살펴보자.
{
	let s1 = String::from("hello");
    let s2 = s1;
}
  • Rust에서는 String과 같은 힙 영역에 할당되는 데이터에 관해 아래 그림과 같이 이해할 수 있다.

  • 이 때, let s2 = s1; 코드에 관해서는 아래와 같이 Shallow Copy가 일어난다.

  • 하지만, Rust 에서는 위 그림과 같이 두 개의 포인터가 하나의 영역을 가리키지 않는다.
  • 이 때, Ownership의 개념이 들어온다.

  • 따라서, 아래 코드는 에러를 일으킨다.
fn main() {
	let s1 = String::from("hello");
    let s2 = s1;
    
    println!("{}", world!", s1);		// s1의 값은 s2로 `move` 했기 때문에 compile error 를 일으킨다.
    
    
}

1.4.2 [_] clone

  • 만약 String에 대하여 Deep Copy를 하고 싶다면, clone함수를 사용하면 된다.

fn main() {
  let s1 = String::from("hello");
  let s2 = s1.clone();

  println!("{}, world!", s1);    
}

1.4.3 [_] copy

  • 스택에만 저장되는 타입(integer 등)에 대해서는 Copy이라는 trait이 구현되어 있다.
  • 따라서, 힙에 저장되는 데이터처럼 clone 함수가 따로 필요 없다.
fn main() {
	let x = 5;
    let y = x;
}
  • 특히, 튜플의 경우 내부 원소들이 모두 Copy가 구현된 타입이라면 Copy를 사용할 수 있다.

1.5 Ownership and Functions

  • 1.4 에서 살펴본 move, clone, copy의 개념을 토대로 함수에서와 연관지어 살펴보자.

1.5.1 Parameters (arguments)

  • 아래 코드와 주석을 참고하자.
fn main() {
  let s = String::from("hello");	// s 가 선언됨
  takes_ownership(s);				// s 의 데이터는 함수 안으로 `move` 함.
  									// 따라서, s 는 더 이상 이 scope 내에서 유효하지 않음

  let n = 5;						// n 이 선언됨
  makes_copy(n);					// n 은 i32 이므로, `Copy`가 일어남
  									// 따라서, n 은 이 scope 내에서 계속 유효함
}	// s, n 이 scope를 벗어남.
	// 하지만, s 는 `move`로 함수 내부에 값이 들어갔으므로 `drop`되지 않음


fn takes_ownership(some_string: String) {
  println!("{}", some_string);
}	// some_string 이 scope 를 벗어났기 때문에, `drop`이 호출되고 메모리가 반환됨.


fn makes_copy(some_integer: i32) {       
  println!("{}", some_integer);
}	// some_integer 가 scope 를 벗어남
  • 즉, 저장되는 공간이 스택, 둘 중 어느 곳이냐에 따라 다르게 동작함을 확인할 수 있다.

1.5.2 Return Value

  • 함수가 값을 return하는 것도 ownership을 이동시킬 수 있다.
  • 아래 코드와 주석을 참고하자.
fn main() {
  let s1 = gives_ownership();			// gives_ownership() 함수의 return value는
  										// s1 으로 `move` 한다.

  let s2 = String::from("hello");

  let s3 = takes_and_gives_back(s2);	// s2 가 takes_and_gives_back() 함수 안으로 `move` 하므로,
  										// s2 는 더 이상 이 scope 에서 유효하지 않다.
  										// 이후 다시 return 되어 s3 로 `move` 한다.
}	// s2 는 유효하지 않은 변수이므로 제외하고
	// s1, s3 는 scope가 끝났으므로 `drop` 함수의 호출에 의해 메모리를 반환한다.


fn gives_ownership() -> String {
  let some_string = String::from("yours");	// some_string 을 선언함
  some_string								// some_string 이 return 되며 `move` 한다.
}	// some_string 은 함수의 리턴값으로서 `move` 되었으므로,
	// `drop` 함수를 호출하지 않는다.

fn takes_and_gives_back(a_string: String) -> String {	// a_string 으로 소유권이 `move`되어 들어옴
  a_string												// a_string 이 return 되며 `move` 한다.
}	// a_string 은 함수의 리턴값으로서 `move` 되었으므로,
	// `drop` 함수를 호출하지 않는다.
  • 함수를 호출할 때마다, ownership을 이동하는 것은 사실 좀 번거로운 작업이다.
  • 다행히 Rust에서는 reference 라는 개념을 사용할 수 있다!

2. References and Borrowing

2.1 References

  • Reference란 포인터처럼 어떤 변수가 소유하고 있는 데이터의 주소값이다.
  • 하지만, 포인터와는 달리 Reference는 특정 자료형의 유효한 값임을 보장한다.
  • 아래 코드는 Reference에 대한 예제 코드이다.
fn main() {
  let s1 = String::from("hello");

  let len = calculate_length(&s1);    // `move` 가 아니라 `referencing` 임

  println!("The length of '{}' is {}.", s1, len);
}

fn calculate_length(s: &String) -> usize {
  s.len()
}
  • 변수 s, s1, 그리고 String::from("hello") 에 대한 개념은 아래 그림을 통해 이해할 수 있다.

  • C++에서 자주 사용하던 &와 비슷한 개념으로 이해할 수 있다.
  • 따라서, 위의 코드에서 calculate_length()가 끝났다고 하더라도 s: &Stringdrop되지 않는다.

2.2 Mutable References

  • 함수 인자로 전달한 데이터를 mutable하게 다루고 싶을 때는 mutable references를 이용하면 된다.
fn main() {
  let mut msg = String::from("This is a message");

  change_message(&mut msg);						// mutable reference

  println!("{}", msg);
}

fn change_message(message: &mut String) {		// mutable reference
  message.push_str(", I added this Lol");
}
  • Rust에서는 mutable reference를 통해 데이터 값을 빌려주는(borrowing) 것은 한 번에 하나만 가능하다.
  • 아래 코드는 컴파일 에러를 일으킨다.

  • 이런 식의 제한을 두는 이유는 통제된 상황에서 데이터를 변경하고자 함이다.
  • Rust는 이를 통해, 컴파일 타임에서 발생하는 Data Race를 해소할 수 있다.
    • Data Race는 아래 3가지 경우에 발생한다.
      1. 두 개 이상의 포인터가 하나의 데이터에 접근하려 할 때
      2. 최소한 하나 이상의 포인터가 write하려고 할 때
      3. 데이터로의 접근을 동기화시킬 메커니즘이 없을 때
  • 또, Rust에서는 mutable referenceimmutable reference를 혼합하여 사용할 수 없다.
  • 아래의 경우도 컴파일 에러를 일으킨다.
fn main() {
  let mut s = String::from("Lol");

  let r1 = &s;      // immutable, no problem
  let r2 = &s;      // immutable, no problem
  let r3 = &mut s;  // mutable, BIG PROBLEM 

  println!("{}, {}, and {}", r1, r2, r3);   

}

  • 위 컴파일 에러에 대한 해결방법이 있다!
  • Reference&로 지정되고, 마지막으로 사용될 때 까지를 하나의 scope 로 본다.
  • 따라서, 아래와 같이 코드를 수정하면 컴파일 에러 없이 실행시킬 수 있다.
fn main() {
  let mut s = String::from("Lol");

  let r1 = &s;      // immutable, no problem
  let r2 = &s;      // immutable, no problem

  println!("{}, and {}", r1, r2);	// r1, r2 의 사용이 끝났으므로 scope 가 끝난 셈이다.

  let r3 = &mut s;  // mutable, BIG PROBLEM 
  println!("{}", r3);
}

2.3 Dangling References

  • Dangling Pointer와 마찬가지로, 비어있는 위치를 가리키는 Reference를 의미한다.
  • 보통 메모리 할당을 해제하는 타이밍을 잘못 잡았을 때 발생한다고 한다.
  • Rust에서는 컴파일 단에서 이를 막아준다.
fn main() {
  let s = get_msg();

  println!("{}", s);
}

fn get_msg() -> &String {      
  let a = String::from("TEST");
  &a
}

  • 에러 메세지는 borrowed value를 받아 오지만, 데이터가 없음을 말하고 있다.

3. The Slice Type

  • Slice는 이름 그대로 전체 데이터가 아니라 연속된 일부 데이터를 의미한다.
  • Reference의 일종이므로, Ownership이 없다.
  • 아래 코드를 보자.
fn first_word(msg: &String) -> ?
  • 어떠한 String 값을 받되, Ownershipmove로 받지 않는다고 할 때
  • String의 일부인 '첫 번째 단어의 사이즈'를 return 하고자 한다면, 아래와 같이 코딩할 수 있다.
fn main() {
  let s = String::from("This is a message");  

  let word = first_word(&s);
  println!("{}", word);
}

fn first_word(msg: &String) -> usize {        
  let bytes = msg.as_bytes();					// `공백`을 찾기 위해 String 을 bytes로 변환함.

  for (i, &item) in bytes.iter().enumerate() {	// iter() 는 bytes 에서 iterating 하기 위해 호출
  												// enumerate() 는 iter 의 결과를 wrap 해서 tuple로 반환
                                               	// tuple 의 첫 번째 값은 index
                                                // tuple 의 두 번째 값은 원소의 Reference
                                                // destructure 를 통해 i 와 &item 에 값을 받음
    if item == b' ' {
      return i;
    }
  }

  msg.len()
}
  • 이런 식으로 구현은 할 수 있겠지만, 여기서 받은 *첫 번쨰 단어의 사이즈인 변수 word의 크기가 계속 유효하다는 보장은 없다.
fn main() {
  let s = String::from("This is a message");
  
  let word = first_word(&s);
  
  s.clear();
  
  // ...
}
  • 위 코드에서 s.clear() 이후에는 더 이상 word == 4 라고 할 수 없다.
  • 이는, sword 가 각각 따로 존재하는 변수이기 때문이다.
  • Rust 에서는 이러한 임의 collection 에 대해 참조하기 위해 Slice 라는 개념을 제공한다.

3.1 String Slice

  • String Slice란 , String 일부에 대한 Reference 로 아래와 같이 이용할 수 있다.
  • 구체적으로, String Slice의 타입은 &str 이다.
  let s = String::from("hello world");
  
  let hello = &s[0..5];
  let world = &s[6..11];
  • 즉, String Slice의 format은 [starting_index .. ending_index] 이다.
  • 내부적으로 Slicestarting_indexending_index - starting_index의 값을 이용해서 일부분을 참조한다.
  • 위 코드에 대한 개념적인 그림은 아래와 같다.

  • Slice 를 다음과 같이 사용하는 것도 가능하다.
  let s = String::from("hello");
  
  let slice = &s[0..3];
  let slice = &s[..3];	// == [0..3]
  
  let len = s.len();
  let slice = &s[2..len];
  let slice = &s[2..];	// == [2..len]
  
  let slice = &s[..];	// entire string

한 가지 주의할 점UTF-8이 4Byte를 갖는다는 것에 유의하여 slice 를 해야 한다는 것

  • 이제 Slice 를 이용해서 처음에 구현하고자 한 first_word() 함수를 구현하면 아래와 같다.
fn first_word(msg: &String) -> &str {
  let bytes = msg.as_bytes();

  for (i, &item) in bytes.iter().enumerate() {
    if item == b' ' {
      return &msg[..i];
    }
  }

  &msg[..]
}
  • 위 코드에 대해 앞에서 했던 s.clear() 같은 코드를 추가하면 컴파일 단계에서 에러가 나는 것이 자명하다.

3.2 String Literals

  • String Literals 는 바이너리 코드에 그대로 저장된다는 점을 상기해보자.
  let s = "Hello, world!";
  • 이 때, 변수 s의 타입은 &str 이다.
  • String Literalsimmutable 임을 확인할 수 있다.

3.3 String Slices as Parameters

  • 앞에서 구현한 first_word() 는 함수 인자로 &String 을 받았었다.
  • 하지만, &String 대신 &str을 쓰면, 두 타입을 모두 넘겨받아 쓸 수 있다.
  • 이는 Deref Coercions 라는 특성에 의해 가능한데, 이는 15 장에서 확인할 수 있다.

3.4 Other Slices

  • 일반적인 Slice 도 가능하다.
  • 아래의 코드처럼 Array에 대해서도 slice 를 사용할 수 있다.
  let a = [1, 2, 3, 4, 5];
  
  let slice = &a[1..3];
  
  assert_eq!(slice, &[2, 3]);
  • 위에서 slice 변수의 타입은 &[i32] 이다.

Summary

  • Ownership, Borrowing, Slices 들은 Rustmemory safety를 가능하게 해준다.
  • Rust는 다른 언어들처럼 메모리 사용에 대한 권한을 제공해줌과 동시에, ownership이라는 개념으로 메모리를 자동으로 해제할 수 있도록 한다.
  • Ownership 이라는 개념은 Rust 전체에 걸쳐 중요한 개념이므로, 앞으로의 챕터들에서도 주의깊게 살펴보도록 한다.

좋은 웹페이지 즐겨찾기