Rust 면접 질문: 2026년 완벽 가이드
Rust 기술 면접에서 자주 출제되는 25개 질문을 완벽하게 다룹니다. 소유권, 빌림, 라이프타임, 트레이트, async/await, 동시성에 대한 상세한 답변과 코드 예제를 제공합니다.

Rust 기술 면접에서는 언어 고유의 소유권 시스템, 가비지 컬렉터 없는 메모리 관리, 안전한 동시성 코드 작성 능력이 평가됩니다. 이 가이드는 소유권 기초부터 고급 async 및 동시성 패턴까지 필수 질문을 체계적으로 다루고 있습니다.
면접관은 Rust의 메모리 안전성 보장에 대한 이해를 보여주는 설명을 중시합니다. 컴파일러가 컴파일 타임에 버그를 방지하는 원리를 설명할 수 있는 것이 합격과 불합격을 가릅니다.
소유권과 빌림
질문 1: Rust의 소유권 시스템을 설명해 주세요
소유권은 Rust의 핵심 개념으로, 가비지 컬렉터 없이 컴파일 타임 메모리 안전성을 보장하는 메모리 관리를 실현합니다.
// The three fundamental rules of ownership
fn main() {
// Rule 1: Each value has a single owner
let s1 = String::from("hello"); // s1 is the owner
// Rule 2: Only one variable can own a value at a time
let s2 = s1; // s1 is MOVED to s2
// println!("{}", s1); // ERROR: s1 is no longer valid
println!("{}", s2); // OK: s2 is now the owner
// Rule 3: When the owner goes out of scope, the value is dropped
{
let s3 = String::from("world");
// s3 is valid here
} // s3 goes out of scope, memory is automatically freed
// Copy types: simple types are copied, not moved
let x = 5;
let y = x; // x is COPIED, not moved
println!("x = {}, y = {}", x, y); // Both are valid
}
// Move in action with functions
fn take_ownership(s: String) {
// s takes ownership of the String
println!("{}", s);
} // s is dropped here, memory freed
fn makes_copy(i: i32) {
// i is a copy of the argument
println!("{}", i);
} // i goes out of scope, nothing special (Copy type)
fn ownership_with_functions() {
let s = String::from("hello");
take_ownership(s); // s is moved into the function
// println!("{}", s); // ERROR: s is no longer valid
let x = 5;
makes_copy(x); // x is copied
println!("{}", x); // OK: x is still valid
}소유권을 통해 use-after-free, double-free, 메모리 누수와 같은 일반적인 메모리 버그가 제거됩니다. 컴파일러가 이러한 속성을 컴파일 타임에 보장합니다.
질문 2: 불변 빌림과 가변 빌림의 차이점은 무엇입니까
빌림은 소유권을 가져오지 않고 값을 사용하는 메커니즘이며, 데이터 경합을 방지하기 위한 엄격한 규칙이 있습니다.
// Immutable and mutable references
fn main() {
let mut s = String::from("hello");
// IMMUTABLE REFERENCES (&T)
// Can coexist in unlimited numbers
let r1 = &s; // immutable reference
let r2 = &s; // another immutable reference
println!("{} and {}", r1, r2); // OK
// MUTABLE REFERENCE (&mut T)
// Only one at a time, and no simultaneous immutable references
let r3 = &mut s; // mutable reference
// let r4 = &s; // ERROR: cannot have both immutable and mutable
// let r5 = &mut s; // ERROR: only one mutable reference allowed
r3.push_str(" world");
println!("{}", r3);
// Reference scopes are limited to their last use
let r6 = &s; // OK because r3 is no longer used
println!("{}", r6);
}
// Practical example: modifying a struct
struct User {
name: String,
age: u32,
}
impl User {
// &self: read-only access
fn get_name(&self) -> &str {
&self.name
}
// &mut self: modification access
fn set_name(&mut self, name: String) {
self.name = name;
}
// self: takes ownership (consumes the instance)
fn into_name(self) -> String {
self.name // The User instance no longer exists after this
}
}
fn borrowing_with_structs() {
let mut user = User {
name: String::from("Alice"),
age: 30,
};
// Reading
println!("Name: {}", user.get_name());
// Modifying
user.set_name(String::from("Bob"));
// Consuming
let name = user.into_name();
// user.age; // ERROR: user has been consumed
}이러한 규칙을 통해 컴파일 타임에 데이터 경합의 부재가 보장됩니다. 성능 희생 없이 이 보장을 제공하는 언어는 다른 곳에 없습니다.
Rust 2018 이후, 컴파일러는 NLL(Non-Lexical Lifetimes)을 사용하여 참조가 더 이상 사용되지 않는 시점을 더 정확하게 판단하며, 유연성이 향상되었습니다.
질문 3: 라이프타임이란 무엇이며, 언제 어노테이션이 필요합니까
라이프타임은 참조의 유효 기간을 컴파일러에게 알려주는 어노테이션으로, 댕글링 참조를 방지합니다.
// Understanding and annotating lifetimes
// ERROR: dangling reference
// fn dangling() -> &String {
// let s = String::from("hello");
// &s // s is dropped at function end, reference invalid
// }
// The compiler often infers lifetimes automatically
fn first_word(s: &str) -> &str {
// Elided lifetime: compiler understands the return
// has the same lifetime as the input
match s.find(' ') {
Some(i) => &s[..i],
None => s,
}
}
// Explicit annotation needed with multiple references
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
// 'a means: the return lives at least as long
// as the shorter of the two inputs
if x.len() > y.len() { x } else { y }
}
fn lifetime_example() {
let string1 = String::from("long string");
let result;
{
let string2 = String::from("xyz");
result = longest(&string1, &string2);
println!("Longest: {}", result); // OK here
}
// println!("{}", result); // ERROR: string2 is dropped
}
// Lifetimes in structs
struct ImportantExcerpt<'a> {
part: &'a str, // Struct cannot outlive part
}
impl<'a> ImportantExcerpt<'a> {
// Method returning a reference with the same lifetime
fn level(&self) -> i32 {
3
}
// Elided lifetime for &self returning a new reference
fn announce_and_return_part(&self, announcement: &str) -> &str {
println!("Attention: {}", announcement);
self.part // Returns with lifetime 'a
}
}
// Static lifetime: lives for the entire program duration
fn static_lifetime() {
let s: &'static str = "hello"; // Stored in the binary
// Constants have implicit 'static lifetime
const MAX_POINTS: u32 = 100_000;
}
// Combining lifetimes and generics
fn longest_with_announcement<'a, T>(
x: &'a str,
y: &'a str,
ann: T,
) -> &'a str
where
T: std::fmt::Display,
{
println!("Announcement: {}", ann);
if x.len() > y.len() { x } else { y }
}라이프타임은 컴파일 타임에 검증됩니다. 코드가 컴파일되면, 참조의 유효성이 보장됩니다.
트레이트와 제네릭
질문 4: Rust의 트레이트는 어떻게 작동합니까
트레이트는 서로 다른 타입 간에 공유되는 동작을 정의하는 것으로, 인터페이스와 유사하면서 추가 기능을 갖추고 있습니다.
// Defining and implementing traits
// Trait definition
trait Summary {
// Required method (no body)
fn summarize(&self) -> String;
// Method with default implementation
fn summarize_author(&self) -> String {
String::from("(Anonymous)")
}
// Default method that calls a required method
fn full_summary(&self) -> String {
format!("By {} - {}", self.summarize_author(), self.summarize())
}
}
// Implementation for different types
struct NewsArticle {
headline: String,
location: String,
author: String,
content: String,
}
impl Summary for NewsArticle {
fn summarize(&self) -> String {
format!("{}, by {} ({})", self.headline, self.author, self.location)
}
fn summarize_author(&self) -> String {
format!("@{}", self.author)
}
}
struct Tweet {
username: String,
content: String,
reply: bool,
retweet: bool,
}
impl Summary for Tweet {
fn summarize(&self) -> String {
format!("{}: {}", self.username, self.content)
}
}
// Trait bounds: constraining generics
fn notify<T: Summary>(item: &T) {
println!("Breaking news! {}", item.summarize());
}
// Alternative syntax with where
fn notify_verbose<T>(item: &T)
where
T: Summary,
{
println!("Breaking news! {}", item.summarize());
}
// Multiple trait bounds
fn notify_complex<T: Summary + Clone + std::fmt::Display>(item: &T) {
println!("{}", item);
}
// Return a type that implements a trait
fn create_summarizable() -> impl Summary {
Tweet {
username: String::from("rust_lang"),
content: String::from("Rust 2026 is amazing!"),
reply: false,
retweet: false,
}
}트레이트는 클래스 상속 없이 다형성을 실현하며, 상속보다 합성을 우선합니다.
질문 5: 정적 디스패치와 동적 디스패치의 차이점을 설명해 주세요
Rust는 다형성에 대해 단형화(정적)와 트레이트 객체(동적)의 두 가지 접근 방식을 제공합니다.
// Static vs dynamic dispatch
trait Animal {
fn speak(&self) -> String;
fn name(&self) -> &str;
}
struct Dog { name: String }
struct Cat { name: String }
impl Animal for Dog {
fn speak(&self) -> String { String::from("Woof!") }
fn name(&self) -> &str { &self.name }
}
impl Animal for Cat {
fn speak(&self) -> String { String::from("Meow!") }
fn name(&self) -> &str { &self.name }
}
// STATIC DISPATCH (monomorphization)
// Compiler generates a version for each concrete type
fn make_speak_static<T: Animal>(animal: &T) {
// At compile time, becomes make_speak_Dog and make_speak_Cat
println!("{} says {}", animal.name(), animal.speak());
}
// Advantages: inlining possible, no runtime overhead
// Disadvantages: larger binary, type must be known at compile time
// DYNAMIC DISPATCH (trait objects)
// Uses a vtable to resolve methods at runtime
fn make_speak_dynamic(animal: &dyn Animal) {
// Resolved via a pointer table (vtable) at runtime
println!("{} says {}", animal.name(), animal.speak());
}
// Advantages: can store different types, smaller binary
// Disadvantages: indirection overhead, no inlining
fn main() {
let dog = Dog { name: String::from("Rex") };
let cat = Cat { name: String::from("Whiskers") };
// Static: type is known at compile time
make_speak_static(&dog);
make_speak_static(&cat);
// Dynamic: type is resolved at runtime
make_speak_dynamic(&dog);
make_speak_dynamic(&cat);
// Heterogeneous collection (requires dynamic dispatch)
let animals: Vec<Box<dyn Animal>> = vec![
Box::new(Dog { name: String::from("Buddy") }),
Box::new(Cat { name: String::from("Luna") }),
];
for animal in animals.iter() {
println!("{} says {}", animal.name(), animal.speak());
}
}
// Object safety: not all traits can become trait objects
trait ObjectSafe {
fn method(&self);
// No Self in return type
// No generic parameters
}
// NOT object safe (cannot be dyn NotObjectSafe)
trait NotObjectSafe {
fn create() -> Self; // Self in return
fn generic<T>(&self, t: T); // Generic
}성능을 중시하는 경우 정적 디스패치가 적합합니다. 이종 컬렉션이나 유연성이 필요한 경우 동적 디스패치가 유용합니다.
Rust 면접 준비가 되셨나요?
인터랙티브 시뮬레이터, flashcards, 기술 테스트로 연습하세요.
에러 처리
질문 6: Result와 Option을 사용한 에러 처리 방법은
Rust에는 예외가 없습니다. 에러 처리는 Result<T, E>와 Option<T> 타입을 패턴 매칭으로 수행합니다.
// Idiomatic error handling in Rust
use std::fs::File;
use std::io::{self, Read};
// Option<T>: presence or absence of a value
fn find_user(id: u32) -> Option<String> {
match id {
1 => Some(String::from("Alice")),
2 => Some(String::from("Bob")),
_ => None, // No user found
}
}
// Result<T, E>: success or error
fn divide(a: f64, b: f64) -> Result<f64, String> {
if b == 0.0 {
Err(String::from("Division by zero"))
} else {
Ok(a / b)
}
}
fn option_combinators() {
let user = find_user(1);
// Pattern matching
match user {
Some(name) => println!("Found: {}", name),
None => println!("Not found"),
}
// unwrap_or: default value
let name = find_user(99).unwrap_or(String::from("Unknown"));
// map: transform the value if present
let upper = find_user(1).map(|n| n.to_uppercase());
// and_then (flatMap): chain Options
let first_char = find_user(1).and_then(|n| n.chars().next());
// if let: simplified pattern matching
if let Some(name) = find_user(2) {
println!("User 2 is {}", name);
}
}
fn result_handling() -> Result<(), Box<dyn std::error::Error>> {
// The ? operator propagates errors automatically
let result = divide(10.0, 2.0)?;
println!("Result: {}", result);
// Equivalent to:
// let result = match divide(10.0, 2.0) {
// Ok(v) => v,
// Err(e) => return Err(e.into()),
// };
Ok(())
}
// File reading with error propagation
fn read_file_contents(path: &str) -> Result<String, io::Error> {
let mut file = File::open(path)?; // Propagates error if failure
let mut contents = String::new();
file.read_to_string(&mut contents)?;
Ok(contents)
}
// Custom errors
#[derive(Debug)]
enum AppError {
IoError(io::Error),
ParseError(String),
NotFound(String),
}
impl std::fmt::Display for AppError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
AppError::IoError(e) => write!(f, "IO error: {}", e),
AppError::ParseError(s) => write!(f, "Parse error: {}", s),
AppError::NotFound(s) => write!(f, "Not found: {}", s),
}
}
}
impl std::error::Error for AppError {}
// Automatic conversion with From
impl From<io::Error> for AppError {
fn from(error: io::Error) -> Self {
AppError::IoError(error)
}
}
fn complex_operation() -> Result<String, AppError> {
let contents = std::fs::read_to_string("config.txt")?; // Auto-convert
if contents.is_empty() {
return Err(AppError::NotFound(String::from("Config is empty")));
}
Ok(contents)
}? 연산자를 통해 코드가 간결해지면서도 명시적인 에러 처리가 강제됩니다. 런타임에 예상치 못한 에러가 발생하는 일이 없습니다.
unwrap()과 expect()는 None 또는 Err인 경우 패닉을 일으킵니다. 프로토타이핑이나 실패가 불가능한 경우에만 사용해야 합니다. 프로덕션 환경에서는 ?를 통한 전파 또는 컴비네이터 사용이 권장됩니다.
질문 7: thiserror를 사용한 커스텀 에러 생성 방법은
thiserror 크레이트는 인체공학적인 커스텀 에러 생성을 간소화합니다.
// Custom errors with thiserror
use thiserror::Error;
// Error definition with derive macro
#[derive(Error, Debug)]
pub enum DataStoreError {
#[error("connection failed: {0}")]
ConnectionFailed(String),
#[error("query failed: {query}")]
QueryFailed { query: String, source: std::io::Error },
#[error("record not found: id={id}")]
NotFound { id: u64 },
#[error("invalid data: {0}")]
InvalidData(#[from] serde_json::Error),
#[error(transparent)] // Delegates Display to source
Other(#[from] anyhow::Error),
}
// Implementation with rich context
pub struct DataStore {
connection_string: String,
}
impl DataStore {
pub fn connect(conn_str: &str) -> Result<Self, DataStoreError> {
if conn_str.is_empty() {
return Err(DataStoreError::ConnectionFailed(
"Empty connection string".into()
));
}
Ok(Self { connection_string: conn_str.to_string() })
}
pub fn get_record(&self, id: u64) -> Result<Record, DataStoreError> {
// Query simulation
if id == 0 {
return Err(DataStoreError::NotFound { id });
}
Ok(Record { id, data: format!("Record {}", id) })
}
}
pub struct Record {
pub id: u64,
pub data: String,
}
// Usage with anyhow for applications
use anyhow::{Context, Result};
fn application_code() -> Result<()> {
let store = DataStore::connect("postgres://localhost/db")
.context("Failed to connect to database")?;
let record = store.get_record(42)
.context("Failed to fetch user record")?;
println!("Got: {}", record.data);
Ok(())
}
// Pattern: converting errors with context
fn read_config() -> Result<Config> {
let contents = std::fs::read_to_string("config.toml")
.context("Failed to read config file")?;
let config: Config = toml::from_str(&contents)
.context("Failed to parse config file")?;
Ok(config)
}
#[derive(Debug)]
struct Config {
// ...
}thiserror는 라이브러리(타입 에러)에 최적이며, anyhow는 애플리케이션(최대한의 유연성)에 적합합니다.
스마트 포인터
질문 8: Box, Rc, Arc, RefCell에 대해 설명해 주세요
스마트 포인터는 힙 메모리를 관리하고, 단순한 소유권으로는 실현할 수 없는 패턴을 가능하게 합니다.
// Main smart pointers in Rust
use std::rc::Rc;
use std::sync::Arc;
use std::cell::RefCell;
// BOX<T>: heap allocation
// Used for: recursive types, large types, trait objects
fn box_example() {
// Simple heap allocation
let b = Box::new(5);
println!("b = {}", b);
// Recursive type (impossible without Box)
#[derive(Debug)]
enum List {
Cons(i32, Box<List>),
Nil,
}
let list = List::Cons(1,
Box::new(List::Cons(2,
Box::new(List::Cons(3,
Box::new(List::Nil))))));
println!("{:?}", list);
}
// RC<T>: Reference Counting (single-threaded)
// Multiple owners for the same data
fn rc_example() {
let data = Rc::new(vec![1, 2, 3]);
// Clone increments the reference counter
let data_clone1 = Rc::clone(&data); // count = 2
let data_clone2 = Rc::clone(&data); // count = 3
println!("Reference count: {}", Rc::strong_count(&data)); // 3
// Each clone can read the data
println!("data_clone1: {:?}", data_clone1);
// Data is freed when the last Rc is dropped
}
// ARC<T>: Atomic Reference Counting (thread-safe)
// Like Rc but usable across threads
fn arc_example() {
use std::thread;
let data = Arc::new(vec![1, 2, 3, 4, 5]);
let mut handles = vec![];
for i in 0..3 {
let data_clone = Arc::clone(&data);
let handle = thread::spawn(move || {
// Each thread has its own Arc
println!("Thread {}: {:?}", i, data_clone);
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
}
// REFCELL<T>: Interior Mutability
// Allows mutation even with an immutable reference
fn refcell_example() {
let data = RefCell::new(5);
// borrow() returns an immutable reference
println!("Value: {}", *data.borrow());
// borrow_mut() returns a mutable reference
*data.borrow_mut() += 1;
println!("After mutation: {}", *data.borrow());
// Borrowing rules are checked at RUNTIME
// Panics if rules are violated
// let r1 = data.borrow();
// let r2 = data.borrow_mut(); // PANIC: already borrowed
}
// Common combination: Rc<RefCell<T>>
// Multiple owners with possible mutation
fn rc_refcell_example() {
#[derive(Debug)]
struct Node {
value: i32,
children: Vec<Rc<RefCell<Node>>>,
}
let node1 = Rc::new(RefCell::new(Node {
value: 1,
children: vec![],
}));
let node2 = Rc::new(RefCell::new(Node {
value: 2,
children: vec![Rc::clone(&node1)], // node1 is child of node2
}));
// Modify node1 from anywhere
node1.borrow_mut().value = 10;
println!("node2 child value: {}",
node2.borrow().children[0].borrow().value); // 10
}
// For threads: Arc<Mutex<T>> or Arc<RwLock<T>>
fn arc_mutex_example() {
use std::sync::Mutex;
use std::thread;
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let counter = Arc::clone(&counter);
let handle = thread::spawn(move || {
let mut num = counter.lock().unwrap();
*num += 1;
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("Final count: {}", *counter.lock().unwrap()); // 10
}상황에 맞는 적절한 스마트 포인터를 선택해야 합니다. 단순 힙 할당에는 Box, 공유에는 Rc/Arc, 내부 가변성에는 RefCell/Mutex가 적합합니다.
동시성
질문 9: Rust는 어떻게 스레드 안전성을 보장합니까
Rust의 타입 시스템은 Send 트레이트와 Sync 트레이트를 통해 컴파일 타임에 데이터 경합을 방지합니다.
// Concurrent safety guarantees
use std::thread;
use std::sync::{Arc, Mutex, mpsc};
// SEND: a type can be transferred to another thread
// SYNC: a type can be shared between threads via references
// Most types are Send and Sync automatically
// Exceptions: Rc (not Send/Sync), RefCell (not Sync), raw pointers
fn send_example() {
let data = vec![1, 2, 3];
// Vec is Send, so it can be moved to another thread
let handle = thread::spawn(move || {
println!("Data in thread: {:?}", data);
});
handle.join().unwrap();
}
// The compiler prevents concurrency errors
fn compile_time_safety() {
// This would NOT compile:
// let data = std::rc::Rc::new(5);
// thread::spawn(move || {
// println!("{}", data); // ERROR: Rc is not Send
// });
// Solution: use Arc
let data = Arc::new(5);
let data_clone = Arc::clone(&data);
thread::spawn(move || {
println!("{}", data_clone); // OK: Arc is Send
});
}
// Mutex for thread-safe shared mutation
fn mutex_pattern() {
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let counter = Arc::clone(&counter);
let handle = thread::spawn(move || {
// lock() blocks until exclusive access is obtained
let mut num = counter.lock().unwrap();
*num += 1;
// MutexGuard is dropped here, releasing the lock
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("Result: {}", *counter.lock().unwrap());
}
// RwLock for multiple reads / exclusive write
fn rwlock_example() {
use std::sync::RwLock;
let data = Arc::new(RwLock::new(vec![1, 2, 3]));
let mut handles = vec![];
// Multiple simultaneous readers
for i in 0..3 {
let data = Arc::clone(&data);
handles.push(thread::spawn(move || {
let read = data.read().unwrap();
println!("Reader {}: {:?}", i, *read);
}));
}
// Only one writer at a time
{
let data = Arc::clone(&data);
handles.push(thread::spawn(move || {
let mut write = data.write().unwrap();
write.push(4);
println!("Writer added 4");
}));
}
for handle in handles {
handle.join().unwrap();
}
}
// Channels for inter-thread communication
fn channel_example() {
let (tx, rx) = mpsc::channel(); // Multi-producer, single-consumer
// Clone the sender for multiple producers
let tx1 = tx.clone();
thread::spawn(move || {
tx1.send("from thread 1").unwrap();
});
thread::spawn(move || {
tx.send("from thread 2").unwrap();
});
// Receive messages
for received in rx {
println!("Got: {}", received);
}
}"두려움 없는 동시성": 코드가 컴파일되면, 데이터 경합은 존재하지 않습니다. 컴파일러가 첫 번째 방어선 역할을 합니다.
스레드가 Mutex를 보유한 상태에서 패닉하면, Mutex는 "포이즌" 상태가 됩니다. 이후의 lock() 호출은 에러를 반환하지만, into_inner()로 복구할 수 있습니다.
질문 10: Rust의 async/await는 어떻게 작동합니까
Rust의 비동기 처리는 제로 비용 Future에 기반하며, 언어에 런타임이 내장되어 있지 않습니다.
// Asynchronous programming in Rust
use tokio::time::{sleep, Duration};
// async fn returns a Future that must be executed
async fn fetch_data(url: &str) -> Result<String, reqwest::Error> {
// await suspends execution without blocking the thread
let response = reqwest::get(url).await?;
let body = response.text().await?;
Ok(body)
}
// Futures are lazy: nothing executes without await or poll
async fn lazy_example() {
let future = async {
println!("This won't print yet");
};
// Nothing happened
future.await; // Now it executes
}
// Parallel execution of futures
async fn parallel_execution() {
// join! executes multiple futures in parallel
let (result1, result2) = tokio::join!(
fetch_data("https://api.example.com/1"),
fetch_data("https://api.example.com/2"),
);
println!("Results: {:?}, {:?}", result1, result2);
}
// select! for the first completed future
async fn race_example() {
tokio::select! {
result = fetch_data("https://api1.example.com") => {
println!("API 1 responded first: {:?}", result);
}
result = fetch_data("https://api2.example.com") => {
println!("API 2 responded first: {:?}", result);
}
_ = sleep(Duration::from_secs(5)) => {
println!("Timeout!");
}
}
}
// Streams: asynchronous iterators
use tokio_stream::StreamExt;
async fn stream_example() {
let mut stream = tokio_stream::iter(vec![1, 2, 3, 4, 5]);
while let Some(value) = stream.next().await {
println!("Got: {}", value);
}
}
// Spawn for background tasks
async fn spawn_tasks() {
let handle = tokio::spawn(async {
sleep(Duration::from_secs(1)).await;
"Task completed"
});
println!("Task spawned, doing other work...");
let result = handle.await.unwrap();
println!("Result: {}", result);
}
// Entry point with tokio
#[tokio::main]
async fn main() {
// The tokio runtime executes futures
parallel_execution().await;
}
// Alternative: multi-threaded or single-threaded runtime
#[tokio::main(flavor = "current_thread")]
async fn main_single_thread() {
// Everything runs on a single thread
}
#[tokio::main(flavor = "multi_thread", worker_threads = 4)]
async fn main_multi_thread() {
// Pool of 4 worker threads
}Rust의 비동기 처리는 "런타임을 직접 가져오는" 방식입니다. tokio, async-std, smol 등 사용 사례에 맞는 최적화가 가능합니다.
Rust 면접 준비가 되셨나요?
인터랙티브 시뮬레이터, flashcards, 기술 테스트로 연습하세요.
고급 패턴
질문 11: Rust에서 Builder 패턴을 설명해 주세요
Builder 패턴은 다수의 선택적 필드를 가진 복잡한 구조체를 생성하기 위한 Rust의 관용적 패턴입니다.
// Idiomatic Builder pattern in Rust
#[derive(Debug, Clone)]
pub struct Server {
host: String,
port: u16,
max_connections: usize,
timeout_seconds: u64,
tls_enabled: bool,
tls_cert_path: Option<String>,
}
// Builder with consuming approach (ownership)
#[derive(Default)]
pub struct ServerBuilder {
host: String,
port: u16,
max_connections: usize,
timeout_seconds: u64,
tls_enabled: bool,
tls_cert_path: Option<String>,
}
impl ServerBuilder {
pub fn new() -> Self {
Self {
host: String::from("localhost"),
port: 8080,
max_connections: 100,
timeout_seconds: 30,
tls_enabled: false,
tls_cert_path: None,
}
}
// Each method takes self and returns Self for chaining
pub fn host(mut self, host: impl Into<String>) -> Self {
self.host = host.into();
self
}
pub fn port(mut self, port: u16) -> Self {
self.port = port;
self
}
pub fn max_connections(mut self, max: usize) -> Self {
self.max_connections = max;
self
}
pub fn timeout(mut self, seconds: u64) -> Self {
self.timeout_seconds = seconds;
self
}
pub fn enable_tls(mut self, cert_path: impl Into<String>) -> Self {
self.tls_enabled = true;
self.tls_cert_path = Some(cert_path.into());
self
}
// build() consumes the builder and creates the final structure
pub fn build(self) -> Result<Server, String> {
if self.tls_enabled && self.tls_cert_path.is_none() {
return Err("TLS enabled but no certificate path provided".into());
}
Ok(Server {
host: self.host,
port: self.port,
max_connections: self.max_connections,
timeout_seconds: self.timeout_seconds,
tls_enabled: self.tls_enabled,
tls_cert_path: self.tls_cert_path,
})
}
}
// Fluent usage
fn create_server() -> Result<Server, String> {
ServerBuilder::new()
.host("0.0.0.0")
.port(443)
.max_connections(1000)
.timeout(60)
.enable_tls("/etc/ssl/cert.pem")
.build()
}
// Alternative with derive macro (typed-builder crate)
// #[derive(TypedBuilder)]
// pub struct Config {
// #[builder(default = "localhost".to_string())]
// host: String,
// #[builder(default = 8080)]
// port: u16,
// }
// Pattern with type-level validation (typestate pattern)
pub struct Unvalidated;
pub struct Validated;
pub struct Request<State = Unvalidated> {
url: String,
method: String,
headers: Vec<(String, String)>,
_state: std::marker::PhantomData<State>,
}
impl Request<Unvalidated> {
pub fn new(url: &str) -> Self {
Self {
url: url.to_string(),
method: "GET".to_string(),
headers: vec![],
_state: std::marker::PhantomData,
}
}
pub fn method(mut self, method: &str) -> Self {
self.method = method.to_string();
self
}
// validate() changes the state type
pub fn validate(self) -> Result<Request<Validated>, String> {
if self.url.is_empty() {
return Err("URL cannot be empty".into());
}
Ok(Request {
url: self.url,
method: self.method,
headers: self.headers,
_state: std::marker::PhantomData,
})
}
}
impl Request<Validated> {
// send() is only available on validated requests
pub async fn send(self) -> Result<Response, reqwest::Error> {
// Implementation...
todo!()
}
}
struct Response;Typestate 패턴을 통해 특정 작업이 올바른 상태에서만 호출될 수 있음이 컴파일 타임에 보장됩니다.
질문 12: 외부 타입에 트레이트를 구현하는 방법은
"고아 규칙"은 외부 트레이트를 외부 타입에 구현하는 것을 금지하지만, 우회 방법이 존재합니다.
// The Newtype pattern to work around the orphan rule
use std::fmt;
// ORPHAN RULE: cannot implement Display (std) for Vec (std)
// impl fmt::Display for Vec<i32> { ... } // ERROR
// SOLUTION 1: Newtype wrapper
struct Wrapper(Vec<String>);
impl fmt::Display for Wrapper {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "[{}]", self.0.join(", "))
}
}
fn newtype_example() {
let w = Wrapper(vec![
String::from("hello"),
String::from("world"),
]);
println!("{}", w); // [hello, world]
}
// Transparent access with Deref
use std::ops::Deref;
impl Deref for Wrapper {
type Target = Vec<String>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
fn deref_example() {
let w = Wrapper(vec![String::from("test")]);
println!("Length: {}", w.len()); // Calls Vec::len via Deref
}
// SOLUTION 2: Extension trait (to add methods)
trait VecExt<T> {
fn first_or_default(&self) -> Option<&T>;
}
impl<T> VecExt<T> for Vec<T> {
fn first_or_default(&self) -> Option<&T> {
self.first()
}
}
fn extension_trait_example() {
let v = vec![1, 2, 3];
println!("First: {:?}", v.first_or_default());
}
// Newtype with domain semantics
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct Email(String);
impl Email {
pub fn new(email: &str) -> Result<Self, &'static str> {
if email.contains('@') && email.contains('.') {
Ok(Self(email.to_string()))
} else {
Err("Invalid email format")
}
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for Email {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
struct UserId(u64);
impl UserId {
pub fn new(id: u64) -> Self {
Self(id)
}
}
// Newtypes add type safety without runtime overhead
fn process_user(id: UserId, email: Email) {
println!("Processing user {} with email {}", id.0, email);
}
fn type_safety_example() {
let id = UserId::new(42);
let email = Email::new("user@example.com").unwrap();
process_user(id, email);
// This would not compile:
// process_user(email, id); // Types reversed
// process_user(UserId::new(42), "string"); // String instead of Email
}Newtype은 메모리 표현의 동일성 보장 덕분에 런타임 비용이 제로입니다.
질문 13: 프로시저 매크로의 사용법은
프로시저 매크로는 컴파일 타임에 코드를 생성하는 기능이며, 커스텀 derive 등에 사용됩니다.
// Understanding procedural macros
// Proc macros are defined in a separate crate with proc-macro = true
// Crate: my_derive (Cargo.toml: proc-macro = true)
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};
// DERIVE MACRO: #[derive(MyTrait)]
#[proc_macro_derive(MyDebug)]
pub fn my_debug_derive(input: TokenStream) -> TokenStream {
// Parse input as a type definition
let input = parse_macro_input!(input as DeriveInput);
let name = input.ident;
// Generate implementation code
let expanded = quote! {
impl std::fmt::Debug for #name {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, stringify!(#name))
}
}
};
TokenStream::from(expanded)
}
// ATTRIBUTE MACRO: #[my_attribute]
#[proc_macro_attribute]
pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream {
// attr contains the attribute arguments
// item contains the annotated element (function, struct, etc.)
let method_path = attr.to_string(); // "GET, /users"
let input = parse_macro_input!(item as syn::ItemFn);
let fn_name = &input.sig.ident;
let expanded = quote! {
#input
// Additionally generated code
fn register_#fn_name() {
println!("Registered route: {}", #method_path);
}
};
TokenStream::from(expanded)
}
// FUNCTION-LIKE MACRO: my_macro!(...)
#[proc_macro]
pub fn make_answer(_input: TokenStream) -> TokenStream {
"fn answer() -> u32 { 42 }".parse().unwrap()
}
// --- Usage in client code ---
// Derive macro
#[derive(MyDebug)]
struct Point {
x: i32,
y: i32,
}
// Attribute macro
#[route("GET", "/users")]
fn get_users() -> Vec<User> {
vec![]
}
// Function-like macro
make_answer!(); // Generates fn answer() -> u32 { 42 }
fn main() {
let p = Point { x: 1, y: 2 };
println!("{:?}", p); // Uses our MyDebug
println!("Answer: {}", answer()); // 42
}
struct User;프로시저 매크로는 반복적인 코드에 대해 강력한 기능을 발휘합니다. 직렬화, 웹 라우팅, 유효성 검사 등이 대표적인 용도입니다.
메모리 안전성과 unsafe
질문 14: unsafe를 사용해야 하는 시점과 방법은
unsafe 블록은 저수준 코드에 대해 컴파일러의 특정 검사를 우회할 수 있도록 허용합니다.
// Understanding unsafe and its guarantees
// The 5 superpowers of unsafe:
// 1. Dereference raw pointers
// 2. Call unsafe functions
// 3. Access/modify mutable static variables
// 4. Implement unsafe traits
// 5. Access union fields
// RAW POINTERS
fn raw_pointers() {
let mut num = 5;
// Creating raw pointers is safe
let r1 = &num as *const i32;
let r2 = &mut num as *mut i32;
// Dereferencing requires unsafe
unsafe {
println!("r1 is: {}", *r1);
*r2 = 10;
println!("r2 is: {}", *r2);
}
}
// UNSAFE FUNCTION
// The function guarantees safety IF preconditions are met
unsafe fn dangerous() {
// Code that assumes the caller verified invariants
}
fn call_dangerous() {
// Must be in an unsafe block
unsafe {
dangerous();
}
}
// SAFE ABSTRACTION over unsafe code
fn split_at_mut(values: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
let len = values.len();
let ptr = values.as_mut_ptr();
assert!(mid <= len); // Runtime check
unsafe {
// We know the two slices don't overlap
(
std::slice::from_raw_parts_mut(ptr, mid),
std::slice::from_raw_parts_mut(ptr.add(mid), len - mid),
)
}
}
// FFI: calling C code
extern "C" {
fn abs(input: i32) -> i32;
}
fn call_c_function() {
unsafe {
println!("Absolute value: {}", abs(-3));
}
}
// Export a function for C
#[no_mangle]
pub extern "C" fn call_from_c() {
println!("Called from C!");
}
// MUTABLE STATIC
static mut COUNTER: u32 = 0;
fn increment_counter() {
unsafe {
COUNTER += 1;
println!("COUNTER: {}", COUNTER);
}
}
// UNSAFE TRAIT
unsafe trait Dangerous {
// Implementers guarantee invariants
}
unsafe impl Dangerous for i32 {
// Implementer asserts respecting the trait's invariants
}
// Practical example: structure with internal pointer
pub struct MyVec<T> {
ptr: *mut T,
len: usize,
capacity: usize,
}
impl<T> MyVec<T> {
pub fn new() -> Self {
Self {
ptr: std::ptr::null_mut(),
len: 0,
capacity: 0,
}
}
pub fn push(&mut self, value: T) {
if self.len == self.capacity {
self.grow();
}
unsafe {
std::ptr::write(self.ptr.add(self.len), value);
}
self.len += 1;
}
fn grow(&mut self) {
// Unsafe allocation/reallocation...
}
}
impl<T> Drop for MyVec<T> {
fn drop(&mut self) {
unsafe {
// Properly free memory
for i in 0..self.len {
std::ptr::drop_in_place(self.ptr.add(i));
}
if self.capacity > 0 {
let layout = std::alloc::Layout::array::<T>(self.capacity).unwrap();
std::alloc::dealloc(self.ptr as *mut u8, layout);
}
}
}
}unsafe 코드의 범위를 최소화해야 합니다. unsafe 코드는 불변 조건을 보장하는 안전한 추상화 안에 캡슐화합니다. unsafe 코드가 주변의 안전한 메모리를 손상시켜서는 안 됩니다.
질문 15: 빌림 검사기는 어떻게 작동합니까
빌림 검사기는 Rust 컴파일러의 핵심으로, 소유권과 빌림 규칙을 검증합니다.
// Understanding how the borrow checker works
fn borrow_checker_basics() {
let mut v = vec![1, 2, 3];
// RULE 1: Either multiple immutable references or one mutable
let r1 = &v;
let r2 = &v;
println!("{:?} {:?}", r1, r2); // OK: multiple immutable references
// From here, r1 and r2 are no longer used (NLL)
let r3 = &mut v; // OK thanks to Non-Lexical Lifetimes
r3.push(4);
}
// The borrow checker tracks lifetimes
fn lifetime_tracking() {
let mut data = String::from("hello");
let slice = &data[..]; // Immutable borrow starts
// data.push_str(" world"); // ERROR: cannot mutate during borrow
println!("{}", slice); // Last use of slice
data.push_str(" world"); // OK: borrow ended
}
// Common problems and solutions
mod common_patterns {
// Problem: borrowing two mutable fields
struct Data {
field1: Vec<i32>,
field2: Vec<i32>,
}
fn problem(data: &mut Data) {
// This sometimes doesn't compile directly:
// let f1 = &mut data.field1;
// let f2 = &mut data.field2;
// Solution: destructuring
let Data { field1, field2 } = data;
field1.push(1);
field2.push(2);
}
// Problem: iterate and modify
fn iterate_and_modify() {
let mut v = vec![1, 2, 3, 4, 5];
// Does not compile:
// for &x in &v {
// if x % 2 == 0 {
// v.push(x * 2); // ERROR: borrowed by iterator
// }
// }
// Solution 1: collect indices first
let to_add: Vec<i32> = v.iter()
.filter(|&&x| x % 2 == 0)
.map(|&x| x * 2)
.collect();
v.extend(to_add);
// Solution 2: use explicit indices
let len = v.len();
for i in 0..len {
if v[i] % 2 == 0 {
let new_val = v[i] * 2;
v.push(new_val);
}
}
}
// Problem: self-referential struct
// struct SelfRef {
// data: String,
// slice: &str, // Reference to data - IMPOSSIBLE
// }
// Solution: use indices or crates like ouroboros
struct SafeSelfRef {
data: String,
slice_start: usize,
slice_end: usize,
}
impl SafeSelfRef {
fn get_slice(&self) -> &str {
&self.data[self.slice_start..self.slice_end]
}
}
}
// Patterns to work around limitations
mod workarounds {
use std::cell::RefCell;
// Interior mutability when borrow checker is too restrictive
struct Graph {
nodes: RefCell<Vec<Node>>,
}
struct Node {
value: i32,
}
impl Graph {
fn add_node(&self, value: i32) {
// Mutation possible despite &self
self.nodes.borrow_mut().push(Node { value });
}
fn get_node(&self, index: usize) -> Option<i32> {
self.nodes.borrow().get(index).map(|n| n.value)
}
}
}빌림 검사기는 처음에는 제약이 엄격하게 느껴질 수 있지만, 이러한 제약을 통해 다른 언어에서 발생하는 전체 버그 카테고리가 제거됩니다.
결론
Rust 기술 면접에서는 소유권 시스템에 대한 깊은 이해, 메모리 안전성 보장, 데이터 경합 없는 동시성 코드의 작성 능력이 평가됩니다. 이러한 개념의 숙달이 Rust의 고유한 장점을 활용할 수 있는 개발자를 구별하는 핵심입니다.
면접 준비 체크리스트
- 소유권, 빌림, 3가지 기본 규칙의 이해
- 라이프타임 어노테이션이 필요한 시점과 방법 파악
- 트레이트와 정적/동적 디스패치 차이점 숙달
- Result와 Option을 활용한 관용적 에러 처리
- 상황에 맞는 적절한 스마트 포인터 선택
- Arc, Mutex, 채널을 사용한 동시성 코드 작성
- async/await와 tokio 등 런타임의 이해
- unsafe의 안전한 사용 시점과 방법 파악
연습을 시작하세요!
면접 시뮬레이터와 기술 테스트로 지식을 테스트하세요.
Rust 면접 준비에는 언어 고유의 소유권 개념에 대한 연습이 필수적입니다. Exercism에서의 연습, 개인 프로젝트, Rust 생태계에 대한 기여가 가장 까다로운 기술 면접에 대비한 지식 정착에 효과적입니다.

작성자
Anthony Fillion-Maillet풀스택 개발자, SharpSkill 창업자
10년 이상 풀스택 개발을 해왔습니다. SharpSkill을 운영하며 이곳에 게시되는 모든 내용에 책임을 집니다.
2026년 4월 1일 업데이트
태그
공유
관련 기사

Rust의 소유권과 빌림: 완벽 가이드
Rust의 소유권과 빌림 시스템을 마스터합니다. 소유권 규칙, 참조, 라이프타임, 고급 메모리 관리 패턴을 다룹니다.

Rust 기초: 경험 있는 개발자를 위한 2026년 가이드
C++, Java, Python 경험을 활용하여 Rust를 효율적으로 학습하는 가이드입니다. 소유권, 빌림, 라이프타임 등 Rust 핵심 개념을 체계적으로 설명합니다.

Rust 스마트 포인터 완벽 정리: Box, Rc, Arc와 RefCell (2026)
Rust 스마트 포인터 Box, Rc, Arc, RefCell을 컴파일 가능한 2026 예제, 결정 표, 자주 나오는 면접 질문과 함께 설명합니다.