Post

[RUST] 러스트 프로그래밍 공식 가이드(제2판) 15장 요약

스마트 포인터

  • 포인터 : 메모리의 주솟값을 담고 있는 변수에 대한 일반적인 개념
  • 스마트 포인터 : 포인터처럼 작동할 뿐 아니라 추가적인 메타데이터와 능력을 가지고 있는 데이터 구조

Box를 사용하여 힙에 있는 데이터 가리키기

  • 스마트 포인터 박스(box)는 Box<T> 로 쓰이는 타입이며 힙 메모리에 데이터를 저장하고, 그 데이터를 가리키는 포인터(Box) 자체는 스택(Stack) 에 저장
  • Box 타입은 **Deref 트레이트를 구현**하고 있기 때문에 스마트 포인터이며, 이는 Box값이 참조자와 같이 취급되도록 허용해줌
  • 박스는 스코프를 벗어날 때 할당이 해제되며 할당 해제는 스택에 저장된 박스와 이것이 가리키고 있는 힙에 저장된 데이터 모두에게 일어나는데 이는 Drop 트레이트의 구현 때문

Box를 사용하여 힙에 데이터 저장하기

  • let b = Box::new(5); 와 같이 사용할 수 있으며, 여기서 5 는 힙에 할당됨

박스로 재귀적 타입 가능하게 하기

  • 재귀적 타입의 값은 자신 안에 동일한 타입의 또 다른 값을 담을 수 있지만, 러스트는 컴파일 타임에 어떤 타입이 얼마만큼의 공간을 차지하는지 알아야 하기 때문에 재귀적 타입은 문제를 일으킴
  • 박스는 알려진 크기를 갖고 있으므로, 재귀적 타입의 정의에 박스를 집어넣어 재귀적 타입을 가능하게 함

Deref 트레이트로 스마트 포인터를 보통의 참조자처럼 취급하기

  • Deref 트레이트를 구현하면 역참조 연산자(dereference operator) * 동작의 커스터마이징을 가능하게 해주며, 스마트 포인터가 보통의 참조자처럼 취급될 수 있도록 해줌
    • 역참조 연산이란, 어떠한 변수의 참조자에 *를 사용하여 참조자를 따라가서 이 참조자가 가리키고 있는 값을 얻어내는 것을 말함

Deref 트레이트를 구현하여 임의의 타입을 참조자처럼 다루기

  • Deref 트레이트는 deref 라는 이름의 메서드 하나를 구현하도록 요구하는데, 이 함수는 self 를 빌려와서 내부 데이터의 참조자를 반환함
  • deref 메서드는 컴파일러가 Deref를 구현한 어떤 타입의 값에 대해 deref 메서드를 호출하여, 자신이 역참조하는 방법을 알고 있는 & 참조자를 가져올 수 있는 기능을 제공
    • 러스트는 * 연산자에 deref 메서드 호출과 보통의 역참조를 대입하므로 deref 메서드를 호출할 필요가 있는지 없는지에 대해 생각하지 않아도 됨 ( *변수*(변수.deref()) 와 같이 작동됨 )
  • defer 메서드가 값의 참조자를 반환하고, *(변수.deref())에서 괄호 바깥의 일반 역참조가 여전히 필요한 이유는 소유권 시스템과 함께 작동시키기 위함 ( 만약 deref 메서드가 값의 참조자 대신 값을 직접 반환했다면, 그 값은 self 바깥으로 이동 할 것임 )

함수와 메서드를 이용한 암묵적 역참조 강제

  • 역참조 강제는 Deref를 구현한 어떤 타입의 참조자를 다른 타입의 참조자로 바꿔줌
    • 예를들어, 역참조 강제는 &String 을 &str 로 바꿔줄 수 있는데, 이는 String의 Deref 트레이트 구현이 &str을 반환하도록 했기 때문
    • 어떤 특정한 타입값에 대한 참조자를 함수 혹은 메서드의 인수로 전달할 때, 이 함수나 메서드의 정의에는 그 매개변수 타입이 맞지 않을 때 자동으로 발생 (일련의 deref 메서드 호출이 인수로 제공한 타입을 매개변수로서 필요한 타입으로 변경해줌)
  • 역참조 강제는 함수와 메서드 호출을 작성하는 프로그래머들이 &와 *를 사용하여 수많은 명시적인 참조 및 역참조를 추가할 필요가 없도록 하기 위해 도입됨
  • 인수로 넣어진 타입에 대해 Defer 트레이트가 정의되어 있다면, 해당 타입을 분석하고 Deref::deref를 필요한 만큼 사용하여 매개변수 타입과 일치하는 참조자를 얻음 (Deref::deref가 추가되어야 하는 횟수는 컴파일 타임에 분석되므로, 역참조 강제의 이점을 얻는데에 관해서 어떠한 런타임 페널티도 없음)

역참조 강제가 가변성과 상호작용하는 법

  • Deref 트레이트 : 불변 참조자에 대한 * 연산자를 오버라이딩
  • DerefMut 트레이트 : 가변 참조자에 대한 * 연산자를 오버라이딩

Drop 트레이트로 메모리 정리 코드 실행하기

  • Drop 트레이트 : 어떤 값이 스코프 밖으로 벗어나려고 할 때 무슨 일을 할 지 커스터마이징하게끔 해줌
    • drop 메서드를 구현하야하며, 이 메서드는 self에 대한 가변 참조자를 매개변수로 갖음
  • Drop 트레이트는 프렐루드에 포함되어 있으며 drop 메서드는 명시적으로 호출할 필요가 없이 스코프를 벗어나면 호출이 됨
  • 변수들은 만들어진 순서의 역순으로 버려짐

std::mem::drop으로 값을 일찍 버리기

  • 러스트는 수동으로 Drop 트레이트의 drop 메서드를 호출하게 해주지 않는 대신, 표준 라이브러리가 제공하는 std::mem::drop 함수를 호출하여 스코프가 끝나기 전에 강제로 값을 버리도록 할 수 있음
  • std::mem::drop 함수는 Drop 트레이트에 있는 drop 메서드와는 다르며 프렐루드에 포함되어 있고, 일찍 버리려고 하는 값을 인수로 넘겨 호출함

Rc, 참조 카운트 스마트 포인터

  • 명시적으로 복수 소유권을 가능하게 하려면 러스트의 Rc 타입(참조 카운팅(reference counting))을 이용해야하며, Rc 타입은 어떤 값의 참조자 개수를 추적하여 해당 값이 계속 사용중인지를 판단하고 어떤 값에 대한 참조자가 0개라면 해당 값은 참조 유효성 문제 없이 메모리가 정리될 수 있음
  • Rc는 불변 참조자를 통하여 읽기 전용으로 프로그램의 여러 부분에서 데이터를 공유하도록 해줌 ( 대여 규칙을 위반하지 않음 )
  • Rc 타입은 프로그램의 여러 부분에서 읽을 데이터를 힙에 할당하고 싶은데 컴파일 타임에는 어떤 부분이 그 데이터를 마지막에 이용하게 될지 알 수 없는 경우 사용됨

Rc 사용하기

  • Rc는 프렐루드에 포함되어 있지 않으므로 이를 스코프로 가져오려면 use 구문을 추가해서 사용
  • Rc::clone의 호출은 모든 데이터에 대한 깊은 복사를 하지 않으며, 오직 참조 카운트만 증가시킴 (단순 카운트 증가 작업으로 매우 빠르고 비용이 거의 없음)
  • Rc는 스코프를 벗어나면 Drop 트레이트의 구현체가 자동으로 참조 카운트를 감소시킴
  • Rc::strong_count 함수를 호출하여 참조 카운트 값을 얻을 수 있음

RefCell와 내부 가변성 패턴

  • 내부 가변성은 어떤 데이터에 대한 불변 참조자가 있을 때라도 데이터를 변경할 수 있게 해주는 러스트의 디자인 패턴
    • 보통 대여 규칙에 의해 허용되지 않지만, 데이터를 변경하기 위해서 이 패턴은 데이터 구조 내에서 unsafe 한 코드를 사용하여 변경과 대여를 지배하는 러스트의 일반적인 규칙을 우회함 ( unsafe 코드는 19장에서 자세히 알아봄 )
    • unsafe 코드는 이 규칙을 지키고 있는지에 대한 검사를 컴파일러에 맡기는 대신 수동으로 하는 중임을 컴파일러에게 알림
  • 컴파일러는 대여 규칙을 준수함을 보장할 수 없을지라도, 우리가 이를 런타임에 보장할 수 있는 경우라면 내부 가변성 패턴을 쓰는 타입을 사용할 수 있음

RefCell으로 런타임에 대여 규칙 집행하기

  • RefCell타입은 가지고 있는 데이터에 대한 단일 소유권을 나타냄
  • RefCell를 이용할 때 대여 규칙의 불변성은 **런타임**에 집행 되며, 규칙을 어기면 프로그램은 panic! 을 일으키고 종료됨 ( 런타임에 검사되는 가변 대여를 허용하기 때문에, RefCell가 불변일 때도 내부의 값을 변경할 수 있음)
  • RefCell타입은 코드가 대여 규칙을 준수한다는 것을 컴파일러는 이해하거나 보장할 순 없지만 대여 규칙을 준수한다는 확신이 있는 경우 유용함
  • RefCell는 싱글 스레드 에서만 사용 가능하고, 멀티스레드 콘텍스트에서 사용을 시도할 경우 컴파일 에러가 발생함 (RefCell의 기능을 멀티 스레드에서 사용하는 방법은 다음장에서 확인)

RefCell로 런타임에 대여 추적하기

  • RefCell는 현재 활성화된 Ref와 RefMut 스마트 포인터들이 몇 개나 있는지 추적
    • borrow 메서드 : RefCell의 불변 참조자가 활성화된 개수를 증가시키고, 스마트 포인터 타입인 Ref 를 반환 (스코프 밖으로 벗어날 때 불변 대여의 개수가 하나 감소)
    • borrow_mut 메서드 : 동시에 한번만 가변 대여할 수 있고, 스마트 포인터 타입 RefMut를 반환 (동일 스코프 내에서 해당 메서드를 여러 번 호출하게되면 already borrowed: BorrowMutError 라는 메시지와 함께 패닉을 일으킴)

Rc 와 RefCell를 조합하여 가변 데이터의 복수 소유자 만들기

  • Rc는 어떤 데이터에 대해 복수의 소유자를 허용하지만, 그 데이터에 대한 불변 접근만 허용하는데, RefCell를 들고있는 Rc를 가지게 된다면, 가변이면서 동시에 복수의 소유자를 갖는 값을 얻을 수 있음

순환 참조는 메모리 누수를 발생시킬 수 있습니다

  • Rc 및 RefCell를 사용하면 러스트에서 메모리 누수가 허용되는데, 이는 아이템들이 서로 순환 참조하는 참조자를 만드는 것이 가능하며 메모리 누수를 발생시킴 (순환 고리 안의 각 아이템의 참조 카운트는 결코 0이 되지 않을 것이고 그러므로 값들은 버려지지 않을 것이기 때문)

순환 참조 방지하기: Rc를 Weak로 바꾸기

  • Rc::clone 과 강한 참조 (strong reference)
    • Rc::clone(&rc) → 같은 데이터를 공유하는 강한 참조(strong reference) 를 하나 더 만들며, strong_count(참조 카운트)가 +1 증가함.
    • Rc 인스턴스는 strong_count == 0이 될 때 메모리에서 제거(dropped) 됨
    • 즉, 강한 참조는 소유권을 공유하는 것이며, 모든 소유자가 사라질 때 까지 값을 살아있음
  • Rc::downgrade와 약한 참조 (weak reference)
    • Rc::downgrade(&rc) → 소유권을 가지지 않는 참조를 만들며 반환 타입은 Weak
    • Weak는 weak_count만 +1 증가시키고, strong_count에는 영향을 주지 않음
    • Rc는 제거될 때 strong_count만 고려하고, weak_count는 고려하지 않기 때문에 Weak가 가리키고 있는 값으로 어떤 일을 하기 위해서는 그 값이 존재하는지 반드시 확인해야함
  • Weak::upgrade()
    • Weak는 실제 값에 직접 접근할 수 없기 때문에 upgrade 메서드를 호출해 Option<Rc>를 반환
      • 값이 살아있으면 Some(Rc)
      • 이미 버려졌다면 None
This post is licensed under CC BY 4.0 by the author.