Rust

89 posts
Rust 기초: Sync 트레이트로 스레드 간 안전한 참조 공유하기

Rust 기초: Sync 트레이트로 스레드 간 안전한 참조 공유하기

Send 트레이트를 공부하다 보면 자연스럽게 따라오는 질문이 있는데요. "값을 스레드로 옮기지 않고, 여러 스레드에서 동시에 참조만 하고 싶으면 어떻게 하지?" 예를 들어 Arc로 RefCell을 감싸서 여러 스레드에서 접근하려고 하면 이런 컴파일 에러가 납니다. "스레드 간에 안전하게 공유할 수 없다"는 이 에러의 핵심이 바로 Sync 트레이트입니다. Send가 소유권 이동의 안전성을 보장한다면, Sync는 참조 공유의 안전성을 보장하는 건데요. 이 글에서 Sync가 어떤 역할을 하고 어떤 타입이 Sync이고 어떤 타입은 아닌지,

Rust 기초: Option과 Result에서 as_deref() 활용하기

Rust 기초: Option과 Result에서 as_deref() 활용하기

Rust에서 Option<String>을 다루다 보면 꽤 답답한 순간이 찾아옵니다. Option 안에 들어있는 String을 &str과 비교하고 싶은데 타입이 맞지 않아 컴파일러가 거부하는 상황이죠. Option<String>과 Option<&str>은 서로 다른 타입이라 직접 비교할 수 없습니다. 이런 상황에서 as_deref()를 알고 있으면 아주 깔끔하게 해결할 수 있는데요. 이 글에서는 as_ref()와 비교하면서 as_deref()가 왜 필요하고 어떻게 동작하는지 살펴보겠습니다. Option에서 소유와 참조 문제의 근본 원

Rust 기초: Send 트레이트로 스레드 안전성 보장하기

Rust 기초: Send 트레이트로 스레드 안전성 보장하기

Rust로 멀티스레드 프로그래밍을 하다 보면 이런 컴파일 에러를 만나게 되는 경우가 있는데요. "스레드 간에 안전하게 보낼 수 없다"는 말은 대체 무슨 뜻일까요? 그리고 Send 트레이트는 뭘까요? 🤔 사실 이 에러 메시지 안에 Rust가 동시성 프로그래밍에서 데이터 경합(data race)을 원천 차단하는 메커니즘이 담겨 있습니다. Send 트레이트가 어떻게 동작하고, 어떤 상황에서 우리를 보호해 주는지 살펴볼게요. Send 트레이트란? Send는 Rust 표준 라이브러리의 std::marker 모듈에 정의된 마커 트레이트(ma

Rust 기초: Mutex와 RwLock으로 스레드 간 데이터 수정하기

Rust 기초: Mutex와 RwLock으로 스레드 간 데이터 수정하기

Arc로 데이터를 여러 스레드에 나눠 주는 것까지는 잘됩니다. 그런데 그 데이터를 고치려고 하면 막히죠. Arc가 불변 참조만 주기 때문인데요. 단일 스레드에서라면 RefCell로 풀던 문제라, 똑같이 감싸 보면 어떨까 싶어집니다. RefCell이 빌림을 세는 카운터에는 아무런 동기화가 없어서, 여러 스레드가 동시에 건드리면 숫자가 꼬입니다. 그래서 Sync를 구현하지 않고, 컴파일러가 미리 막아 주는 거예요. 여기서 필요한 게 Mutex입니다. 이름은 상호 배제(mutual exclusion)에서 왔는데, 말 그대로 한 번에 하나

Rust 기초: Arc로 스레드 간 데이터 공유하기

Rust 기초: Arc로 스레드 간 데이터 공유하기

Rust의 소유권 시스템은 메모리 안전성을 보장해주지만, 여러 스레드에서 같은 데이터를 공유하려고 하면 컴파일러가 허용하지 않습니다. "한 번에 하나의 소유자만 존재할 수 있다"는 규칙 때문입니다. 소유권을 이동하면 다른 스레드에서 사용할 수 없고, 참조를 전달하려고 하면 수명(lifetime) 문제로 컴파일 오류가 발생합니다. 실제로는 여러 스레드가 같은 설정 값을 읽거나, 공유 데이터를 참조해야 하는 상황이 많습니다. 예를 들어 웹 서버에서 모든 워커 스레드가 동일한 설정 파일을 읽어야 하거나, 여러 스레드가 같은 캐시 데이터를

Rust 기초: Weak로 순환 참조 끊기

Rust 기초: Weak로 순환 참조 끊기

Rc로 노드를 만들어 서로 연결하다 보면 이상한 일을 겪게 됩니다. 분명 스코프를 벗어났는데 소멸자가 실행되지 않는 거죠. Rust는 메모리 안전한 언어인데 어떻게 누수가 나느냐고요? 😱 직접 확인해 보겠습니다. 서로를 짝으로 가리키는 노드 둘을 만들고, 해제될 때 메시지를 찍도록 Drop을 구현했습니다. "해제됨"이 한 번도 찍히지 않았습니다. 두 노드 모두 메모리에 그대로 남아 있는 거예요. 강한 참조끼리 물면 카운트가 0이 되지 않습니다 원인은 참조 카운트에 있습니다. Rc는 자기를 가리키는 강한 참조(strong refer

Rust 기초: Rc로 단일 스레드에서 데이터 공유하기

Rust 기초: Rc로 단일 스레드에서 데이터 공유하기

Rust의 소유권 시스템은 "한 번에 하나의 소유자만 존재할 수 있다"는 명확한 규칙을 가지고 있습니다. 대부분의 경우 이 규칙만으로 충분하지만, 때로는 여러 부분에서 같은 데이터를 소유해야 하는 상황이 있습니다. 예를 들어 그래프 자료구조에서 여러 노드가 같은 노드를 가리켜야 할 수 있습니다. 단일 스레드 환경에서 이런 공유 소유권이 필요할 때 Rc(Reference Counted)를 사용합니다. Rc는 참조 카운팅을 통해 여러 소유자가 같은 데이터를 공유할 수 있게 해주는 스마트 포인터입니다. 데이터를 가리키는 참조가 몇 개인지

Rust 기초: RefCell로 불변 참조에서 값 바꾸기

Rust 기초: RefCell로 불변 참조에서 값 바꾸기

구조체 안에 호출 횟수를 세는 필드 하나 두려다가 컴파일러에게 막혀 보신 적 있으신가요? 로그를 찍는 김에 카운터도 올리고 싶을 뿐인데 말이죠. 컴파일러 제안대로 &mut self로 바꾸면 되긴 합니다. 그런데 그러면 log를 부르는 쪽도 전부 가변 참조를 들고 있어야 하고, 여러 곳에서 로거를 공유하던 구조가 통째로 무너집니다. 겨우 카운터 하나 때문에요. 😅 소유권과 빌림 규칙이 원래 그렇습니다. 불변 참조가 하나라도 살아 있으면 값을 바꿀 수 없어요. 그런데 이 규칙을 지키면서도 값을 바꿀 방법이 있습니다. 검사 시점을 컴파

Rust Box::pin은 언제 사용할까?

Rust Box::pin은 언제 사용할까?

Rust 비동기 코드를 읽다 보면 이런 타입을 마주칠 때가 있습니다. 처음 보면 부담스럽습니다. Box도 알고, Future도 대충 알겠는데 중간에 Pin이 끼어들면서 갑자기 어려워 보이죠. 게다가 예제 코드에서는 Box::new()가 아니라 Box::pin()을 쓰기도 합니다. 이건 단순히 future를 박스에 넣는 코드일까요? 아니면 뭔가 더 특별한 일을 하는 걸까요? 이번 글에서는 Box::pin이 언제 필요한지 차근차근 살펴보겠습니다. Box 자체가 아직 익숙하지 않다면 Box로 힙에 데이터 저장하기를 먼저 읽어보시면 좋습니

Rust 기초: Box로 힙에 데이터 저장하기

Rust 기초: Box로 힙에 데이터 저장하기

Rust에서 대부분의 값은 스택에 저장됩니다. 스택은 빠르고 효율적이지만 컴파일 시점에 크기를 알 수 있는 데이터만 다룰 수 있다는 제약이 있죠. 그런데 프로그래밍을 하다 보면 크기를 미리 알 수 없는 데이터를 다루거나 큰 데이터를 복사 없이 전달하고 싶을 때가 있습니다. 이럴 때 쓰는 게 바로 Box입니다. Box는 Rust에서 가장 단순하면서도 자주 쓰이는 스마트 포인터로, 데이터를 힙 메모리에 저장하고 그 포인터를 스택에 두는 방식으로 동작해요. 이 글에서는 Box가 무엇이고 언제 필요한지, 실전에서 어떻게 활용하는지 예제와

Rust 비동기 런타임: Tokio 크레이트 사용법

Rust 비동기 런타임: Tokio 크레이트 사용법

Rust로 네트워크 서버를 만들거나 여러 작업을 동시에 처리해야 할 때, 어디서부터 시작해야 할지 막막한 적이 있으신가요? Rust는 async/await 문법을 언어 차원에서 지원하지만, 이걸 실제로 실행하려면 비동기 런타임이 필요합니다. 그리고 Rust 생태계에서 가장 널리 쓰이는 비동기 런타임이 바로 Tokio입니다. 이번 글에서는 Tokio가 무엇이고, 왜 필요한지부터 시작해서 실제로 비동기 코드를 작성하고 실행하는 방법까지 차근차근 다뤄보겠습니다. 비동기 프로그래밍이 필요한 이유 웹 서버를 하나 만든다고 생각해볼까요? 클라

Rust 기초: Deref 트레이트와 역참조 강제(Deref Coercion)

Rust 기초: Deref 트레이트와 역참조 강제(Deref Coercion)

Rust로 코드를 작성하다 보면 신기한 장면을 목격할 때가 있습니다. Box<String>을 넘겼는데 &str을 기대하는 함수가 아무 문제없이 호출된다거나, Rc<Vec<i32>>에 대고 .iter()를 바로 호출할 수 있다거나 하는 것들이죠. 분명 타입이 다른데 컴파일러가 알아서 잘 처리해줍니다. 이런 마법 같은 일이 가능한 건 Rust의 Deref 트레이트와 역참조 강제(Deref coercion)라는 메커니즘 덕분입니다. 이 글에서는 역참조가 무엇인지부터 시작해서 Deref 트레이트를 직접 구현해보고, 역참조 강제가 실제로 어

Rust Future란 무엇인가: async fn이 바로 실행되지 않는 이유

Rust Future란 무엇인가: async fn이 바로 실행되지 않는 이유

Rust에서 비동기 코드를 처음 보면 async와 await는 꽤 익숙해 보입니다. JavaScript나 Python을 써보셨다면 더 그렇죠. 그런데 막상 Rust에서 async fn을 호출해보면 살짝 당황스러운 지점이 나옵니다. 함수를 호출했는데 실행이 바로 시작되지 않습니다. 반환값도 우리가 기대한 값이 아니라 Future입니다. 그리고 .await를 붙이지 않으면 아무 일도 일어나지 않는 것처럼 보이죠. 🤔 Tokio 입문 글에서도 이 차이를 잠깐 다뤘는데요. 이번 글에서는 Tokio 같은 런타임을 쓰기 전에, Rust의 F

Rust 기초: From과 Into 트레이트

Rust 기초: From과 Into 트레이트

자료형 간의 명시적이고 안전한 데이터 변환은 Rust의 중요한 철학 중 하나입니다. Rust는 From과 Into라는 표준 트레이트을 제공하여 데이터 변환을 안전하고 명확하게 할 수 있도록 돕는데요. 이 글에서는 이 두 트레이트의 관계와 차이점, 그리고 활용법을 살펴보겠습니다. From 트레이트란? From 트레이트은 다른 자료형부터(from) 현재 자료형으로 변환하는 방법을 정의할 때 사용합니다. From 트레이트의 from() 메서드는 다른 제네릭(generic) 타입을 인자로 받고 자신의 타입을 반환합니다. 예를 들어, Rus

Rust blanket impl: 여러 타입에 트레이트를 한 번에 구현하기

Rust blanket impl: 여러 타입에 트레이트를 한 번에 구현하기

Rust 표준 라이브러리 문서를 읽다 보면 이런 구현을 자주 마주칩니다. 처음 보면 조금 낯섭니다. impl Into<String> for MyType처럼 특정 타입에 트레이트를 구현하는 건 이해가 되는데, 여기서는 impl<T, U>로 모든 타입을 열어두고 있죠. "모든 T에 대해 Into<U>를 구현한다"는 말처럼 보이는데, 정말 그래도 되는 걸까요? 이런 구현을 보통 blanket implementation, 줄여서 blanket impl이라고 부릅니다. 한국어로는 "포괄 구현" 정도로 옮길 수 있지만, Rust 문맥에서는 원

Rust 기초: Copy와 Clone 트레이트 이해하기

Rust 기초: Copy와 Clone 트레이트 이해하기

Rust에서 변수를 다른 변수에 할당하면 값이 복사될 때도 있고 소유권이 이동할 때도 있습니다. 정수는 let y = x; 해도 x를 계속 쓸 수 있는데, String은 같은 걸 하면 원래 변수를 못 쓰게 되죠. 이 차이를 결정하는 게 바로 Copy와 Clone 트레이트입니다. 둘 다 "값을 복사한다"는 점은 같지만, 동작 방식과 쓰임새가 꽤 다릅니다. 이 글에서는 Copy와 Clone이 각각 무엇이고, 어떤 관계이며, 실제로 어떻게 쓰는지 알아보겠습니다. 소유권과 빌림에서 다룬 이동(move)과 복사(copy) 개념을 알고 있으면

Rust 기초: 수명(Lifetime)으로 참조의 유효 범위 관리하기

Rust 기초: 수명(Lifetime)으로 참조의 유효 범위 관리하기

Rust로 코드를 짜다 보면 어김없이 만나게 되는 오류가 있습니다. 분명히 변수를 선언하고 참조했을 뿐인데 "충분히 오래 살지 못한다"는 야박한 평가를 받는데요. 이 오류는 Rust의 또 다른 핵심 개념인 수명(lifetime)과 관련이 있습니다. 수명은 모든 참조가 가지는 속성이고, 컴파일러의 빌림 검사기(borrow checker)가 메모리 안전성을 보장하는 데 쓰는 도구이기도 합니다. 이 글에서는 수명이 무엇인지, 컴파일러가 어떻게 검증하는지, 우리가 직접 표기해야 하는 순간은 언제인지 알아보겠습니다. 기본적인 소유권과 빌림

Rust 기초: 트레이트(Trait) 사용법

Rust 기초: 트레이트(Trait) 사용법

구조체와 열거형으로 데이터의 형태를 정의하는 방법을 배웠는데요. 그런데 서로 다른 타입이 같은 동작을 공유해야 하는 상황이 자주 생깁니다. 원과 직사각형은 완전히 다른 구조의 데이터이지만 둘 다 "넓이를 구한다"는 동작은 가지고 있잖아요. Rust에서는 이런 공통 동작을 트레이트(Trait)로 정의합니다. 이 글에서는 트레이트가 무엇이고 어떻게 정의하고 구현하는지 예제와 함께 살펴보겠습니다. 트레이트란? 트레이트(Trait)는 여러 타입이 공통으로 가져야 하는 동작(behavior)을 정의하는 방법입니다. Java나 TypeScri

Rust의 Option: null 없는 세상에서 값의 부재를 다루는 법

Rust의 Option: null 없는 세상에서 값의 부재를 다루는 법

자바스크립트나 자바를 쓰다 보면 null이나 undefined 때문에 한 번쯤 데어 본 경험이 있으실 겁니다. 멀쩡하게 돌던 코드가 운영 환경에서 갑자기 Cannot read property of null을 뱉으며 죽는 그 순간 말이죠. 💥 Rust는 아예 null이라는 개념을 빼버렸습니다. 대신 "값이 있을 수도, 없을 수도 있다"를 타입으로 표현하는데, 그게 바로 Option<T>입니다. 이 글에서는 Option을 어떻게 만들고, 안에 든 값을 어떻게 안전하게 꺼내며, 실무에서 자주 만나는 패턴을 어떻게 풀어내는지 차근차근 살

Rust 기초: 소유권(Ownership)과 빌림(Borrowing)

Rust 기초: 소유권(Ownership)과 빌림(Borrowing)

Rust를 처음 배우다 보면 컴파일러가 자꾸 "value used here after move"나 "borrow of moved value" 같은 오류를 뱉어서 당황하게 되죠. 분명 올바른 코드를 작성한 것 같은데 컴파일이 안 되니 답답하기도 하고요. 이런 오류들은 모두 Rust의 핵심 개념인 소유권(Ownership)과 관련이 있습니다. 대부분의 프로그래밍 언어는 가비지 컬렉터(GC)를 통해 메모리를 관리하거나, C/C++처럼 프로그래머가 직접 메모리를 할당하고 해제합니다. Rust는 이 두 가지 방식 대신 소유권이라는 독특한 시

Discord