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

Rust는 해마다 채택률이 증가하고 있으며, 컴파일 타임에 메모리 안전성을 보장하면서 C++ 수준의 성능을 제공하고 현대적인 에코시스템을 갖춘 언어로 자리 잡았습니다. C++, Java, Python 등의 경험이 있는 개발자에게 Rust 학습은 처음에 다소 낯설 수 있지만, 핵심 개념을 이해하면 빠르게 직관적으로 다가옵니다.
Rust는 Stack Overflow에서 8년 연속 '가장 사랑받는 언어'로 선정되었습니다. Microsoft, Google, Amazon, Meta가 핵심 컴포넌트에 도입하고 있으며, 가비지 컬렉터 없이 메모리 안전성을 달성하는 유일한 주요 언어입니다.
Rust 개발 환경 설정
코드를 작성하기 전에 공식 버전 관리 도구인 rustup을 통해 Rust를 설치해야 합니다.
# install.sh
# Install Rust via rustup (macOS, Linux)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# Verify installation
rustc --version
cargo --versionCargo는 Rust의 패키지 매니저이자 빌드 도구입니다. npm, Maven, Make의 기능을 하나의 통합 도구로 결합한 것이라고 볼 수 있습니다.
# project-setup.sh
# Create a new project
cargo new my_project
cd my_project
# Generated structure:
# my_project/
# ├── Cargo.toml # Manifest (like package.json)
# └── src/
# └── main.rs # Entry point
# Essential commands
cargo build # Compile the project
cargo run # Compile and run
cargo test # Run tests
cargo check # Check without building (faster)변수와 기본 불변성
Rust는 일반적인 언어와 반대 접근법을 취합니다. 변수는 **기본적으로 불변(immutable)**입니다. 이 설계는 가변성에 대해 명시적으로 사고하도록 강제하며, 많은 버그를 사전에 방지합니다.
fn main() {
// Immutable by default
let x = 5;
// x = 6; // Compile error!
// Explicitly mutable variable
let mut y = 5;
y = 6; // OK
// Shadowing: redeclaration in the same scope
let x = x + 1; // Creates a new variable x
let x = x * 2; // x is now 12
// Shadowing also allows changing the type
let spaces = " "; // &str
let spaces = spaces.len(); // usize
}섀도잉은 새로운 변수를 생성하는 반면, mut는 기존 값을 수정합니다. 섀도잉을 사용하면 명확한 이름을 유지하면서 값을 변환할 수 있습니다.
기본 데이터 타입
Rust는 강력한 타입 추론 기능을 갖춘 정적 타입 언어입니다. 반드시 알아야 할 기본 프리미티브 타입은 다음과 같습니다.
fn main() {
// Signed integers: i8, i16, i32, i64, i128, isize
let age: i32 = 30;
// Unsigned integers: u8, u16, u32, u64, u128, usize
let count: u64 = 1_000_000; // Underscores for readability
// Floats: f32, f64 (default)
let pi: f64 = 3.14159;
// Boolean
let active: bool = true;
// Unicode character (4 bytes)
let emoji: char = '🦀';
// Tuple: fixed collection of different types
let person: (String, i32) = (String::from("Alice"), 28);
let (name, age) = person; // Destructuring
// Array: fixed size, same type
let numbers: [i32; 5] = [1, 2, 3, 4, 5];
let first = numbers[0];
// Slice: view into a portion of data
let slice: &[i32] = &numbers[1..4]; // [2, 3, 4]
}소유권: Rust의 혁신적 개념
소유권(Ownership)은 Rust의 가장 핵심적인 혁신입니다. 가비지 컬렉터 없이 메모리 안전성을 보장하는 시스템이며, 초기 학습 비용이 있지만 그 가치는 충분합니다.
소유권의 세 가지 규칙
fn main() {
// Rule 1: Each value has a single owner
let s1 = String::from("hello");
// Rule 2: When the owner goes out of scope, the value is freed
{
let s2 = String::from("world");
// s2 is valid here
}
// s2 is dropped, no longer accessible
// Rule 3: Only one ownership at a time (move)
let s3 = s1; // s1 is MOVED to s3
// println!("{}", s1); // Error: s1 is no longer valid!
println!("{}", s3); // OK: s3 is the owner
}이동(Move)과 복제(Clone)
fn main() {
// Simple types (stack): automatic Copy
let x = 5;
let y = x; // Copy, not move
println!("x = {}, y = {}", x, y); // Both are valid
// Complex types (heap): Move by default
let s1 = String::from("hello");
let s2 = s1; // Move
// s1 is no longer usable
// Explicit clone to duplicate
let s3 = String::from("world");
let s4 = s3.clone(); // Deep copy
println!("s3 = {}, s4 = {}", s3, s4); // Both valid
}값을 함수에 전달하면 소유권이 이동합니다. 함수가 소유자가 되며, 호출 이후에는 해당 값에 접근할 수 없습니다. 반환값으로 돌려받지 않는 한 원래 스코프에서 사용이 불가능합니다.
fn main() {
let s = String::from("hello");
takes_ownership(s);
// println!("{}", s); // Error: s has been moved
let x = 5;
makes_copy(x);
println!("{}", x); // OK: i32 implements Copy
// To regain ownership, return the value
let s2 = String::from("hello");
let s3 = takes_and_gives_back(s2);
println!("{}", s3); // OK
}
fn takes_ownership(s: String) {
println!("{}", s);
} // s is dropped here
fn makes_copy(x: i32) {
println!("{}", x);
}
fn takes_and_gives_back(s: String) -> String {
s // Returns ownership
}빌림: 소유권 이전 없는 참조
빌림(Borrowing)을 사용하면 소유권을 이전하지 않고 값을 활용할 수 있습니다. Rust에서 가장 자주 사용되는 메커니즘입니다.
fn main() {
let s1 = String::from("hello");
// Immutable reference: read-only
let len = calculate_length(&s1);
println!("Length of '{}': {}", s1, len); // s1 still valid
// Mutable reference: modification allowed
let mut s2 = String::from("hello");
change(&mut s2);
println!("{}", s2); // "hello, world"
}
fn calculate_length(s: &String) -> usize {
s.len()
} // s goes out of scope but doesn't drop (it's a reference)
fn change(s: &mut String) {
s.push_str(", world");
}빌림의 규칙
fn main() {
let mut s = String::from("hello");
// Rule 1: Multiple immutable references simultaneously OK
let r1 = &s;
let r2 = &s;
println!("{} and {}", r1, r2);
// Rule 2: ONLY ONE mutable reference at a time
let r3 = &mut s;
// let r4 = &mut s; // Error: already borrowed mutably
println!("{}", r3);
// Rule 3: No mutable ref if immutable ref exists
let r5 = &s;
// let r6 = &mut s; // Error: r5 is still active
println!("{}", r5);
// Once r5 is used for the last time, mutable borrow is allowed
let r7 = &mut s; // OK: r5 is no longer used after this
r7.push_str("!");
}Rust 면접 준비가 되셨나요?
인터랙티브 시뮬레이터, flashcards, 기술 테스트로 연습하세요.
라이프타임: 참조 유효성 보장
라이프타임은 참조가 유효한 상태로 유지되도록 보장합니다. 컴파일러가 대부분 자동으로 추론하지만, 명시적 어노테이션이 필요한 경우도 있습니다.
// Classic error: reference to freed data
// fn dangling() -> &String {
// let s = String::from("hello");
// &s // Error: s will be dropped, invalid reference!
// }
// Solution: return the owned value
fn no_dangle() -> String {
let s = String::from("hello");
s // Ownership transferred, no problem
}라이프타임 어노테이션
// The compiler can't figure out which reference will be returned
// fn longest(x: &str, y: &str) -> &str { ... } // Error!
// Explicit annotation: return lives as long as BOTH x AND y
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
fn main() {
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 dropped
}Rust에는 라이프타임 생략 규칙(Elision Rules)이 있어 단순한 경우에는 어노테이션을 생략할 수 있습니다. 초보자의 경우 컴파일러의 에러 메시지를 따르는 것만으로도 충분히 대응 가능합니다.
구조체와 메서드 구현
구조체(Struct)는 Rust에서 사용자 정의 타입을 만드는 기본 구성 요소입니다.
// Struct definition
#[derive(Debug)] // Enables printing with {:?}
struct User {
username: String,
email: String,
active: bool,
sign_in_count: u64,
}
// Implementation block for methods
impl User {
// Constructor (convention: fn new or descriptive name)
fn new(username: String, email: String) -> Self {
Self {
username,
email,
active: true,
sign_in_count: 1,
}
}
// Method: takes &self (reference to instance)
fn is_active(&self) -> bool {
self.active
}
// Method with mutation: takes &mut self
fn deactivate(&mut self) {
self.active = false;
}
// Method consuming self (rare)
fn into_username(self) -> String {
self.username
}
}
fn main() {
let mut user = User::new(
String::from("alice"),
String::from("alice@example.com"),
);
println!("Active: {}", user.is_active());
user.deactivate();
println!("Active: {}", user.is_active());
// Debug print
println!("{:?}", user);
}열거형과 패턴 매칭
Rust의 열거형(Enum)은 대부분의 언어에서 제공하는 enum보다 훨씬 강력합니다. 각 배리언트가 데이터를 포함할 수 있습니다.
// Simple enum
enum Direction {
North,
South,
East,
West,
}
// Enum with data (algebraic data type)
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(u8, u8, u8),
}
impl Message {
fn process(&self) {
match self {
Message::Quit => println!("Quitting"),
Message::Move { x, y } => println!("Moving to ({}, {})", x, y),
Message::Write(text) => println!("Writing: {}", text),
Message::ChangeColor(r, g, b) => {
println!("Color: rgb({}, {}, {})", r, g, b)
}
}
}
}
fn main() {
let msg = Message::Move { x: 10, y: 20 };
msg.process();
let msg2 = Message::Write(String::from("Hello Rust!"));
msg2.process();
}Option과 Result: 에러 처리의 기초
use std::fs::File;
use std::io::{self, Read};
fn main() {
// Option<T>: present or absent value (replaces null)
let numbers = vec![1, 2, 3];
let first: Option<&i32> = numbers.first();
match first {
Some(n) => println!("First: {}", n),
None => println!("Empty list"),
}
// Utility methods
let value = first.unwrap_or(&0);
let doubled = first.map(|n| n * 2);
// Result<T, E>: success or error
let result = read_file("config.txt");
match result {
Ok(content) => println!("Content: {}", content),
Err(e) => println!("Error: {}", e),
}
}
fn read_file(path: &str) -> Result<String, io::Error> {
let mut file = File::open(path)?; // ? propagates the error
let mut content = String::new();
file.read_to_string(&mut content)?;
Ok(content)
}? 연산자는 에러 전파를 위한 문법적 설탕입니다. Result가 Err인 경우 자동으로 에러를 반환하고, Ok인 경우 내부 값을 꺼냅니다.
트레이트: Rust의 다형성
트레이트는 공통 동작을 정의하는 메커니즘으로, Java의 인터페이스나 Swift의 프로토콜과 유사한 역할을 합니다.
// Trait definition
trait Summary {
fn summarize(&self) -> String;
// Method with default implementation
fn preview(&self) -> String {
format!("{}...", &self.summarize()[..50.min(self.summarize().len())])
}
}
struct Article {
title: String,
author: String,
content: String,
}
struct Tweet {
username: String,
content: String,
}
// Implementation for Article
impl Summary for Article {
fn summarize(&self) -> String {
format!("{} by {}", self.title, self.author)
}
}
// Implementation for Tweet
impl Summary for Tweet {
fn summarize(&self) -> String {
format!("@{}: {}", self.username, self.content)
}
}
// Function accepting any type implementing Summary
fn notify(item: &impl Summary) {
println!("Breaking news: {}", item.summarize());
}
// Equivalent syntax with trait bound
fn notify_generic<T: Summary>(item: &T) {
println!("Breaking news: {}", item.summarize());
}
fn main() {
let article = Article {
title: String::from("Rust 2026"),
author: String::from("Community"),
content: String::from("..."),
};
let tweet = Tweet {
username: String::from("rustlang"),
content: String::from("Rust is awesome!"),
};
notify(&article);
notify(&tweet);
}표준 라이브러리의 주요 컬렉션
Rust 표준 라이브러리에는 강력한 컬렉션 타입이 제공됩니다.
use std::collections::HashMap;
fn main() {
// Vec<T>: dynamic array
let mut numbers: Vec<i32> = Vec::new();
numbers.push(1);
numbers.push(2);
numbers.push(3);
// vec! macro for initialization
let nums = vec![1, 2, 3, 4, 5];
// Iteration
for n in &nums {
println!("{}", n);
}
// Functional methods
let doubled: Vec<i32> = nums.iter().map(|x| x * 2).collect();
let sum: i32 = nums.iter().sum();
let evens: Vec<&i32> = nums.iter().filter(|x| *x % 2 == 0).collect();
// String: growable UTF-8 string
let mut s = String::from("Hello");
s.push_str(", World!");
s.push('!');
// Concatenation
let s1 = String::from("Hello, ");
let s2 = String::from("World!");
let s3 = s1 + &s2; // s1 moved, s2 borrowed
// or with format!
let s4 = format!("{}{}", "Hello, ", "World!");
// HashMap<K, V>
let mut scores: HashMap<String, i32> = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Red"), 50);
// Access with get (returns Option)
if let Some(score) = scores.get("Blue") {
println!("Blue: {}", score);
}
// Entry API for conditional insertion
scores.entry(String::from("Yellow")).or_insert(25);
}Rust의 관용적 에러 처리
올바른 에러 처리는 Rust 개발에서 필수적입니다. 다음은 권장되는 패턴입니다.
use std::fs::File;
use std::io::{self, Read};
use std::num::ParseIntError;
// Define a custom error type
#[derive(Debug)]
enum AppError {
Io(io::Error),
Parse(ParseIntError),
Custom(String),
}
// Implement From for automatic conversion
impl From<io::Error> for AppError {
fn from(err: io::Error) -> Self {
AppError::Io(err)
}
}
impl From<ParseIntError> for AppError {
fn from(err: ParseIntError) -> Self {
AppError::Parse(err)
}
}
// Function returning Result with custom error
fn read_number_from_file(path: &str) -> Result<i32, AppError> {
let mut file = File::open(path)?; // io::Error -> AppError
let mut content = String::new();
file.read_to_string(&mut content)?;
let number: i32 = content.trim().parse()?; // ParseIntError -> AppError
Ok(number)
}
fn main() {
match read_number_from_file("number.txt") {
Ok(n) => println!("Number: {}", n),
Err(AppError::Io(e)) => println!("IO error: {}", e),
Err(AppError::Parse(e)) => println!("Parse error: {}", e),
Err(AppError::Custom(msg)) => println!("Error: {}", msg),
}
}실제 프로젝트에서는 thiserror 크레이트(라이브러리용)와 anyhow 크레이트(애플리케이션용)를 활용하면 에러 처리 구현을 크게 간소화할 수 있습니다.
Rust의 테스트 기능
Rust는 테스트 프레임워크를 언어 자체에 내장하고 있습니다. 외부 라이브러리 없이 바로 테스트를 작성할 수 있습니다.
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
pub fn divide(a: i32, b: i32) -> Result<i32, String> {
if b == 0 {
Err(String::from("Division by zero"))
} else {
Ok(a / b)
}
}
// Test module (compiled only for `cargo test`)
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add() {
assert_eq!(add(2, 3), 5);
}
#[test]
fn test_add_negative() {
assert_eq!(add(-1, 1), 0);
}
#[test]
fn test_divide_success() {
assert_eq!(divide(10, 2), Ok(5));
}
#[test]
fn test_divide_by_zero() {
assert!(divide(10, 0).is_err());
}
#[test]
#[should_panic(expected = "index out of bounds")]
fn test_panic() {
let v = vec![1, 2, 3];
let _ = v[99]; // Panic!
}
}결론
Rust는 메모리 안전성과 성능을 동시에 달성하는 독자적인 패러다임을 제공합니다. 소유권과 빌림 개념은 처음에는 제약처럼 느껴지지만, 실전 경험이 쌓이면 자연스러워집니다. Rust 컴파일러는 강력한 동반자이며, 업계 최고 수준의 에러 메시지를 제공합니다.
Rust 학습 체크리스트
- rustup으로 Rust를 설치하고 Cargo 기본 명령어를 숙달합니다
- 불변성과 mut를 통한 명시적 가변성의 차이를 이해합니다
- 소유권의 세 가지 규칙을 확실히 익힙니다
&와&mut참조를 사용한 빌림을 실전에서 연습합니다- null과 예외 대신
Option과Result를 활용합니다 #[test]어트리뷰트로 테스트를 작성합니다
연습을 시작하세요!
면접 시뮬레이터와 기술 테스트로 지식을 테스트하세요.
Rust 커뮤니티는 협력적이며 학습 자료도 풍부합니다. 공식 문서인 'The Rust Programming Language'는 온라인에서 무료로 제공됩니다. 이 가이드에서 다룬 기초를 탄탄히 다진 후, async/await, 매크로, WebAssembly 등 고급 주제로 나아가는 것을 권장합니다.
태그
공유
관련 기사

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

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

Rust 소유권과 빌림: 메모리 안전성의 핵심 원리 완벽 해설
Rust의 소유권(Ownership), 빌림(Borrowing), 라이프타임(Lifetime) 개념을 체계적으로 분석합니다. 빌림 검사기의 동작 원리와 실무 패턴, 기술 면접 대비 핵심 포인트를 다룹니다.