Search

Rc와 Arc로 소유권을 공유하기

Rc와 Arc란?

러스트는 메모리 안전성을 보장하기 위해 소유권(ownership) 시스템을 갖추고 있기 때문에,
러스트에서는 RC(Reference Counting) ARC(Automatic Reference Counting)이라는 두 가지 스마트 포인터 타입을 제공합니다. 스마트 포인터는 러스트에서 메모리 관리를 도와주는 타입으로, 힙에 할당된 데이터를 참조하는 데 사용됩니다. RC와 ARC는 러스트에서 공유된 소유권(Shared Ownership)을 관리하기 위해 사용됩니다.

공유된 소유권?

러스트에서는 소유권이라는 개념을 가지고 있기 때문에, 변수를 다른 함수로 넘길 때 다음과 같은 소유권 개념이 존재합니다.
1.
소유권 이전(Ownership Transfer): 변수의 소유권은 다른 함수로 이전될 수 있습니다. 이 경우, 변수의 소유권은 전달된 함수로 이동하며, 원본 변수는 해당 함수에서 더 이상 사용할 수 없습니다. 이를 통해 러스트는 데이터의 유일한 소유자를 유지하고, 예기치 않은 동시 수정을 방지하여 메모리 안전성을 보장합니다.
2.
소유권 반환(Ownership Return): 함수가 소유권을 다른 함수에게 전달한 경우, 해당 함수는 소유권을 다시 반환할 수 있습니다. 반환된 소유권은 원래의 소유자에게 돌아가며, 다시 변수를 사용하거나 다른 함수로 전달할 수 있습니다.
하지만 여러 함수에서 소유권이 필요하면 위의 개념으로 구현하기에는 까다롭습니다.
따라서 러스트에는 “공유된 소유권” 이라는 개념으로 Rc와 Arc 가 존재합니다.
공유된 소유권(Shared Ownership) 개념을 통해 여러 개의 변수가 동일한 데이터에 대한 소유권을 공유할 수 있습니다. 소유권을 공유하는 변수들은 참조자(reference)를 통해 데이터에 접근하며, 공유된 소유권을 갖는 변수들 중 가장 긴 라이프타임(lifetime)을 가진 변수가 데이터의 소유자가 됩니다. 이를 통해 여러 변수가 동시에 데이터를 읽을 수 있지만, 데이터의 수정은 소유자에 의해서만 이루어질 수 있습니다.

참조 카운팅이란?

참조 카운팅(Reference Counting)은 러스트에서 사용되는 메모리 관리 방식 중 하나로, RC(Reference Counting)와 ARC(Automatic Reference Counting) 스마트 포인터를 통해 구현됩니다. 참조 카운팅은 데이터의 소유권을 추적하고, 데이터에 대한 참조자의 수를 세어 참조자가 없을 때 자동으로 데이터를 해제하는 메커니즘입니다.
참조 카운팅을 사용하는 과정은 다음과 같습니다:
1.
데이터에 대한 참조 생성: 데이터를 참조하려는 변수나 객체가 생성될 때, 참조 카운트가 1로 초기화됩니다. 이 시점에서 데이터의 소유자가 생성되었다고 볼 수 있습니다.
2.
데이터에 대한 추가 참조 생성: 변수나 객체가 데이터에 대한 새로운 참조를 생성할 때, 참조 카운트가 증가합니다. 이는 데이터를 추가적으로 참조하고 있는 변수나 객체의 수를 의미합니다.
3.
데이터에 대한 참조 해제: 변수나 객체가 데이터에 대한 참조를 해제할 때, 참조 카운트가 감소합니다. 참조 카운트가 0이 되면 데이터는 더 이상 참조되지 않는 것으로 간주되며, 이때 메모리에서 자동으로 해제됩니다.
4.
데이터의 해제: 참조 카운트가 0이 되면, 즉 더 이상 데이터를 참조하는 변수나 객체가 없을 때, 데이터는 자동으로 해제됩니다. 이는 데이터의 메모리를 자동으로 관리하여 메모리 누수를 방지하고, 사용되지 않는 데이터를 효율적으로 정리하는 역할을 합니다.
sequenceDiagram
    participant P1 as Pointer 1
    participant P2 as Pointer 2
    participant C as Count
    participant D as Data

    P1 ->> C: Create reference
    P1 ->> C: Increase count
    P2 ->> C: Create reference
    P2 ->> C: Increase count

    Note over P1, D: P1 references D
    Note over P2, D: P2 references D

    P1 ->> C: Release reference
    P1 ->> C: Decrease count
    P2 ->> C: Release reference
    P2 ->> C: Decrease count

    alt Count is 0
        C ->> D: Data released
    else
        Note over C: Count is still > 0
    end
Mermaid
복사

Rc와 Arc의 차이점

RcArc의 차이점은 스레드 안전성에 있습니다. Rc는 단일 스레드 환경에서만 사용되며, 이는 참조 카운팅에 대한 동시성 제어가 없다는 의미입니다. 따라서, 여러 스레드에서 동시에 참조 카운트를 증가시키거나 감소시키는 등의 연산을 수행할 경우 데이터 레이스 조건이 발생할 수 있습니다.
반면에 Arc(Atomic Reference Counted)는 참조 카운팅을 위해 atomic 연산을 사용합니다. atomic 연산은 멀티스레드 환경에서도 안전하게 작동합니다. 즉, Arc는 여러 스레드에서 안전하게 사용할 수 있습니다.
따라서, ArcRc의 차이점은 "참조 카운트를 업데이트 할 때 atomic 연산을 사용하는지 여부"에 있습니다. 이는 lock을 사용하는 것과는 다르며, 실제로 Arc로 감싼 데이터에 접근할 때 lock을 사용하지 않습니다. Arc로 감싼 데이터에 대한 동시 접근을 제어하려면 별도의 동기화 메커니즘(예: Mutex 또는 RwLock 등)을 사용해야 합니다.

Arc의 병목 가능성?

Arc 는 여러 스레드가 동시에 같은 메모리에 접근할 수 있게 해주는 도구로, 이를 가능하게 하기 위해 내부적으로 동기화 기법(Atomic)을 사용합니다. 이 동기화 작업은 약간의 오버헤드를 가지기는 하지만, 일반적으로는 이 오버헤드가 병목을 일으키지는 않습니다.
그러나 극단적으로 높은 병렬 처리를 수행하는 경우 (즉, 많은 수의 스레드가 동시에 동일한 Arc에 레퍼런스를 생성하려고 시도하는 경우)에는 참조 카운터의 업데이트가 병목이 될 수 있습니다. 이러한 상황에서는 락 경쟁(lock contention)이 발생할 수 있습니다. 이는 Arc가 내부적으로 원자적 연산(atomic operations)을 사용하여 참조 카운트를 관리하기 때문입니다. 원자적 연산은 하나의 연산이 완전히 수행되기 전까지 다른 연산이 간섭하지 못하도록 보장하는 성질을 가지며, 이로 인해 병목이 발생할 수 있습니다.
따라서 Arc를 사용할 때는 이러한 상황을 염두에 두고, 필요하다면 Arc의 사용을 최소화하거나, 다른 동기화 기법 (예: fine-grained locking, lock-free data structures 등)을 고려해 볼 수 있습니다.

Arc 사용 시 주의할 점

Arc 는 Reference Count 를 늘리고 줄이는 기능에 대해서만 Atomic 하게 동작합니다. 이 뜻은 실제 Data의 변형에 대해 어떠한 Atomic 한 동작을 기대해서는 안된다는 것 입니다.
다중 스레드에서 동시 수정 시 데이터의 원자성 보장은 Mutex 혹은 Semaphore 기법을 활용해야 합니다.

사용 예

다음과 같이 구성한 예제를 한번 살펴 보겠습니다.
1.
DB 커넥션 파일: 데이터베이스와의 연결을 설정하고 관리하는데 사용됩니다. 이 파일은 데이터베이스에 연결하고 해당 연결을 반환하는 기능을 가집니다.
2.
Repository 파일들: 각각의 Repository는 데이터베이스의 특정 부분을 캡슐화하고 있습니다. 이러한 Repository는 데이터베이스와의 상호작용을 처리하는 역할을 합니다.
3.
Arc는 여러 스레드에서 DB 커넥션을 안전하게 공유하기 위해 사용됩니다.
이러한 구조를 통해 데이터베이스 연결을 생성하고, 이 연결을 여러 Repository와 공유하는 Rust 코드를 만들 수 있습니다.
먼저, 데이터베이스 연결을 설정하는 connection.rs 파일을 만들어 봅시다. 이 예에서는 postgres를 사용한다고 가정합니다.
connection.rs
// connection.rs use tokio_postgres::{Config, NoTls, Error}; use std::sync::Arc; pub async fn establish_connection() -> Result<Arc<tokio_postgres::Client>, Error> { let mut config = Config::new(); config.user("your_user_name"); config.password("your_password"); config.host("your_host"); config.dbname("your_database_name"); let (client, connection) = config.connect(NoTls).await?; // The connection object performs the actual communication with the database, // so spawn it off to run on its own. tokio::spawn(async move { if let Err(e) = connection.await { eprintln!("connection error: {}", e); } }); Ok(Arc::new(client)) }
Rust
복사
user_repository.rs
// user_repository.rs use tokio_postgres::Client; use std::sync::Arc; pub struct UserRepository { pub db: Arc<Client>, } impl UserRepository { pub async fn get_user_by_id(&self, user_id: &str) -> Result<(), tokio_postgres::Error> { self.db.query("SELECT * FROM users WHERE id = $1", &[&user_id]).await?; // Here, add your code to process the query results and return the user data Ok(()) } }
Rust
복사
product_repository.rs
// product_repository.rs use tokio_postgres::Client; use std::sync::Arc; pub struct ProductRepository { pub db: Arc<Client>, } impl ProductRepository { pub async fn get_product_by_id(&self, product_id: &str) -> Result<(), tokio_postgres::Error> { self.db.query("SELECT * FROM products WHERE id = $1", &[&product_id]).await?; // Here, add your code to process the query results and return the product data Ok(()) } }
Rust
복사
main.rs
// main.rs mod connection; mod user_repository; mod product_repository; use connection::establish_connection; use user_repository::UserRepository; use product_repository::ProductRepository; use std::sync::Arc; #[tokio::main] async fn main() { // Establish the connection let db = match establish_connection().await { Ok(client) => client, Err(e) => panic!("Error establishing connection: {}", e), }; // Create the repositories let user_repository = UserRepository { db: Arc::clone(&db), }; let product_repository = ProductRepository { db: Arc::clone(&db), }; // Now you can use the repositories to interact with the database match user_repository.get_user_by_id("user_id").await { Ok(_) => println!("User retrieved successfully."), Err(e) => eprintln!("Error retrieving user: {}", e), }; match product_repository.get_product_by_id("product_id").await { Ok(_) => println!("Product retrieved successfully."), Err(e) => eprintln!("Error retrieving product: {}", e), }; }
Rust
복사
이 코드는 각각의 Repository가 데이터베이스 연결을 공유하고 있음을 보여줍니다. 이렇게 함으로써, 각 Repository는 독립적으로 작동할 수 있으며, 모든 Repository가 동일한 데이터베이스 연결을 사용하도록 보장할 수 있습니다.
Arc는 Rust의 동시성 라이브러리에서 제공하는 타입으로, 참조 카운팅을 통해 여러 스레드에서 안전하게 공유될 수 있는 데이터를 감싸는 데 사용됩니다. 따라서 데이터베이스 클라이언트를 Arc로 감싸서, 다수의 Repository(또는 스레드)가 동시에 접근할 수 있도록 했습니다.