2시간에 배우는 Rust

도서 관리 시스템을 점진적으로 발전시키며 학습
출처 doc.rust-kr.org (한글 공식)

0. 들어가며

Rust는 Java와 지향점이 다릅니다. Java가 "개발자가 편하게 짜도 JVM이 안전하게 실행"에 집중한다면, Rust는 "컴파일 시점에 안전성을 검증하고, 런타임에는 C/C++ 수준의 성능"을 추구합니다.

구분JavaRust
메모리 관리가비지 컬렉터(GC) - 런타임에 자동소유권 시스템 - 컴파일 시점에 결정
실행 방식JVM 위에서 바이트코드 해석네이티브 바이너리 (LLVM 백엔드)
에러 처리Exception (try/catch)Result 타입 + ? 연산자
Null 안전Java 14+ Optional 권장Option<T> (강제)
상속extends / implements상속 없음 - trait으로 행동 공유
동시성Thread + synchronized컴파일러가 데이터 레이스 차단

첫 프로그램

예제 0-1: Hello World
Java
1public class Main {
2 public static void main(String[] args) {
3 System.out.println("Hello, Book Library!");
4 }
5}
Rust
1fn main() {
2 println!("Hello, Book Library!");
3}
💡 Rust 문법 첫 인상
  • fn main() — Java의 public static void main에 해당 (진입점)
  • println! 뒤의 느낌표(!)는 매크로 표시. 함수가 아닌 컴파일 시점 코드 생성입니다.
  • Rust는 클래스 없이 최상위에 함수를 놓을 수 있습니다.

함수 정의

예제 0-2: 덧셈 함수
Java
1public class Main {
2 static int add(int a, int b) {
3 return a + b;
4 }
5
6 public static void main(String[] args) {
7 int result = add(3, 5);
8 System.out.println("3 + 5 = " + result);
9 }
10}
Rust
1fn add(a: i32, b: i32) -> i32 {
2 a + b
3}
4
5fn main() {
6 let result = add(3, 5);
7 println!("3 + 5 = {result}");
8}
항목JavaRust
함수 키워드없음 (메서드만 존재)fn
반환 타입 위치이름 앞 (int add(...))이름 뒤 (fn add(...) -> i32)
매개변수 타입앞에 (int a)뒤에 (a: i32)
return 생략불가 - 항상 return 필요가능 - 마지막 표현식이 반환값

표현식 지향 언어

예제 0-3: 조건 표현식
Java
1public class Main {
2 static int max(int a, int b) {
3 return a > b ? a : b;
4 }
5
6 public static void main(String[] args) {
7 System.out.println("max = " + max(10, 7));
8 }
9}
Rust
1fn max(a: i32, b: i32) -> i32 {
2 if a > b { a } else { b }
3}
4
5fn main() {
6 println!("max = {}", max(10, 7));
7}
💡 표현식 vs 문장
Rust는 거의 모든 것이 표현식입니다. if도 값을 반환합니다. Java의 삼항 연산자 a > b ? a : b를 Rust에서는 일반 if로 쓸 수 있습니다. 이건 섹션 2에서 자세히.
이 가이드에서 사용하는 약어
약어풀네임 (영문)의미 / 설명
Rust시스템 프로그래밍 언어 (Mozilla 출신, 메모리 안전성이 핵심)
GCGarbage Collector가비지 컬렉터 — Java의 자동 메모리 관리 시스템
JVMJava Virtual Machine자바 가상 머신 — Java 바이트코드 실행 환경
NPENullPointerException널 포인터 예외 — Java에서 null 참조 접근 시
ADTAlgebraic Data Type대수적 데이터 타입 — enum이 대표적
NLLNon-Lexical Lifetimes비렉시컬 수명 — 참조 수명을 실제 사용 범위로 정확히 추적 (Rust 2018+)
RcReference Counted참조 카운팅 스마트 포인터 (단일 스레드)
ArcAtomic Reference Counted원자적 참조 카운팅 (멀티 스레드 안전)
VecVector동적 배열 — Java의 ArrayList에 대응
mpscMultiple Producer, Single Consumer다중 생산자 단일 소비자 — Rust 표준 채널
Fn / FnMut / FnOnce(클로저 트레잇)캡처 방식별 클로저 — 읽기 / 수정 / 소유권 이동
E0597, E0106, E0502(Rust Compiler Error Codes)러스트 컴파일러 에러 식별 코드 (공식 인덱스)
UTF-8 / UTF-16Unicode Transformation Format유니코드 인코딩 방식 (Rust는 UTF-8, Java는 UTF-16)
TRPLThe Rust Programming Language러스트 공식 교재 — 한글판
REPLRead-Eval-Print Loop읽기-평가-출력 순환 — 대화형 실행 환경
💡 참고
이 가이드는 Wandbox의 rust-1.82.0openjdk-22 환경에서 실행됩니다. 모든 예제는 브라우저 상단 ▷ 실행 버튼으로 바로 실행 가능합니다.

1. 변수와 타입

Rust의 let은 Java의 var와 비슷해 보이지만 결정적 차이가 있습니다: Rust의 변수는 기본 불변(immutable)입니다. Java의 final이 기본값이라고 생각하시면 됩니다.

불변 변수 (기본값)

예제 1-1: 기본 선언
Java
1public class Main {
2 public static void main(String[] args) {
3 int x = 10;
4 System.out.println("x = " + x);
5 }
6}
Rust
1fn main() {
2 let x = 10;
3 println!("x = {x}");
4}

가변 변수 (mut)

예제 1-2: 값 변경이 필요할 때
Java
1public class Main {
2 public static void main(String[] args) {
3 int count = 0;
4 count = count + 1;
5 count = count + 1;
6 System.out.println("count = " + count);
7 }
8}
Rust
1fn main() {
2 let mut count = 0;
3 count = count + 1;
4 count = count + 1;
5 println!("count = {count}");
6}
⚠️ mut 없이 재할당하면?
예제 1-2의 Rust 코드에서 let mutmut을 빼고 재할당하면 컴파일 에러가 납니다:
error[E0384]: cannot assign twice to immutable variable `count`

섀도잉 - Rust만의 특이 문법

예제 1-3: 같은 이름 재선언
Java
1public class Main {
2 public static void main(String[] args) {
3 int x = 5;
4 x = x + 1; // Java는 재할당 (같은 변수)
5 x = x * 2;
6 System.out.println("x = " + x);
7 }
8}
Rust
1fn main() {
2 // 1) 같은 타입 재선언 (기존)
3 let x = 5;
4 let x = x + 1; // 이전 x를 숨김(shadow)
5 let x = x * 2;
6 println!("x = {x}"); // 12
7
8 // 2) 타입을 바꾸는 쉐도잉 - Rust만의 강점
9 let spaces = " "; // &str 타입
10 let spaces = spaces.len(); // usize 타입으로 쉐도잉
11 println!("빈칸 개수: {spaces}"); // 3
12
13 // 3) 쉐도잉된 변수를 나중에 사용
14 let name = "Alice"; // &str
15 let name = name.to_uppercase(); // String으로 변환+쉐도잉
16 let name = format!("[{}]", name); // String으로 꾸밈
17 println!("{name}"); // [ALICE]
18}
💡 섀도잉 vs mut 차이
  • 섀도잉(let x = ... 재선언): 완전히 새 변수. 타입이 바뀌어도 됨. (예: i32String)
  • mut 재할당: 같은 변수에 새 값 대입. 타입은 그대로 유지.

상수 (const)

예제 1-4: 프로그램 전체에서 고정된 값
Java
1public class Main {
2 static final int MAX_PAGES = 10000;
3
4 public static void main(String[] args) {
5 System.out.println("MAX_PAGES = " + MAX_PAGES);
6 }
7}
Rust
1const MAX_PAGES: u32 = 10000;
2
3fn main() {
4 println!("MAX_PAGES = {MAX_PAGES}");
5}
💡 const 문법 상세
  • 타입 명시 필수: const MAX_PAGES: u32 = 10000; (타입 생략 불가)
  • 이름 규칙: 대문자 + 언더스코어 (SCREAMING_SNAKE_CASE)
  • 컴파일 시점 상수: 런타임 계산 불가 (함수 호출 결과 등 사용 불가)
  • let과의 차이: constmut 불가, 스코프 제한 없음(모듈/함수 어디든)
  • static과의 차이: static은 메모리 고정 주소, const는 인라인(사본 생성)
구분JavaRust
기본 선언var x = 10; (변경 가능)let x = 10; (변경 불가)
가변 변수var x = 10;let mut x = 10;
지역 상수final int x = 10;let x = 10; (이게 기본)
전역 상수static final int X = 10;const X: i32 = 10;
타입 명시int x = 10;let x: i32 = 10;
타입 재사용불가 (같은 스코프에서 재선언 에러)가능 (섀도잉)

기본 숫자 타입

예제 1-5: 정수 타입
Java
1public class Main {
2 public static void main(String[] args) {
3 byte small = 127;
4 int page = 300;
5 long big = 9_000_000_000L;
6 // Java에는 unsigned 없음 (int 사용)
7 int unsignedLike = 500;
8 System.out.println(
9 small + ", " + page + ", "
10 + big + ", " + unsignedLike);
11 }
12}
Rust
1fn main() {
2 let small: i8 = 127;
3 let page: i32 = 300;
4 let big: i64 = 9_000_000_000;
5 let unsigned: u32 = 500;
6 println!(
7 "{small}, {page}, {big}, {unsigned}");
8}
크기Java (부호 있음만)Rust (부호 있음 / 없음)
8비트byte (-128 ~ 127)i8 / u8 (0 ~ 255)
16비트shorti16 / u16
32비트int (기본)i32 (기본) / u32
64비트longi64 / u64
128비트없음i128 / u128
플랫폼 의존없음isize / usize (포인터 크기)
예제 1-6: 실수 · 불리언 · 문자
Java
1public class Main {
2 public static void main(String[] args) {
3 double price = 29.99;
4 boolean inStock = true;
5 // Java char는 2바이트 (UTF-16)
6 char grade = 'A';
7 System.out.println(
8 "$" + price + ", " + inStock + ", " + grade);
9 }
10}
Rust
1fn main() {
2 let price: f64 = 29.99;
3 let in_stock: bool = true;
4 // Rust char는 4바이트 (Unicode)
5 let grade: char = 'A';
6 println!("${price}, {in_stock}, {grade}");
7}
⚠️ char 크기 주의 - Java와 다름!
Java char2바이트 (UTF-16), Rust char4바이트 (Unicode Scalar Value)입니다. Rust char는 한글 '한'이나 이모지도 단일 값으로 표현 가능합니다.

튜플과 배열 - Book 정보 묶기

예제 1-7 (심화): 첫 Book 데이터 구조

책 정보를 한 덩어리로 다루고 싶습니다. Java에는 record(Java 16+)나 클래스가 있고, Rust에는 튜플이 있습니다 (구조체는 섹션 8에서).

Java
1public class Main {
2 public static void main(String[] args) {
3 // 개별 변수로 (record는 뒤에서)
4 String title = "Rust Book";
5 int pages = 300;
6 double price = 29.99;
7 System.out.println(
8 title + ": " + pages + "p, $" + price);
9
10 // 배열
11 int[] scores = {95, 87, 92};
12 System.out.println("First score: " + scores[0]);
13 }
14}
Rust
1fn main() {
2 // 튜플: 다른 타입 묶기
3 let book: (&str, i32, f64) = ("Rust Book", 300, 29.99);
4 let (title, pages, price) = book; // 구조 분해
5 println!("{title}: {pages}p, ${price}");
6
7 // 배열: 같은 타입, 고정 크기
8 let scores: [i32; 3] = [95, 87, 92];
9 println!("첫 점수: {}", scores[0]);
10}
💡 Java의 record는 Rust의 struct와 비슷
Java는 튜플이 없지만, Java 14+의 record로 비슷한 역할을 할 수 있습니다. record불변 데이터 클래스로, 생성자/getter/equals/hashCode가 자동 생성됩니다. Rust의 struct + #[derive(PartialEq)] 조합과 역할이 같습니다.
예제 1-7b: Java record ↔ Rust struct
Java
1// Java 14+ record - 자바의 '튜플 비슷한' 대안
2public class Main {
3 // record = 불변 데이터 클래스 (필드+생성자+getter+equals+hashCode 자동)
4 public record Book(String title, int pages, double price) {}
5
6 public static void main(String[] args) {
7 Book b = new Book("Rust", 300, 29.99);
8
9 // 필드 접근: 메서드 호출 (getter 자동 생성)
10 System.out.println(b.title() + ": " + b.pages() + "p, $" + b.price());
11
12 // equals/hashCode 자동 - 값 비교 가능
13 Book b2 = new Book("Rust", 300, 29.99);
14 System.out.println("same? " + b.equals(b2)); // true
15 }
16}
Rust
1// 튜플 struct - Java record와 호출 스타일이 비슷
2// 필드 이름 없이 타입만 나열, 값은 순서대로 넣고 b.0, b.1, b.2로 접근
3#[derive(Debug, PartialEq)]
4struct Book(String, u32, f64);
5
6fn main() {
7 // Java: new Book("Rust", 300, 29.99) 와 유사한 스타일
8 let b = Book(String::from("Rust"), 300, 29.99);
9
10 // 필드 접근: 이름 대신 인덱스 (0부터)
11 println!("{}: {}p, ${}", b.0, b.1, b.2);
12
13 // PartialEq derive로 == 비교 (Java record equals 대응)
14 let b2 = Book(String::from("Rust"), 300, 29.99);
15 println!("same? {}", b == b2);
16}
항목Java recordRust 튜플 struct
선언public record Book(String t, int p) {}struct Book(String, u32);
호출 스타일new Book("Rust", 300)Book(String::from("Rust"), 300)
필드 접근b.title() — 메서드b.0, b.1 — 인덱스
값 비교자동 (equals)#[derive(PartialEq)] 필요
불변성자동 (모든 필드 final)선택 (let 또는 let mut)
출력자동 (toString)#[derive(Debug)] 필요
💡 튜플 struct vs 일반 struct — 언제 무엇을 쓸까?
  • 튜플 struct (struct Book(String, u32, f64);): 필드 이름이 불필요할 때. 예: Point(i32, i32), Color(u8, u8, u8). Java record호출 스타일이 가장 비슷합니다.
  • 일반 struct (struct Book { title, pages, price }): 필드가 많거나 이름으로 의미를 명확히 해야 할 때. 실무에서 더 흔히 쓰입니다.
  • 각 struct는 타입명이 다르면 서로 다른 타입으로 취급됩니다. 예: Point(i32, i32)Color(i32, i32, i32)는 완전히 다른 타입.
참고: 일반 struct 버전 (이름 있는 필드)
Rust
1// 일반 struct - 필드에 이름이 있어 가독성 좋음 (실무에서 더 흔함)
2#[derive(Debug, PartialEq)]
3struct Book {
4 title: String,
5 pages: u32,
6 price: f64,
7}
8
9fn main() {
10 let b = Book {
11 title: String::from("Rust"),
12 pages: 300,
13 price: 29.99,
14 };
15
16 // 필드 접근: 점 표기법 + 이름
17 println!("{}: {}p, ${}", b.title, b.pages, b.price);
18
19 let b2 = Book {
20 title: String::from("Rust"),
21 pages: 300,
22 price: 29.99,
23 };
24 println!("same? {}", b == b2);
25}
💡 Rust 포매팅 문법
println!("{title}")처럼 중괄호에 변수명을 바로 쓸 수 있습니다 (Rust 1.58+). Java의 "text " + var 또는 String.format("%s", var) 대체입니다.

2. 제어 흐름

Rust의 if, loop, while, for는 Java와 비슷하지만 표현식이라는 큰 차이가 있습니다. 값을 반환할 수 있다는 뜻입니다.

if 기본 사용

예제 2-1: 단순 분기
Java
1public class Main {
2 public static void main(String[] args) {
3 int age = 20;
4 if (age >= 18) {
5 System.out.println("Adult");
6 } else {
7 System.out.println("Minor");
8 }
9 }
10}
Rust
1fn main() {
2 let age = 20;
3 if age >= 18 {
4 println!("성인");
5 } else {
6 println!("미성년");
7 }
8}

if는 표현식이다 - Rust의 핵심 개념

예제 2-2: 삼항 연산자 대체
Java
1public class Main {
2 public static void main(String[] args) {
3 int score = 85;
4 // Java는 3항 연산자 필요
5 String grade = score >= 90 ? "A"
6 : score >= 80 ? "B"
7 : "C";
8 System.out.println("Grade: " + grade);
9 }
10}
Rust
1fn main() {
2 let score = 85;
3 // if가 값을 반환 (3항 연산자 대체)
4 let grade = if score >= 90 { "A" }
5 else if score >= 80 { "B" }
6 else { "C" };
7 println!("등급: {grade}");
8}
💡 if가 표현식이라는 것의 의미
Java에서 if문장(statement)이라 값을 반환하지 않습니다. 삼항 연산자 ?:가 별도로 있어야 값을 쓸 수 있죠. Rust의 if표현식(expression)이라 자연스럽게 값을 반환합니다. 삼항 연산자가 불필요.

단, if의 모든 분기는 같은 타입을 반환해야 합니다. 그렇지 않으면 컴파일 에러.

loop - 무한 루프 + 값 반환

예제 2-3: break로 값 반환하기
Java
1public class Main {
2 public static void main(String[] args) {
3 int counter = 0;
4 int result = 0;
5 while (true) {
6 counter++;
7 if (counter == 10) {
8 result = counter * 2;
9 break;
10 }
11 }
12 System.out.println("result = " + result);
13 }
14}
Rust
1fn main() {
2 let mut counter = 0;
3 // loop는 값을 반환할 수 있음 (break 뒤에 값)
4 let result = loop {
5 counter += 1;
6 if counter == 10 {
7 break counter * 2;
8 }
9 };
10 println!("result = {result}");
11}
💡 Java에 없는 기능
Java의 while(true) { ... break; }는 값을 반환할 수 없어서 별도 변수가 필요합니다. Rust loopbreak value;로 값을 반환할 수 있습니다 (loop가 표현식이라서).

while

예제 2-4: 조건부 반복
Java
1public class Main {
2 public static void main(String[] args) {
3 int n = 3;
4 while (n > 0) {
5 System.out.println(n + "...");
6 n -= 1;
7 }
8 System.out.println("Go!");
9 }
10}
Rust
1fn main() {
2 let mut n = 3;
3 while n > 0 {
4 println!("{n}...");
5 n -= 1;
6 }
7 println!("발사!");
8}

for - 범위(range)와 컬렉션 순회

예제 2-5: for ... in
Java
1public class Main {
2 public static void main(String[] args) {
3 // Java for문
4 for (int i = 1; i < 4; i++) {
5 System.out.println("i = " + i);
6 }
7
8 // 향상된 for문 (for-each)
9 String[] books = {"Rust", "Java", "Go"};
10 for (String book : books) {
11 System.out.println(book);
12 }
13 }
14}
Rust
1fn main() {
2 // range: 1..4 = [1, 2, 3] (끝 미포함)
3 for i in 1..4 {
4 println!("i = {i}");
5 }
6
7 // 배열 순회
8 let books = ["Rust", "Java", "Go"];
9 for book in books {
10 println!("{book}");
11 }
12}
구문JavaRust
범위 반복for(int i=1; i<4; i++)for i in 1..4 (끝 미포함)
끝 포함 범위수동 (i<=3)for i in 1..=3
컬렉션 순회for(var x : list)for x in list
역순for(int i=n; i>0; i--)for i in (1..n).rev()
인덱스 포함별도 변수 관리for (i, x) in list.iter().enumerate()

중첩 루프 라벨 (심화)

예제 2-6: 바깥 루프 탈출
Java
1public class Main {
2 public static void main(String[] args) {
3 outer:
4 for (int i = 1; i <= 3; i++) {
5 for (int j = 1; j <= 3; j++) {
6 if (i * j > 4) {
7 System.out.println("Exit: " + i + "*" + j + "=" + (i*j));
8 break outer; // 바깥 루프 탈출
9 }
10 }
11 }
12 System.out.println("End");
13 }
14}
Rust
1fn main() {
2 'outer: for i in 1..=3 {
3 for j in 1..=3 {
4 if i * j > 4 {
5 println!("탈출: {i}*{j}={}", i*j);
6 break 'outer; // 바깥 루프 바로 탈출
7 }
8 }
9 }
10 println!("끝");
11}
💡 라벨과 break value 조합
loop(while/for 아님)에서는 break 'label value; 형태로 라벨을 가리키면서 값까지 반환할 수 있습니다. 중첩 loop에서 안쪽에서 바깥 loop를 값과 함께 종료할 때 유용합니다.
예제 2-6b: 'outer loop에서 값 반환
Rust
1fn main() {
2 // loop + 라벨 + break value 조합
3 let result = 'outer: loop {
4 let mut count = 0;
5 loop {
6 count += 1;
7 if count == 5 {
8 break 'outer count * 10; // 바깥 loop에서 값 반환!
9 }
10 }
11 };
12 println!("result = {result}"); // 50
13}
💡 라벨 문법 차이
  • Java: outer: (콜론 뒤에 이름)
  • Rust: 'outer: (작은따옴표로 시작) - 라이프타임과 같은 기호 사용
라이프타임은 섹션 6에서 자세히.

3. 문장과 표현식 ⭐

Rust는 "거의 모든 것이 표현식"이라는 강한 철학을 가집니다. Java와 가장 다른 언어 설계 중 하나이고, 앞으로 모든 섹션에 영향을 미치는 핵심 개념입니다.

Part 1. 개념 — 거의 모든 것이 표현식

표현식(expression)은 값을 반환하는 코드 조각입니다. 문장(statement)은 어떤 일을 수행하지만 값을 반환하지 않습니다. Rust에서는 거의 모든 것이 표현식입니다.

구문표현식?반환 타입
리터럴 (5, "hi")타입별
연산 (x + y)연산 결과 타입
함수 호출반환 타입
블록 { ... }마지막 표현식 타입
if / if-else브랜치 타입
matcharm 타입
loopbreak value 타입
while / for항상 ()
let x = 5;문장
5; (세미콜론 있음)문장 (값 버림)
예제 3-1: 개념 소개
Rust
1fn main() {
2 let x = 5; // 5는 리터럴(표현식), let문 전체는 문장
3 let y = x + 10; // 연산: 표현식
4 // if도 표현식이라 값을 반환
5 let z = if x > 0 { "양수" } else { "음수" };
6 println!("{x} / {y} / {z}");
7}
Part 2. 블록 { }도 표현식

블록 { ... }은 여러 문장을 묶어주는 것처럼 보이지만, 실제로는 값을 반환하는 표현식입니다. 블록의 값은 마지막 표현식입니다 (세미콜론이 없어야 함).

예제 3-2: 블록의 값
Rust
1fn main() {
2 // 블록 { ... }의 마지막 표현식이 블록의 값
3 let x = {
4 let a = 10;
5 let b = 20;
6 a + b // 세미콜론 없음 → 블록의 값
7 };
8 println!("x = {x}"); // 30
9}
예제 3-3: 블록 스코프
Rust
1fn main() {
2 // 블록 내부 변수는 블록 끝나면 소멸
3 let result = {
4 let temp = 100;
5 temp * 2 + 5
6 };
7 // 여기서 temp 접근 불가 (스코프 벗어남)
8 println!("result = {result}"); // 205
9}
💡 never 타입 ! — return, panic!, loop 등의 타입
return, panic!(), unreachable!(), process::exit(), 무한 loop {} 등은 절대 값을 반환하지 않으므로 never 타입 !을 가집니다.

!어떤 타입으로든 변환 가능한 "바닥 타입(bottom type)"입니다. 그래서 아래처럼 한쪽은 값, 다른 쪽은 return인 표현식도 성립합니다:
let x = if cond { 10 } else { return };  // ✓ OK
//              i32        !  (i32로 변환됨)
println!, vec! 같은 매크로는 그 자체가 "never"는 아닙니다 (정상적으로 값을 반환할 수 있음). 하지만 panic!은 never입니다.
💡 블록이 값을 가진다는 의미
함수 본문 자체가 블록입니다. 즉, 함수의 반환값 = 블록의 마지막 표현식. 이것이 Rust에서 return 키워드를 자주 안 쓰는 이유입니다.
Part 3. if / match / loop 모두 표현식

Java에서는 if가 문장이라 삼항 연산자 ? :가 필요합니다. Rust에서는 if가 표현식이라 삼항 연산자가 없습니다 — 필요 없으니까요.

예제 3-4: if와 match를 값으로
Rust
1fn main() {
2 let day = 3;
3
4 // if 표현식 - 삼항 연산자 대체
5 let kind = if day == 1 || day == 7 {
6 "주말"
7 } else {
8 "평일"
9 };
10
11 // match 표현식
12 let name = match day {
13 1 => "월", 2 => "화", 3 => "수",
14 4 => "목", 5 => "금",
15 _ => "주말",
16 };
17 println!("{day}일: {kind}, {name}요일");
18}
예제 3-5: loop의 break value
Rust
1fn main() {
2 let mut counter = 0;
3 // loop도 표현식 - break 뒤에 값을 붙여 반환
4 let result = loop {
5 counter += 1;
6 if counter == 10 {
7 break counter * 2; // loop의 값
8 }
9 };
10 println!("result = {result}"); // 20
11}
💡 loop vs while 차이
loop는 무한 루프라 "끝낼 때" break를 명시적으로 해야 합니다. 그래서 break value값을 확정해서 반환할 수 있습니다. 반면 while/for는 조건이 false가 되어 자연 종료될 수도 있어 반환 값을 확정할 수 없습니다 — 항상 ().
Part 4. 세미콜론의 의미 ⭐

세미콜론 하나가 표현식을 문장으로 바꾸고, 반환 타입을 바꿉니다. Rust 초보자가 가장 자주 마주치는 에러의 원인입니다.

코드의미반환값
x + 1표현식연산 결과
x + 1;문장 (값 버림)()
함수 마지막 x + 1함수 반환값연산 결과
함수 마지막 x + 1;() 반환타입 불일치 에러
예제 3-6: 세미콜론 없음 = 반환값
Rust
1// 세미콜론 없음 = 이 표현식이 반환값
2fn add(a: i32, b: i32) -> i32 {
3 a + b
4}
5
6// return 키워드로 명시해도 됨 (같은 결과)
7fn multiply(a: i32, b: i32) -> i32 {
8 return a * b;
9}
10
11fn main() {
12 println!("{}", add(3, 5)); // 8
13 println!("{}", multiply(3, 5)); // 15
14}
예제 3-7: 실수 - 세미콜론 때문에 타입 불일치
Rust
1// 다음은 컴파일 에러 예시 (주석 처리됨):
2// fn broken(a: i32, b: i32) -> i32 {
3// a + b; // ❌ 세미콜론 → () 반환 → i32와 타입 불일치
4// }
5
6// 올바른 예:
7fn good(a: i32, b: i32) -> i32 {
8 a + b // 세미콜론 없음 → i32 반환 OK
9}
10
11fn main() {
12 println!("{}", good(3, 5)); // 8
13 println!("세미콜론 하나가 반환 타입을 바꿉니다");
14}
❌ 세미콜론 실수 — 실제 컴파일러 에러
fn broken(a: i32, b: i32) -> i32 { a + b; // ❌ 세미콜론 있음 } -------------------------------------- error[E0308]: mismatched types --> src/main.rs:1:30 | 1 | fn broken(a: i32, b: i32) -> i32 { | ------ ^^^ expected `i32`, found `()` | | | implicitly returns `()` as its body has no tail or `return` expression 2 | a + b; | - help: remove this semicolon to return this value
친절하게도 Rust 컴파일러는 "이 세미콜론을 제거하라"고 알려줍니다.
Part 5. while / for의 특수성 — 항상 ()

whilefor는 표현식이지만 반환 타입이 항상 ()로 고정됩니다. 반면 loopbreak value로 다른 타입 반환 가능.

예제 3-8: while vs loop
Rust
1fn main() {
2 // while은 표현식이지만 타입이 항상 ()
3 let x: () = while false { break; };
4 println!("while의 값: {:?}", x); // ()
5
6 // loop는 break value로 값 반환 가능
7 let y: i32 = loop { break 100; };
8 println!("loop의 값: {y}");
9}
Part 6. Java vs Rust 비교
항목JavaRust
if문장표현식
블록 { }문장표현식 (값 반환)
switch문장 (Java 14+ 표현식 추가)match 기본 표현식
삼항 연산자 ? :필수없음 (필요 없음)
반환문return 필수마지막 표현식 (또는 return)
언어 철학문장 중심"거의 모든 것이 표현식"
예제 3-9: 등급 계산 비교
Java
1// Java: if는 문장 → 값을 쓰려면 삼항 연산자 필요
2public class Main {
3 public static void main(String[] args) {
4 int score = 85;
5 // Java는 if가 문장이라 이런 식 불가:
6 // String g = if (score >= 80) "B" else "C"; // ❌
7 // 삼항 연산자 필요
8 String grade = score >= 90 ? "A"
9 : score >= 80 ? "B"
10 : "C";
11 System.out.println("Grade: " + grade);
12 }
13}
Rust
1// Rust: if가 표현식 → 삼항 불필요, 일관된 문법
2fn main() {
3 let score = 85;
4 let grade = if score >= 90 { "A" }
5 else if score >= 80 { "B" }
6 else { "C" };
7 println!("등급: {grade}");
8}
💡 왜 Rust는 표현식 중심인가
함수형 언어(Haskell, ML, Scala 등)에서 영향을 받은 설계입니다. 모든 것이 값을 가진다는 일관성 덕분에 코드가 간결해지고, 불필요한 임시 변수가 줄어들며, null/에러 타입을 일관되게 다룰 수 있습니다 (Option, Result 섹션에서 체감).
Part 7. Book 가격 책정 (심화)

지금까지 배운 표현식을 모두 동원해 도서 가격 책정 로직을 짭니다. match → if-else → 블록이 한 흐름으로 엮여 함수 반환값까지 이어집니다.

예제 3-10: 표현식 체인으로 가격 계산
Rust
1struct Book {
2 pages: i32,
3 genre: &'static str,
4}
5
6fn calculate_price(book: &Book) -> f64 {
7 // match 표현식: 장르별 기본가
8 let base: f64 = match book.genre {
9 "기술" => 30.0,
10 "소설" => 20.0,
11 _ => 15.0,
12 };
13
14 // if 표현식: 페이지 수 할증
15 let multiplier: f64 = if book.pages > 500 { 1.5 }
16 else if book.pages > 300 { 1.2 }
17 else { 1.0 };
18
19 // 블록 표현식: 최종 계산 후 반올림
20 {
21 let raw = base * multiplier;
22 (raw * 100.0).round() / 100.0 // 블록의 값 = 함수 반환값
23 }
24}
25
26fn main() {
27 let b1 = Book { pages: 450, genre: "기술" };
28 let b2 = Book { pages: 200, genre: "소설" };
29 let b3 = Book { pages: 600, genre: "기타" };
30 println!("기술서(450p): ${}", calculate_price(&b1));
31 println!("소설(200p): ${}", calculate_price(&b2));
32 println!("기타(600p): ${}", calculate_price(&b3));
33}
✅ 섹션 3 정리
  • Rust는 "거의 모든 것이 표현식" — 블록, if, match, loop 모두 값을 반환
  • 세미콜론 = 값 버림 (표현식 → 문장으로 전환)
  • 함수 마지막 줄은 보통 세미콜론 없이 반환값으로 씀
  • while/for는 예외: 항상 () 반환
  • 이 개념은 이후 모든 섹션에서 반복 등장

4. 소유권 ⭐

Part 1. 왜 소유권인가

프로그램은 실행 중 메모리를 관리해야 합니다. 세 가지 접근법이 있습니다:

방식대표 언어장점단점
가비지 컬렉터 Java, C#, Python, Go 개발자가 메모리 신경 안 써도 됨 런타임 오버헤드 / GC 멈춤 현상
수동 관리 C, C++ (전통) 최고 성능 / 완전한 제어 메모리 누수 / use-after-free 버그
소유권 Rust 런타임 비용 0 + 컴파일 시점 안전 학습 곡선 가파름
💡 소유권의 3가지 규칙
  1. Rust의 각각의 값은 소유자(owner)라고 불리는 변수를 갖는다
  2. 한 번에 단 하나의 소유자만 존재할 수 있다
  3. 소유자가 스코프를 벗어나면 값은 자동으로 제거(drop)된다
Part 2. 스코프와 자동 해제

Rust에서 변수는 중괄호 { } 스코프 안에서 살고, 벗어나면 자동으로 메모리에서 해제됩니다. Java의 GC와 달리 정확한 해제 시점이 예측 가능합니다 (결정론적 drop).

예제 4-1: 스코프 종료 시 자동 해제
Java
1public class Main {
2 public static void main(String[] args) {
3 {
4 String s = "hello";
5 System.out.println("Inner: " + s);
6 } // Java: GC가 나중에 정리 (시점 불확정)
7
8 System.out.println("Cannot use s outside");
9 }
10}
Rust
1fn main() {
2 {
3 let s = String::from("hello");
4 println!("안쪽: {s}");
5 } // <-- 여기서 s가 자동 해제 (drop)
6
7 println!("바깥에서는 s 사용 불가");
8}
항목JavaRust
해제 시점GC가 알아서 (불확정)스코프 끝나는 순간 (확정)
해제 비용런타임 GC 사이클컴파일 시 삽입된 해제 코드
리소스 정리try-with-resources 필요자동 (모든 타입에 적용)
메모리 누수드물지만 가능 (순환 참조)원칙적으로 불가능
Part 3. Move - 소유권 이동

Java에서 String s2 = s1;은 같은 객체를 둘 다 가리킵니다 (참조 복사). Rust에서 let s2 = s1;소유권이 s1에서 s2로 이동(move)하고, s1은 더 이상 사용할 수 없게 됩니다.

예제 4-2: Java vs Rust의 핵심 차이
Java
1public class Main {
2 public static void main(String[] args) {
3 String s1 = "hello";
4 String s2 = s1; // 참조만 복사 (같은 객체)
5 System.out.println(s2);
6 System.out.println(s1); // Java는 둘 다 사용 가능
7 }
8}
Rust
1fn main() {
2 let s1 = String::from("hello");
3 let s2 = s1; // 소유권 s1 → s2로 이동
4 println!("{s2}"); // s2 사용 가능
5 // println!("{s1}"); // ❌ 에러: s1은 더 이상 유효하지 않음
6}
❌ Rust에서 주석을 풀면? (실제 컴파일러 에러)
예제 3-2의 Rust 코드에서 // println!("{s1}"); 주석을 풀고 컴파일하면:
error[E0382]: borrow of moved value: `s1` --> src/main.rs:5:17 | 2 | let s1 = String::from("hello"); | -- move occurs because `s1` has type `String`, | which does not implement the `Copy` trait 3 | let s2 = s1; | -- value moved here 4 | println!("{s2}"); 5 | println!("{s1}"); | ^^^^ value borrowed here after move
핵심: move occursvalue moved herevalue borrowed here after move
Rust 컴파일러는 언제 이동이 일어났고, 언제 잘못 쓰려 했는지를 정확히 알려줍니다.

clone()으로 명시적 복사하기

예제 4-3: move 대신 복제

값을 정말 복사하고 싶다면 .clone()을 명시적으로 호출합니다. 이건 비용이 드는 작업이라는 것을 코드에 드러내는 Rust 철학입니다.

Rust
1fn main() {
2 let s1 = String::from("hello");
3 let s2 = s1.clone(); // 실제 복사 (두 개의 String)
4 println!("s1 = {s1}"); // ✓ 여전히 사용 가능
5 println!("s2 = {s2}");
6}
🟢 출력
s1 = hello s2 = hello (둘 다 각자의 String 소유)
💡 구조체도 복사하고 싶다면?
#[derive(Clone)] 또는 #[derive(Copy, Clone)]을 구조체 위에 붙이면 복사가 가능해집니다. derive 메커니즘과 여러 트레잇 자동 구현은 섹션 8의 마지막 Part에서 자세히 다룹니다.
class="part-h">Part 4. Copy trait - 왜 i32는 move 안 되나

그런데 의문이 생깁니다. "그럼 let y = x; 했을 때 x도 못 쓰게 되는 거야?" — 아닙니다. 고정 크기의 단순 타입은 move 대신 복사됩니다.

예제 4-4: i32, bool은 Copy
Java
1public class Main {
2 public static void main(String[] args) {
3 int x = 5; // Java primitive - 항상 값 복사
4 int y = x;
5 System.out.println("x = " + x + ", y = " + y);
6
7 boolean b = true;
8 boolean c = b;
9 System.out.println("b = " + b + ", c = " + c);
10 }
11}
Rust
1fn main() {
2 let x = 5; // i32는 Copy
3 let y = x; // 복사됨 (move 아님)
4 println!("x = {x}, y = {y}"); // ✓ 둘 다 사용 가능
5
6 let b = true; // bool도 Copy
7 let c = b;
8 println!("b = {b}, c = {c}"); // ✓
9}

어떤 타입이 Copy인가

타입 그룹Copy?이유
정수 (i32, u64, ...)YES스택에 저장되는 고정 크기
실수 (f32, f64)YES스택에 저장되는 고정 크기
bool, charYES스택에 저장되는 고정 크기
Copy 타입만 포함한 튜플YES예: (i32, bool)
StringNO힙 포인터 포함 (이중 해제 위험)
Vec, HashMapNO힙 데이터 소유
파일 핸들, 네트워크 소켓NO복제 시 리소스 중복
💡 왜 String은 Copy가 아닌가
String은 내부적으로 (포인터, 길이, 용량) 구조체입니다. 단순 비트 복사를 하면 두 String이 같은 힙 메모리를 가리키게 되고, 둘 다 스코프 종료 시 해제하려 해서 이중 해제(double free) 오류가 납니다. Rust는 이를 컴파일 시점에 차단하기 위해 String을 move 동작으로 강제합니다.

자바의 primitive (int, boolean) vs Object (String, List) 구분과 유사하지만, Rust는 이걸 Copy trait으로 명시화합니다.

🧠 Move의 내부 동작 — 메모리로 이해하기

Java 개발자에게 가장 낯선 개념: Move = ptr/len/cap 3워드만 복사 + 원본을 컴파일러 단에서 사용 금지 표시. 런타임 비용이 아닌 컴파일러 체크입니다.

🌍 원문 (TRPL 4.1 - What is Ownership?)
after the line `let s2 = s1;`, Rust considers s1 as no longer valid. 
Therefore, Rust doesn't need to free anything when s1 goes out of scope.
원문 번역: let s2 = s1; 이후 Rust는 s1을 더 이상 유효하지 않은 것으로 취급한다. 따라서 s1이 스코프를 벗어나도 해제할 것이 없다.
let s1 = String::from("hello"); — 생성 직후 Stack s1 (String) ptr len: 5 cap: 5 Heap (index) (value) 0 'h' 1 'e' 2 'l' 3 'l' 4 'o' • 스택: ptr(포인터) + len(길이) + cap(용량) — 3워드 • 힙: 실제 문자 데이터 저장
let s2 = s1; — s1 invalidated (얕은 복사 + 원본 무효화) Stack s1 ⚠ INVALID ptr, len, cap 컴파일러가 사용 금지 표시 s2 (String) ptr len: 5 cap: 5 Heap "hello" (힙 데이터는 복사 안 됨) • ptr/len/cap 3워드만 복사 (얕은 복사) • s1은 컴파일 시점부터 사용 불가 → double free 방지
💡 Move가 필요한 근본 이유 — double free 방지
만약 s1과 s2가 같은 힙 데이터를 모두 소유한다면: 각자 스코프를 벗어날 때 둘 다 해제(drop) 시도 → 같은 메모리를 두 번 해제 (double free) → 메모리 오염, 보안 취약점.

Rust는 let s2 = s1; 순간 s1을 컴파일 시점부터 "사용 불가"로 표시합니다. 런타임 비용 없이 안전성 확보.
let s2 = s1.clone(); — 깊은 복사 (둘 다 유효) Stack s1 (String) ptr len: 5 cap: 5 s2 (String) ptr len: 5 cap: 5 Heap "hello" (원본) "hello" (복사본) ✓ 힙 데이터까지 전부 복사 → s1, s2 둘 다 독립적으로 사용 가능 (비용 큼)

🎯 실전: 어디서 Move가 발생하는가

Java 개발자가 Rust에서 가장 자주 당황하는 3가지 Move 상황입니다. 전부 실전 코드입니다.

예제 4-실전-1: HashMap 삽입 시 소유권 이동
Rust
1// HashMap에 String을 삽입하면 소유권이 이동
2use std::collections::HashMap;
3
4fn main() {
5 let key = String::from("language");
6 let val = String::from("Rust");
7
8 let mut map = HashMap::new();
9 map.insert(key, val); // key와 val의 소유권이 map으로 이동
10
11 // println!("{}", key); // ❌ E0382: key는 이미 move됨
12 println!("{:?}", map);
13}
💡 컬렉션의 공통 규칙
HashMap::insert, Vec::push, BTreeMap::insert컬렉션에 값을 넣으면 소유권이 컬렉션으로 이동합니다.

이후 원본 변수는 사용 불가. 값을 유지하려면 key.clone()으로 복사하거나 &key로 참조 전달을 고려 (참조는 섹션 5에서).
예제 4-실전-2: for 루프는 요소의 소유권을 가져간다
Rust
1// for v in vec 은 요소를 move해 가져옴 (into_iter)
2fn main() {
3 let names = vec![String::from("A"), String::from("B"), String::from("C")];
4
5 for name in names { // names의 각 요소가 move됨
6 println!("{}", name);
7 }
8
9 // println!("{:?}", names); // ❌ E0382: for 루프가 모두 move해감
10}
💡 for 루프의 숨겨진 Move
for name in names는 내부적으로 names.into_iter()를 호출해 각 요소를 소유권과 함께 가져옵니다. 루프 종료 후 names는 사용 불가.

해결책:
  • for name in &names — 불변 참조로 순회 (가장 흔한 선택)
  • for name in &mut names — 가변 참조로 순회 (수정 필요 시)
  • for name in names.iter() — 위와 동일
섹션 16 "클로저와 Iterator"에서 자세히 다룹니다.
예제 4-실전-3: 구조체의 부분 Move
Rust
1// 구조체 필드 일부만 move (부분 move)
2struct User {
3 name: String,
4 age: u32,
5}
6
7fn main() {
8 let u = User {
9 name: String::from("Alice"),
10 age: 30,
11 };
12
13 let name = u.name; // u.name만 move
14
15 // println!("{}", u.name); // ❌ u.name은 move됨
16 println!("u.age = {}", u.age); // ✓ age는 Copy라 여전히 접근 가능
17 println!("name = {}", name);
18}
💡 "부분 move" — Java에 없는 개념
구조체 필드 중 일부만 move되어도 나머지 Copy 필드는 여전히 사용 가능합니다.

규칙:
  • Move된 필드(u.name): 사용 불가 (E0382)
  • Copy 필드(u.age, i32): 계속 사용 가능
  • 구조체 전체(u): 부분 move 이후엔 전체로는 사용 불가
구조체를 반환하거나 전달해야 한다면 필드를 move하기 전에 계획 필요.
Part 5. 함수 호출과 소유권

함수에 값을 전달하는 것도 소유권 이동입니다. 함수 호출 = 매개변수로의 move.

예제 4-5: 함수에 전달 = move
Java
1public class Main {
2 static void printBook(String title) {
3 System.out.println(title);
4 }
5
6 public static void main(String[] args) {
7 String book = "Rust Book";
8 printBook(book); // 참조 전달
9 // Java는 book 여전히 사용 가능
10 System.out.println("book = " + book);
11 }
12}
Rust
1fn print_book(title: String) {
2 println!("{title}");
3} // <-- 여기서 title drop
4
5fn main() {
6 let book = String::from("Rust Book");
7 print_book(book); // book의 소유권이 함수로 이동!
8 // println!("{book}"); // ❌ 에러: book은 이미 이동됨
9 println!("끝");
10}
❌ 함수 전달 후 원본 사용 시도
예제 3-5의 Rust에서 주석을 풀면:
error[E0382]: borrow of moved value: `book` --> src/main.rs:8:17 | 6 | let book = String::from("Rust Book"); | ---- move occurs because `book` has type `String` 7 | print_book(book); | ---- value moved here 8 | println!("{book}"); | ^^^^^^ value borrowed here after move
예제 4-6: i32는 함수 전달해도 Copy (문제 없음)
Rust
1fn double(n: i32) -> i32 {
2 n * 2
3}
4
5fn main() {
6 let x = 10;
7 let y = double(x); // x는 Copy - 함수에 복사본 전달
8 println!("x = {x}, y = {y}"); // ✓ x 여전히 사용 가능
9}
🟢 출력
x = 10, y = 20 — x는 함수 전달 후에도 살아있음

소유권 돌려받기 - 반환값

예제 4-7: 함수가 소유권을 반환
Rust
1fn take_and_return(s: String) -> String {
2 println!("함수 안: {s}");
3 s // 소유권을 호출자에게 다시 반환
4}
5
6fn main() {
7 let book = String::from("Rust Book");
8 let book = take_and_return(book); // 받았다가 다시 받음
9 println!("main으로 돌아온: {book}"); // ✓ 사용 가능
10}
예제 4-8: 여러 값 반환 - 튜플 활용
Rust
1fn analyze(s: String) -> (String, usize) {
2 let len = s.len();
3 (s, len) // 원본 소유권 + 길이를 튜플로 반환
4}
5
6fn main() {
7 let book = String::from("Rust Book");
8 let (book, length) = analyze(book);
9 println!("{book} (길이: {length})");
10}
⚠️ 이런 식으로 매번 주고받는 건 불편하다
예제 3-7, 3-8처럼 함수마다 소유권을 주고받으면 코드가 장황해집니다. Rust는 이를 해결하기 위해 빌림(borrowing, &)이라는 개념을 제공합니다. 소유권은 그대로 두고 "잠깐 참조만" 허용하는 것이죠. 이건 섹션 5에서 자세히 다룹니다.
Part 6. 종합 - Book 대여 시스템

지금까지 배운 소유권 개념을 모두 적용해, 도서관에서 책을 대여하고 반납하는 시나리오를 구현합니다.

예제 3-9 (심화): 대여와 반납 — 소유권이 왔다 갔다
Java
1public class Main {
2 record Book(String title, int pages) {}
3
4 static void lend(Book b) {
5 System.out.println(" lending: "
6 + b.title() + " (" + b.pages() + "p)");
7 }
8
9 public static void main(String[] args) {
10 Book book = new Book("Rust Book", 300);
11
12 // Java는 참조 복사라 lend 후에도 book 유효
13 System.out.println("Library has: " + book.title());
14 lend(book);
15 System.out.println("After return: " + book.title());
16
17 lend(book); // 또 대여 가능
18 System.out.println("Re-check: " + book.title());
19 }
20}
Rust
1struct Book {
2 title: String,
3 pages: i32,
4}
5
6// 대여: 책의 소유권을 받음, 반납을 위해 되돌려줌
7fn lend(b: Book) -> Book {
8 println!(" 대여 중: {} ({}p)", b.title, b.pages);
9 b
10}
11
12fn main() {
13 let book = Book {
14 title: String::from("Rust Book"),
15 pages: 300,
16 };
17
18 println!("도서관 보유: {}", book.title);
19 let book = lend(book); // 대여 → 반납
20 println!("반납 완료: {}", book.title);
21
22 let book = lend(book); // 또 대여
23 println!("다시 반납: {}", book.title);
24}
💡 이 예제가 보여주는 것
  • Java: lend(book) 후에도 book 계속 사용 가능 (참조 복사)
  • Rust: lend(book)로 소유권 이동 → lendBook을 반환해서 let book = lend(book);으로 되찾아 옴
  • 이렇게 주고받기 방식으로 소유권을 관리할 수 있지만 불편합니다.
  • 실제로는 섹션 5의 빌림(&)을 쓰는 게 훨씬 편합니다 → fn lend(b: &Book) 이면 소유권 이동 없이 참조만 전달.
✅ 섹션 3 정리
  • Rust는 메모리를 소유권 규칙으로 관리한다 (규칙 3가지)
  • 값 대입 = move, 함수 호출 = move (단, Copy 타입은 복사)
  • 스코프 종료 = 자동 drop
  • 복제가 필요하면 .clone()을 명시적으로
  • "주고받기가 불편하다" → 다음 섹션 빌림(&)에서 해결

5. 빌림 (Borrowing) ⭐

섹션 4에서 "소유권을 주고받는 게 불편하다"는 말씀을 드렸습니다. 이를 해결하는 것이 빌림입니다. 참조(&)를 전달하면 소유권은 그대로 두고 값에만 접근할 수 있습니다.

🧠 참조의 본질 — 소유권 이동 없는 "빌림"

Java 개발자에게 참조는 "포인터처럼 동작하는 것"으로 익숙하지만, Rust의 &T소유권 이동 없이 원본을 가리키는 작은 주소입니다. 원본(소유자)은 계속 유효하고, 참조는 원본 수명 안에서만 살 수 있습니다.

참조(&T) = 원본을 가리키는 작은 주소 — 소유권 없음 Stack s: String (소유자) ptr len: 5 cap: 5 r: &String (참조) ptr Heap "hello" (원본 - s가 소유) ✓ r은 s의 힙 데이터를 가리키지만 소유하지 않음 — s는 계속 유효 • 실선: 소유자의 포인터 • 점선: 참조 (빌림)
💡 참조의 2대 규칙 (TRPL 4.2 The Rules of References)
  • 규칙 1: 임의 시점에, 하나의 가변 참조(&mut T) 또는 여러 개의 불변 참조(&T) — 둘 중 하나만 가능
  • 규칙 2: 참조는 항상 유효한 대상을 가리켜야 함 (댕글링 참조 차단)
이 규칙은 데이터 레이스 방지(규칙 1)와 use-after-free 방지(규칙 2)를 컴파일 시점에 보장합니다.
빌림 규칙 — 가변 XOR 불변 (동시 불가) ✓ 케이스 1: 불변 참조 여러 개 동시 가능 (읽기만 하므로 안전) let s = String::from("hi"); let r1 = &s; let r2 = &s; let r3 = &s; ✓ ✓ 케이스 2: 가변 참조 1개만 가능 (수정 중엔 단독) let mut s = String::from("hi"); let r = &mut s; ✓ (단, 다른 참조 불가) ❌ 케이스 3: 가변과 불변 혼용 불가 — E0502 let r1 = &s; let r2 = &mut s; println!("{}", r1); → 데이터 레이스 위험 (같은 데이터를 읽는 중에 수정 시도)
NLL (Non-Lexical Lifetimes) — 참조 수명은 "마지막 사용"까지 let mut s = String::from("hello"); let r = &s; // r 시작 println!("{}", r); // r 마지막 사용 (NLL: 수명 끝) s.push_str(", world"); // ✓ r이 이미 끝났으므로 가변 조작 OK println!("{}", s); // ✓ r 수명 ✓ 렉시컬(스코프 끝까지)이 아닌, 실제 마지막 사용 지점에서 수명 종료 • Rust 2018 Edition부터 도입 — 불필요한 에러를 대폭 줄임
💡 NLL이 왜 중요한가
NLL(Non-Lexical Lifetimes)이 없던 시절에는 참조의 수명이 스코프 끝까지였습니다. 그래서 &s를 잠깐 쓰고 나서도 같은 스코프에서 s를 수정할 수 없었습니다.

Rust 2018 Edition부터 NLL 도입 — 참조의 수명은 마지막 사용 지점까지로 축소. 훨씬 자연스러운 코드가 허용됩니다.

🚫 댕글링 참조 — 러스트가 컴파일 시점에 차단하는 악몽

C/C++에서 함수가 지역 변수의 주소를 반환하면 댕글링 포인터가 되어 런타임 크래시가 납니다. Rust는 이런 시도를 컴파일 시점에 거부합니다.

예제 5-실전-1: 댕글링 참조 반환 시도 — E0106
Rust
1// 지역 변수의 참조를 반환하려는 시도 - 컴파일 에러
2fn dangle() -> &String { // ❌ 라이프타임 없음 (E0106)
3 let s = String::from("hello");
4 &s // s는 함수 끝에서 drop
5}
6
7fn main() {
8 let r = dangle();
9 println!("{}", r);
10}
❌ E0106: missing lifetime specifier
함수가 참조를 반환하면서 이 참조의 수명이 어디서 왔는지 명시하지 않음. 컴파일러는 "반환된 참조가 얼마나 살 수 있는지 알 수 없다"고 거부.

이 예제의 경우 s는 함수 끝에서 drop되므로 참조가 어디에서 왔든 안전하지 않음. 섹션 6 라이프타임에서 자세히 다룹니다.
예제 5-실전-2: 해결 — 소유권을 돌려줘라
Rust
1// 해결: 소유권을 반환 (String 그대로)
2fn safe() -> String {
3 let s = String::from("hello");
4 s // ✓ 소유권 이동으로 반환
5}
6
7fn main() {
8 let r = safe();
9 println!("{}", r);
10}
✅ 가장 흔한 해결 패턴
지역 변수의 참조를 반환하지 말고 값(소유권)을 반환하세요. String을 반환하면 호출자가 소유권을 받게 되어 안전합니다.

"내부에서 만든 것은 소유권으로, 외부에서 받은 것은 참조로" — 간단한 원칙.
예제 5-실전-3: Iterator 중 원본 수정 시도 — E0502
Rust
1// 반복 중 원본 수정 시도
2fn main() {
3 let mut v = vec![1, 2, 3];
4
5 for x in &v { // v를 불변 참조로 순회
6 if *x == 2 {
7 // v.push(4); // ❌ E0502: 순회 중 수정 불가
8 }
9 }
10
11 v.push(4); // ✓ 순회 끝나고 수정 OK
12 println!("{:?}", v);
13}
❌ E0502: 순회 중 수정 불가
for x in &vv불변 참조를 유지합니다. 루프 안에서 v.push(4)가변 참조가 필요 — 규칙 1 위반.

만약 허용됐다면: 벡터가 재할당되면서 x가 무효한 메모리를 가리키게 됨(use-after-free). Java의 ConcurrentModificationException이 런타임 예외인 반면, Rust는 컴파일 시점 차단.

해결: 순회가 끝난 후 수정하거나, 필요한 작업을 별도 컬렉션에 모아 두고 나중에 일괄 적용.
예제 5-실전-4: TRPL 공식 first_word + NLL
Rust
1// 문자열 슬라이스 유지 중 원본 clear (TRPL 공식 시나리오)
2fn first_word(s: &String) -> &str {
3 let bytes = s.as_bytes();
4 for (i, &item) in bytes.iter().enumerate() {
5 if item == b' ' {
6 return &s[..i];
7 }
8 }
9 &s[..]
10}
11
12fn main() {
13 let mut s = String::from("hello world");
14 let word = first_word(&s); // s를 불변 참조
15
16 // s.clear(); // ❌ E0502: word가 s를 참조 중
17
18 println!("{}", word); // word 사용 끝
19 s.clear(); // ✓ NLL 덕분에 이제 OK
20}
💡 TRPL 공식 예제의 실전 의미
words 내부의 일부(슬라이스)를 가리킵니다. 따라서 s.clear()로 원본을 비우면 word가 무효해질 위험.

하지만 NLL 덕분에: wordprintln!에서 마지막 사용한 뒤로는 수명이 끝나고, 그 이후 s.clear()는 자유롭게 가능.

핵심: "참조 사용 → 원본 수정" 순서로 코드를 배치하면 자연스럽게 해결됩니다.
Part 1. 불변 참조 (&T)

& 기호로 읽기 전용 참조를 만듭니다. 원본 소유자는 그대로 유지됩니다.

예제 5-1: 참조 전달
Java
1public class Main {
2 static void printBook(String title) {
3 System.out.println("[Book] " + title);
4 }
5
6 public static void main(String[] args) {
7 String book = "Rust Book";
8 printBook(book);
9 System.out.println("Original: " + book);
10 }
11}
Rust
1fn print_book(title: &String) { // 참조만 빌림
2 println!("{title}");
3}
4
5fn main() {
6 let book = String::from("Rust Book");
7 print_book(&book); // 참조 전달
8 println!("원본 여전히 유효: {book}");
9}
💡 Java와의 차이
Java는 모든 객체 전달이 자동으로 참조 복사입니다. Rust는 참조 전달을 &로 명시해야 합니다. 명시되지 않으면 소유권 이동입니다.
예제 5-2: 여러 개의 불변 참조
Rust
1fn length(s: &String) -> usize { s.len() }
2
3fn main() {
4 let book = String::from("Rust");
5 let r1 = &book; // 참조 1
6 let r2 = &book; // 참조 2
7 println!("{}, {}, {}", r1, r2, length(&book));
8}
Part 2. 가변 참조 (&mut T)

값을 수정하려면 &mut 참조가 필요합니다. 단, 원본 변수도 mut이어야 합니다.

예제 5-3: 가변 참조로 수정
Rust
1fn add_subtitle(s: &mut String) {
2 s.push_str(" - 2nd Edition");
3}
4
5fn main() {
6 let mut book = String::from("Rust Book");
7 add_subtitle(&mut book); // 가변 참조로 전달
8 println!("{book}");
9}
Part 3. 빌림 규칙 ⭐

Rust의 가장 엄격한 규칙이자 메모리 안전성의 핵심입니다:

상황가능?이유
불변 참조 여러 개 (&T)OK읽기만 하므로 동시에 여럿 가능
가변 참조 1개 (&mut T)OK독점 수정 권한
불변 + 가변 동시NO읽는 도중 수정되면 위험
가변 참조 여러 개 동시NO데이터 레이스
예제 5-4: 불변 참조는 여러 개 OK
Rust
1fn main() {
2 let book = String::from("Rust Book");
3 let r1 = &book;
4 let r2 = &book;
5 let r3 = &book;
6 println!("{r1}, {r2}, {r3}"); // ✓ 불변 참조는 여러 개 OK
7}
예제 5-5: &와 &mut의 시간차 공존
Rust
1fn main() {
2 let mut book = String::from("Rust");
3 let r1 = &book; // 불변 참조
4 let r2 = &book; // 또 불변 참조 - OK
5 println!("{r1}, {r2}"); // 여기서 r1, r2의 생명 끝
6
7 let r3 = &mut book; // 이제 가변 참조 가능
8 r3.push_str(" Book");
9 println!("{r3}");
10}
⚠️ 에러 확인 예제 — 직접 실행해 컴파일 에러를 확인해보세요

아래 3개 예제는 모두 컴파일 에러가 나는 코드입니다. ▷ 실행 버튼을 눌러 러스트 컴파일러의 실제 에러 메시지를 확인해보세요. 에러 메시지를 읽는 연습이 가장 좋은 학습법입니다.

예제 5-5 Bad 1: 불변 참조 사용 중 가변 참조 시도
Rust
1fn main() {
2 let mut book = String::from("Rust");
3 let r1 = &book; // 불변 참조
4 let r2 = &book; // 또 불변 참조 - OK
5 let r3 = &mut book; // ❌ r1, r2가 아래에서 쓰이므로 &mut 불가
6 println!("{r1}, {r2}, {r3}");
7}
❌ 예상 에러: E0502
cannot borrow `book` as mutable because it is also borrowed as immutable
이유: r1, r2(불변 참조)가 마지막 println!에서 사용되기 전에 r3 = &mut book으로 가변 참조를 만들려고 함. 불변 참조가 살아있을 때 같은 값에 가변 참조를 만들 수 없습니다.
예제 5-5 Bad 2: 가변 참조 두 개 동시에
Rust
1fn main() {
2 let mut book = String::from("Rust");
3 let r1 = &mut book; // 가변 참조 1
4 let r2 = &mut book; // ❌ 가변 참조는 동시에 오직 하나
5 r1.push_str(" A");
6 r2.push_str(" B");
7}
❌ 예상 에러: E0499
cannot borrow `book` as mutable more than once at a time
이유: 가변 참조는 동시에 오직 하나만 존재할 수 있습니다. r1r2가 둘 다 book에 대한 가변 참조라서 충돌. 데이터 경쟁(data race) 방지를 위한 핵심 규칙입니다.
예제 5-5 Bad 3: 가변 참조 중 불변 참조 시도
Rust
1fn main() {
2 let mut book = String::from("Rust");
3 let r1 = &mut book; // 가변 참조
4 let r2 = &book; // ❌ 가변 참조 중에는 불변 참조도 불가
5 r1.push_str(" Book");
6 println!("{r2}");
7}
❌ 예상 에러: E0502
cannot borrow `book` as immutable because it is also borrowed as mutable
이유: 가변 참조(r1)가 살아있을 때는 불변 참조조차 만들 수 없습니다. 가변 참조는 "유일한 접근자"를 보장하므로, 다른 어떤 참조와도 공존 불가.
💡 빌림 규칙 요약 (TRPL 4.2)
어느 시점에서든 다음 중 하나만 가능합니다:
  • 불변 참조(&T) 여러 개
  • 가변 참조(&mut T) 단 하나
섞어서 쓸 수는 없습니다. 하지만 NLL(Non-Lexical Lifetimes) 덕분에 참조의 마지막 사용 지점 이후에는 새 참조를 만들 수 있습니다 (예제 5-5 참조).
❌ 규칙 위반 시 - 실제 컴파일러 에러
한 변수에 가변 참조 2개 만들기:
error[E0499]: cannot borrow `book` as mutable more than once at a time --> src/main.rs:4:14 | 3 | let r1 = &mut book; | --------- first mutable borrow occurs here 4 | let r2 = &mut book; | ^^^^^^^^^ second mutable borrow occurs here
Part 4. 슬라이스 (참조의 일부)

슬라이스는 컬렉션의 일부분만 참조하는 참조입니다. 소유권 없이 일부분 접근만 합니다.

예제 5-6: 문자열 슬라이스
Rust
1fn main() {
2 let book = String::from("Rust Book");
3 let first = &book[0..4]; // 0~3번 문자
4 let second = &book[5..9]; // 5~8번 문자
5 println!("{first} / {second}");
6}
예제 5-7: &str 파라미터 (더 유연)
Rust
1fn first_word(s: &str) -> &str {
2 let bytes = s.as_bytes();
3 for (i, &b) in bytes.iter().enumerate() {
4 if b == b' ' { return &s[0..i]; }
5 }
6 &s[..]
7}
8
9fn main() {
10 let book = String::from("Rust Book");
11 println!("첫 단어: {}", first_word(&book));
12}
💡 &str vs &String — 미리 엿보기 (Deref Coercion)
함수 인자가 &str인데 &String을 넘겨도 왜 될까요? → Rust에는 'Deref coercion(역참조 강제 변환)'이라는 기능이 있어 &String&str자동 변환해줍니다.

지금은 이렇게 이해하면 충분합니다:
  • &str를 받는 함수는 String, 문자열 리터럴 둘 다 받을 수 있음
  • 컴파일 타임에 변환 → 런타임 오버헤드 없음
  • 그래서 Rust 관례는 &str을 선호
📚 이게 작동하는 원리(Deref trait)는 섹션 9 「스마트 포인터」에서 정식으로 배웁니다. 지금은 "이런 게 있어서 &String&str처럼 쓸 수 있다"만 기억하시면 됩니다.
예제 5-D1: Deref Coercion 시연 (&String → &str)
Rust
1// Deref coercion - 컴파일러가 자동으로 &String을 &str로 변환
2fn take_str(s: &str) {
3 println!("{}", s);
4}
5
6fn main() {
7 let owned = String::from("Rust");
8
9 // 아래 세 줄 모두 정상 작동
10 take_str(&owned); // ✓ &String → &str (자동 변환)
11 take_str(&(*owned)); // ✓ 수동으로 역참조 (동등)
12 take_str("hello"); // ✓ &str 리터럴 그대로
13}
예제 5-D2: Vec도 같은 원리 (&Vec<i32> → &[i32])
Rust
1// Vec<T>도 같은 원리 - &Vec<i32>가 &[i32]로 자동 변환
2fn sum(nums: &[i32]) -> i32 {
3 nums.iter().sum()
4}
5
6fn main() {
7 let v = vec![1, 2, 3];
8
9 println!("vec 합: {}", sum(&v)); // ✓ &Vec<i32> → &[i32]
10 println!("배열 합: {}", sum(&[4, 5, 6])); // ✓ 배열 슬라이스 그대로
11}
💡 Deref coercion 없을 때와 비교
만약 이 편의 기능이 없다면 &String&str 받는 함수에 넘길 때 이렇게 슬라이스를 명시해야 합니다:
take_str(&owned[..]);   // 슬라이스 명시 (수동)
Deref coercion 덕분에 take_str(&owned)만 써도 됩니다. 코드가 훨씬 간결해집니다.
Part 5. Book 대여 시스템 v2 (심화)

섹션 4의 대여 시스템을 빌림으로 개선합니다. 소유권 주고받기 없이 참조만으로 처리됩니다.

예제 5-8: 참조 기반 대여
Java
1public class Main {
2 static class Book {
3 String title;
4 boolean available;
5 Book(String t, boolean a) { title = t; available = a; }
6 }
7
8 static void read(Book b) {
9 System.out.println(" Reading: " + b.title);
10 }
11
12 static void borrowBook(Book b) {
13 if (b.available) {
14 b.available = false;
15 System.out.println(" borrow complete: " + b.title);
16 }
17 }
18
19 public static void main(String[] args) {
20 Book book = new Book("Rust Book", true);
21 read(book);
22 borrowBook(book);
23 read(book);
24 System.out.println("State: borrowed = " + !book.available);
25 }
26}
Rust
1struct Book {
2 title: String,
3 available: bool,
4}
5
6// 참조만 빌림 - 소유권 이동 없음
7fn read(b: &Book) {
8 println!(" 읽는 중: {}", b.title);
9}
10
11fn borrow_book(b: &mut Book) {
12 if b.available {
13 b.available = false;
14 println!(" 대여 완료: {}", b.title);
15 }
16}
17
18fn main() {
19 let mut book = Book {
20 title: String::from("Rust Book"),
21 available: true,
22 };
23
24 read(&book); // 불변 빌림
25 borrow_book(&mut book); // 가변 빌림
26 read(&book); // 또 불변 빌림
27 println!("상태: 대여중 = {}", !book.available);
28}
✅ 섹션 4 정리
  • &T 불변 참조: 여러 개 동시 가능
  • &mut T 가변 참조: 한 번에 하나만
  • 불변과 가변 동시 공존 불가 (시간차로 분리하면 OK)
  • 슬라이스 &s[a..b]는 일부분만 참조
  • 참조가 원본보다 오래 살 수는 없다 → 다음 섹션 라이프타임

6. 라이프타임 (Lifetimes) ⭐

라이프타임은 참조가 유효한 범위를 컴파일 시점에 추적하는 시스템입니다. 이 섹션은 실제로 마주칠 수 있는 10가지 문제 상황과 해결 방법을 다룹니다.

💡 라이프타임의 핵심
  • 라이프타임은 메모리를 바꾸지 않습니다 — 컴파일러를 위한 검증 정보일 뿐
  • 'a 같은 표기는 "이들은 같은 수명을 공유한다"는 선언
  • 각 문제 상황마다 그림 → 코드 → 에러 → 해결 그림 → 해결 코드 순으로 봅니다
에러 코드상황해결 방법
E0597참조가 원본보다 오래 삼스코프 조정
E0106라이프타임 명시 누락'a 명시 or 소유값 반환
E0502불변+가변 참조 공존수명 분리
E0515지역 변수 참조 반환소유값 반환
시나리오 1. 댕글링 참조

🔴 문제 상황

내부 스코프의 값을 바깥에서 참조하려고 하면, 값은 drop되고 참조만 남습니다.

문제: 내부 스코프의 x가 drop된 후 r 사용 main { 내부 스코프 } x drop! r ↑ dangling 구간 E0597: `x` does not live long enough
예제 6-1a: 문제 코드
Rust
1fn main() {
2 let r;
3 {
4 let x = 5;
5 r = &x; // ❌ x는 내부 스코프 끝에서 drop
6 }
7 println!("{}", r); // r이 죽은 x를 가리킴
8}
❌ 컴파일러 에러
error[E0597]: `x` does not live long enough --> 01_bad.rs:5:13 | 4 | let x = 5; | - binding `x` declared here 5 | r = &x; // ❌ x는 내부 스코프 끝에서 drop | ^^ borrowed value does not live long enough 6 | } | - `x` dropped here while still borrowed 7 | println!("{}", r); // r이 죽은 x를 가리킴

🟢 해결 방법

원본 값을 참조와 같은 스코프 이상에 선언합니다.

해결: x를 main 스코프에 선언 main (x와 r 같은 수명) x r ✓ r의 수명이 x 안에 완전히 포함됨
예제 6-1b: 해결 코드
Rust
1fn main() {
2 let x = 5; // x를 main 스코프에 선언
3 let r = &x;
4 println!("{}", r); // ✓ x가 살아있는 동안 r 사용
5}
시나리오 2. 지역 변수 참조 반환

🔴 문제 상황

함수 내부에서 만든 값의 참조를 반환하면, 함수가 끝나면서 값이 drop됩니다.

문제: 함수 내부 String의 참조 반환 fn dangle() 힙 데이터 s: "hello" return &s 호출자 참조할 대상 없음 (dangling) ← 함수 종료 시 s drop! 반환된 참조가 가리킬 데이터가 사라짐 E0106: missing lifetime specifier
예제 6-2a: 문제 코드
Rust
1fn dangle() -> &String {
2 let s = String::from("hello");
3 &s // ❌ s는 함수 종료 시 drop
4}
5
6fn main() {
7 let reference = dangle();
8 println!("{}", reference);
9}
❌ 컴파일러 에러
error[E0106]: missing lifetime specifier --> 02_bad.rs:1:16 | 1 | fn dangle() -> &String { | ^ expected named lifetime parameter | = help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from help: consider using the `'static` lifetime | 1 | fn dangle() -> &'static String {

🟢 해결 방법

참조 대신 소유값(String 등)을 반환해 소유권을 호출자에게 넘깁니다.

해결: 참조 대신 소유값(String) 반환 fn no_dangle() 힙 데이터 s: "hello" move (소유권 이동) 호출자 owned ✓ 데이터가 호출자로 이동 — 안전 호출자가 drop 책임을 가짐
예제 6-2b: 해결 코드
Rust
1// 해결: 참조 대신 소유값 반환
2fn no_dangle() -> String {
3 let s = String::from("hello");
4 s // ✓ 소유권을 호출자에게 이동
5}
6
7fn main() {
8 let owned = no_dangle();
9 println!("{}", owned);
10}
시나리오 3. 라이프타임 명시 누락

🔴 문제 상황

여러 참조 중 어느 것에서 반환되는지 컴파일러가 모를 때 에러가 납니다.

문제: 반환 참조의 수명이 불명확 fn longest(s1: &str, s2: &str) -> &str ??? ??? 반환은 s1? s2? 컴파일러: "반환 참조가 어느 입력에서 오는지 알 수 없음" E0106: missing lifetime specifier
예제 6-3a: 문제 코드
Rust
1// ❌ 라이프타임 명시 없음
2fn longest(s1: &str, s2: &str) -> &str {
3 if s1.len() > s2.len() { s1 } else { s2 }
4}
5
6fn main() {
7 let s1 = String::from("long string");
8 let s2 = String::from("short");
9 println!("{}", longest(&s1, &s2));
10}
❌ 컴파일러 에러
error[E0106]: missing lifetime specifier --> 03_bad.rs:2:35 | 2 | fn longest(s1: &str, s2: &str) -> &str { | ---- ---- ^ expected named lifetime parameter | = help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `s1` or `s2` help: consider introducing a named lifetime parameter | 2 | fn longest<'a>(s1: &'a str, s2: &'a str) -> &'a str {

🟢 해결 방법

'a로 두 입력과 반환이 같은 수명임을 명시합니다.

해결: 'a로 두 입력과 반환을 같은 수명으로 연결 fn longest<'a>(s1: &'a str, s2: &'a str) -> &'a str 세 위치가 같은 'a로 연결됨 ✓ 반환 수명 = 두 입력 중 더 짧은 쪽의 수명 컴파일러가 수명 관계를 검증 가능
예제 6-3b: 해결 코드
Rust
1// ✓ 'a로 라이프타임 명시
2fn longest<'a>(s1: &'a str, s2: &'a str) -> &'a str {
3 if s1.len() > s2.len() { s1 } else { s2 }
4}
5
6fn main() {
7 let s1 = String::from("long string");
8 let s2 = String::from("short");
9 println!("{}", longest(&s1, &s2));
10}
📚 라이프타임 완전 정복 — 기초 개념부터 패턴까지
📖 Part A: 라이프타임 기초 개념 (먼저 확실히!)
'a, 'b 같은 표기를 이해하려면 먼저 라이프타임의 기본 개념을 확실히 알아야 합니다. Part A에서는 공식 문서(TRPL 10.3 + Rust By Example)의 정확한 정의를 5가지 포인트로 정리합니다.
A-1️⃣ 라이프타임이란 무엇인가? (공식 정의)
🌍 공식 원문 (TRPL 10.3)
every reference in Rust has a lifetime, 
which is the scope for which that reference is valid.
원문 번역
러스트의 모든 참조는 라이프타임을 가진다. 
라이프타임이란 그 참조가 유효한 스코프이다.
💡 핵심: 'a"참조 자체가 유효한 구간"입니다. (원본 데이터의 수명이 아닙니다!)
변수의 수명(원본) vs 참조의 수명(빌림) — 다른 개념! 시간 → x (변수) 생성 drop r (참조) let r = &x 마지막 사용 ✓ r의 수명 ⊆ x의 수명 핵심: 참조는 원본이 살아있는 동안만 유효
A-2️⃣ 두 가지 수명 — "변수의 수명" vs "참조의 수명"
🌍 공식 원문 (Rust By Example)
a variable's lifetime begins when it is created and ends when it is destroyed. 
While lifetimes and scopes are often referred to together, they are not the same.
원문 번역: 변수의 라이프타임은 생성될 때 시작되고 소멸될 때 끝난다. 라이프타임과 스코프는 자주 같이 언급되지만 같은 것은 아니다.
변수의 수명 (원본 데이터)참조의 수명 (빌림)
시작변수 생성 시참조 선언 시
변수 drop 시참조 마지막 사용 후
예시let x = 5;let r = &x;
'a 표기직접 표기 안 함&'a T 형태
예제 A-1: 기본 참조 — 두 수명 관찰
Rust
1fn main() {
2 let x = 5; // x 생성 (변수 수명 시작)
3 let r = &x; // r은 x를 빌림 (참조 수명 시작)
4 println!("x = {}, r = {}", x, r);
5 // r의 마지막 사용 후 참조 수명 끝
6 // x는 main 끝까지 살아있음
7}
A-3️⃣ 참조는 원본보다 먼저 끝나야 한다 — 규칙 위반 시 댕글링 ⚠️
🌍 공식 원문 (Rust By Example)
the borrow is valid as long as it ends before the lender is destroyed.
원문 번역: 빌림은 빌림을 주는 대상(원본)이 소멸되기 전에 끝나야 유효하다.

핵심 규칙: 참조의 수명 ≤ 원본의 수명
위반하면? 댕글링 참조(dangling reference) 발생 → 러스트가 컴파일 에러로 막음
댕글링 참조 — 참조가 원본보다 오래 살려고 하면? 시간 → x (원본) 생성 drop (❗) r (참조) let r = &x ❌ r 사용 시도 E0597: x가 먼저 사라져서 r은 해제된 메모리를 가리킴 — 이것을 댕글링 참조라 함
예제 A-2: 댕글링 참조 시도 — E0597 에러 확인
Rust
1fn main() {
2 let r;
3 {
4 let x = 5;
5 r = &x; // x를 빌림
6 } // ❌ x drop → r은 무효한 참조
7 println!("{}", r); // E0597 에러
8}
❌ E0597: `x` does not live long enough
x는 내부 스코프 끝에서 drop되는데, r은 외부 스코프까지 살아남습니다. r이 이미 drop된 메모리를 가리키는 상황 — 댕글링 참조. 러스트 컴파일러가 미리 차단해줍니다.
A-4️⃣ NLL — 참조 수명은 "마지막 사용"까지
NLL (Non-Lexical Lifetimes): 참조의 수명은 스코프 끝까지가 아니라 마지막으로 사용된 지점까지입니다. 이 덕분에 유연하게 코드를 쓸 수 있습니다.
예제 A-3: NLL 확인 — 참조 마지막 사용 후 원본 수정
Rust
1fn main() {
2 let mut v = vec![1, 2, 3];
3 let r = &v[0];
4 println!("r = {}", r); // r의 마지막 사용 — 참조 수명 여기서 끝
5
6 v.push(4); // ✓ NLL 덕분에 OK (r은 이제 무효지만 안 쓰므로)
7 println!("v = {:?}", v);
8}
✅ NLL 효과
let r = &v[0]; println!("{}", r);에서 r의 수명은 println!의 마지막 사용까지. 그 이후엔 v를 수정해도 문제없음. (NLL 없이 어휘적 스코프 끝까지였다면 수정 불가였을 것)
A-5️⃣ 라이프타임의 본질 — 스코프의 이름 (The Rustonomicon)
🌍 공식 원문 (Nomicon - Lifetimes)
Lifetimes are effectively just names for scopes somewhere in the program. 
Each reference, and anything that contains a reference, 
is tagged with a lifetime specifying the scope it's valid for.
원문 번역
라이프타임은 본질적으로 프로그램 내 스코프의 이름일 뿐이다.
모든 참조(그리고 참조를 포함하는 모든 것)는 
자신이 유효한 스코프를 지정하는 라이프타임으로 태그된다.
💡 핵심 인사이트: 'a는 어렵고 신비로운 것이 아니라 코드 속 어떤 스코프(블록)의 이름일 뿐입니다. 모든 참조는 "나는 이 스코프 안에서만 유효하다"는 라이프타임 태그를 갖습니다.
A-5️⃣-① 각 let은 암묵적 스코프를 만든다
🌍 공식 원문
each let statement implicitly introduces a scope.
원문 번역: 각 let 문은 암묵적으로 스코프를 만든다.

평소에는 보이지 않지만, 컴파일러 내부에서는 let마다 새 스코프가 생긴다고 봅니다. 이 덕분에 참조들 사이의 수명 관계가 정확히 추적됩니다.
⚠️ 주의 — 다음에 나오는 'a: { 문법은 실제 Rust가 아님
Nomicon 원문에도 // NOTE: `'a: {` and `&'b x` is not valid syntax!라고 명시되어 있습니다.
이 문법은 컴파일러 내부 동작을 시각화하기 위한 설명용 가상 문법일 뿐, 실제 코드로 작성하면 컴파일 에러납니다. 개념 이해를 위한 "desugar(설탕 벗긴 형태)"로만 활용하세요.
A-5️⃣-② Desugar 예시 1 — 단순 참조 체인
아래 원본 코드를 컴파일러가 내부적으로 어떻게 보는지 desugar로 표현합니다.

👇 원본 코드 (실제 Rust, 실행 가능)
원본 코드
Rust
1fn main() {
2 let x = 0;
3 let y = &x;
4 let z = &y;
5 println!("x={}, y={}, z={}", x, y, z);
6}
👇 컴파일러 내부 관점 (가상 문법 desugar)
🌍 공식 원문 (Nomicon)
The borrow checker always tries to minimize the extent of a lifetime, 
so it will likely desugar to the following...
원문 번역: 빌림 검사기는 항상 라이프타임의 범위를 최소화하려 한다.
Desugar 결과 (컴파일러 내부 관점, 실행 불가)
Rust (설명용 가상 문법)
1// NOTE: 이 문법은 실제 Rust 문법이 아닙니다 (설명용)
2// Nomicon 공식 예시
3'a: {
4 let x: i32 = 0;
5 'b: {
6 // lifetime used is 'b because that's good enough.
7 let y: &'b i32 = &'b x;
8 'c: {
9 // ditto on 'c
10 let z: &'c &'b i32 = &'c y;
11 }
12 }
13}
각 let이 암묵적 스코프를 만든다 — 'a ⊃ 'b ⊃ 'c 중첩 구조 'a 스코프 { let x: i32 = 0; // x는 'a 수명 'b 스코프 { let y: &'b i32 = &'b x; // y는 'b 수명, x를 'b 동안 빌림 'c 스코프 { let z: &'c &'b i32 = &'c y; } } } 포함 관계: 'c ⊂ 'b ⊂ 'a | 참조는 항상 자신이 가리키는 원본의 수명 안에 포함
✅ 읽는 방법
  • 'a 스코프: x가 사는 구역 (가장 바깥)
  • 'b 스코프: y = &x가 사는 구역 ('a 안)
  • 'c 스코프: z = &y가 사는 구역 ('b 안)
포함 관계: 'c ⊂ 'b ⊂ 'a
참조는 항상 자신이 가리키는 원본의 수명 안에 포함됩니다 (그래야 댕글링이 없음).
A-5️⃣-③ Desugar 예시 2 — 외부 스코프로 참조 전달
참조를 외부 스코프의 변수에 대입하면, 컴파일러는 해당 참조의 수명을 외부 스코프의 크기로 확장합니다.

🌍 공식 원문 (Nomicon)
Actually passing references to outer scopes will cause Rust to 
infer a larger lifetime.
원문 번역: 참조를 외부 스코프로 전달하면, 러스트는 더 큰 수명을 추론한다.
원본 코드
Rust
1fn main() {
2 let x = 0;
3 let z;
4 let y = &x;
5 z = y;
6 println!("z = {}", z);
7}
Desugar 결과 (가상 문법, 실행 불가)
Rust (설명용 가상 문법)
1// NOTE: 이 문법은 실제 Rust 문법이 아닙니다 (설명용)
2// 참조가 외부 스코프로 전달되면 수명이 확장됨
3'a: {
4 let x: i32 = 0;
5 'b: {
6 let z: &'b i32;
7 'c: {
8 // Must use 'b here because this reference is
9 // being passed to that scope.
10 let y: &'b i32 = &'b x;
11 z = y;
12 }
13 }
14}
✅ 핵심 포인트
y = &x'c 스코프에서 만들어졌지만, z = y외부 'b 스코프로 전달되었으므로 컴파일러는 y의 수명을 'c가 아닌 'b로 확장합니다.

원본 x'a에 살지만, 빌림 검사기가 "최소 크기 원칙"에 따라 'b면 충분하다고 판단한 결과입니다.
📚 공식 문서
더 깊이 알고 싶다면 The Rustonomicon — Lifetimes 챕터를 참고하세요. 'a: { } 설명용 문법은 이 문서의 공식 표기법입니다.
A-6️⃣ 이제 Part B로 — 함수에서 'a가 어떻게 쓰이는가?
기초 개념을 확실히 알았으니, 이제 함수 시그니처에서 'a가 하는 역할longest 예제로 깊이 살펴봅니다 (Part B).

Part A 요약:
  1. 모든 참조는 유효한 스코프(수명)를 가진다
  2. 변수 수명과 참조 수명은 다른 개념
  3. 참조 수명 ≤ 원본 수명 (위반 시 댕글링)
  4. 참조 수명은 마지막 사용까지 (NLL)
📘 Part B: longest 예제 심층 분석

예제 6-3의 longest 함수는 Rust 공식 문서 TRPL 10.3에서 라이프타임을 도입하는 핵심 예제입니다. 이 개념을 정확히 이해하면 나머지 시나리오들이 훨씬 쉬워집니다.

1️⃣ 컴파일러의 역할
당신이 러스트 컴파일러라고 상상해보세요. 임무는:
  • 참조가 항상 유효하도록 보장
  • 참조(reference)보다 그 참조가 가리키는 데이터가 먼저 사라지면 안 됨
이를 위해 함수 시그니처만 보고도 반환 참조의 수명이 어느 입력에서 왔는지 알아야 합니다.
2️⃣ 문제의 본질
fn longest(s1: &str, s2: &str) -> &str에서 반환되는 참조가 s1인지 s2인지 컴파일러는 알 수 없습니다. 실제로 우리도 모릅니다if 조건에 따라 런타임에 결정되니까요.

공식 문서의 정확한 표현:
help: this function's return type contains a borrowed value,
      but the signature does not say whether it is borrowed from `s1` or `s2`
3️⃣ 왜 컴파일러가 반환 수명을 알아야 하나?
호출자 코드의 안전성을 검증하기 위해서입니다. 함수 시그니처만 보고도 호출 규칙을 결정할 수 있어야 합니다. 아래 예시를 직접 실행해보세요:
예제 6-3 심화: 라이프타임이 왜 필요한지 실감하기
Rust
1// 이 코드는 컴파일 에러가 납니다.
2// ▷ 실행 버튼으로 직접 러스트 컴파일러의 반응을 확인해보세요.
3fn longest<'a>(s1: &'a str, s2: &'a str) -> &'a str {
4 if s1.len() > s2.len() { s1 } else { s2 }
5}
6
7fn main() {
8 let s1 = String::from("long string");
9 let result;
10 {
11 let s2 = String::from("short");
12 result = longest(&s1, &s2); // ← 반환된 참조는 얼마나 살 수 있는가?
13 } // s2 drop됨
14 println!("{}", result); // ← 여기서 result 써도 안전한가?
15}
❌ 예상 에러: E0597
`s2` does not live long enough
longests2를 반환했을 수도 있는데, s2는 내부 스코프 끝에서 drop됩니다. 그러면 result이미 drop된 메모리를 가리키게 됩니다. 컴파일러가 이를 미리 차단하기 위해 'a로 수명 관계를 명시하라고 요구한 것입니다.
4️⃣ Lifetime Elision Rules — 왜 자동 추론이 안 되나?
러스트는 일부 경우 라이프타임을 자동 추론해줍니다. 생략 규칙 3가지:
  • 규칙 1: 각 입력 참조는 각자 다른 라이프타임을 받음
    fn foo(x: &str, y: &str)fn foo<'a, 'b>(x: &'a str, y: &'b str)
  • 규칙 2: 입력 라이프타임이 하나뿐이면 → 그것이 모든 출력에 적용
    fn first_word(s: &str) -> &strfn first_word<'a>(s: &'a str) -> &'a str
  • 규칙 3: &self&mut self가 있으면 → self의 라이프타임이 출력에 적용 (메서드에만 해당)
longest는?
  • 규칙 1: 적용 → fn longest<'a, 'b>(s1: &'a str, s2: &'b str) -> &? str
  • 규칙 2: ❌ 입력이 2개라 적용 불가
  • 규칙 3: ❌ self가 없으니 적용 불가
  • 결론: 반환값 라이프타임을 결정할 수 없음 → E0106
5️⃣ 'a를 어디에, 왜 붙이는가? — 문법 상세
'a라이프타임 매개변수(Lifetime Parameter)입니다. 타입 매개변수 <T>와 같은 제네릭의 일종입니다.

① 문법 구조 — 3곳에 붙여야 함
fn longest<'a>(s1: &'a str, s2: &'a str) -> &'a str
          ↑          ↑            ↑             ↑
      ① 선언     ② 입력 1      ③ 입력 2        ④ 반환
  • ① 함수명 뒤 <'a>: 라이프타임 "이름" 선언
  • ② ③ 입력 참조 앞 'a: "이 참조는 'a에 속함"
  • ④ 반환 참조 앞 'a: "반환도 'a에 속함"
② 왜 '(작은따옴표)로 시작?
문법 규칙: 라이프타임 이름은 '로 시작. 타입 매개변수 T와 시각적으로 구분.

③ 이름 규칙
  • 관례: 'a, 'b, 'c...
  • 의미 있는 이름: 'input, 'longer 가능
  • 특수: 'static (프로그램 전체 수명)
④ 여러 개 선언 가능
fn foo<'a, 'b>(x: &'a str, y: &'b str) -> &'a str
// 'a와 'b가 다른 수명 — 시나리오 5에서 활용
⑤ 제네릭 <T>와 비교
비교제네릭 타입라이프타임
선언<T><'a>
사용x: Ts: &'a str
의미"어떤 타입이든 OK""어떤 수명이든 OK"
결정 시점호출 시 (타입 추론)호출 시 (수명 추론)
6️⃣ 'a가 정확히 뜻하는 것 — 공식 문서 직접 인용
🌍 공식 원문 (TRPL 10.3)
for some lifetime 'a, the function takes two parameters, 
both of which are string slices that live at least as long as lifetime 'a. 
The function signature also tells Rust that the string slice returned 
from the function will live at least as long as lifetime 'a.
원문 번역
'a라는 수명을 하나 정해두자.
이 함수의 두 인자(s1, s2)는 최소한 'a 구간 동안은 살아있어야 한다
 (더 오래 살아도 OK).
이 함수가 돌려주는 반환값도 최소한 'a 구간 동안은 살아있도록 보장된다.
💡 제네릭으로서의 'a
'a함수 정의 시점에 구체적 값이 없습니다. 함수는 "어떤 수명 'a가 들어와도 내 약속만 지키면 OK"라고 선언합니다. 구체적 값은 호출 시점에 결정됩니다 (다음 포인트 참조).
⚠️ 오해 금지 — 공식 문서 원문 인용
🌍 공식 원문 (TRPL 10.3)
Remember, when we specify the lifetime parameters in this function signature, 
we're not changing the lifetimes of any values passed in or returned. 
Rather, we're specifying that the borrow checker should reject 
any values that don't adhere to these constraints.
원문 번역
"함수 시그니처에 라이프타임 매개변수를 명시한다고 해서, 전달되거나 반환되는 값의 수명을 바꾸는 것이 아닙니다. 제약에 맞지 않는 값을 빌림 검사기가 거부하도록 지정할 뿐입니다."

정리하면: 'a메모리 수명을 실제로 바꾸지 않습니다. 단지 컴파일러에게 "이 수명 약속을 지켜서 검증해줘"라고 알려주는 역할만 합니다.
7️⃣ 'a의 구체적인 값은 호출 시점에 결정된다
🌍 공식 원문 (TRPL 10.3)
When concrete references are passed to longest, 
the concrete lifetime that gets substituted for 'a is 
the part of the scope of x that overlaps with the scope of y. 
Since scopes always nest, another way to say this is that 
the generic lifetime 'a will get the concrete lifetime 
equal to the smaller of the lifetimes of x and y.
원문 번역
longest 함수가 실제로 호출될 때 'a의 구체적 값은 이렇게 결정된다:
 x가 살아있는 시간 구간과 y가 살아있는 시간 구간이 
 '서로 겹치는 부분'.
러스트에서 스코프는 항상 중첩되므로, 다르게 표현하면 
 'a는 'x와 y 중 더 짧은 쪽'의 수명과 같다.
💡 쉽게 이해하기 (원칙 + 실용)
관점설명
📐 원칙'a = x와 y가 동시에 살아있는 기간 (두 스코프의 겹치는 부분)
🎯 실용'a = x와 y 중 더 짧은 쪽의 수명 (먼저 끝나는 쪽)
🔗 이유러스트 스코프는 항상 중첩되므로 두 표현이 같음
📐 스코프 관계 다이어그램
케이스 1: 중첩 (s2가 s1 안에 포함 — 가장 흔함)
  s1: ────────────────────   ← 더 긴 수명
  s2:     ──────             ← 더 짧은 수명 (s1 안)
  'a:     ──────             ← 겹치는 부분 = s2의 전체 (더 짧은 쪽)

케이스 2: 동일한 수명
  s1: ───────────────
  s2: ───────────────
  'a: ───────────────        ← 둘이 같으므로 어느 쪽이든 동일

케이스 3: 완전 분리 (함수에 동시 전달 불가)
  s1: ────
  s2:         ────
  겹침: 없음                   ← 한 시점에 둘 다 유효하지 않아 호출 불가
📝 실제 코드로 확인해보기
케이스 A와 케이스 B를 비교하며 'a의 결정 과정을 실감해보세요. 케이스 A(같은 스코프)는 컴파일 성공, 케이스 B(s2가 내부 스코프)는 E0597 에러.

케이스 B 코드는 위의 '예제 6-3 심화'에 이미 있습니다.
케이스 A: 두 변수 같은 스코프 (예제 6-3 심화와 비교)
Rust
1// 케이스 A: 같은 스코프 - 'a = main 전체, OK
2fn longest<'a>(s1: &'a str, s2: &'a str) -> &'a str {
3 if s1.len() > s2.len() { s1 } else { s2 }
4}
5
6fn main() {
7 let s1 = String::from("long");
8 let s2 = String::from("short");
9 let r = longest(&s1, &s2); // 'a = main 전체
10 println!("{}", r); // ✓ OK
11}
✅ 케이스 A 결과
s1s2같은 스코프에 있으므로 둘 다 main 끝까지 유효. 'a = main 전체가 되어 rmain 끝까지 안전하게 사용 가능. 컴파일 성공 → "short" 출력.

반면 예제 6-3 심화(위)는 s2가 내부 스코프에 있어 'a가 짧아짐 → r'a 밖에서 쓰려 하면 E0597.
8️⃣ "왜 러스트는 자동으로 추론 안 해주나?"
"컴파일러가 if/else 본문을 보면 둘 다 반환할 수 있으니 자동으로 min(s1, s2)로 처리하면 되지 않나?" 라고 생각할 수 있습니다. 공식 문서의 답변:
함수를 정의할 때는 어떤 값이 들어올지 모릅니다. ifelse냐는 런타임에 결정되므로, 컴파일러는 시그니처만 보고 호출자 코드를 검증해야 합니다.
러스트의 철학: 함수 시그니처 = 계약
  • 시그니처만 보면 호출 규칙이 전부 드러나야 함
  • 본문을 바꿔도 호출자는 영향 받지 않아야 함
  • 그래서 프로그래머가 'a로 관계를 명시
9️⃣ 비유로 이해하기
시그니처현실 비유
fn longest(s1, s2) -> &str "뭔가를 돌려줄게" (뭔지 안 말함 → 컴파일러 혼란)
fn longest<'a>(s1: &'a str, s2: &'a str) -> &'a str "s1과 s2 중 하나를 돌려줄게. 둘 다 살아있는 동안만 유효해."

에러를 이해하는 3단계 요약:
  1. 컴파일러는 반환 참조의 수명을 알아야 함 (호출자 검증 목적)
  2. 자동 추론은 3가지 규칙으로만 가능 — longest는 해당 없음
  3. 그래서 프로그래머가 'a로 직접 선언해야 함
📚 공식 문서
TRPL 10.3 「참조자 유효성을 라이프타임으로 검사하기」에서 이 개념을 자세히 다룹니다.
다음 시나리오 4는 이 해설의 마지막 예시를 실제로 다룹니다: "서로 다른 스코프의 참조".
📘 Part C: 여러 라이프타임 <'a, 'b> — 4가지 패턴 완전 정복
🎯 Part C의 목적
지금까지 Part B에서 배운 longest<'a>한 가지 패턴일 뿐입니다. 실제로 함수 시그니처에서 라이프타임을 쓰는 패턴은 4가지가 있습니다. 각 패턴이 언제 필요한지, 왜 <'a, 'b>처럼 두 개를 쓰는 경우가 있는지 완전히 이해해봅시다.

공식 문서 출처: Rust By Example의 Functions 섹션
C-1️⃣ 공식 규칙 (Rust By Example)
🌍 공식 원문
Ignoring elision, function signatures with lifetimes have a few constraints:
  • any reference must have an annotated lifetime.
  • any reference being returned must have the same lifetime as an input 
    or be static.
원문 번역: 생략을 무시하면, 함수 시그니처의 라이프타임에는 몇 가지 제약이 있다:
  • 모든 참조는 라이프타임이 명시되어야 한다
  • 반환되는 모든 참조는 입력 중 하나와 같은 라이프타임을 갖거나 'static이어야 한다
→ 이 규칙을 바탕으로 4가지 패턴이 파생됩니다.
C-2️⃣ 4가지 패턴 한눈에 비교
#시그니처특징
1fn f<'a>(x: &'a T)참조 1개
2fn f<'a, 'b>(x: &'a T, y: &'b T)여러 개, 반환 없음 → 독립 수명
3fn skip_prefix<'a, 'b>(line: &'a str, prefix: &'b str) -> &'a str반환 = line('a)과 같은 수명만
4fn f<'a>(x: &'a T, y: &'a T) -> &'a T둘 다 같은 수명으로 묶음
함수에서 라이프타임을 쓰는 4가지 패턴 패턴 1: 참조 하나 fn find_max<'a>(nums: &'a [i32]) • 참조 1개만 • 'a 하나로 충분 패턴 2: 여러 개, 반환 없음 fn log_event<'a,'b>(user,action) • 'a, 'b 독립 • 반환 없으므로 묶을 필요 ❌ 패턴 3: 반환 = 특정 입력 fn skip_prefix<'a,'b>(x,y)->&'a • 반환은 x(='a)에만 묶임 • y는 자유롭게 drop 가능 패턴 4: 둘 다 같은 수명 fn longest<'a>(x,y)->&'a • 반환이 x나 y 중 하나 • 둘 다 'a로 묶음
C-3️⃣ 패턴 1: fn find_max<'a>(nums: &'a [i32]) -> Option<&'a i32> — 참조 하나
가장 단순한 형태. 참조가 하나뿐이라 라이프타임도 하나. 슬라이스에서 원소의 참조를 반환할 때 반환값이 입력에 묶이는 전형적 케이스. elision 규칙에 의해 'a 생략 가능하지만 여기서는 명시하여 이해를 돕습니다.
패턴 1: find_max<'a>(nums: &'a [i32]) -> Option<&'a i32> fn find_max nums: &'a [i32] &'a i32 (원소 참조) • 반환 참조는 입력 슬라이스의 한 원소 → 수명이 입력에 묶임 • 'a는 실제 호출 시점에 결정 (elision으로 생략 가능한 전형적 케이스)
예제 C-1: 패턴 1 — find_max: 슬라이스에서 최댓값 참조 반환
Rust
1// 패턴 1: 참조 하나 - fn f<'a>(x: &'a T)
2// 슬라이스에서 최댓값의 참조를 반환
3fn find_max<'a>(nums: &'a [i32]) -> Option<&'a i32> {
4 nums.iter().max()
5}
6
7fn main() {
8 let scores = vec![85, 92, 78, 95, 88];
9 match find_max(&scores) {
10 Some(top) => println!("최고 점수: {}", top),
11 None => println!("빈 배열"),
12 }
13}
C-4️⃣ 패턴 2: fn log_event<'a, 'b>(user: &'a str, action: &'b str) — 독립 수명
🌍 공식 원문 (Rust By Example)
Multiple elements with different lifetimes. In this case, 
it would be fine for both to have the same lifetime 'a, 
but in more complex cases, different lifetimes may be required.
원문 번역: 서로 다른 라이프타임을 가진 여러 요소. 이 경우 둘 다 같은 'a로 해도 OK지만, 더 복잡한 경우에는 다른 라이프타임이 필요할 수 있다.

💡 왜 'a, 'b 다르게?
  • 반환값이 없음 → 인자들 사이 관계를 묶을 필요 없음
  • useraction서로 다른 스코프의 원본을 참조할 수 있음을 허용
  • 예: user는 세션 전체 살아있고, action은 요청 단위로 임시 생성되는 흔한 상황
패턴 2: log_event<'a, 'b>(user: &'a str, action: &'b str) — 독립적 수명 fn log_event user: &'a str action: &'b str (로그 출력만, 반환 없음) ✓ 'a(user 세션)와 'b(action 임시)는 독립 — 수명 달라도 OK • 반환값 없음 → user는 장기 보관, action은 요청 단위 임시 생성되는 실전 흔한 패턴
예제 C-2: 패턴 2 — log_event: user(장기) + action(임시) 동시 처리
Rust
1// 패턴 2: 입력 여러 개, 반환 없음 - fn f<'a, 'b>(x: &'a, y: &'b)
2// user는 오래, action은 임시 - 독립 수명
3fn log_event<'a, 'b>(user: &'a str, action: &'b str) {
4 println!("[LOG] user={}, action={}", user, action);
5}
6
7fn main() {
8 let user = String::from("alice"); // 오래 살아있는 값
9 {
10 let action = format!("login_{}", 42); // 임시로 만들어진 값
11 log_event(&user, &action); // 서로 다른 수명 OK
12 }
13 // action drop - 하지만 user는 살아있음
14 println!("user {} is still alive", user);
15}
✅ 관찰 포인트
user는 외부 스코프(세션 전체)에서, action은 내부 스코프(요청 임시)에서 생성됩니다. 수명이 완전히 다르지만 log_event(&user, &action) 호출 가능. 'a = user의 수명, 'b = action의 수명으로 컴파일러가 각자 결정합니다.
C-5️⃣ 패턴 3: fn skip_prefix<'a, 'b>(line, prefix) -> &'a str — 반환은 line의 수명만
🌍 공식 원문 (Rust By Example)
Returning references that have been passed in is acceptable. 
However, the correct lifetime must be returned.

fn pass_x<'a, 'b>(x: &'a i32, _: &'b i32) -> &'a i32 { x }
💡 왜 반환에 'a?
  • 반환값 = line의 일부 슬라이스
  • 규칙: 반환 참조는 입력 중 하나의 수명을 받아야 함
  • line의 수명이 'a이므로 반환도 'a
  • prefix는 길이만 쓰고 반환과 무관 → 'b로 독립
🎯 핵심 효과: prefix는 함수 내부에서 길이만 계산하고 끝이므로, 호출자가 일찍 drop해도 반환값(line 슬라이스)은 유효.
패턴 3: skip_prefix<'a, 'b>(line, prefix) -> &'a str — 반환은 line 수명만 fn skip_prefix line: &'a str prefix: &'b str (길이만 계산) &'a str (line 슬라이스) ✓ 반환값은 'a(line)에만 묶임 → prefix('b)는 일찍 drop해도 OK • 핵심: prefix는 길이만 쓰고 반환과 무관 → 수명을 묶을 필요 없음
예제 C-3: 패턴 3 — prefix가 일찍 drop되어도 반환값 유효
Rust
1// 패턴 3: 반환이 특정 입력에만 관계 - fn f<'a, 'b>(line: &'a str, prefix: &'b str) -> &'a str
2// 반환 수명이 'a (line의 수명)에만 묶임 → prefix는 자유롭게 drop 가능
3fn skip_prefix<'a, 'b>(line: &'a str, prefix: &'b str) -> &'a str {
4 &line[prefix.len()..] // line에서 prefix 길이만큼 건너뛴 슬라이스
5}
6
7fn main() {
8 let line = String::from("lang:en=Hello");
9 let v;
10 {
11 let prefix = String::from("lang:en="); // prefix는 짧은 스코프
12 v = skip_prefix(&line, &prefix); // 반환은 line을 참조
13 } // prefix drop (v는 무관)
14 println!("v = {}", v); // ✓ line이 살아있으니 OK
15}
✅ 관찰 포인트
내부 스코프에서 prefix가 drop되어도 v = skip_prefix(&line, &prefix)의 반환값은 'a (= line의 수명)에만 묶여있으므로 계속 유효합니다. prefix는 함수 안에서 prefix.len()만 계산하고 끝나므로 반환과 무관합니다. 이것이 여러 라이프타임의 핵심 사용 이유입니다.
C-6️⃣ 패턴 4: fn longest<'a>(x, y) -> &'a — 둘 다 묶음
언제 이 패턴? 반환값이 x일 수도 y일 수도 있을 때. 어느 쪽을 반환하든 안전하려면 둘 다 같은 수명으로 묶어야 합니다.

📌 이 패턴은 Part B에서 이미 심층 분석했습니다 — longest 예제 참조. 여기서는 다른 패턴들과 비교하기 위해 간단히 재확인만 합니다.
패턴 4: longest<'a>(x,y) -> &'a — 둘 다 같은 수명으로 묶음 fn longest 공통 'a x: &'a str y: &'a str &'a str (x 또는 y) ✓ 'a = x와 y가 둘 다 살아있는 구간 (= 더 짧은 쪽의 수명) • 반환값이 x일 수도 y일 수도 있으므로 둘 다 같은 'a로 묶어야 안전
C-7️⃣ 패턴 결정 흐름도
어느 패턴을 쓸지 결정하는 간단한 흐름도입니다. 실제 코드 작성 시 이 순서대로 생각하면 됩니다.
어느 패턴을 쓸까? — 의사결정 흐름도 Q1: 반환값에 참조가 있는가? NO YES Q2: 인자끼리 관련 있나? Q3: 반환이 어느 입력에서? NO 패턴 2 <'a, 'b> YES 패턴 4 <'a> → &'a 특정 1개 패턴 3 <'a,'b> → &'a 둘 다 가능 패턴 4 <'a> → &'a 예: log_event (패턴 2) | skip_prefix (패턴 3) | find_max (패턴 1) | longest (패턴 4) 대부분의 함수: 입력이 하나면 자동 추론(elision)으로 'a 생략 가능
C-8️⃣ 잘못된 예 — 'a 하나로 묶으면 과한 제약 ⚠️
만약 패턴 3 대신 skip_prefix<'a> 하나로 쓴다면 어떻게 될까요? 직접 실행해서 확인해보세요.
예제 C-4: skip_prefix를 'a 하나로 쓰면 — 과한 제약 에러
Rust
1// 패턴 3을 'a 하나로 쓰면? — 과한 제약 에러
2fn skip_prefix_single<'a>(line: &'a str, prefix: &'a str) -> &'a str {
3 &line[prefix.len()..]
4}
5
6fn main() {
7 let line = String::from("lang:en=Hello");
8 let v;
9 {
10 let prefix = String::from("lang:en=");
11 // 'a = min(line, prefix) = prefix의 짧은 수명
12 v = skip_prefix_single(&line, &prefix);
13 } // prefix drop → v도 무효
14 println!("v = {}", v); // ❌ E0597
15}
❌ E0597: `b` does not live long enough
'a 하나로 묶으면 'a = min(line 수명, prefix 수명) = prefix의 짧은 수명이 됩니다. 반환값 v'a에 묶이므로 prefix가 drop되면 v도 무효.
→ 반환은 line에서만 오는데, prefix의 수명까지 불필요하게 제약받은 것.

패턴 3 ('a, 'b)을 쓰면 이 문제 해결 (예제 C-3).
C-9️⃣ 특수 케이스: 'static
'static프로그램 전체 수명을 의미하는 특별한 라이프타임입니다. 문자열 리터럴 ("Not Found")이나 static 변수가 여기 해당.

실전 활용: HTTP 상태 메시지, 에러 코드 설명, 설정 키 이름 같은 프로그램 전체에서 쓰이는 상수'static str로 반환하는 것이 자연스럽습니다.

💡 규칙: 'static 참조는 어떤 'a로든 사용 가능 — 프로그램 끝까지 살아있으니 당연히 어떤 구간에서든 유효.
예제 C-5: http_status — 'static 상수 반환 (HTTP 상태 메시지)
Rust
1// 패턴: 'static 반환 - 프로그램 전체 수명
2// HTTP 상태 코드 → 메시지 (상수 문자열)
3fn http_status(code: u16) -> &'static str {
4 match code {
5 200 => "OK",
6 404 => "Not Found",
7 500 => "Internal Server Error",
8 _ => "Unknown Status",
9 }
10}
11
12fn main() {
13 let msg1 = http_status(404);
14 let msg2 = http_status(200);
15 println!("404: {}", msg1);
16 println!("200: {}", msg2);
17
18 // 'static은 어디든 저장 가능 (프로그램 전체 살아있음)
19 let cached: &'static str = http_status(500);
20 println!("cached: {}", cached);
21}
🎓 Part C 요약
  1. 패턴 1 <'a>: 참조 하나 (예: find_max — 슬라이스에서 원소 참조 반환)
  2. 패턴 2 <'a, 'b>: 여러 참조, 반환 없음 → 독립 수명 (예: log_event — 다른 수명의 인자 동시 처리)
  3. 패턴 3 <'a, 'b> -> &'a: 반환은 특정 입력에만 묶임 (예: skip_prefix) → 가장 유연
  4. 패턴 4 <'a> -> &'a: 둘 다 묶음 → 더 짧은 쪽에 맞춰짐 (예: longest)
선택 기준: 반환값과 입력의 관계를 최대한 정확히 표현하여 호출자의 자유도를 확보.
시나리오 4. 구조체 필드의 같은 'a — 짧은 쪽에 끌려가기

🔴 문제 상황

'a 하나로 묶인 여러 참조는 공통 수명을 공유합니다. 공통 수명은 항상 가장 짧은 원본에 맞춰집니다. 구조체의 참조 필드가 대표적인 예입니다.

💡 구조체 라이프타임 — 지금은 간단히 (섹션 8에서 자세히)
이 예제의 struct Pair<'a> { ... }섹션 8 "구조체와 메서드"에서 자세히 다룹니다. 지금은 다음만 기억하세요:
  • 구조체가 참조(&T)를 필드로 가지면 <'a> 선언 + &'a T 필요
  • 의미: "이 구조체는 자기가 참조하는 원본보다 오래 살 수 없다"
  • 같은 'a로 묶으면 공통 수명 (가장 짧은 원본에 맞춰짐)
  • 다른 'a, 'b로 분리하면 필드별 독립 수명 (더 유연)
문제: Pair<'a> 두 필드가 같은 'a → 짧은 쪽에 끌려감 long_str main 끝까지 short_str drop! struct Pair<'a> { first: &'a str → long_str second: &'a str → short_str } 'a 약속값 'a 끝 (short_str drop) E0597: short_str이 drop되면서 'a가 짧아짐 → pair.first(long_str)도 접근 불가
예제 6-4a: 문제 코드 — 두 필드를 같은 'a로 묶었는데 원본이 다른 스코프
Rust
1struct Pair<'a> {
2 first: &'a str,
3 second: &'a str,
4}
5
6fn main() {
7 let long_str = String::from("Hello long");
8 let pair;
9 {
10 let short_str = String::from("short");
11 pair = Pair {
12 first: &long_str,
13 second: &short_str,
14 };
15 }
16 println!("{}", pair.first);
17}
❌ E0597: short_str does not live long enough
short_str이 내부 스코프 끝에서 drop되면서 Pair'a가 그 짧은 수명에 묶입니다. pair.first는 실제로 long_str(살아있음)을 참조하지만, 컴파일러는 'a 계약을 보고 거절합니다.

🟢 해결 방법 1: 원본들을 같은 스코프에

가장 간단한 해결: 모든 원본을 같은 스코프에 두어 'a를 긴 공통 수명으로 만듭니다.

해결 1: 원본들을 같은 스코프에 — 'a가 긴 공통 수명 long_str short_str 둘 다 main 끝까지 Pair<'a> first → long_str, second → short_str 'a ✓ 'a = min(long_str, short_str) = main 전체 → 모든 필드 안전
예제 6-4b: 해결 1 — 원본 통일
Rust
1struct Pair<'a> {
2 first: &'a str,
3 second: &'a str,
4}
5
6fn main() {
7 let long_str = String::from("Hello long");
8 let short_str = String::from("short");
9
10 let pair = Pair {
11 first: &long_str,
12 second: &short_str,
13 };
14
15 println!("{} | {}", pair.first, pair.second);
16}
✅ 왜 해결되나?
long_strshort_str같은 스코프에 있으므로 둘 다 main 끝까지 유효. 'a = min(long_str, short_str) = main 전체가 되어 모든 필드 안전.

🟢 해결 방법 2: 필드별 독립 라이프타임 <'a, 'b> (더 유연)

각 필드에 다른 라이프타임을 부여하면 원본의 스코프가 달라도 됩니다. Part C 패턴 3의 함수 버전과 같은 원리입니다.

해결 2: struct Pair<'a, 'b> — 필드별 독립 수명 (가장 유연) long_str main 끝까지 short_str 내부 스코프만 struct Pair<'a, 'b> { first: &'a str → long_str ('a = main) second: &'b str → short_str ('b = 내부) } pair_first = pair.first 'a만 뽑아서 외부로 ✓ short_str drop 후에도 pair_first ('a) 사용 가능 — 독립 수명의 힘
예제 6-4c: 해결 2 — Pair<'a, 'b>로 필드별 독립 수명
Rust
1// 각 필드에 독립적인 라이프타임 부여
2struct Pair<'a, 'b> {
3 first: &'a str,
4 second: &'b str,
5}
6
7fn main() {
8 let long_str = String::from("Hello long");
9 let pair_first;
10 {
11 let short_str = String::from("short");
12 let pair = Pair {
13 first: &long_str,
14 second: &short_str,
15 };
16 pair_first = pair.first;
17 println!("inside: {} | {}", pair.first, pair.second);
18 }
19 println!("outside: {}", pair_first);
20}
✅ 왜 해결되나?
first'a(long_str 수명), second'b(short_str 수명)로 분리.
pair_first = pair.first'a에만 묶이므로 short_str이 drop된 후에도 외부에서 계속 사용 가능.

Part C의 skip_prefix<'a, 'b>와 정확히 같은 원리 — 함수 인자에서 배운 패턴이 구조체 필드에도 적용됩니다.
📊 두 해결 방법 비교 — 언제 어느 것?
해결 1: Pair<'a>해결 2: Pair<'a, 'b>
문법단일 라이프타임필드별 독립 라이프타임
제약모든 원본이 공통 수명 (짧은 쪽)원본 스코프 달라도 OK
호출자 자유도낮음높음
복잡도단순약간 복잡
언제 쓸까원본들이 보통 같이 사는 경우원본 수명이 서로 다를 수 있는 경우
시나리오 5. 'a 하나로 묶어 제약 과함

🔴 문제 상황

반환에 s1만 쓰는데도 s2도 'a로 묶여 불필요한 수명 제약이 생깁니다.

문제: s1, s2가 같은 'a로 묶여 짧은 수명에 끌려감 fn first<'a>(s1: &'a str, s2: &'a str) -> &'a str 세 위치 모두 같은 'a main s1 s2 ← s2 drop! E0597: `s2` does not live long enough — result가 s2에 묶임
예제 6-5a: 문제 코드
Rust
1// 실제로는 s1만 반환 가능한데 s2도 'a로 묶음
2fn first<'a>(s1: &'a str, s2: &'a str) -> &'a str {
3 println!("두 번째: {}", s2);
4 s1 // 실제로 s1만 반환
5}
6
7fn main() {
8 let s1 = String::from("long");
9 let result;
10 {
11 let s2 = String::from("짧음");
12 result = first(&s1, &s2); // ❌ result가 s2 수명에 묶임
13 } // s2 drop
14 println!("{}", result); // 실제로는 s1만 참조하는데 에러
15}
❌ 컴파일러 에러
error[E0597]: `s2` does not live long enough --> 05_bad.rs:12:29 | 11 | let s2 = String::from("짧음"); | -- binding `s2` declared here 12 | result = first(&s1, &s2); // ❌ result가 s2 수명에 묶임 | ^^^ borrowed value does not live long enough 13 | } // s2 drop | - `s2` dropped here while still borrowed 14 | println!("{}", result); // 실제로는 s1만 참조하는데 에러

🟢 해결 방법

'a, 'b로 분리해 반환에 연결되는 수명만 제약합니다.

해결: 'a, 'b 분리 — 반환에 연결된 수명만 제약 fn first<'a, 'b>(s1: &'a str, s2: &'b str) -> &'a str 'a: s1 ↔ 반환 / 'b: s2 독립 main s1 s2 s2 drop돼도 OK ✓ ✓ result는 s1 수명 'a를 따라감 → 바깥까지 유효
예제 6-5b: 해결 코드
Rust
1// ✓ 'a, 'b 분리: 반환은 s1에만 연결
2fn first<'a, 'b>(s1: &'a str, s2: &'b str) -> &'a str {
3 println!("두 번째: {}", s2);
4 s1
5}
6
7fn main() {
8 let s1 = String::from("long");
9 let result;
10 {
11 let s2 = String::from("짧음");
12 result = first(&s1, &s2); // ✓ result는 'a = s1 수명
13 }
14 println!("{}", result); // ✓ s2 drop 돼도 s1만 필요
15}
시나리오 6. 구조체 참조 필드 라이프타임 누락

🔴 문제 상황

구조체에 참조 필드가 있으면 반드시 라이프타임을 명시해야 합니다.

문제: 구조체 참조 필드에 라이프타임 명시 없음 struct BookRef { title: &str, } 이 참조, 어느 수명인가? 컴파일러: 원본 데이터의 수명을 알 수 없음 E0106: missing lifetime specifier
예제 6-6a: 문제 코드
Rust
1// ❌ 라이프타임 명시 없이 참조 필드
2struct BookRef {
3 title: &str,
4}
5
6fn main() {
7 let s = String::from("Rust Book");
8 let b = BookRef { title: &s };
9 println!("{}", b.title);
10}
❌ 컴파일러 에러
error[E0106]: missing lifetime specifier --> 06_bad.rs:3:12 | 3 | title: &str, | ^ expected named lifetime parameter | help: consider introducing a named lifetime parameter | 2 ~ struct BookRef<'a> { 3 ~ title: &'a str,

🟢 해결 방법

구조체에 <'a> 매개변수를 추가합니다.

해결: 구조체에 <'a> 추가 — 같은 라이프타임으로 연결 struct BookRef<'a> { title: &'a str, } 같은 'a로 연결됨 ✓ 구조체와 필드의 수명이 같음 — 원본보다 오래 못 삼 컴파일러가 수명 관계 추적 가능
예제 6-6b: 해결 코드
Rust
1// ✓ 구조체에 <'a> 라이프타임 매개변수 추가
2struct BookRef<'a> {
3 title: &'a str,
4}
5
6fn main() {
7 let s = String::from("Rust Book");
8 let b = BookRef { title: &s };
9 println!("{}", b.title);
10}
시나리오 7. 구조체가 원본보다 오래 살려고 함

🔴 문제 상황

참조 필드를 가진 구조체는 원본 값보다 오래 살 수 없습니다.

문제: book_ref가 원본 s보다 오래 살려고 함 main { 내부 스코프 } s drop! book_ref ↑ book_ref.title이 drop된 s 가리킴 E0597: `s` does not live long enough
예제 6-7a: 문제 코드
Rust
1struct BookRef<'a> { title: &'a str }
2
3fn main() {
4 let book_ref;
5 {
6 let s = String::from("Rust");
7 book_ref = BookRef { title: &s }; // ❌
8 } // s drop - book_ref.title이 죽은 s 가리킴
9 println!("{}", book_ref.title);
10}
❌ 컴파일러 에러
error[E0597]: `s` does not live long enough --> 07_bad.rs:7:37 | 6 | let s = String::from("Rust"); | - binding `s` declared here 7 | book_ref = BookRef { title: &s }; // ❌ | ^^ borrowed value does not live long enough 8 | } // s drop - book_ref.title이 죽은 s 가리킴 | - `s` dropped here while still borrowed 9 | println!("{}", book_ref.title);

🟢 해결 방법

원본 값을 구조체와 같은 스코프 이상에 선언합니다.

해결: 원본 s를 바깥 스코프에 선언 main (s, book_ref 같은 수명) s book_ref ✓ s가 book_ref 전체 수명 동안 살아있음
예제 6-7b: 해결 코드
Rust
1struct BookRef<'a> { title: &'a str }
2
3fn main() {
4 let s = String::from("Rust"); // 원본을 바깥으로
5 let book_ref = BookRef { title: &s };
6 println!("{}", book_ref.title); // ✓ s와 book_ref 수명 일치
7}
시나리오 8. 불변 + 가변 참조 수명 충돌

🔴 문제 상황

불변 참조가 아직 쓰이는 중에 가변 참조를 만들면 안 됩니다.

문제: r1(불변)이 살아있을 때 r2(가변) 생성 시간 let r1=&s let r2=&mut s println!(r1,r2) r1 (불변) r2 (가변) ❌ 충돌: r1(불변) + r2(가변) 공존 불가 E0502: cannot borrow `s` as mutable
예제 6-8a: 문제 코드
Rust
1fn main() {
2 let mut s = String::from("hello");
3 let r1 = &s; // 불변 참조
4 let r2 = &mut s; // ❌ 불변 참조 r1이 아직 살아있는데 가변 참조 생성
5 println!("{}, {}", r1, r2);
6}
❌ 컴파일러 에러
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable --> 08_bad.rs:4:14 | 3 | let r1 = &s; // 불변 참조 | -- immutable borrow occurs here 4 | let r2 = &mut s; // ❌ 불변 참조 r1이 아직 살아있는데 가변 참조 생성 | ^^^^^^ mutable borrow occurs here 5 | println!("{}, {}", r1, r2); | -- immutable borrow later used here

🟢 해결 방법

불변 참조 사용이 끝난 후 가변 참조를 만듭니다 (NLL이 자동 처리).

해결: r1 사용 종료 후 r2 생성 (수명 분리) 시간 let r1 println!(r1) let r2 println!(r2) r1 (불변) r2 (가변) 수명 분리 (NLL) ✓ 불변 사용 완료 → 가변 참조 가능
예제 6-8b: 해결 코드
Rust
1fn main() {
2 let mut s = String::from("hello");
3 let r1 = &s;
4 println!("{}", r1); // r1 사용 끝 (수명 종료)
5
6 let r2 = &mut s; // ✓ r1 수명 끝난 후 가변 참조 생성
7 r2.push_str(" world");
8 println!("{}", r2);
9}
시나리오 9. 벡터 참조 유지 중 벡터 수정

🔴 문제 상황

v.push()는 재할당 가능성이 있어, 기존 참조를 무효화할 수 있습니다.

문제: 벡터 참조 유지 중 push로 재할당 가능 이전 벡터 [1, 2, 3] 1 2 3 first → &v[0] push 후 (재할당 가능) 1 2 3 4 (다른 주소) first → 해제된 메모리 재할당 시 first는 해제된 메모리 가리킴 E0502: cannot borrow `v` as mutable
예제 6-9a: 문제 코드
Rust
1fn main() {
2 let mut v = vec![1, 2, 3];
3 let first = &v[0]; // 첫 요소 참조
4 v.push(4); // ❌ 벡터 수정 - 재할당 시 first가 무효화될 수 있음
5 println!("{}", first);
6}
❌ 컴파일러 에러
error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable --> 09_bad.rs:4:5 | 3 | let first = &v[0]; // 첫 요소 참조 | - immutable borrow occurs here 4 | v.push(4); // ❌ 벡터 수정 - 재할당 시 first가 무효화될 수 있음 | ^^^^^^^^^ mutable borrow occurs here 5 | println!("{}", first); | ----- immutable borrow later used here

🟢 해결 방법

값을 복사(Copy 타입) 또는 clone한 후 벡터를 수정합니다.

해결: 참조 대신 값 복사 (i32는 Copy) 벡터 [1, 2, 3, 4] 1 2 3 4 push 가능 ✓ first (독립) 값 1 (복사본) ← v와 무관 ✓ 참조가 없으므로 v 수정 자유 참조형(String 등)은 .clone() 사용
예제 6-9b: 해결 코드
Rust
1fn main() {
2 let mut v = vec![1, 2, 3];
3 let first = v[0]; // 값 복사 (i32는 Copy)
4 v.push(4); // ✓ 참조가 아니므로 벡터 수정 가능
5 println!("{}, {:?}", first, v);
6}
💡 더 알아보기 — Vec<String> 같은 non-Copy 타입이면?
위 해결법 let first = v[0];i32Copy 트레이트를 구현해서 자동 값 복사가 되기 때문에 가능합니다. String처럼 Copy가 없는 타입이면 E0507 에러가 납니다.

왜 그런가? String은 힙에 데이터를 가진 소유 타입이라 Copy가 구현되지 않습니다. v[0]로 꺼내려 하면 벡터 안의 요소를 move하려는 것인데, 벡터 중간 요소만 move하면 벡터가 "구멍 뚫린 상태"가 되어버립니다. 러스트는 이를 금지합니다.
문제 시연 — Vec<String>에 v[0] 시도
Rust
1// 문제 시연: Vec<String>에 값 복사를 시도하면?
2fn main() {
3 let mut v = vec![
4 String::from("apple"),
5 String::from("banana"),
6 String::from("cherry"),
7 ];
8
9 let first = v[0]; // ❌ E0507: cannot move out of index
10 v.push(String::from("date"));
11 println!("{}", first);
12}
❌ E0507: cannot move out of index of `Vec<String>`
v[0]은 벡터 요소의 소유권을 꺼내려는 시도 — non-Copy 타입에서는 불가능. 다음 5가지 해결책을 상황에 맞게 사용하세요.
📋 5가지 해결책 비교
방법코드 핵심벡터 변화특징언제 쓸까
A v[0].clone() 유지 독립 복사본 생성 복사본이 필요할 때 (작은 데이터)
B 참조 + NLL 유지 복사 없음 (효율) 읽은 후 벡터 수정해도 되는 경우
C iter().position() 유지 인덱스(usize)만 저장 조건 검색 후 보관 (DB id 패턴)
D VecDeque::pop_front() 요소 제거 소유권 이동, O(1) 큐/파이프라인, 앞에서 꺼내기
E mem::take(&mut v[0]) 길이 유지, 값 빔 자리 유지 + 소유권 이동 요소 위치 유지하며 값만 꺼낼 때
방법 A: .clone() — 명시적 복사
Rust
1// 방법 A: .clone()으로 명시적 복사
2fn main() {
3 let mut v = vec![
4 String::from("apple"),
5 String::from("banana"),
6 String::from("cherry"),
7 ];
8
9 let first = v[0].clone(); // ✓ 독립된 복사본 (소유권 이동 없음)
10 v.push(String::from("date"));
11
12 println!("first = {}", first);
13 println!("v = {:?}", v);
14}
✅ 방법 A 설명
v[0].clone()은 원본과 독립된 새 String을 만듭니다. 소유권 이동이 아니라 복사이므로 벡터는 그대로 유지. 이후 v.push() 자유롭게 가능.

트레이드오프: 큰 데이터를 clone하면 비용이 큼. 정말 독립된 복사본이 필요한지 확인.
방법 B: 참조 + NLL — 먼저 쓰고 나중에 수정
Rust
1// 방법 B: 참조 사용 후 NLL로 자연스럽게 끝내기
2fn main() {
3 let mut v = vec![
4 String::from("apple"),
5 String::from("banana"),
6 String::from("cherry"),
7 ];
8
9 let first = &v[0];
10 println!("first = {}", first); // ✓ first 마지막 사용 (NLL)
11
12 v.push(String::from("date")); // ✓ 이제 벡터 수정 OK
13 println!("v = {:?}", v);
14}
✅ 방법 B 설명
firstprintln!에서 마지막으로 사용한 순간 참조 수명이 끝남 (NLL, Part A-4 참고). 그 이후 v.push()는 충돌 없이 가능.

핵심: 코드 순서를 "참조 사용 → 벡터 수정" 으로 재배치하면 복사 없이 해결.
방법 C: iter().position() — 조건으로 인덱스 찾기
Rust
1// 방법 C: iter().position()으로 조건에 맞는 인덱스 찾기
2// 참조 대신 인덱스를 저장하면 참조 수명 문제가 없음
3fn main() {
4 let mut v = vec![
5 String::from("apple"),
6 String::from("banana"),
7 String::from("cherry"),
8 ];
9
10 // "b"로 시작하는 요소의 인덱스 찾기
11 let idx = v.iter().position(|s| s.starts_with("b")).unwrap();
12
13 v.push(String::from("date")); // ✓ 인덱스는 참조 아님 - 수정 가능
14
15 println!("찾은 것: {} (idx={})", v[idx], idx);
16 println!("v = {:?}", v);
17}
✅ 방법 C 설명
iter().position(|x| ...)은 조건을 만족하는 첫 요소의 인덱스(usize)를 반환합니다. 인덱스는 참조가 아니므로 벡터를 수정해도 문제 없고, 나중에 v[idx]로 접근 가능.

실전: DB에서 레코드를 id로 찾아 저장하는 패턴과 비슷. 조건 검색 + 결과 보관이 필요할 때 자주 씀.

주의: 벡터가 수정되면 인덱스 유효성 확인 필요 (remove/swap 등 후엔 달라질 수 있음).
방법 D: VecDeque — 앞쪽에서 꺼내기 O(1)
Rust
1// 방법 D: VecDeque - 앞쪽에서 꺼내기가 O(1)
2// pop_front로 첫 요소의 소유권을 꺼내면 참조 문제 없음
3use std::collections::VecDeque;
4
5fn main() {
6 let mut v: VecDeque<String> = VecDeque::from([
7 String::from("apple"),
8 String::from("banana"),
9 String::from("cherry"),
10 ]);
11
12 let first = v.pop_front().unwrap(); // ✓ 앞에서 꺼내기 (소유권 이동)
13 v.push_back(String::from("date"));
14
15 println!("first = {}", first);
16 println!("v = {:?}", v);
17}
✅ 방법 D 설명
VecDeque(이중 연결 큐)는 pop_front()/push_back()이 모두 O(1)입니다. 앞에서 꺼내는 순간 소유권이 이동하므로 참조 수명 문제 자체가 사라집니다.

실전: 큐/작업 파이프라인 구현, FIFO 처리에 이상적. Vecremove(0)은 O(n)인 반면 VecDeque::pop_front()는 O(1).

트레이드오프: 벡터에서 요소가 제거됨. 원본 유지 필요 시 부적합.
방법 E: mem::take — 자리에 Default 두고 원본 꺼내기
Rust
1// 방법 E: mem::take - 자리에 빈 값(Default)을 두고 원본 꺼내기
2// 벡터 길이는 유지, 해당 인덱스 값만 소유권 이동
3use std::mem;
4
5fn main() {
6 let mut v = vec![
7 String::from("apple"),
8 String::from("banana"),
9 String::from("cherry"),
10 ];
11
12 let first = mem::take(&mut v[0]); // ✓ v[0]은 빈 String으로 바뀜
13 v.push(String::from("date"));
14
15 println!("first = {}", first);
16 println!("v = {:?}", v); // ["", "banana", "cherry", "date"]
17}
✅ 방법 E 설명
std::mem::take(&mut v[0])v[0] 자리에 Default 값(String의 경우 빈 String "")을 넣고 원본의 소유권을 반환합니다. 벡터 길이는 유지되고 해당 인덱스 값만 이동됩니다.

실전: 구조체 필드에서 큰 값을 꺼낼 때, 요소 위치를 바꾸지 않고 "비워두기" 필요할 때.

요구사항: 타입이 Default 트레이트 구현 필요 (String/Vec/HashMap 등 대부분 OK).
주의: 벡터에 빈 값이 남으므로 이후 반복/처리 시 고려 필요.
🎓 요약
  • i32 같은 Copy 타입: let first = v[0]; 값 복사로 간단히 해결
  • String 같은 non-Copy 타입: 5가지 접근 — clone / NLL 참조 / 인덱스 검색 / VecDeque / mem::take
  • 벡터 유지가 중요하면 A/B/C, 소유권 이동이 필요하면 D/E
  • Copy 여부는 타입의 특성 — 섹션 4 소유권에서 다룸
시나리오 10. 함수 반환값을 더 오래 쓰려 함

🔴 문제 상황

함수 내 지역 String의 참조를 반환하려는 시도입니다.

문제: 함수 내 지역 String의 참조 반환 fn get_name() 힙 데이터 name: "Rust" return &name 호출자 참조할 대상 없음 (dangling) ↓ 함수 종료 시 name drop! 반환 참조 → 해제된 메모리 E0106: missing lifetime specifier
예제 6-10a: 문제 코드
Rust
1fn get_name() -> &str {
2 let name = String::from("Rust");
3 &name // ❌ 지역 변수 참조 반환
4}
5
6fn main() {
7 let name = get_name();
8 println!("{}", name);
9}
❌ 컴파일러 에러
error[E0106]: missing lifetime specifier --> 10_bad.rs:1:18 | 1 | fn get_name() -> &str { | ^ expected named lifetime parameter | = help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from help: consider using the `'static` lifetime | 1 | fn get_name() -> &'static str {

🟢 해결 방법

String 타입으로 반환 — 소유권 이동.

해결: String(소유값) 반환 — 소유권 이동 fn get_name() 힙 데이터 name: "Rust" move 호출자 name (소유) ✓ 호출자가 소유 — 더 이상 참조 문제 없음 drop 책임도 호출자에게 이동
예제 6-10b: 해결 코드
Rust
1// 해결: 소유값으로 반환
2fn get_name() -> String {
3 let name = String::from("Rust");
4 name // ✓ 소유권 이동
5}
6
7fn main() {
8 let name = get_name(); // name이 소유
9 println!("{}", name);
10}
✅ 섹션 6 정리 — 해결 3단계
  1. 라이프타임 생략 규칙 적용되나? → 그냥 생략
  2. 입력들이 같은 수명이어야 하나?'a 하나로 묶기
  3. 입력마다 독립적인가?'a, 'b 분리

그래도 해결 안 되면 참조 대신 소유값 사용을 고려하세요.

7. 문자열 (String vs &str)

Rust 문자열은 자바 개발자에게 가장 헷갈리는 부분 중 하나입니다. 두 가지 타입이 있고, 각각의 쓰임새가 명확히 다르기 때문입니다.

타입저장 위치크기소유권
String가변 (늘리고 줄이기 가능)소유 O
&str어디든 (포인터만)고정 (읽기 전용)소유 X (참조)
Part 1. String vs &str 기본
예제 7-1: 두 타입의 차이
Java
1public class Main {
2 public static void main(String[] args) {
3 String lit = "literal"; // 문자열 풀
4 String owned = "owned"; // Java는 구분 없음
5 System.out.println(lit + " / " + owned);
6 }
7}
Rust
1fn main() {
2 let lit: &str = "리터럴"; // 스택: 프로그램 종료까지
3 let owned: String = String::from("소유"); // 힙: 크기 변경 가능
4 let slice: &str = &owned[..]; // String → &str 슬라이스
5 println!("{lit} / {owned} / {slice}");
6}
예제 7-2: String 생성 방법들
Rust
1fn main() {
2 let s1 = String::new(); // 빈 문자열
3 let s2 = String::from("Rust"); // &strString
4 let s3 = "Book".to_string(); // &strString
5 let s4 = String::from("Rust") + " Book"; // 연결
6 println!("[{s1}] {s2} / {s3} / {s4}");
7}
Part 2. String 조작
예제 7-3: push_str / push
Java
1public class Main {
2 public static void main(String[] args) {
3 StringBuilder s = new StringBuilder("Rust");
4 s.append(" Book");
5 s.append('!');
6 System.out.println(s);
7 System.out.println("length: " + s.length());
8 }
9}
Rust
1fn main() {
2 let mut s = String::from("Rust");
3 s.push_str(" Book"); // &str 추가
4 s.push('!'); // char 추가
5 println!("{s}"); // Rust Book!
6 println!("길이: {}", s.len());
7}
예제 7-4: 슬라이스
Rust
1fn main() {
2 let s = String::from("Rust Book");
3 let hello = &s[0..4]; // 0..4 = 0~3번 바이트
4 let world = &s[5..]; // 5번부터 끝까지
5 println!("{hello} / {world}");
6}
⚠️ 한글이나 이모지는 바이트 기반 슬라이싱 주의
&s[0..3]바이트 인덱스입니다. 한글은 UTF-8에서 3바이트이므로 "안녕"[0..2]는 에러가 납니다. 문자 기반으로 처리하려면 s.chars()를 사용하세요.
Part 2½. 메서드 2가지 방식 — 자기 변경 vs 새 값 반환

String 메서드는 크게 두 부류로 나뉩니다. 메서드 이름이 아니라 self 타입이 동작을 결정합니다.

🌍 원문 (Rust 공식 - String::push_str)
push_str is designed to modify the String in place, 
without creating a new String object. 
This is key to understanding why it returns ().
원문 번역: push_str은 String을 제자리에서 수정하도록 설계되었으며, 새 String 객체를 만들지 않습니다. 이것이 반환 타입이 ()인 이유입니다.
💡 메서드 시그니처 3가지 — self 타입이 행동을 결정
  • fn method(&self, ...): 원본 읽기만 → 보통 새 값 반환
  • fn method(&mut self, ...): 원본 변경 → 보통 () 반환
  • fn method(self, ...): 원본 소유권 가져감 → 변형된 값 반환
Java와의 차이: Java String은 항상 불변이라 toUpperCase()처럼 새 객체만 반환합니다. Rust는 두 방식 모두 제공 — 성능이 필요하면 제자리 변경, 원본 유지가 필요하면 새 값.
String 메서드 2가지 방식 — self 타입으로 동작이 결정됨 🔴 자기 변경 (&mut self) let mut s = String::from("Hi"); s.push_str(", World"); 결과: s 자체가 "Hi, World"로 변함 반환값: () - 빈 튜플 BEFORE: s = "Hi" AFTER: s = "Hi, World" (같은 변수!) 특징: • mut 선언 필수 • 추가 메모리 할당 최소 (효율) • 체이닝 불가 (() 반환) 🟢 새 값 반환 (&self) let s = String::from("hi"); let u = s.to_uppercase(); 결과: s는 "hi" 그대로, u = "HI" 반환값: 새로운 String BEFORE: s = "hi" AFTER: s = "hi", u = "HI" (둘 다!) 특징: • mut 선언 불필요 • 원본 보존, 새 객체 생성 • 체이닝 가능: s.trim().to_uppercase() ✓ 메서드 이름이 아니라 <self 타입>과 <반환 타입>으로 동작이 정해진다

🔴 자기 자신 변경 메서드 10종 (&mut self)

mut 선언 필수. 반환값은 () 또는 제거된 값. 체이닝 불가.

예제 7-Mut: 자기 변경 메서드 전체 실행
Rust
1// 자기 자신을 변경하는 메서드들: &mut self, 반환값 ()
2// String은 mut 선언 필수
3fn main() {
4 // 1. push_str: 문자열 추가
5 let mut s1 = String::from("Hello");
6 s1.push_str(", World!");
7 println!("push_str: {}", s1);
8
9 // 2. push: 문자 하나 추가
10 let mut s2 = String::from("Hi");
11 s2.push('!');
12 println!("push: {}", s2);
13
14 // 3. insert: 특정 위치에 문자 삽입
15 let mut s3 = String::from("Hllo");
16 s3.insert(1, 'e');
17 println!("insert: {}", s3);
18
19 // 4. insert_str: 특정 위치에 문자열 삽입
20 let mut s4 = String::from("Hello !");
21 s4.insert_str(6, "World");
22 println!("insert_str: {}", s4);
23
24 // 5. clear: 내용 전부 지우기
25 let mut s5 = String::from("Hello");
26 s5.clear();
27 println!("clear: [{}] (빈 문자열)", s5);
28
29 // 6. truncate: 지정 길이로 자르기
30 let mut s6 = String::from("Hello World");
31 s6.truncate(5);
32 println!("truncate(5): {}", s6);
33
34 // 7. pop: 마지막 문자 제거하고 반환
35 let mut s7 = String::from("Hello!");
36 let popped = s7.pop();
37 println!("pop: s7={} / 반환={:?}", s7, popped);
38
39 // 8. remove: 특정 위치의 문자 제거
40 let mut s8 = String::from("Hello");
41 let removed = s8.remove(0);
42 println!("remove(0): s8={} / 반환={}", s8, removed);
43
44 // 9. make_ascii_uppercase: 제자리에서 대문자화
45 let mut s9 = String::from("hello");
46 s9.make_ascii_uppercase();
47 println!("make_ascii_upper: {}", s9);
48
49 // 10. += 연산자 (push_str과 동일)
50 let mut s10 = String::from("Hello");
51 s10 += ", World!";
52 println!("+=: {}", s10);
53}
📋 자기 변경 메서드 정리
메서드역할반환
push_str(&str)문자열 뒤에 추가()
push(char)문자 하나 추가()
insert(idx, char)위치에 문자 삽입()
insert_str(idx, &str)위치에 문자열 삽입()
clear()전부 지움()
truncate(len)지정 길이로 자름()
pop()마지막 문자 제거Option<char>
remove(idx)위치의 문자 제거char
make_ascii_uppercase()제자리 대문자화()
+= 연산자push_str과 동일()

🟢 새 값 반환 메서드 10종 (&self)

mut 불필요. 원본 그대로 유지. 체이닝 가능.

예제 7-New: 새 값 반환 메서드 전체 실행
Rust
1// 새 값을 반환하는 메서드들: 원본 불변, 새 값 반환
2// 원본은 mut 선언 불필요
3fn main() {
4 let s = String::from("Hello World");
5
6 // 1. to_uppercase / to_lowercase: 새 String 반환
7 let upper = s.to_uppercase();
8 let lower = s.to_lowercase();
9 println!("to_uppercase: 원본={} / 새값={}", s, upper);
10 println!("to_lowercase: 새값={}", lower);
11
12 // 2. replace: 새 String 반환
13 let replaced = s.replace("World", "Rust");
14 println!("replace: 원본={} / 새값={}", s, replaced);
15
16 // 3. trim: 새 &str 반환 (슬라이스)
17 let padded = String::from(" hello ");
18 let trimmed = padded.trim();
19 println!("trim: 원본=[{}] / 새값=[{}]", padded, trimmed);
20
21 // 4. repeat: 새 String 반환
22 let repeated = s.repeat(2);
23 println!("repeat(2): 새값={}", repeated);
24
25 // 5. + 연산자: 새 String 반환 (좌항은 소유권 이동)
26 let a = String::from("Hello");
27 let b = String::from(" World");
28 let c = a + &b; // a는 move됨!
29 println!("+ 연산자: 새값={} (a는 move됨)", c);
30
31 // 6. format! 매크로: 새 String 반환
32 let x = String::from("Hello");
33 let y = String::from("World");
34 let z = format!("{} {}", x, y);
35 println!("format!: 원본유지 x={}, y={} / 새값={}", x, y, z);
36
37 // 7. split: Iterator 반환 (& 슬라이스)
38 let words: Vec<&str> = s.split(' ').collect();
39 println!("split: 원본={} / 새값={:?}", s, words);
40
41 // 8. chars: Iterator 반환
42 let first_char = s.chars().next();
43 println!("chars.next: 원본={} / 새값={:?}", s, first_char);
44
45 // 9. len / is_empty: 숫자/불린 반환
46 println!("len: {} (원본 그대로)", s.len());
47 println!("is_empty: {}", s.is_empty());
48}
📋 새 값 반환 메서드 정리
메서드역할반환
to_uppercase()대문자 새 StringString
to_lowercase()소문자 새 StringString
replace(from, to)치환한 새 StringString
trim()공백 제거 슬라이스&str
repeat(n)n번 반복한 새 StringString
split(pat)분리한 IteratorIterator<&str>
chars()문자 IteratorIterator<char>
len()바이트 길이usize
is_empty()빈 문자열인지bool
format! 매크로포매팅된 StringString
⚠️ 헷갈리는 포인트
  • 이름 비슷한데 동작 반대:
    to_ascii_uppercase() → 새 String 반환 (원본 불변)
    make_ascii_uppercase() → 제자리 변경 (() 반환)
    "make_*"는 "이대로 만들어라" = 제자리 변경 신호.
  • + 연산자 특수함:
    let c = a + &b; — 좌항 a소유권 이동. 이후 a 사용 불가.
    참고로 우항 b는 참조로 전달하므로 그대로 유지.
  • trim()String이 아닌 &str 반환:
    원본의 슬라이스(빌림)일 뿐, 새 String이 필요하면 .trim().to_string().
  • 체이닝 가능 여부:
    s.trim().to_uppercase().replace("A", "B") (전부 새 값 반환)
    s.push_str("x").push('!') (push_str이 () 반환, 연속 불가)
✅ 선택 기준
  • 성능 중요 + 원본 불필요 → 자기 변경 (&mut self)
  • 원본 보존 필요 or 체이닝 쓰고 싶음 → 새 값 반환 (&self)
  • 기본적으로는 &self 방식 선호 — 부작용 적고 추론 쉬움
Part 3. 함수 파라미터 - &str이 유연
예제 7-5: &str 파라미터
Rust
1// &str를 받으면 String도, &str도 모두 전달 가능
2fn print_title(s: &str) {
3 println!("{s}");
4}
5
6fn main() {
7 let owned = String::from("Rust Book");
8 print_title(&owned); // String → &str 자동 변환
9 print_title("Go Book"); // 리터럴 직접 전달
10}
Part 4. 주요 메서드 비교
예제 7-6: 자주 쓰는 메서드
Java
1public class Main {
2 public static void main(String[] args) {
3 String s = "Rust Book";
4 System.out.println("length: " + s.length());
5 System.out.println("contains: " + s.contains("Rust"));
6 System.out.println("upper: " + s.toUpperCase());
7 System.out.println("replace: " + s.replace("Rust", "Go"));
8 }
9}
Rust
1fn main() {
2 let s = String::from("Rust Book");
3 println!("길이: {}", s.len());
4 println!("포함: {}", s.contains("Rust"));
5 println!("대문자: {}", s.to_uppercase());
6 println!("치환: {}", s.replace("Rust", "Go"));
7}
기능JavaRust
길이.length() (문자).len() (바이트)
포함 여부.contains(s).contains(s)
대문자.toUpperCase().to_uppercase()
공백 제거.trim().trim()
분할.split(",").split(',')
치환.replace(a, b).replace(a, b)
정수 변환Integer.parseInt(s)s.parse::<i32>()
Part 5. Book 제목 관리 (심화)
예제 7-7: String을 가진 Book
Rust
1struct Book {
2 title: String,
3}
4
5impl Book {
6 fn new(title: &str) -> Book { // &str 받아서 String으로
7 Book { title: title.to_string() }
8 }
9 fn add_prefix(&mut self, prefix: &str) {
10 self.title = format!("{prefix}{}", self.title);
11 }
12 fn get_title(&self) -> &str { // String → &str 반환
13 &self.title
14 }
15}
16
17fn main() {
18 let mut b = Book::new("Rust Book");
19 b.add_prefix("[2025] ");
20 println!("{}", b.get_title());
21}

8. 구조체와 메서드

Rust의 struct는 Java의 class와 비슷하지만, 데이터와 메서드가 분리되어 있습니다. struct로 필드를 정의하고 impl 블록에서 메서드를 구현합니다.

Part 1. struct 기본
예제 8-1: 기본 선언
Java
1public class Main {
2 static class Book {
3 String title;
4 int pages;
5 Book(String t, int p) { title = t; pages = p; }
6 }
7
8 public static void main(String[] args) {
9 Book b = new Book("Rust Book", 300);
10 System.out.println(b.title + ": " + b.pages + "p");
11 }
12}
Rust
1struct Book {
2 title: String,
3 pages: i32,
4}
5
6fn main() {
7 let b = Book {
8 title: String::from("Rust Book"),
9 pages: 300,
10 };
11 println!("{}: {}p", b.title, b.pages);
12}
Part 2. impl 블록과 메서드

Rust는 데이터 정의(struct)행동 정의(impl)가 분리되어 있습니다. Java 클래스처럼 한 곳에 모아 놓지 않습니다.

예제 8-2: 연관함수 vs 메서드
Java
1public class Main {
2 static class Book {
3 String title;
4 int pages;
5 Book(String t, int p) { title = t; pages = p; }
6 String info() {
7 return title + " (" + pages + "p)";
8 }
9 }
10
11 public static void main(String[] args) {
12 Book b = new Book("Rust Book", 300);
13 System.out.println(b.info());
14 }
15}
Rust
1struct Book {
2 title: String,
3 pages: i32,
4}
5
6impl Book {
7 // 연관함수 (Java static) - self 없음
8 fn new(title: &str, pages: i32) -> Book {
9 Book { title: title.to_string(), pages }
10 }
11 // 메서드 - &self (불변 빌림)
12 fn info(&self) -> String {
13 format!("{} ({}p)", self.title, self.pages)
14 }
15}
16
17fn main() {
18 let b = Book::new("Rust Book", 300);
19 println!("{}", b.info());
20}
용어JavaRust
필드int pages;pages: i32
생성자new Book(...)관례적으로 Book::new(...) 연관함수
인스턴스 메서드b.info()b.info() (내부는 &self)
정적 메서드Book.defaultBook()Book::default() (self 없음)
Part 3. self의 3가지 형태

Rust 메서드의 첫 인자는 self입니다. 형태에 따라 소유권 동작이 다릅니다:

형태의미언제 사용?
&self불변 빌림읽기만 할 때 (getter 등)
&mut self가변 빌림필드 수정할 때
self소유권 이동인스턴스 소멸하거나 변환할 때
예제 8-3: 3가지 self 비교
Rust
1struct Book { title: String, read: bool }
2
3impl Book {
4 fn show(&self) { // 읽기만
5 println!("{} [{}]", self.title, self.read);
6 }
7 fn mark_read(&mut self) { // 수정
8 self.read = true;
9 }
10 fn discard(self) { // 소유권 가져감 (소멸)
11 println!("폐기: {}", self.title);
12 }
13}
14
15fn main() {
16 let mut b = Book {
17 title: String::from("Rust Book"),
18 read: false,
19 };
20 b.show();
21 b.mark_read();
22 b.show();
23 b.discard();
24 // b.show(); // ❌ b는 이미 이동됨
25}
Part 4. #[derive] - 자동 구현

자주 쓰는 trait(Clone, Debug, PartialEq 등)을 어노테이션으로 자동 구현합니다.

예제 8-4: #[derive]
Rust
1#[derive(Debug, Clone)]
2struct Book {
3 title: String,
4 pages: i32,
5}
6
7fn main() {
8 let b1 = Book {
9 title: String::from("Rust"),
10 pages: 300,
11 };
12 let b2 = b1.clone(); // Clone 자동 생성
13 println!("{:?}", b1); // Debug 자동 생성
14 println!("{:?}", b2);
15}
Part 5. 튜플 구조체
예제 8-5: 이름 없는 필드
Rust
1// 이름 없는 필드 - 튜플처럼
2struct Price(f64);
3struct Year(i32);
4
5fn main() {
6 let p = Price(29.99);
7 let y = Year(2025);
8 println!("${}, 연도 {}", p.0, y.0); // .0으로 접근
9}
Part 6. 연관함수 활용
예제 8-6: 여러 생성자
Rust
1struct Book { title: String, pages: i32 }
2
3impl Book {
4 fn new(title: &str, pages: i32) -> Book {
5 Book { title: title.to_string(), pages }
6 }
7 fn short(title: &str) -> Book {
8 Book::new(title, 100) // 다른 연관함수 호출
9 }
10 fn default() -> Book {
11 Book::new("Unknown", 0)
12 }
13}
14
15fn main() {
16 let a = Book::new("Rust", 300);
17 let b = Book::short("Intro");
18 let c = Book::default();
19 println!("{} {}p / {} {}p / {}",
20 a.title, a.pages, b.title, b.pages, c.title);
21}
Part 7. 도서관 시스템 (심화)
예제 8-7: Book 대여/반납 메서드
Java
1public class Main {
2 static class Book {
3 String title;
4 int pages;
5 boolean available;
6
7 Book(String t, int p) {
8 title = t; pages = p; available = true;
9 }
10 boolean borrowBook() {
11 if (available) {
12 available = false;
13 return true;
14 }
15 return false;
16 }
17 void returnBook() { available = true; }
18 }
19
20 public static void main(String[] args) {
21 Book b = new Book("Rust Book", 300);
22 System.out.println("Borrow: " + b.borrowBook());
23 System.out.println("Re-borrow: " + b.borrowBook());
24 b.returnBook();
25 System.out.println("After return: " + b.borrowBook());
26 }
27}
Rust
1#[derive(Debug)]
2struct Book {
3 title: String,
4 pages: i32,
5 available: bool,
6}
7
8impl Book {
9 fn new(title: &str, pages: i32) -> Book {
10 Book {
11 title: title.to_string(),
12 pages,
13 available: true,
14 }
15 }
16 fn borrow(&mut self) -> bool {
17 if self.available {
18 self.available = false;
19 true
20 } else { false }
21 }
22 fn return_book(&mut self) {
23 self.available = true;
24 }
25}
26
27fn main() {
28 let mut b = Book::new("Rust Book", 300);
29 println!("대여 시도: {}", b.borrow()); // true
30 println!("재대여 시도: {}", b.borrow()); // false
31 b.return_book();
32 println!("반납 후 대여: {}", b.borrow()); // true
33}
Part — #[derive]로 자동 구현하기

#[derive(...)]는 구조체에 붙여서 컴파일러가 트레잇 구현을 자동 생성하도록 지시하는 속성(attribute)입니다. 직접 impl을 쓰지 않아도 되어 코드가 간결해집니다.

💡 컴파일러가 자동으로 하는 일
#[derive(Clone)]을 붙이면 컴파일러가 아래와 같은 코드를 자동으로 만듭니다:
impl Clone for Book {
    fn clone(&self) -> Self {
        Book {
            title: self.title.clone(),   // 필드마다 .clone() 호출
            pages: self.pages.clone(),
        }
    }
}
필드마다 .clone() 호출하는 단순한 코드입니다. 조건: 모든 필드가 해당 트레잇을 구현하고 있어야 derive 가능.
예제 8-D1: Debug{:?}로 출력
Rust
1#[derive(Debug)]
2struct Book {
3 title: String,
4 pages: u32,
5}
6
7fn main() {
8 let b = Book { title: String::from("Rust"), pages: 300 };
9 println!("{:?}", b); // 한 줄 Debug
10 println!("{:#?}", b); // pretty print (들여쓰기)
11}

Debug가 없으면 println!("{:?}", b)는 컴파일 에러입니다. 대부분의 구조체에 기본적으로 붙이는 트레잇입니다.

예제 8-D2: Clone — 명시적 복사
Rust
1#[derive(Debug, Clone)]
2struct Book {
3 title: String, // String은 Copy가 아니므로 Clone 필요
4 pages: u32,
5}
6
7fn main() {
8 let b1 = Book { title: String::from("Rust"), pages: 300 };
9 let b2 = b1.clone(); // 명시적 복사
10 // 컴파일러가 자동 생성: Book { title: b1.title.clone(), pages: b1.pages.clone() }
11 println!("{:?}", b1); // ✓ b1 사용 가능
12 println!("{:?}", b2); // ✓ b2도 사용 가능
13}

String, Vec 등을 필드로 가진 구조체는 Copy 불가이므로 Clone만 파생합니다. 복사가 필요할 때 .clone()명시적으로 호출합니다.

예제 8-D3: Copy + Clone — 암묵적 복사
Rust
1// Copy 파생 조건: 모든 필드가 Copy여야 함 (String 있으면 불가)
2#[derive(Debug, Copy, Clone)]
3struct Point {
4 x: i32,
5 y: i32,
6}
7
8fn main() {
9 let p1 = Point { x: 1, y: 2 };
10 let p2 = p1; // 암묵적 Copy (.clone() 불필요)
11 println!("{:?}", p1); // ✓ p1도 사용 가능
12 println!("{:?}", p2);
13}
⚠️ Copy는 Clone과 함께 (supertrait 관계)
CopyClone의 특수 버전이라서 혼자 쓸 수 없습니다. 반드시 #[derive(Copy, Clone)]로 같이 써야 합니다. 조건: 모든 필드가 Copy여야 가능 (String, Vec 등 힙 소유 타입이 하나라도 있으면 불가).
예제 8-D4: PartialEq== 비교
Rust
1#[derive(Debug, PartialEq)]
2struct Book {
3 title: String,
4 pages: u32,
5}
6
7fn main() {
8 let b1 = Book { title: String::from("Rust"), pages: 300 };
9 let b2 = Book { title: String::from("Rust"), pages: 300 };
10 let b3 = Book { title: String::from("Go"), pages: 500 };
11
12 // == 와 != 사용 가능 (필드별 비교)
13 println!("b1 == b2: {}", b1 == b2); // true
14 println!("b1 == b3: {}", b1 == b3); // false
15}
📋 필드 구성별 derive 선택 가이드
필드 구성권장 derive복사 방식
모든 필드 primitive (i32, bool 등) #[derive(Debug, Copy, Clone, PartialEq)] 암묵적 (move 없음)
String, Vec 등 포함 #[derive(Debug, Clone, PartialEq)] 명시적 .clone()
가변 참조 &mut T 포함 #[derive(Clone)] (Copy 불가) 명시적 .clone()
복제 안 하고 싶음 (move만) derive 생략 move만 가능
🎯 자주 쓰는 derive 트레잇
트레잇자동 생성되는 기능사용 예
Debug디버그 출력println!("{:?}", x)
Clone명시적 복사let y = x.clone()
Copy암묵적 복사 (Clone 필수)let y = x; (원본 유지)
PartialEq, Eq== != 비교if a == b {}
Hash해시값 계산HashMap/HashSet
Default기본값 생성Book::default()
Ord, PartialOrd크기 비교, 정렬.sort() 가능
✅ 실전 패턴
  • 99%의 구조체에는 #[derive(Debug)] 추가 (디버깅 편의)
  • 함수 인자로 복사해서 넘기거나 여러 곳에 저장할 필요가 있으면 Clone 추가
  • HashMap 키로 쓰려면 Hash + Eq (PartialEq도 자동) 추가
  • 수동 impl특수 로직이 필요할 때만. 대부분 derive로 충분.

9. enum과 패턴 매칭 ⭐

Rust의 enum은 Java의 enum과 이름만 같고 훨씬 강력합니다. 각 variant가 서로 다른 데이터를 가질 수 있는 대수적 데이터 타입(ADT)입니다. 함수형 언어의 강점을 Rust에 가져온 핵심 기능입니다.

Part 1. 기본 enum
예제 9-1: 단순 enum
Java
1public class Main {
2 enum Status { AVAILABLE, BORROWED, LOST }
3
4 public static void main(String[] args) {
5 Status s = Status.BORROWED;
6 switch (s) {
7 case AVAILABLE -> System.out.println("Available");
8 case BORROWED -> System.out.println("Borrowed");
9 case LOST -> System.out.println("Lost");
10 }
11 }
12}
Rust
1enum Status {
2 Available,
3 Borrowed,
4 Lost,
5}
6
7fn main() {
8 let s = Status::Borrowed;
9 match s {
10 Status::Available => println!("보유"),
11 Status::Borrowed => println!("대여중"),
12 Status::Lost => println!("분실"),
13 }
14}
Part 2. 데이터를 가진 enum (Java에 없는 강력한 기능)

각 variant가 서로 다른 형태의 데이터를 가질 수 있습니다. 이것이 Rust enum의 진짜 힘입니다.

예제 9-2: variant별 다른 데이터
Rust
1// 각 variant가 다른 데이터를 가질 수 있음
2enum Event {
3 Borrowed(String), // 대여자 이름
4 Returned, // 데이터 없음
5 Damaged { page: i32 }, // 구조체 형태
6}
7
8fn main() {
9 let e = Event::Borrowed(String::from("Alice"));
10 match e {
11 Event::Borrowed(name) => println!("대여: {name}"),
12 Event::Returned => println!("반납"),
13 Event::Damaged { page } => println!("{page}쪽 손상"),
14 }
15}
대응JavaRust
단순 enumenum Status { A, B, C }enum Status { A, B, C }
데이터 포함Java 16+ sealed + recordenum E { V1(String), V2 { x: i32 } } (기본)
패턴 매칭Java 21+ switch expressionmatch (기본, 항상 완전성 보장)
Part 3. match - 강력한 패턴 매칭

match는 Java의 switch보다 훨씬 강력합니다. 모든 경우를 처리하지 않으면 컴파일 에러입니다 (완전성 검사).

예제 9-3: 여러 값, 범위
Rust
1fn main() {
2 let day = 3;
3 let name = match day {
4 1 | 7 => "주말", // 여러 값
5 2..=6 => "평일", // 범위
6 _ => "잘못된 입력", // 기본값 (default)
7 };
8 println!("{day}일: {name}");
9}
Part 4. if let - 간결한 단일 매칭

match에서 하나의 variant만 관심 있을 때 사용하는 간결한 문법입니다.

예제 9-4: if let
Rust
1enum Status { Available, Borrowed(String) }
2
3fn main() {
4 let s = Status::Borrowed(String::from("Alice"));
5
6 // match보다 간결
7 if let Status::Borrowed(name) = s {
8 println!("대여자: {name}");
9 } else {
10 println!("다른 상태");
11 }
12}
예제 9-5: while let
Rust
1fn main() {
2 let mut stack = vec![1, 2, 3];
3
4 // pop()이 Some을 반환하는 동안 계속
5 while let Some(top) = stack.pop() {
6 println!("pop: {top}");
7 }
8}
Part 5. let else (Rust 1.65+)

"성공이 아니면 조기 반환"이라는 흔한 패턴을 위한 문법입니다.

예제 9-6: let ... else
Rust
1enum Result2 { Ok(i32), Err(String) }
2
3fn process(r: Result2) -> i32 {
4 // 성공이 아니면 조기 반환
5 let Result2::Ok(n) = r else {
6 println!("에러 발생");
7 return -1;
8 };
9 n * 2
10}
11
12fn main() {
13 println!("{}", process(Result2::Ok(10))); // 20
14 println!("{}", process(Result2::Err("실패".into()))); // -1
15}
Part 6. 고급 패턴
예제 9-7: match guard (if 조건 추가)
Rust
1enum Book { Paper(String, i32), Digital(String) }
2
3fn main() {
4 let b = Book::Paper(String::from("Rust"), 300);
5 match b {
6 Book::Paper(title, p) if p > 200 => {
7 println!("두꺼운 책: {title}");
8 }
9 Book::Paper(title, _) => println!("얇은 책: {title}"),
10 Book::Digital(title) => println!("전자책: {title}"),
11 }
12}
예제 9-8: 구조체 분해
Rust
1struct Book { title: String, pages: i32 }
2
3fn main() {
4 let b = Book { title: String::from("Rust"), pages: 300 };
5 let Book { title, pages } = b; // 분해
6 println!("{title}: {pages}p");
7}
Part 7. 도서 상태 시스템 (심화)
예제 9-9: 복잡한 상태 enum
Java
1public class Main {
2 sealed interface BookState
3 permits OnShelf, Borrowed, Lost, Reserved {}
4 record OnShelf() implements BookState {}
5 record Borrowed(String by, int days) implements BookState {}
6 record Lost() implements BookState {}
7 record Reserved(String name) implements BookState {}
8
9 static String describe(BookState s) {
10 return switch (s) {
11 case OnShelf os -> "On shelf";
12 case Borrowed b -> b.by() + " " + b.days() + " days borrowed";
13 case Lost l -> "Lost";
14 case Reserved r -> r.name() + " reserved";
15 };
16 }
17
18 public static void main(String[] args) {
19 BookState s1 = new OnShelf();
20 BookState s2 = new Borrowed("Alice", 5);
21 BookState s3 = new Reserved("Bob");
22 System.out.println("1: " + describe(s1));
23 System.out.println("2: " + describe(s2));
24 System.out.println("3: " + describe(s3));
25 }
26}
Rust
1#[derive(Debug)]
2enum BookState {
3 OnShelf,
4 Borrowed { by: String, days: i32 },
5 Lost,
6 Reserved(String), // 예약자 이름
7}
8
9fn describe(state: &BookState) -> String {
10 match state {
11 BookState::OnShelf => "서가에 있음".to_string(),
12 BookState::Borrowed { by, days } => {
13 format!("{by}가 {days}일째 대여중")
14 }
15 BookState::Lost => "분실됨".to_string(),
16 BookState::Reserved(name) => format!("{name}가 예약함"),
17 }
18}
19
20fn main() {
21 let s1 = BookState::OnShelf;
22 let s2 = BookState::Borrowed { by: "Alice".into(), days: 5 };
23 let s3 = BookState::Reserved("Bob".into());
24 println!("1: {}", describe(&s1));
25 println!("2: {}", describe(&s2));
26 println!("3: {}", describe(&s3));
27}
💡 Java 21 sealed interface와의 비교
Java 21의 sealed interface + switch expression은 Rust enum과 거의 동등한 표현력을 가지지만, Rust는 처음부터 이렇게 설계되었고 훨씬 간결합니다.

10. Option<T> - Null의 안전한 대체

Rust에는 null이 없습니다. 대신 Option<T> enum으로 "값이 있을 수도, 없을 수도" 를 타입 시스템에 명시합니다. Java의 Optional<T>와 거의 같은 개념이지만 강제성이 다릅니다.

구분JavaRust
없음 표현null 또는 Optional.empty()None
있음 표현일반 객체 또는 Optional.of(v)Some(v)
NPE 가능?가능 (null 참조 시)불가능 (Option 안 풀고 접근 불가)
Optional 강제?선택 (개발자 판단)강제 (nullable은 무조건 Option)
Part 1. Option 기본
예제 10-1: Some과 None
Java
1import java.util.Optional;
2public class Main {
3 public static void main(String[] args) {
4 Optional<Integer> some = Optional.of(10);
5 Optional<Integer> none = Optional.empty();
6 System.out.println(some + " / " + none);
7 }
8}
Rust
1// Option<T>는 enum { Some(T), None }
2fn main() {
3 let some_n: Option<i32> = Some(10);
4 let none_n: Option<i32> = None;
5 println!("{:?} / {:?}", some_n, none_n);
6}
예제 10-2: Option을 반환하는 함수
Rust
1fn find_book(id: i32) -> Option<String> {
2 if id == 1 { Some(String::from("Rust Book")) }
3 else { None }
4}
5
6fn main() {
7 match find_book(1) {
8 Some(title) => println!("찾음: {title}"),
9 None => println!("없음"),
10 }
11}
Part 2. Option 값 꺼내기
예제 10-3: unwrap / expect (위험)
Rust
1fn main() {
2 let some: Option<i32> = Some(10);
3
4 println!("{}", some.unwrap()); // 10 (안전)
5 println!("{}", some.expect("실패")); // 10 (안전)
6
7 // let none: Option<i32> = None;
8 // none.unwrap(); // ❌ 패닉!
9}
⚠️ unwrap()은 None일 때 panic
프로덕션 코드에서는 unwrap()을 피하고 match, unwrap_or, ? 연산자 등 안전한 방법을 쓰세요. unwrap()은 "절대 None일 리 없다"가 증명된 경우나 프로토타입에만 사용.
예제 10-4: 안전한 기본값 처리
Rust
1fn main() {
2 let none: Option<i32> = None;
3
4 let a = none.unwrap_or(0); // None → 0
5 let b = none.unwrap_or_else(|| 42); // 클로저로 계산
6 println!("{a} / {b}");
7}
메서드None일 때Java Optional 대응
unwrap()panic!.get() (NoSuchElement)
expect("msg")panic! + 메시지.orElseThrow()
unwrap_or(default)기본값 반환.orElse(default)
unwrap_or_else(||...)클로저로 계산.orElseGet(() -> ...)
Part 3. map과 체이닝

Option 안의 값을 변환하는 가장 흔한 방법.

예제 10-5: map
Rust
1fn main() {
2 let page: Option<i32> = Some(100);
3
4 // Some(100) → Some(200)
5 let doubled = page.map(|p| p * 2);
6 println!("{:?}", doubled);
7
8 // None → None (map은 None일 때 아무것도 안 함)
9 let none: Option<i32> = None;
10 println!("{:?}", none.map(|p| p * 2));
11}
Part 4. ? 연산자 맛보기

?는 Option이나 Result에서 "None/Err이면 즉시 반환, 아니면 값 추출"을 한 글자로 해줍니다. 섹션 12에서 자세히.

예제 10-6: ?로 체이닝
Rust
1fn find_book(id: i32) -> Option<String> {
2 if id == 1 { Some(String::from("Rust Book")) } else { None }
3}
4
5fn get_title_len(id: i32) -> Option<usize> {
6 let title = find_book(id)?; // None이면 즉시 반환
7 Some(title.len())
8}
9
10fn main() {
11 println!("{:?}", get_title_len(1)); // Some(9)
12 println!("{:?}", get_title_len(2)); // None
13}
Part 5. 도서 검색 시스템 (심화)
예제 10-7: find로 Option 반환
Java
1import java.util.*;
2public class Main {
3 record Book(int id, String title) {}
4
5 static Optional<Book> find(List<Book> books, int id) {
6 return books.stream().filter(b -> b.id() == id).findFirst();
7 }
8
9 public static void main(String[] args) {
10 List<Book> books = List.of(
11 new Book(1, "Rust"),
12 new Book(2, "Go")
13 );
14
15 find(books, 1).ifPresentOrElse(
16 b -> System.out.println("Found: " + b.title()
17 + " (len " + b.title().length() + ")"),
18 () -> System.out.println("Not found")
19 );
20
21 int len = find(books, 99)
22 .map(b -> b.title().length())
23 .orElse(0);
24 System.out.println("ID 99 title len: " + len);
25 }
26}
Rust
1struct Book { id: i32, title: String }
2
3fn find(books: &[Book], id: i32) -> Option<&Book> {
4 for b in books {
5 if b.id == id { return Some(b); }
6 }
7 None
8}
9
10fn main() {
11 let books = vec![
12 Book { id: 1, title: String::from("Rust") },
13 Book { id: 2, title: String::from("Go") },
14 ];
15
16 match find(&books, 1) {
17 Some(b) => println!("찾음: {} (길이 {})", b.title, b.title.len()),
18 None => println!("없음"),
19 }
20
21 // map으로 체이닝
22 let len = find(&books, 99).map(|b| b.title.len()).unwrap_or(0);
23 println!("99번 제목 길이: {len}");
24}

11. Result<T, E> - 예외 없는 에러 처리

Rust는 예외(exception)가 없습니다. 대신 에러 가능성을 타입으로 표현합니다. Java throws 선언보다 강력하고, 호출자가 에러 처리를 반드시 고려하게 만듭니다.

개념JavaRust
에러 발생throw new IOException(...)Err(...) 반환
성공 반환그냥 값 반환Ok(...)
에러 전파throws 선언? 연산자
처리 강제checked exception만항상 강제
런타임 비용스택 언와인딩제로 (일반 반환과 동일)
Part 1. Result 기본
예제 11-1: Ok와 Err
Rust
1// Result<T, E>는 enum { Ok(T), Err(E) }
2fn divide(a: i32, b: i32) -> Result<i32, String> {
3 if b == 0 { Err(String::from("0으로 나눔")) }
4 else { Ok(a / b) }
5}
6
7fn main() {
8 println!("{:?}", divide(10, 2)); // Ok(5)
9 println!("{:?}", divide(10, 0)); // Err("0으로 나눔")
10}
예제 11-2: match로 처리
Rust
1fn parse_int(s: &str) -> Result<i32, String> {
2 s.parse::<i32>().map_err(|e| e.to_string())
3}
4
5fn main() {
6 match parse_int("42") {
7 Ok(n) => println!("숫자: {n}"),
8 Err(e) => println!("에러: {e}"),
9 }
10 match parse_int("abc") {
11 Ok(n) => println!("숫자: {n}"),
12 Err(e) => println!("에러: {e}"),
13 }
14}
Part 2. 커스텀 에러 타입

실전에서는 에러 종류를 enum으로 정의합니다.

예제 11-3: enum 기반 에러
Rust
1#[derive(Debug)]
2enum BookError {
3 NotFound,
4 Unavailable,
5}
6
7fn borrow_book(id: i32, available: bool) -> Result<String, BookError> {
8 if id != 1 { Err(BookError::NotFound) }
9 else if !available { Err(BookError::Unavailable) }
10 else { Ok(String::from("Rust Book")) }
11}
12
13fn main() {
14 println!("{:?}", borrow_book(1, true)); // Ok
15 println!("{:?}", borrow_book(1, false)); // Err(Unavailable)
16 println!("{:?}", borrow_book(99, true)); // Err(NotFound)
17}
Part 3. 안전한 꺼내기
예제 11-4: unwrap_or와 기본값
Rust
1fn main() {
2 let ok: Result<i32, String> = Ok(10);
3 println!("{}", ok.unwrap()); // 10
4
5 let err: Result<i32, String> = Err(String::from("실패"));
6 println!("{}", err.unwrap_or(0)); // 0 (기본값)
7 // err.unwrap(); // ❌ 패닉!
8}
Part 4. 값/에러 변환
예제 11-5: map / map_err
Rust
1fn main() {
2 let r: Result<i32, String> = Ok(10);
3
4 // 값을 변환 (에러는 그대로)
5 let doubled = r.map(|n| n * 2);
6 println!("{:?}", doubled); // Ok(20)
7
8 // 에러 타입 변환
9 let e: Result<i32, String> = Err("a".into());
10 let e2 = e.map_err(|s| format!("에러: {s}"));
11 println!("{:?}", e2); // Err("에러: a")
12}
Part 5. 도서관 예약 시스템 (심화)
예제 11-6: 다양한 에러 케이스
Java
1public class Main {
2 sealed interface LibResult permits Success, Failure {}
3 record Success(String msg) implements LibResult {}
4 record Failure(String error) implements LibResult {}
5
6 static LibResult borrow(int id, int count) {
7 if (id < 1) return new Failure("BookNotFound(" + id + ")");
8 if (count >= 5) return new Failure("MaxExceeded");
9 if (id == 2) return new Failure("AlreadyBorrowed");
10 return new Success("Book " + id + " borrow complete");
11 }
12
13 public static void main(String[] args) {
14 int[][] cases = {{1,0},{2,0},{99,0},{3,5}};
15 for (int[] c : cases) {
16 LibResult r = borrow(c[0], c[1]);
17 if (r instanceof Success s)
18 System.out.println("[OK] " + s.msg());
19 else if (r instanceof Failure f)
20 System.out.println("[X] " + f.error());
21 }
22 }
23}
Rust
1#[derive(Debug)]
2enum LibError {
3 BookNotFound(i32),
4 AlreadyBorrowed,
5 MaxExceeded,
6}
7
8fn borrow_book(
9 id: i32,
10 total_borrowed: i32,
11) -> Result<String, LibError> {
12 // 유효 ID: 1~10만 존재
13 if id < 1 || id > 10 {
14 return Err(LibError::BookNotFound(id));
15 }
16 if total_borrowed >= 5 { return Err(LibError::MaxExceeded); }
17 if id == 2 { return Err(LibError::AlreadyBorrowed); }
18 Ok(format!("도서 {id} 대여 완료"))
19}
20
21fn main() {
22 for (id, count) in [(1, 0), (2, 0), (99, 0), (3, 5)] {
23 match borrow_book(id, count) {
24 Ok(msg) => println!("✓ {msg}"),
25 Err(e) => println!("✗ {:?}", e),
26 }
27 }
28}
✅ Option vs Result 구분
  • Option<T>: "있을 수도, 없을 수도" — 단순 부재 (검색 결과 없음 등)
  • Result<T, E>: "성공 또는 실패" — 실패 이유가 중요한 경우 (파일 없음, 파싱 실패 등)

12. 에러 처리 — ? 연산자

Result/Option을 매번 match로 풀면 코드가 장황해집니다. Rust는 ? 연산자로 이를 한 글자로 해결합니다.

Part 1. ? 없이 - 장황한 에러 처리
예제 12-1: match 중첩
Rust
1fn parse_two(a: &str, b: &str) -> Result<i32, std::num::ParseIntError> {
2 let x = match a.parse::<i32>() {
3 Ok(n) => n,
4 Err(e) => return Err(e),
5 };
6 let y = match b.parse::<i32>() {
7 Ok(n) => n,
8 Err(e) => return Err(e),
9 };
10 Ok(x + y)
11}
12
13fn main() {
14 println!("{:?}", parse_two("3", "5")); // Ok(8)
15 println!("{:?}", parse_two("3", "abc")); // Err
16}
Part 2. ? 연산자

?는 "Err이면 즉시 반환, Ok이면 값 추출"을 한 글자로 합니다.

예제 12-2: ? 적용
Rust
1fn parse_two(a: &str, b: &str) -> Result<i32, std::num::ParseIntError> {
2 let x = a.parse::<i32>()?; // 에러면 즉시 반환
3 let y = b.parse::<i32>()?;
4 Ok(x + y)
5}
6
7fn main() {
8 println!("{:?}", parse_two("3", "5"));
9 println!("{:?}", parse_two("3", "abc"));
10}
예제 12-3: ? 는 Option에도 사용
Rust
1fn get_first_char(s: &str) -> Option<char> {
2 let first = s.chars().next()?; // None이면 즉시 반환
3 Some(first.to_ascii_uppercase())
4}
5
6fn main() {
7 println!("{:?}", get_first_char("rust")); // Some('R')
8 println!("{:?}", get_first_char("")); // None
9}
Part 3. From trait으로 에러 자동 변환

여러 종류의 에러를 한 타입으로 통합할 때 From trait을 구현하면 ?가 자동 변환을 해줍니다.

예제 12-4: From 구현으로 자동 변환
Rust
1use std::num::ParseIntError;
2
3#[derive(Debug)]
4enum MyError {
5 ParseFailed(String),
6}
7
8// ParseIntError를 MyError로 자동 변환
9impl From<ParseIntError> for MyError {
10 fn from(e: ParseIntError) -> Self {
11 MyError::ParseFailed(e.to_string())
12 }
13}
14
15fn double(s: &str) -> Result<i32, MyError> {
16 let n: i32 = s.parse()?; // ParseIntError → MyError 자동 변환
17 Ok(n * 2)
18}
19
20fn main() {
21 println!("{:?}", double("10")); // Ok(20)
22 println!("{:?}", double("abc")); // Err(ParseFailed(...))
23}
Part 4. panic! - 복구 불가능 에러

진짜로 계속 실행할 수 없는 상황에서만 사용합니다. Java의 Error(RuntimeException 아님) 수준.

예제 12-5: panic!
Rust
1fn main() {
2 let n: i32 = 10;
3 if n < 0 {
4 panic!("음수는 허용되지 않음: {n}");
5 }
6 println!("OK: {n}");
7 // panic! 발생하면 프로그램 즉시 종료
8}
⚠️ 언제 panic vs Result?
  • panic!: 버그, 불변 조건 위반, 절대 발생하면 안 되는 상황
  • Result: 사용자 입력 오류, 파일 없음, 네트워크 실패 등 복구 가능한 상황
Part 5. 종합 에러 처리 (심화)
예제 12-6: ? 체이닝
Java
1public class Main {
2 enum LibError { INVALID_ID, NOT_FOUND }
3
4 record Result<T>(T value, LibError error) {
5 static <T> Result<T> ok(T v) { return new Result<>(v, null); }
6 static <T> Result<T> err(LibError e) { return new Result<>(null, e); }
7 boolean isOk() { return error == null; }
8 }
9
10 static Result<Integer> parseId(String s) {
11 try {
12 return Result.ok(Integer.parseInt(s));
13 } catch (NumberFormatException e) {
14 return Result.err(LibError.INVALID_ID);
15 }
16 }
17
18 static Result<String> findTitle(int id) {
19 if (id == 1) return Result.ok("Rust Book");
20 return Result.err(LibError.NOT_FOUND);
21 }
22
23 static Result<String> lookup(String s) {
24 Result<Integer> r1 = parseId(s);
25 if (!r1.isOk()) return Result.err(r1.error());
26 Result<String> r2 = findTitle(r1.value());
27 if (!r2.isOk()) return Result.err(r2.error());
28 return Result.ok("[" + r1.value() + "] " + r2.value());
29 }
30
31 public static void main(String[] args) {
32 for (String input : new String[]{"1", "99", "abc"}) {
33 Result<String> r = lookup(input);
34 if (r.isOk()) System.out.println("✓ " + r.value());
35 else System.out.println("✗ " + r.error());
36 }
37 }
38}
Rust
1#[derive(Debug)]
2enum LibError {
3 InvalidId,
4 NotFound,
5}
6
7fn parse_id(s: &str) -> Result<i32, LibError> {
8 s.parse::<i32>().map_err(|_| LibError::InvalidId)
9}
10
11fn find_title(id: i32) -> Result<String, LibError> {
12 match id {
13 1 => Ok(String::from("Rust Book")),
14 _ => Err(LibError::NotFound),
15 }
16}
17
18// ? 연산자로 체이닝
19fn lookup(s: &str) -> Result<String, LibError> {
20 let id = parse_id(s)?; // 파싱 실패 시 즉시 반환
21 let title = find_title(id)?; // 검색 실패 시 즉시 반환
22 Ok(format!("[{id}] {title}"))
23}
24
25fn main() {
26 for input in ["1", "99", "abc"] {
27 match lookup(input) {
28 Ok(s) => println!("✓ {s}"),
29 Err(e) => println!("✗ {:?}", e),
30 }
31 }
32}
💡 Java try/catch와의 비교
? 연산자는 Java의 try/catch보다 명시적이고 간결합니다. 에러는 일반 값처럼 흐르고, 스택 언와인딩 같은 런타임 비용이 없습니다.

13. 제네릭 (Generics)

Rust 제네릭은 Java와 문법은 비슷하지만 내부 동작이 완전히 다릅니다. Java는 타입 소거(erasure), Rust는 단형화(monomorphization)입니다.

항목JavaRust
방식타입 소거 (erasure)단형화 (monomorphization)
런타임모두 Object로 변환타입별 별도 함수 생성
성능박싱/언박싱 비용제로 비용 추상화
코드 크기작음 (공유)큼 (타입별 복제)
타입 정보런타임엔 소거됨컴파일 시점에 확정
Part 1. 제네릭 함수
예제 13-1: largest 함수
Java
1import java.util.*;
2public class Main {
3 static <T extends Comparable<T>> T largest(List<T> list) {
4 T largest = list.get(0);
5 for (T item : list) {
6 if (item.compareTo(largest) > 0) largest = item;
7 }
8 return largest;
9 }
10
11 public static void main(String[] args) {
12 List<Integer> nums = List.of(10, 25, 3, 47, 8);
13 List<String> words = List.of("apple", "pear", "banana");
14 System.out.println("max: " + largest(nums));
15 System.out.println("max: " + largest(words));
16 }
17}
Rust
1fn largest<T: PartialOrd>(list: &[T]) -> &T {
2 let mut largest = &list[0];
3 for item in list {
4 if item > largest { largest = item; }
5 }
6 largest
7}
8
9fn main() {
10 let nums = vec![10, 25, 3, 47, 8];
11 let words = vec!["apple", "pear", "banana"];
12 println!("최대: {}", largest(&nums));
13 println!("최대: {}", largest(&words));
14}
Part 2. 제네릭 구조체와 메서드
예제 13-2: 제네릭 구조체
Rust
1struct Pair<T, U> {
2 first: T,
3 second: U,
4}
5
6fn main() {
7 let p1 = Pair { first: 10, second: "열" };
8 let p2 = Pair { first: 3.14, second: true };
9 println!("{}={} / {}={}", p1.first, p1.second, p2.first, p2.second);
10}
예제 13-3: 제네릭 메서드
Rust
1struct Container<T> { value: T }
2
3impl<T: std::fmt::Display> Container<T> {
4 fn show(&self) {
5 println!("값: {}", self.value);
6 }
7}
8
9fn main() {
10 Container { value: 42 }.show();
11 Container { value: "hello" }.show();
12}
Part 3. trait bound - 타입 제약

제네릭 T에 조건을 붙입니다. Java extends Comparable과 유사.

예제 13-4: 단일 trait bound
Rust
1use std::fmt::Display;
2
3// T는 Display를 구현한 타입만
4fn announce<T: Display>(item: T) {
5 println!("🎉 발표: {item}");
6}
7
8fn main() {
9 announce(100);
10 announce("Rust Book");
11 announce(3.14);
12}
예제 13-5: where 절로 복잡한 bound
Rust
1use std::fmt::{Debug, Display};
2
3// 복잡한 bound는 where 절로 (읽기 편함)
4fn process<T>(a: T, b: T)
5where
6 T: Display + Debug + PartialOrd,
7{
8 if a > b {
9 println!("{a} > {b} (debug: {:?})", b);
10 } else {
11 println!("{a} <= {b} (debug: {:?})", a);
12 }
13}
14
15fn main() {
16 process(10, 20);
17 process("apple", "banana");
18}
Part 4. 단형화 - 제로 비용 추상화

Rust는 제네릭 함수를 타입별로 별도 컴파일합니다. 런타임엔 제네릭이 없고, 일반 함수와 동일한 속도입니다.

예제 13-6: double<T>
Rust
1// 컴파일러가 T별로 별도 함수를 생성 (제로 비용 추상화)
2fn double<T: std::ops::Add<Output = T> + Copy>(x: T) -> T {
3 x + x
4}
5
6fn main() {
7 // i32용 double, f64용 double 함수가 각각 만들어짐
8 println!("{}", double(10)); // 20
9 println!("{}", double(3.14)); // 6.28
10}
💡 컴파일러가 하는 일
double(10) 호출 → 컴파일러가 double_i32(x: i32) 함수를 생성
double(3.14) 호출 → 컴파일러가 double_f64(x: f64) 함수를 생성
→ 런타임 오버헤드 0, 하지만 바이너리 크기는 증가
Part 5. 도서관 컨테이너 (심화)
예제 13-7: 제네릭 Shelf
Rust
1// 어떤 타입이든 보관 가능한 책장
2struct Shelf<T> {
3 items: Vec<T>,
4}
5
6impl<T: std::fmt::Debug> Shelf<T> {
7 fn new() -> Shelf<T> {
8 Shelf { items: Vec::new() }
9 }
10 fn add(&mut self, item: T) {
11 self.items.push(item);
12 }
13 fn show(&self) {
14 for item in &self.items {
15 println!("📦 {:?}", item);
16 }
17 }
18}
19
20#[derive(Debug)]
21struct Book { title: String }
22
23fn main() {
24 // String 책장
25 let mut s1: Shelf<String> = Shelf::new();
26 s1.add("Rust".to_string());
27 s1.add("Go".to_string());
28 s1.show();
29
30 // Book 책장
31 let mut s2: Shelf<Book> = Shelf::new();
32 s2.add(Book { title: "Rust Book".to_string() });
33 s2.show();
34}

14. 트레잇 (Trait) ⭐

트레잇은 Java의 interface에 해당합니다. 하지만 상속 없는 Rust에서 유일한 다형성 수단이고, 기본 구현정적/동적 디스패치 선택이 가능합니다.

Part 1. trait 정의와 구현
예제 14-1: 기본 trait
Java
1public class Main {
2 interface Summary {
3 String summarize();
4 }
5
6 static class Book implements Summary {
7 String title, author;
8 Book(String t, String a) { title = t; author = a; }
9 public String summarize() {
10 return title + " by " + author;
11 }
12 }
13
14 public static void main(String[] args) {
15 Book b = new Book("Rust Book", "Steve");
16 System.out.println(b.summarize());
17 }
18}
Rust
1trait Summary {
2 fn summarize(&self) -> String;
3}
4
5struct Book { title: String, author: String }
6
7impl Summary for Book {
8 fn summarize(&self) -> String {
9 format!("{} by {}", self.title, self.author)
10 }
11}
12
13fn main() {
14 let b = Book {
15 title: "Rust Book".into(),
16 author: "Steve".into(),
17 };
18 println!("{}", b.summarize());
19}
용어JavaRust
정의interface I { ... }trait T { ... }
구현class C implements Iimpl T for C { ... }
기본 구현Java 8+ default모든 메서드에 기본 구현 가능
다중 구현여러 interface여러 trait
상속extends (단일)없음 (조합으로 대체)
Part 2. 기본 메서드 구현

Java 8의 default 메서드와 유사.

예제 14-2: 기본 구현
Rust
1trait Summary {
2 fn title(&self) -> String;
3 // 기본 구현 제공 (Java interface default 메서드)
4 fn preview(&self) -> String {
5 format!("{}", self.title())
6 }
7}
8
9struct Book { name: String }
10
11impl Summary for Book {
12 fn title(&self) -> String { self.name.clone() }
13 // preview는 오버라이드 안 함 → 기본 사용
14}
15
16fn main() {
17 let b = Book { name: "Rust".into() };
18 println!("{}", b.preview()); // Rust
19}
Part 3. trait을 파라미터로 (정적 디스패치)
예제 14-3: impl Trait 문법
Rust
1trait Display { fn show(&self); }
2
3struct Book;
4struct Magazine;
5
6impl Display for Book { fn show(&self) { println!("Book"); } }
7impl Display for Magazine { fn show(&self) { println!("Magazine"); } }
8
9// impl Trait 문법 - 제네릭의 간결한 형태
10fn print(item: &impl Display) {
11 item.show();
12}
13
14fn main() {
15 print(&Book);
16 print(&Magazine);
17}
Part 4. dyn Trait (동적 디스패치) ⭐

Java의 일반 다형성은 항상 동적 디스패치(vtable)입니다. Rust는 선택할 수 있습니다.

방식언제?비용
impl Trait / 제네릭타입이 하나로 확정됨런타임 비용 0 (단형화)
dyn Trait런타임에 타입 결정vtable 조회 (Java와 동일)
예제 14-4: dyn Trait
Rust
1trait Display { fn show(&self); }
2
3struct Book;
4struct Magazine;
5impl Display for Book { fn show(&self) { println!("Book"); } }
6impl Display for Magazine { fn show(&self) { println!("Magazine"); } }
7
8fn main() {
9 // dyn Trait = 런타임에 타입 결정 (vtable)
10 let items: Vec<Box<dyn Display>> = vec![
11 Box::new(Book),
12 Box::new(Magazine),
13 ];
14 for item in &items {
15 item.show(); // 각자의 show 호출
16 }
17}
Part 5. trait bound로 제네릭 제약
예제 14-5: 제네릭 + trait
Rust
1use std::fmt::Display;
2
3// T는 Display를 구현해야 함
4fn largest<T: PartialOrd + Display>(list: &[T]) -> &T {
5 let mut largest = &list[0];
6 for item in list {
7 if item > largest { largest = item; }
8 }
9 println!("최대값 계산 중...");
10 largest
11}
12
13fn main() {
14 let nums = vec![10, 25, 3, 47, 8];
15 println!("결과: {}", largest(&nums));
16}
Part 6. #[derive] - 자동 구현
예제 14-6: 표준 trait 자동 구현
Rust
1#[derive(Debug, Clone, PartialEq)]
2struct Book { title: String, pages: i32 }
3
4fn main() {
5 let b1 = Book { title: "Rust".into(), pages: 300 };
6 let b2 = b1.clone(); // Clone
7 println!("{:?}", b1); // Debug
8 println!("같음: {}", b1 == b2); // PartialEq
9}
Part 7. 여러 trait 구현
예제 14-7: 한 타입에 여러 trait
Rust
1trait Readable { fn read(&self); }
2trait Lendable { fn lend(&self); }
3
4struct Book;
5
6impl Readable for Book {
7 fn read(&self) { println!("읽는중"); }
8}
9impl Lendable for Book {
10 fn lend(&self) { println!("📤 대여중"); }
11}
12
13fn main() {
14 let b = Book;
15 b.read();
16 b.lend();
17}
Part 8. trait 객체 반환
예제 14-8: impl vs Box<dyn>
Rust
1trait Animal { fn name(&self) -> String; }
2struct Dog; struct Cat;
3impl Animal for Dog { fn name(&self) -> String { "멍멍".into() } }
4impl Animal for Cat { fn name(&self) -> String { "야옹".into() } }
5
6// impl Trait 반환 - 정적 (단일 타입)
7fn make_dog() -> impl Animal { Dog }
8
9// Box<dyn Trait> 반환 - 동적 (여러 타입 가능)
10fn make(kind: &str) -> Box<dyn Animal> {
11 if kind == "dog" { Box::new(Dog) } else { Box::new(Cat) }
12}
13
14fn main() {
15 println!("{}", make_dog().name());
16 println!("{}", make("dog").name());
17 println!("{}", make("cat").name());
18}
Part 9. 도서관 카탈로그 (심화)
예제 14-9: Item trait + 3가지 구현
Rust
1trait Item {
2 fn name(&self) -> String;
3 fn category(&self) -> &str;
4 // 기본 구현
5 fn describe(&self) -> String {
6 format!("[{}] {}", self.category(), self.name())
7 }
8}
9
10struct Book { title: String }
11struct Magazine { title: String, issue: i32 }
12struct DVD { title: String }
13
14impl Item for Book {
15 fn name(&self) -> String { self.title.clone() }
16 fn category(&self) -> &str { "도서" }
17}
18impl Item for Magazine {
19 fn name(&self) -> String {
20 format!("{} #{}", self.title, self.issue)
21 }
22 fn category(&self) -> &str { "잡지" }
23}
24impl Item for DVD {
25 fn name(&self) -> String { self.title.clone() }
26 fn category(&self) -> &str { "영상" }
27}
28
29fn print_catalog(items: &[Box<dyn Item>]) {
30 for i in items { println!("{}", i.describe()); }
31}
32
33fn main() {
34 let items: Vec<Box<dyn Item>> = vec![
35 Box::new(Book { title: "Rust Book".into() }),
36 Box::new(Magazine { title: "Tech Weekly".into(), issue: 42 }),
37 Box::new(DVD { title: "Matrix".into() }),
38 ];
39 print_catalog(&items);
40}

15. 컬렉션 (Vec, HashMap, HashSet)

Rust 표준 라이브러리의 3가지 주요 컬렉션을 다룹니다. Java의 ArrayList, HashMap, HashSet에 대응합니다.

Part 1. Vec<T> - 동적 배열
예제 15-1: Vec 기본
Java
1import java.util.*;
2public class Main {
3 public static void main(String[] args) {
4 List<String> books = new ArrayList<>();
5 books.add("Rust");
6 books.add("Go");
7 books.add("Python");
8 System.out.println("total " + books.size() + " items: " + books);
9 System.out.println("first: " + books.get(0));
10 }
11}
Rust
1fn main() {
2 let mut books = Vec::new();
3 books.push("Rust");
4 books.push("Go");
5 books.push("Python");
6 println!("총 {}권: {:?}", books.len(), books);
7 println!("첫 번째: {}", books[0]);
8}
예제 15-2: vec! 매크로
Rust
1fn main() {
2 let books = vec!["Rust", "Go", "Python"]; // 초기화 매크로
3 let zeros = vec![0; 5]; // [0,0,0,0,0]
4 println!("{:?}", books);
5 println!("{:?}", zeros);
6}
작업JavaRust
생성new ArrayList<>()Vec::new() 또는 vec![]
추가.add(x).push(x)
접근.get(i)v[i] 또는 .get(i)
길이.size().len()
제거.remove(i).remove(i)
끝 제거.remove(size()-1).pop()
예제 15-3: Vec 순회
Rust
1fn main() {
2 let books = vec!["Rust", "Go", "Python"];
3
4 for b in &books { // 불변 참조로 순회
5 println!("{b}");
6 }
7 println!("총 {}권", books.len()); // books 여전히 유효
8}
예제 15-4: 수정/제거
Rust
1fn main() {
2 let mut nums = vec![1, 2, 3, 4, 5];
3
4 // iter_mut로 수정 순회
5 for n in nums.iter_mut() {
6 *n *= 10;
7 }
8 println!("{:?}", nums); // [10,20,30,40,50]
9
10 // pop(), remove()
11 nums.pop(); // 끝 제거
12 nums.remove(0); // 인덱스 0 제거
13 println!("{:?}", nums); // [20,30,40]
14}
💡 iter() / iter_mut() / into_iter()
이 3가지는 섹션 16에서 자세히 다룹니다. Rust 철학의 핵심입니다.
Part 2. HashMap<K, V>
예제 15-5: HashMap 기본
Java
1import java.util.*;
2public class Main {
3 public static void main(String[] args) {
4 Map<String, Integer> inv = new HashMap<>();
5 inv.put("Rust", 5);
6 inv.put("Go", 3);
7 inv.put("Python", 8);
8
9 if (inv.containsKey("Rust")) {
10 System.out.println("Rust stock: " + inv.get("Rust"));
11 }
12
13 inv.forEach((book, count) ->
14 System.out.println(book + ": " + count + " items"));
15 }
16}
Rust
1use std::collections::HashMap;
2
3fn main() {
4 let mut inventory = HashMap::new();
5 inventory.insert("Rust", 5);
6 inventory.insert("Go", 3);
7 inventory.insert("Python", 8);
8
9 // get() 반환: Option<&V>
10 if let Some(count) = inventory.get("Rust") {
11 println!("Rust 재고: {count}");
12 }
13
14 for (book, count) in &inventory {
15 println!("{book}: {count}권");
16 }
17}
⚠️ use 문 필요
HashMap은 기본으로 import되지 않습니다. use std::collections::HashMap; 필요.
예제 15-6: entry() 패턴
Rust
1use std::collections::HashMap;
2
3fn main() {
4 let text = "rust is fast rust is safe";
5 let mut counts: HashMap<&str, i32> = HashMap::new();
6
7 for word in text.split_whitespace() {
8 // entry().or_insert() 패턴
9 *counts.entry(word).or_insert(0) += 1;
10 }
11
12 for (word, count) in &counts {
13 println!("{word}: {count}");
14 }
15}
Part 3. HashSet<T>
예제 15-7: 중복 제거
Rust
1use std::collections::HashSet;
2
3fn main() {
4 let mut borrowed_ids: HashSet<i32> = HashSet::new();
5 borrowed_ids.insert(1);
6 borrowed_ids.insert(2);
7 borrowed_ids.insert(1); // 중복 - 무시됨
8
9 println!("대여중: {} 권", borrowed_ids.len()); // 2
10 println!("1번 대여중? {}", borrowed_ids.contains(&1));
11}
Part 4. 도서관 재고 관리 (심화)
예제 15-8: HashMap + Book
Rust
1use std::collections::HashMap;
2
3#[derive(Debug)]
4struct Book { title: String, count: i32 }
5
6fn main() {
7 let mut library: HashMap<i32, Book> = HashMap::new();
8
9 library.insert(1, Book { title: "Rust".into(), count: 5 });
10 library.insert(2, Book { title: "Go".into(), count: 3 });
11
12 // 대여 (count 감소)
13 if let Some(b) = library.get_mut(&1) {
14 b.count -= 1;
15 println!("대여: {} (남은 {})", b.title, b.count);
16 }
17
18 // 전체 재고
19 let total: i32 = library.values().map(|b| b.count).sum();
20 println!("총 재고: {total}권");
21}

16. 클로저와 Iterator ⭐

Java Stream API와 람다에 익숙한 분들에게 가장 비슷해 보이지만, Rust는 소유권이 클로저에 녹아 있어 훨씬 정교합니다. 이 섹션은 길지만 꼼꼼히 봐주세요.

Part 1. 클로저 기본

클로저는 주변 환경의 변수를 캡처할 수 있는 익명 함수입니다. Java 람다와 문법은 유사하지만 3가지 형태가 있습니다.

예제 16-1: 기본 문법
Rust
1fn main() {
2 // 클로저: |x| x + 1 (Java x -> x + 1)
3 let add_one = |x: i32| x + 1;
4 println!("{}", add_one(10)); // 11
5
6 // 여러 줄 클로저
7 let greet = |name: &str| {
8 let greeting = "안녕";
9 format!("{greeting}, {name}!")
10 };
11 println!("{}", greet("Rust"));
12}
구분JavaRust
기본 문법x -> x + 1|x| x + 1
타입 명시(int x) -> x + 1|x: i32| x + 1
여러 줄x -> { ...; return ...; }|x| { ...; expr }
외부 변수final만 캡처3가지 방식 (Fn/FnMut/FnOnce)
예제 16-2: 타입 추론
Rust
1fn main() {
2 let square = |x| x * x; // 타입 추론
3 let result = square(5); // i32로 확정
4 println!("{result}");
5 // square("hello"); // ❌ 이미 i32로 고정됨
6}
Part 2. 환경 캡처 3가지 방식 (Fn / FnMut / FnOnce) ⭐

클로저가 외부 변수를 어떻게 빌리는지에 따라 3가지 trait으로 자동 분류됩니다. 이것이 Rust 클로저의 핵심입니다.

Trait캡처 방식호출 횟수Java 대응
Fn&T (불변 참조)여러 번Function / Consumer
FnMut&mut T (가변 참조)여러 번 (상태 변경)상태 있는 람다
FnOnceT (소유권 이동)단 한 번Java에 직접 대응 없음
예제 16-3: Fn - 읽기만
Rust
1fn main() {
2 let prefix = String::new();
3 // prefix를 &로 빌림 (Fn)
4 let add_prefix = |s: &str| format!("{prefix}{s}");
5
6 println!("{}", add_prefix("Rust"));
7 println!("{}", add_prefix("Go")); // 여러 번 호출 OK
8 println!("원본: {prefix}"); // prefix 여전히 유효
9}
예제 16-4: FnMut - 수정
Rust
1fn main() {
2 let mut count = 0;
3 // count를 &mut로 빌림 (FnMut)
4 let mut increment = || {
5 count += 1;
6 println!("호출 #{count}");
7 };
8 increment();
9 increment();
10 increment();
11 println!("최종: {count}");
12}
예제 16-5: FnOnce - 소유권 이동 (move 키워드)
Rust
1fn main() {
2 let book = String::from("Rust Book");
3 // move 키워드로 소유권 이동 (FnOnce)
4 let consume = move || {
5 println!("소비: {book}");
6 book // book 반환 (소유권 이동)
7 };
8 let result = consume();
9 // consume(); // ❌ book 이미 이동
10 println!("결과: {result}");
11}
💡 자동 추론
개발자가 "이건 FnMut야"라고 명시할 필요는 없습니다. 클로저가 외부 변수를 어떻게 쓰는지 보고 컴파일러가 자동 결정합니다. 단, move 키워드로 강제로 소유권 이동을 지정할 수 있습니다 (스레드에 전달할 때 필수).
Part 3. 클로저를 함수 인자로

함수가 클로저를 받는 3가지 문법:

예제 16-6: 제네릭 / impl Trait
Rust
1// 제네릭 방식
2fn apply<F: Fn(i32) -> i32>(f: F, x: i32) -> i32 {
3 f(x)
4}
5
6// impl Trait 방식 (더 간결)
7fn apply2(f: impl Fn(i32) -> i32, x: i32) -> i32 {
8 f(x)
9}
10
11fn main() {
12 let double = |x| x * 2;
13 println!("{}", apply(double, 5)); // 10
14 println!("{}", apply2(|x| x + 1, 5)); // 6
15}
Part 4. 클로저를 반환하기

팩토리 패턴. Java의 Supplier<Function>에 해당하지만 Rust는 더 명시적입니다.

예제 16-7: impl Fn vs Box<dyn Fn>
Rust
1// impl Trait으로 반환 (정적)
2fn make_adder(n: i32) -> impl Fn(i32) -> i32 {
3 move |x| x + n
4}
5
6// Box<dyn Fn>으로 반환 (동적, 조건에 따라 다른 클로저)
7fn make_op(op: &str) -> Box<dyn Fn(i32) -> i32> {
8 match op {
9 "+1" => Box::new(|x| x + 1),
10 "*2" => Box::new(|x| x * 2),
11 _ => Box::new(|x| x),
12 }
13}
14
15fn main() {
16 let add5 = make_adder(5);
17 println!("{}", add5(10)); // 15
18
19 let op = make_op("*2");
20 println!("{}", op(7)); // 14
21}
상황반환 타입비용
단일 타입 반환impl Fn(..) -> ..제로 (정적)
여러 타입 중 하나 반환Box<dyn Fn(..) -> ..>힙 할당 + vtable
Part 5. Iterator 기본
예제 16-8: iter() 사용
Rust
1fn main() {
2 let nums = vec![1, 2, 3, 4, 5];
3
4 // iter() - 불변 참조 순회
5 for n in nums.iter() {
6 print!("{n} ");
7 }
8 println!();
9 println!("원본: {:?}", nums); // ✓ nums 여전히 사용 가능
10}
Part 6. iter / iter_mut / into_iter ⭐

Rust 철학의 정수. 같은 데이터를 3가지 다른 "소유권 모드"로 순회할 수 있습니다.

메서드반환원본 컬렉션언제?
iter()Iterator<Item=&T>그대로 유지읽기만
iter_mut()Iterator<Item=&mut T>그대로 유지 (요소 수정)각 요소 수정
into_iter()Iterator<Item=T>소멸소유권이 필요할 때
예제 16-9: 3가지 비교
Rust
1fn main() {
2 let nums = vec![1, 2, 3];
3
4 // 1) iter() - &T
5 let sum: i32 = nums.iter().sum(); // 원본 유지
6 println!("합: {sum}, 원본: {:?}", nums);
7
8 let mut nums2 = vec![1, 2, 3];
9 // 2) iter_mut() - &mut T
10 for n in nums2.iter_mut() { *n *= 10; }
11 println!("iter_mut 후: {:?}", nums2);
12
13 let nums3 = vec![1, 2, 3];
14 // 3) into_iter() - T (소유권 이동)
15 let doubled: Vec<i32> = nums3.into_iter().map(|x| x * 2).collect();
16 // nums3 이후 사용 불가
17 println!("into_iter 후: {:?}", doubled);
18}
⚠️ for 루프의 암묵적 선택
  • for x in &vv.iter() (불변)
  • for x in &mut vv.iter_mut() (가변)
  • for x in vv.into_iter() (소유권 이동 - v 소멸!)
Part 7. Iterator 메서드 - Java Stream 대응
예제 16-10: map / filter / collect
Java
1import java.util.*;
2import java.util.stream.*;
3public class Main {
4 public static void main(String[] args) {
5 List<Integer> nums = List.of(1, 2, 3, 4, 5, 6);
6
7 List<Integer> result = nums.stream()
8 .filter(n -> n % 2 == 0)
9 .map(n -> n * 10)
10 .collect(Collectors.toList());
11
12 System.out.println(result);
13 }
14}
Rust
1fn main() {
2 let nums = vec![1, 2, 3, 4, 5, 6];
3
4 let result: Vec<i32> = nums.iter()
5 .filter(|&&n| n % 2 == 0) // 짝수만
6 .map(|&n| n * 10) // *10
7 .collect(); // 수집
8
9 println!("{:?}", result); // [20, 40, 60]
10}
작업Java StreamRust Iterator
변환.map(x -> ..).map(|x| ..)
필터.filter(x -> ..).filter(|x| ..)
수집.collect(Collectors.toList()).collect::<Vec<_>>()
합계.mapToInt(..).sum().sum()
접기.reduce(0, Integer::sum).fold(0, |a, x| a + x)
정렬.sorted().sort() (in-place)
개수.count().count()
찾기.findFirst().find(..)
예제 16-11: sum / count / fold
Rust
1fn main() {
2 let nums = vec![1, 2, 3, 4, 5];
3
4 let sum: i32 = nums.iter().sum();
5 let count = nums.iter().count();
6 let product: i32 = nums.iter().product();
7
8 // fold: 누적 연산
9 let folded = nums.iter().fold(0, |acc, &x| acc + x * x);
10 // = 1+4+9+16+25 = 55
11
12 println!("합: {sum}, 개수: {count}");
13 println!("곱: {product}, 제곱합: {folded}");
14}
예제 16-12: enumerate / zip
Rust
1fn main() {
2 let books = vec!["Rust", "Go", "Python"];
3
4 // enumerate - 인덱스 포함
5 for (i, b) in books.iter().enumerate() {
6 println!("{i}: {b}");
7 }
8
9 // zip - 두 iterator 결합
10 let prices = vec![30, 25, 35];
11 for (b, p) in books.iter().zip(prices.iter()) {
12 println!("{b}: ${p}");
13 }
14}
Part 8. 외부 함수에서 Fn/FnMut/FnOnce 비교
예제 16-13: 3종 인자 받기
Rust
1// Fn - 여러 번 호출 가능
2fn call_thrice<F: Fn()>(f: F) {
3 f(); f(); f();
4}
5
6// FnMut - 호출마다 상태 변경
7fn call_5times<F: FnMut()>(mut f: F) {
8 for _ in 0..5 { f(); }
9}
10
11// FnOnce - 한 번만 호출 가능
12fn call_once<F: FnOnce() -> String>(f: F) -> String {
13 f()
14}
15
16fn main() {
17 call_thrice(|| println!("Fn 호출"));
18
19 let mut n = 0;
20 call_5times(|| { n += 1; });
21 println!("n = {n}");
22
23 let s = String::from("Rust");
24 let result = call_once(move || s); // s 소유권 이동
25 println!("{result}");
26}
예제 16-14: sort / sort_by
Rust
1fn main() {
2 let mut nums = vec![5, 2, 8, 1, 9];
3 nums.sort(); // 오름차순
4 println!("{:?}", nums);
5
6 let mut words = vec!["banana", "apple", "cherry"];
7 words.sort_by(|a, b| a.len().cmp(&b.len())); // 길이순
8 println!("{:?}", words);
9}
Part 9. 도서관 검색 파이프라인 (심화)
예제 16-15: 종합 Iterator 활용
Java
1import java.util.*;
2import java.util.stream.*;
3public class Main {
4 record Book(String title, int pages, double rating) {}
5
6 public static void main(String[] args) {
7 List<Book> library = List.of(
8 new Book("Rust", 300, 4.5),
9 new Book("Go", 200, 4.2),
10 new Book("Python", 450, 4.8),
11 new Book("Java", 550, 4.0)
12 );
13
14 List<String> titles = library.stream()
15 .filter(b -> b.rating() >= 4.3)
16 .sorted(Comparator.comparingInt(Book::pages))
17 .map(Book::title)
18 .collect(Collectors.toList());
19 System.out.println("Recommended (by pages): " + titles);
20
21 double avg = library.stream()
22 .mapToDouble(Book::rating)
23 .average().orElse(0);
24 System.out.printf("Avg rating: %.2f%n", avg);
25 }
26}
Rust
1#[derive(Debug)]
2struct Book {
3 title: String,
4 pages: i32,
5 rating: f64,
6}
7
8fn main() {
9 let library = vec![
10 Book { title: "Rust".into(), pages: 300, rating: 4.5 },
11 Book { title: "Go".into(), pages: 200, rating: 4.2 },
12 Book { title: "Python".into(), pages: 450, rating: 4.8 },
13 Book { title: "Java".into(), pages: 550, rating: 4.0 },
14 ];
15
16 // 평점 4.3 이상, 페이지 순 정렬, 제목 추출
17 let mut filtered: Vec<&Book> = library.iter()
18 .filter(|b| b.rating >= 4.3)
19 .collect();
20 filtered.sort_by(|a, b| a.pages.cmp(&b.pages));
21
22 let titles: Vec<&str> = filtered.iter()
23 .map(|b| b.title.as_str())
24 .collect();
25 println!("추천 도서 (페이지 적은 순): {:?}", titles);
26
27 // 전체 평균 평점
28 let total: f64 = library.iter().map(|b| b.rating).sum();
29 let avg = total / library.len() as f64;
30 println!("평균 평점: {:.2}", avg);
31}
✅ 섹션 15 정리
  • 클로저는 환경 캡처 방식에 따라 Fn/FnMut/FnOnce 자동 분류
  • move로 강제 소유권 이동 (스레드 전달 등)
  • Iterator는 지연 평가collect()/for/sum() 등 종결 연산이 있어야 실행
  • iter() / iter_mut() / into_iter() 3종은 소유권 모드의 반영

17. 동시성 (Concurrency)

Rust는 "두려움 없는 동시성(Fearless Concurrency)"을 표방합니다. 소유권 시스템이 데이터 레이스를 컴파일 시점에 차단하기 때문입니다. Java의 synchronized 누락 같은 실수가 원천적으로 불가능합니다.

Part 1. 스레드 생성
예제 17-1: thread::spawn
Java
1public class Main {
2 public static void main(String[] args) throws Exception {
3 Thread t = new Thread(() -> {
4 System.out.println("Thread running");
5 });
6 t.start();
7 t.join();
8 System.out.println("Done");
9 }
10}
Rust
1use std::thread;
2
3fn main() {
4 let handle = thread::spawn(|| {
5 println!("스레드 실행 중");
6 42
7 });
8
9 let result = handle.join().unwrap(); // 완료 대기
10 println!("결과: {result}");
11}
작업JavaRust
스레드 생성new Thread(() -> ...)thread::spawn(|| ...)
시작.start()자동
완료 대기.join().join().unwrap()
반환값 받기불가 (별도 설계 필요).join().unwrap() → 값 반환
Part 2. move - 스레드에 데이터 이동

스레드 클로저에 변수를 쓰려면 move 키워드로 소유권 이동이 필수입니다. 컴파일러가 강제합니다.

예제 17-2: move 클로저
Rust
1use std::thread;
2
3fn main() {
4 let book = String::from("Rust Book");
5 let handle = thread::spawn(move || {
6 // move로 book의 소유권을 스레드로 이동
7 println!("스레드에서: {book}");
8 });
9 handle.join().unwrap();
10 // println!("{book}"); // ❌ book은 이미 이동
11}
Part 3. 채널 (mpsc)

Go 스타일 메시지 전달. Multiple Producer, Single Consumer.

예제 17-3: 단일 생산자
Rust
1use std::sync::mpsc;
2use std::thread;
3
4fn main() {
5 let (tx, rx) = mpsc::channel();
6
7 thread::spawn(move || {
8 tx.send("Rust Book").unwrap();
9 });
10
11 let received = rx.recv().unwrap();
12 println!("수신: {received}");
13}
예제 17-4: 여러 생산자
Rust
1use std::sync::mpsc;
2use std::thread;
3
4fn main() {
5 let (tx, rx) = mpsc::channel();
6
7 // tx 복제로 여러 생산자
8 for i in 1..=3 {
9 let tx = tx.clone();
10 thread::spawn(move || {
11 tx.send(format!("메시지 {i}")).unwrap();
12 });
13 }
14 drop(tx); // 원본 tx 드롭 (receiver 종료 조건)
15
16 for msg in rx { // 모든 메시지 수신
17 println!("수신: {msg}");
18 }
19}
Part 4. Mutex - 공유 상태

여러 스레드가 같은 데이터를 수정하려면 Mutex로 보호합니다. Java의 synchronized와 비슷하지만 데이터 자체를 감싸는 형태입니다.

예제 17-5: Mutex 기본
Rust
1use std::sync::Mutex;
2
3fn main() {
4 let counter = Mutex::new(0);
5
6 {
7 let mut num = counter.lock().unwrap();
8 *num += 1;
9 } // lock 자동 해제
10
11 println!("값: {}", counter.lock().unwrap());
12}
Part 5. Arc + Mutex - 스레드 간 공유

여러 스레드가 같은 Mutex를 공유하려면 Arc(Atomic Reference Counted)로 감싸야 합니다.

예제 17-6: Arc<Mutex<T>>
Java
1import java.util.concurrent.atomic.AtomicInteger;
2public class Main {
3 public static void main(String[] args) throws Exception {
4 AtomicInteger counter = new AtomicInteger(0);
5 Thread[] threads = new Thread[5];
6
7 for (int i = 0; i < 5; i++) {
8 threads[i] = new Thread(() -> counter.incrementAndGet());
9 threads[i].start();
10 }
11 for (Thread t : threads) t.join();
12 System.out.println("Final: " + counter.get());
13 }
14}
Rust
1use std::sync::{Arc, Mutex};
2use std::thread;
3
4fn main() {
5 // Arc: Atomic Reference Counted (Java의 AtomicReference와 유사)
6 let counter = Arc::new(Mutex::new(0));
7 let mut handles = vec![];
8
9 for _ in 0..5 {
10 let c = Arc::clone(&counter);
11 let h = thread::spawn(move || {
12 let mut num = c.lock().unwrap();
13 *num += 1;
14 });
15 handles.push(h);
16 }
17
18 for h in handles { h.join().unwrap(); }
19 println!("최종: {}", *counter.lock().unwrap());
20}
타입역할Java 대응
Mutex<T>한 번에 한 스레드만 접근synchronized 블록
Arc<T>여러 스레드가 공유 (참조 카운팅)참조가 기본이라 불필요
Arc<Mutex<T>>공유 + 동기화 (실전 조합)AtomicReference
Part 6. Send / Sync - 컴파일 시점 안전성

Rust는 Send(다른 스레드로 이동 가능)와 Sync(여러 스레드가 공유 가능)라는 자동 구현 trait으로 스레드 안전성을 체크합니다. 위반하면 컴파일 에러.

예제 17-7: Send 체크
Rust
1use std::thread;
2
3// Rust는 Send/Sync trait로 스레드 안전성을 컴파일 시점에 보장
4// - Send: 소유권을 다른 스레드로 이동 가능
5// - Sync: &T를 여러 스레드가 동시에 공유 가능
6
7fn main() {
8 let data = vec![1, 2, 3]; // Vec<i32>는 Send
9 let handle = thread::spawn(move || {
10 println!("{:?}", data);
11 });
12 handle.join().unwrap();
13 // Rc는 Send가 아니라 thread::spawn 전달 시 컴파일 에러!
14}
Part 7. 도서관 대여 카운터 (심화)
예제 17-8: 여러 스레드가 동시 대여
Rust
1use std::sync::{Arc, Mutex};
2use std::thread;
3
4struct Library {
5 rust_count: i32,
6 go_count: i32,
7}
8
9fn main() {
10 let lib = Arc::new(Mutex::new(Library {
11 rust_count: 10,
12 go_count: 5,
13 }));
14
15 let mut handles = vec![];
16
17 // 3명이 Rust 빌리기
18 for i in 1..=3 {
19 let lib = Arc::clone(&lib);
20 let h = thread::spawn(move || {
21 let mut l = lib.lock().unwrap();
22 if l.rust_count > 0 {
23 l.rust_count -= 1;
24 println!("사람 {i}: Rust 대여 (남은 {})", l.rust_count);
25 }
26 });
27 handles.push(h);
28 }
29
30 for h in handles { h.join().unwrap(); }
31
32 let l = lib.lock().unwrap();
33 println!("최종 재고 - Rust: {}, Go: {}", l.rust_count, l.go_count);
34}
Part 8. async/await 맛보기

Rust의 비동기는 Java의 CompletableFuture와 유사하지만 별도 런타임(tokio 등)이 필요합니다. 이 가이드에서는 문법만 소개합니다.

예제 17-9: async fn 정의
Rust
1// async 함수 (Rust 2018+, Wandbox의 rust-1.82는 기본 2021 edition)
2async fn fetch_title() -> String {
3 String::from("Rust Async Book")
4}
5
6// async는 런타임(tokio 등)이 필요해서 여기선 문법만 보여드림
7fn main() {
8 // 실제 실행 예 (tokio 런타임 필요):
9 // #[tokio::main]
10 // async fn main() {
11 // let title = fetch_title().await;
12 // println!("{title}");
13 // }
14 let _future = fetch_title();
15 println!("async fn 정의 완료");
16 println!("(tokio 런타임에서 await으로 실행)");
17}
💡 async 추가 학습
실무에서는 tokio 런타임과 함께 사용합니다. 자세한 내용은 Async Book을 참고하세요.
✅ 섹션 16 정리
  • thread::spawn으로 스레드 생성, move로 소유권 이동
  • mpsc 채널로 메시지 전달 (Go-like)
  • Arc<Mutex<T>>로 공유 상태 (컴파일러가 정합성 검증)
  • Send/Sync trait으로 타입 안전성 자동 체크
  • async는 별도 런타임(tokio) 필요

18. 마무리 - 다음 단계

16개 섹션을 완주하셨습니다. 🎉 이제 Rust의 핵심 개념은 모두 익히셨습니다.

배운 것들
주제핵심 개념
소유권 · 빌림 · 라이프타임Rust의 심장 - GC 없는 메모리 안전성
문자열 (String / &str)소유 vs 빌림의 실전 적용
구조체 · enum · trait타입 시스템의 근간
Option · Result · ?null/exception 없는 에러 처리
제네릭 · 트레잇제로 비용 추상화
컬렉션 · Iterator · 클로저함수형 스타일 코드
동시성 (스레드 · Mutex · Arc)두려움 없는 동시성
추천 다음 단계

1. 전체 레퍼런스 가이드

같은 팀에서 작성한 Java 개발자를 위한 Rust 전체 가이드를 참고하세요. 이 2시간 코스의 모든 개념이 훨씬 상세히, 더 많은 예제와 함께 정리되어 있습니다.

2. 공식 문서 (한글)

3. 실습 프로젝트 추천

4. Cargo 필수 명령어

명령용도
cargo new my_project새 프로젝트 생성
cargo build컴파일
cargo run빌드 + 실행
cargo test테스트 실행
cargo fmt코드 포매팅
cargo clippy린트 (더 나은 스타일 제안)
🎉 환영합니다, Rustacean!
Java 개발자로서 이 가이드를 완주하신 것은 큰 성취입니다. Rust 커뮤니티는 초보자에게 친절합니다. 궁금한 점은 공식 포럼이나 한국 커뮤니티에 질문하세요. 즐거운 Rust 여정이 되길!

📚 전체 18개 섹션 완주 · Java 개발자를 위한 Rust 입문 코스

상세 레퍼런스는 rust-guide-for-java-developer를 참고하세요.