티스토리 뷰

Programming Language/Rust

[Rust] 소유권

SdardewValley 2021. 2. 22. 15:43
반응형

🌱 소유권이란?

 

러스트에서 메모리는 컴파일러가 컴파일 시점에 검사하는 다양한 규칙으로 이루어진 소유권 시스템에 의해 관리된다. 따라서 소유권과 관련된 기능은 프로그램의 실행 성능에 아무런 영향을 미치지 않는다. 이런 방식으로 러스트는 메모리 안정성을 보장한다.

 

📌 스택 메모리 & 힙 메모리

 

러스트 같은 시스템 프로그래밍 언어 환경에서는 스택 메모리 또는 힙 메모리 중 어디에 저장되었는지에 따라 언어의 동작이나 의사결정에 큰 영향을 미친다.

 

스택과 힙은 런타임에 활용되는 메모리의 일부이다. 스택은 순서대로 데이터를 저장하며 데이터를 읽을 때는 역순으로 읽는다. 따라서 마지막에 들어온 데이터를 먼저 읽는 구조(LIFO, Last In First Out)이다.

 

스택에 저장되는 모든 데이터들은 고정된 크기를 가진다. 컴파일 시점에 크기를 알 수 없는 데이터나 런타임에 동적으로 크기가 변하는 데이터는 힙 메모리에 저장한다. 힙을 사용할 때는 일정 공간을 사용중인 메모리로 표시한 후 해당 메모리의 주소값인 포인터를 넘겨준다. 이 과정을 '할당'이라고 한다. 포인터는 크기가 고정된 값이므로 스택에 저장할 수 있다.

 

스택에 데이터를 저장할 때 저장할 공간을 찾을 필요가 없기 때문에 힙에 할당하는 것 보다 빠르다. 힙 메모리에 저장된 데이터에 접근할 때도 포인터를 따라가야 하기 때문에 상대적으로 느리다.

 

코드에서 어디가 힙 메모리를 사용하는지 추적하고 사용하지 않는 데이터를 제거하면 메모리 부족 문제를 해소할 수 있다. 이처럼 러스트의 소유권은 힙 데이터를 관리하려고 존재한다.

 

🌱 소유권 규칙

  • 러스트가 다루는 각각의 값은 소유자(owner)라고 부르는 변수를 가지고 있다
  • 특정 시점에 값의 소유자는 단 하나뿐이다
  • 소유자가 범위를 벗어나면 그 값은 제거된다

🌱 범위: 프로그램 안에서 유효한 한도

변수의 유효 범위를 표현한 것

  1. 변수가 선언되기 전에는 유효하지 않다
  2. 변수가 선언된 이후로 유효하다
  3. 범위를 벗어나면 변수는 유효하지 않다

🌱 String 타입

 

러스트에서는 컴파일 타입에 알수 없는 텍스트(ex. 입력)와 같은 경우를 위해서 String 타입을 제공한다. 이 타입은 힙에 저장되므로 컴파일 시점에 알 수 없는 크기의 문자열을 저장할 수 있다.

from 함수를 이용하면 문자열 리터럴을 이용해 String 인스턴스를 생성할 수 있다.

두 개의 콜론(::)은 from 함수를 String 타입의 이름 공간으로 제한한다.

문자열을 변경하는 코드

이렇게 생성한 문자열은 변경이 가능하다.

 

문자열 리터럴은 컴파일 시점에 내용을 알고 있으므로 최종 실행할 형태로 하드코딩할 수 있다. 따라서 이러한 불변성 때문에 빠르고 효율적이다. 컴파일 시점에 길이를 알 수 없거나 길이가 변경된다면 미리 변환할 수 없다.

 

가변 문자열을 지원하는 String 타입은 다음과 같은 두 가지 절차를 거친다.

  1. 메모리를 런타임에 운영체제에 요청한다.
  2. 사용이 완료된 후에는 메모리를 다시 운영체제에 돌려준다.

(1) Move

1번은 String::from 함수를 호출하면 함수가 필요한 메모미를 요청한다.

2번은 가비지 콜렉터를 사용하거나 개발자가 직접 처리하는 언어가 있다. 러스트는 다른 방식으로 수행한다. 변수에 할당된 메모리는 범위를 벗어나는 순간 자동으로 해제한다. 변수가 범위를 벗어나면, 즉 닫는 중괄호를 만나면 러스트는 drop이라는 함수를 자동으로 호출한다. 

 

정수 대입

x에 5를 대입하고 값을 복사하여 y에 대입한다. 정수는 크기가 고정되어 있기 때문에 5가 스택에 2개 저장된다.

string 대입
s1 구조

String 타입은 포인터, 길이, 용량 세 부분으로 구성된다. 이 데이터는 스택에 저장된다. 

📌 길이와 용량

  • 길이: 바이트 단위의 메모리 사용량
  • 용량: String 타입이 운영체제로부터 할당받은 총 메모리

변수 s1을 s2에 대입하면 힙 메모리에 있는 데이터가 아니라 문자열에 대한 포인터, 길이, 용량이 스택에 복사된다. 만약 힙 메모리에 있는 실제 데이터가 복사된다면, 데이터가 큰 경우 런타임 성능이 크게 떨어지게 된다.

 

위에서 drop 함수가 자동으로 힙 메모리를 해제한다고 했다. s1, s2에서 두 포인터가 같은 메모리를 가리키게 되면 같은 메모리를 해제하게 된다. 이것은 이중 해제 에러(double free error)이다. 이는 메모리 안전성 버그 중 하나이다. 메모리를 두 번 해제하는 것은 메모리의 불순화(corruption)를 일으키며 보안상의 취약점이 될 수도 있다.

 

s1은 더 이상 참조가 불가능

러스트는 할당된 메모리를 복사하는 대신 s1이 더 이상 유효하지 않다고 판단하여 메모리 해제를 하지 않는다.

s1을 사용하였을 때 발생한 에러

위와 같이 에러가 발생한다.

 

러스트는 복사하는 것을 얕은 복사라고 하지 않고 이동(move)이라고 한다. 러스트는 절대 자동으로 깊은 복사를 하지 않기 때문에 런타임 성능 관점에서 매우 가벼운 작업이다.

 

(2) Clone

스택 메모리의 데이터가 아닌 힙 메모리의 데이터를 복사하고자 할 때 clone이라는 메서드를 사용하면 된다. 

clone을 사용한 예제

이 코드는 복사하는 메모리 크기에 따라 무거운 작업이 될 수도 있다.

 

(3)  Copy

정수형 데이터를 복사

정수형과 같은 타입은 컴파일 시점에 크기를 알 수 있다. 따라서 실제 값이 복사되어 스택에 저장된다. 

 

<Copy가 가능한 타입>

  • 정수형 타입
  • boolean 타입
  • 부동 소수점 타입
  • copy가 가능한 타입으로만 구성된 튜플

🌱 함수 & 소유권

함수에서도 값이 이동이나 복사가 이루어 진다. 

변수 owner의 소유권이 함수로 이동하고 함수가 끝나면 변수 owner은 더 이상 유요하지 않다. 따라서 변수 test에서 값을 대입할 때 에러가 발생한다.

 

 

🌱 리턴 & 소유권

값을 다른 변수에 할당하면 소유권이 옮겨진다. 힙 메모리에 저장된 변수의 소유권이 다른 변수로 옮겨지지 않으면 범위를 벗어날 때 drop 함수에 의해 제거된다.

인자와 결과값을 튜플로 리턴하는 함수
calculate_length 함수 사용

함수에 변수를 주고 리턴값에 계산결과 값과 변수를 함께 리턴해서 다시 사용할 수 있지만 복잡하다.

 

🌱참조 & 대여

참조로 문자열 길이를 계산하는 함수를 작성

함수를 선언할 때 &String을 인자로 사용했고 사용할 때도 &s1으로 문자열을 참조하였다.

 

이 &(ampersands) 기호로 참조를 할 수 있다. &를 사용하여 소유권을 가져오지 않고 값을 참조할 수 있다.

 

코드의 다이어그램

참조는 소유권을 가지고 있지 않기 때문에 범위를 벗어나더라고 drop 함수가 호출되지 않는다. 

이렇게 매개변수로 참조를 전달하는 것을 대여(borrowing)라고 한다. 

 

참조는 기본적으로 불변이라 변경하려고 할 때 오류가 발생한다. 하지만 이는 가변 참조를 사용하면 해결할 수 있다.

 

가변 참조를 사용하는 함수

가변 참조를 사용하기 위해서 변수에 mut 키워드를 추가하고 &mut로 가변 참조를 생성한다.

 

가변 참조는 한 개만 가능

특정 범위 내의 특정 데이터에 대한 가변 참조는 오직 한 개만 존재해야 한다.

이런 제약으로 데이터 경합(data races)를 컴파일 시점에 방지할 수 있다.

 

📌경합 조건(race condition)

  • 둘 이상의 포인터가 동시에 같은 데이터를 읽거나 쓰기 위해 접근할 때
  • 하나 이상의 포인터가 데이터를 쓰기 위해 사용될 때
  • 데이터에 대한 접근을 동기화할 수 있는 메커니즘이 없을 때

러스트는 경합이 발생할 수 있는 코드의 컴파일을 허용하지 않는 방식으로 문제를 예방한다.

 

러스트는 특정 범위 내의 특정 데이터에 대한 가변 참조는 오직 한 개만 존재해야 한다고 했다. 가변 참조를 동시에 사용할 수는 없지만, 중괄호를 사용하여 새로운 범위를 생성하여 여러개의 가변 참조를 활용할 수 있다.

 

error[E0502]

불변 참조를 사용 중일 때 가변 참조를 사용할 수 없다.

 

데이터를 읽는 동작은 데이터에 영향을 주지 않으므로 불변 참조는 여러 개를 생성해도 괜찮다.

error[E0502]

위도 같은 에러가 발생한다. 가변 참조를 사용 중일 때도 불변 참조를 사용할 수 없다.

 

🌱 죽은 참조

죽은 참조: 해제된 메모리를 참조하는 포인터

error[E0106]
에러 메시지: 리턴 타입은 대여한 값을 포함하지만, 대여한 값이 존재하지 않는다

에러의 원인은 변수 s가 drop 함수에 의해 해제되기 때문이다.

에러를 고친 코드

이 코드는 소유권도 호출한 코드에 함께 이동되어 메모리가 해제되지 않아 에러가 발생하지 않는다.

 

🌱 참조 규칙

  • 특정한 범위 내에서는 하나의 가변 참조, 여러개의 불변 참조를 생성할 수 있지만 둘 다 선언할 수는 없다.
  • 참조는 유효해야한다.

 

🌱 슬라이스 타입

슬라이스도 소유권을 갖지 않는다. 슬라이스로 컬렉션 내의 연속된 요소들을 참조할 수 있다.

첫 번재 단어의 길이를 리턴하는 함수

함수의 리턴값은 매개변수 &String와 무관한 usize 타입이다. String 타입과는 별개이므로 나중에도 이 값이 유효할 것이라는 보장이 없다.

 

 

🌱 문자열 슬라이스

문자열 슬라이스란 String의 일부에 대한 참조이다. 

문자열 슬라이스 예시

[starting_index..ending_index](대괄호 안에 시작 인덱스와 마지막 인덱스의 다음 인덱스)를 기입하는 방식으로 사용된다.

 

문자열 슬라이스의 구조

 

첫번째 인덱스부터 참조할 때

범위 문법(..)를 이용할 대 첫번째 인덱스(0)부터 참조할 때 ".." 앞의 값을 생략해도 된다. ([..ending_index])

 

마지막 인덱스까지 참조할 때

위와 마찬가지고 마지막 인덱스까지 참조할 때 ".." 뒤의 값을 생략해도 된다.

 

String 전체를 참조할 때

String 전체 값을 참조하려면 ".." 앞의 값과 뒤의 값을 생략하면 된다.

 

문자열에서 첫 번째 단어를 리턴하는 함수
error[E0502]

String 타입 제거(clear)를 위해서 가변 참조가 필요하고, 컴파일을 실패한다.

 

 

🌱 문자열 리터럴은 슬라이스이다

문자열 리터럴

변수  s의 타입은 &str이다. 이는 슬라이스이고, &str은 불변 참조이기 때문에 문자열 리터럴은 항상 불변이다.

 

리터럴이다 String 타이브이 값으로부터 슬라이스를 생성할 수 있다. 이를 바탕으로 위에서 작성한 first_word 함수를 개선할 수 있다.

 

기존의 코드
변경한 코드

&str를 통해서 String 타입과 &str 타입 값에 모두 적용할 수 있다. String 타입에 대한 참조 대신 문자열 슬라이스를 함수의 매개변수로 사용하면 같은 기능을 가지면서도 보편적인 사용이 가능하다.

 

🌱 배열의 슬라이스

배열의 슬라이스 예시

슬라이스는 문자열만이 다니라 배열 타입에도 적용 가능하다.

 

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/09   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30
글 보관함