Java 개발자를 위한 Rust 정리
Java 개발자를 위한 단계별 Rust 전환 가이드 |
Java 21 · Rust 2021 (rustc 1.75) · Go 1.21 · C++17
모든 예제 복사 즉시 컴파일·실행 가능 |
Rust Book ·
JLS SE21 ·
JVM Spec SE21
Java 개발자를 위한 Rust 가이드
전체 구성 — 탭별 학습 순서
| 탭 | 섹션 | 핵심 내용 | Java 대응 |
|---|---|---|---|
| 기초 문법 | ① 기본 문법 | 변수·불변성·함수·struct·impl·접근제한자 | 클래스·필드·메서드 |
| ② enum · Option · Result | 대수적 타입, null 대체, 예외 없는 에러 처리 | enum, Optional, throws | |
| ③ 타입 시스템 | 타입 추론·turbofish·as/From/Into·숫자타입·String·match | var, 캐스팅, parseInt | |
| ④ 소유권 · 빌림 | move / & / &mut · Drop · Clone | GC, 참조 전달 | |
| ⑤ Box · Rc · Arc | 힙 할당, 공유 소유권 | new, GC 공유 | |
| ⑥ 클로저 · 람다 | Fn / FnMut / FnOnce · 캡처 방식 | 람다, Function<T,R> | |
| ⑦ 컬렉션 · 함수형 | Vec · HashMap · Iterator 체인 | ArrayList, HashMap, Stream | |
| ⑧ 제네릭 · trait bound | 단형화 vs 타입 소거 · struct 메서드 | <T extends Sound> | |
| ⑨ 라이프타임 | 참조 유효 범위 · dangling 차단 · 생략 규칙 | GC (개념 없음) | |
| 제네릭 · 템플릿 | ① 왜 필요한가 | 타입별 중복 코드 문제 · 세 언어 비교 | 없음 (타입마다 클래스) |
| ② Box<T> 핵심 차이 | Template Instantiation · Type Erasure · Monomorphization | 제네릭 타입 소거 | |
| ③ 함수 템플릿 | 제네릭 메서드 · static_assert · trait bound | <T> 제네릭 메서드 | |
| ④ 타입 제한 | static_assert · extends · trait bound | T extends Number | |
| ⑤ Primitive · 메모리 | Boxing 비용 · 스택/힙 구조 비교 | Integer Autoboxing | |
| ⑥ 런타임 타입 정보 | typeid · TypeId · Type Erasure | getClass() · 리플렉션 | |
| ⑦ Stack<T> 실전 | 완전 구현 · Java/C++/Rust 비교 | Stack<T> 구현 | |
| ⑧ 정적 vs 동적 | Fat Pointer · &dyn · vtable | 인터페이스 동적 디스패치 | |
| 문자열 · 라이프타임 | ① String vs &str | 소유권과 뷰 · 근본 차이 | String (뷰 없음) |
| ② 메모리 구조 | ptr·len·cap · &str fat pointer | Object Header + byte[] | |
| ③ 함수 파라미터 | &str vs String 선택 기준 · Deref coercion | 항상 String | |
| ④ 구조체 필드 | 라이프타임 파라미터 'a · Parser<'a> | GC 자동 관리 | |
| ⑤ 라이프타임 파라미터 | longest<'a> · 컴파일러와의 계약 | 없음 (GC) | |
| ⑥ 실전 — 트레이트 조합 | where 절 · 라이프타임 + 트레이트 바운드 | 제네릭 + 인터페이스 | |
| trait · 디스패치 | 상속 없는 Rust | 컴포지션 · trait 기본 구현 · 4개 언어 비교 | extends, implements |
| impl vs dyn | 정적 vs 동적 디스패치 5가지 비교 | 인터페이스 다형성 | |
| 정적 디스패치 | 단형화 — 타입별 코드 생성, 0 오버헤드 | 없음 (Java는 항상 동적) | |
| 동적 디스패치 | dyn Trait · vtable · fat pointer 16B | 인터페이스 변수 = 자동 | |
| 메모리 비교 | JVM vs Rust 스택/힙 레이아웃 | Object Header 12~16B | |
| 전체 예제 | Main.java / main.rs | 동물원 예제 — 전체 개념 총정리 | 완전한 Java↔Rust 대응 |
| 레퍼런스 | 용어 사전 · 핵심 정리 | Rust 용어 ↔ Java 대응 · 공식 출처 | — |
Java vs Rust — 핵심 차이 5가지
| # | 주제 | ☕ Java | Rust |
|---|---|---|---|
| 1 | 메모리 관리 | GC — 런타임, 비결정적, pause 있음 | 소유권 + Drop — 컴파일타임, 결정적, 0 비용 |
| 2 | null 처리 | null — NPE는 런타임에야 발견 |
Option<T> — 컴파일타임에 처리 강제 |
| 3 | 에러 처리 | throws Exception — 무시 가능 |
Result<T,E> — 무시하면 컴파일 경고 |
| 4 | 힙 vs 스택 | new = 항상 힙 (Object Header 포함) |
기본 스택, Box::new()만 힙 |
| 5 | 다형성 | 인터페이스 변수 → 자동 동적 디스패치 | impl Trait = 정적, dyn Trait = 동적 (명시 필수) |
추천 학습 경로
| 경험 | 추천 순서 |
|---|---|
| Rust 처음 | 기초 문법 ① → ② → ③ → ④ → ⑥ → ⑦ → 전체 예제 |
| 소유권이 헷갈림 | 기초 문법 ④ → ⑤ → ⑨ → trait 탭 |
| 문자열/라이프타임 심화 | 문자열·라이프타임 탭 ① → ② → ③ → ④ → ⑤ |
| 제네릭/템플릿 비교 | 제네릭·템플릿 탭 ① → ② → ③ → ④ → ⑦ |
| trait/인터페이스 비교 | trait·디스패치 탭 전체 |
| 빠른 참조 | 레퍼런스 탭 — 용어 사전 · 핵심 정리 |
Rust 기본 문법 — Java 대응
변수 · 상수 · 불변성
1public class Main { 2 public static void main(String[] args) { 3 // Java: 기본적으로 가변 4 int count = 0; 5 count = 5; // 재할당 가능 6 final int MAX = 100; // 상수 (Rust const) 7 // MAX = 200; // 컴파일 에러 8 var name = "Clean Code"; // 타입 추론 (Java 10+) 9 System.out.println(count + " " + MAX + " " + name); 10 } 11}
1fn main() { 2 let count = 0i32; 3 // count = 5; // E0384 4 let mut count2 = 0i32; 5 count2 = 5; 6 const MAX: u32 = 100; 7 let name = "Clean Code"; 8 println!("{} {} {} {}", count, count2, MAX, name); 9} 10
기본 타입
1public class Main { 2 public static void main(String[] args) { 3 boolean b = true; 4 int i = 42; // 32bit 5 long l = 9999999999L; // 64bit 6 double d = 3.14; // 64bit 7 char c = 'A'; // UTF-16 2B 8 String s = "hello"; // 힙 객체 9 int x = (int) 3.9; // 캐스트→3 10 System.out.println( 11 b + " " + i + " " + l 12 + " " + d + " " + c 13 + " " + s + " " + x); 14 } 15}
1fn main() { 2 let b: bool = true; 3 let i: i32 = 42; 4 let l: i64 = 9999999999; 5 let d: f64 = 3.14; 6 let c: char = 'A'; 7 let s: &str = "hello"; 8 let x = 3.9f64 as i32; 9 println!("{} {} {} {} {} {} {}", b, i, l, d, c, s, x); 10} 11
| 타입 | ☕ Java | Rust | 크기 |
|---|---|---|---|
| 불리언 | boolean | bool | 1B |
| 정수(32bit) | int | i32 | 4B |
| 정수(64bit) | long | i64 | 8B |
| 부동소수 | double | f64 | 8B |
| 문자 | char (UTF-16 2B) | char (Unicode 4B) | 다름! |
| 소유 문자열 | String | String (24B) | 힙 |
| 문자열 참조 | 없음 | &str (16B) | 스택 |
| 부호 없는 정수 | 없음 | u8~u128, usize | Rust만 |
함수 정의
1// 반환 타입이 앞에 2static int add(int a, int b) { 3 return a + b; 4} 5static void greet(String name) { 6 System.out.println("Hello, " + name); 7} 8public static void main(String[] args) { 9 int r = add(2, 3); 10 greet("Rust"); 11 System.out.println(r); // 5 12}
1fn add(a: i32, b: i32) -> i32 { a + b } 2fn greet(name: &str) { println!("Hello, {}", name); } 3fn main() { 4 let r = add(2, 3); 5 greet("Rust"); 6 println!("{}", r); 7} 8
조건문 — if는 표현식
1public class Main { 2 public static void main(String[] args) { 3 int score = 85; 4 // if는 문장 — 값 반환 불가 5 if (score >= 90) System.out.println("A"); 6 else if (score >= 80) System.out.println("B"); 7 else System.out.println("C"); 8 9 // switch expression (Java 14+) 10 String grade = switch (score / 10) { 11 case 10, 9 -> "A"; 12 case 8 -> "B"; 13 case 7 -> "C"; 14 default -> "F"; 15 }; 16 System.out.println(grade); // B 17 18 // for-each 19 int[] nums = {1, 2, 3}; 20 for (int n : nums) System.out.println(n); 21 } 22}
1fn main() { 2 let score = 85i32; 3 4 // if는 표현식 — 값을 변수에 대입 가능 5 // Java: if는 문장, 값 반환 불가 6 let grade = if score >= 90 { 7 "A" 8 } else if score >= 80 { 9 "B" 10 } else { 11 "C" 12 }; 13 println!("{}", grade); // "B" 14 15 // for in — 이터레이터 기반 16 let nums = [1i32, 2, 3]; 17 for n in &nums { 18 println!("{}", n); 19 } 20 21 // while 22 let mut i = 0i32; 23 while i < 3 { 24 i += 1; 25 } 26 27 // loop — 값 반환 가능 (Java에 없음) 28 let mut j = 0i32; 29 let result = loop { 30 j += 1; 31 if j >= 3 { break j * 10; } 32 }; 33 println!("i={}, j={}, loop결과={}", i, j, result); 34} 35
loop · while · for
1public class Main { 2 public static void main(String[] args) { 3 // while 4 int n = 0; 5 while (n < 3) { 6 System.out.print(n + " "); 7 n++; 8 } 9 System.out.println(); 10 11 // do-while (Rust에 없음 → loop로 대체) 12 int count = 0; 13 do { count++; } while (count < 5); 14 System.out.println("count: " + count); 15 16 // for (index) — Rust: for i in 0..5 17 for (int i = 0; i < 5; i++) { 18 System.out.print(i + " "); 19 } 20 System.out.println(); 21 22 // for-each — Rust: for x in &list 23 String[] names = { "Simba", "Donald", "Nagini"}; 24 for (String name : names) { 25 System.out.println("Animal: " + name); 26 } 27 28 // labeled break (Rust도 지원) 29 outer: 30 for (int x = 0; x < 3; x++) { 31 for (int y = 0; y < 3; y++) { 32 if (x == 1 && y == 1) break outer; 33 System.out.print( 34 "(" + x + "," + y + ") "); 35 } 36 } 37 System.out.println(); 38 // Java loop는 값 반환 불가 39 // Rust: let x = loop { break 42; }; 40 } 41} 42
1fn main() { 2 // ─── loop — 무한 루프 + 값 반환 ────────────── 3 let mut count = 0u32; 4 let result = loop { 5 count += 1; 6 if count == 5 { break count * 2; } // break로 값 반환! 7 // Java: while(true) + return/변수 → Rust만의 기능 8 }; 9 println!("loop 결과: {}", result); // 10 10 11 // ─── while ──────────────────────────────────── 12 let mut n = 0u32; 13 while n < 3 { 14 print!("{} ", n); // 0 1 2 15 n += 1; 16 } 17 println!(); 18 19 // ─── for in — 이터레이터 기반 ───────────────── 20 for i in 0..5 { // 0,1,2,3,4 21 print!("{} ", i); 22 } 23 println!(); 24 25 for i in 0..=5 { // 0,1,2,3,4,5 (inclusive) 26 print!("{} ", i); 27 } 28 println!(); 29 30 // ─── for in 컬렉션 ──────────────────────────── 31 let names = [ "Simba", "Donald", "Nagini"]; 32 for name in &names { 33 println!("동물: {}", name); 34 } 35 36 // ─── enumerate — 인덱스 + 값 ────────────────── 37 for (i, name) in names.iter().enumerate() { 38 println!("[{}] {}", i, name); 39 } 40 41 // ─── 중첩 루프 레이블 (Java에도 있음) ────────── 42 'outer: for x in 0..3 { 43 for y in 0..3 { 44 if x == 1 && y == 1 { 45 break 'outer; // 외부 루프 탈출 46 } 47 print!("({},{}) ", x, y); 48 } 49 } 50 println!(); 51} 52
| 구문 | ☕ Java | Rust | 차이 |
|---|---|---|---|
| 무한 루프 | while(true) | loop | Rust: break value로 값 반환 |
| while | while(n<3) | while n<3 | 동일 |
| do-while | do{}while(c) | 없음 | loop로 대체 |
| for(index) | for(int i=0;i<5;i++) | for i in 0..5 | range 기반 |
| for-each | for(T x:list) | for x in &list | 유사 |
| labeled break | label: for | 'label: loop | 둘 다 지원 |
⑧ #[derive] — Java 어노테이션과 비교
1import java.util.HashSet; 2import java.util.Objects; 3// #[derive] 대응 — Java는 직접 구현 or Lombok 4public class Main { 5 static class Book { 6 final String title; 7 final int pages; 8 Book(String t, int p) { title=t; pages=p; } 9 10 // #[derive(Debug)] → toString() 11 // #[derive(Debug)] → toString() 12 @Override public String toString() { 13 return "Book{'" 14 + title + "', " 15 + pages + "}"; 16 } 17 @Override public boolean equals(Object o) { 18 if (!(o instanceof Book b)) return false; 19 return Objects.equals(title, b.title) 20 && pages == b.pages; 21 } 22 // #[derive(Hash)] → hashCode() 23 @Override public int hashCode() { 24 return Objects.hash(title, pages); 25 } 26 } 27 public static void main(String[] args) { 28 Book b1 = new Book("Clean Code", 431); 29 Book b2 = new Book(b1.title, b1.pages); 30 System.out.println(b1); // toString 31 System.out.println(b1.equals(b2)); // equals 32 HashSet<Book> set = new HashSet<>(); 33 set.add(b1); // hashCode 필요 34 System.out.println(set.size()); // 1 35 // Java: 직접 구현 or Lombok @Data 36 // Rust: #[derive(Debug, Clone, // PartialEq, Hash)] 한 줄! 37 } 38} 39
1#[derive(Debug, Clone, PartialEq, Eq, Hash)] 2struct Book { 3 title: String, 4 pages: u32, 5} 6impl Book { 7 fn new(t: &str, p: u32) -> Self { 8 Book { title: t.to_string(), pages: p } 9 } 10} 11 12#[derive(Debug, Default)] 13struct Config { 14 timeout: u32, 15 verbose: bool, 16 name: String, 17} 18 19fn main() { 20 let b1 = Book::new("Clean Code", 431); 21 let b2 = b1.clone(); 22 println!("{:?}", b1); 23 println!("{}", b1 == b2); // true 24 25 use std::collections::HashSet; 26 let mut set = HashSet::new(); 27 set.insert(b1); 28 println!("set: {}", set.len()); // 1 29 30 let cfg = Config::default(); 31 println!("{:?}", cfg); 32} 33
#[derive] | 자동 구현 | Java 대응 |
|---|---|---|
Debug | {:?} 출력 | toString() |
Clone | .clone() | clone() |
PartialEq | == 비교 | equals() |
Hash | HashMap 키 | hashCode() |
Default | Type::default() | 기본 생성자 |
Copy | 비트 복사 (move 없음) | 없음 |
Ord | 완전 순서 | Comparable |
struct · impl — 데이터와 메서드 분리
class는 데이터 + 메서드를 한 곳에. Rust는 데이터(struct)와 메서드(impl)를 분리. 상속 없음 — 대신 trait으로 공통 행동 정의.1public class Main { 2 private String title; 3 private int pages; 4 5 public Main(String title, int pages) { 6 this.title = title; 7 this.pages = pages; 8 } 9 public String summary() { 10 return title + " (" + pages + "p)"; 11 } 12 public boolean isLong() { return pages > 400; } 13 public static void main(String[] args) { 14 Main b = new Main("Clean Code", 431); 15 System.out.println(b.summary()); // Clean Code (431p) 16 System.out.println(b.isLong()); // true 17 } 18}
1// 데이터만 정의 2struct Book { 3 title: String, 4 pages: u32, 5} 6// 메서드는 impl 블록에 분리 7impl Book { 8 // 연관 함수 (Java 생성자/static) 9 fn new(title: &str, pages: u32) -> Book { 10 Book { title: title.to_string(), pages } 11 } 12 // 인스턴스 메서드 (&self = Java this) 13 fn summary(&self) -> String { 14 format!("{} ({}p)", self.title, self.pages) 15 } 16 fn is_long(&self) -> bool { self.pages > 400 } 17} 18fn main() { 19 let b = Book::new("Clean Code", 431); 20 println!("{}", b.summary()); // Clean Code (431p) 21 println!("{}", b.is_long()); // true 22}
| ☕ Java | Rust | 설명 |
|---|---|---|
class Book { ... } | struct Book { ... } | 데이터 정의 |
| 클래스 내부 메서드 | impl Book { ... } | 메서드는 별도 블록 |
new Book(...) | Book::new(...) | 생성 관례 (강제 아님) |
this | &self / &mut self | 인스턴스 참조 |
static 메서드 | self 없는 연관 함수 | Book::new() 패턴 |
튜플 · 슬라이스 — Java에 없는 타입
1fn min_max(nums: &[i32]) -> (i32, i32) { 2 let mut mn = nums[0]; let mut mx = nums[0]; 3 for &n in nums { 4 if n < mn { mn = n; } if n > mx { mx = n; } 5 } 6 (mn, mx) // 튜플로 여러 값 반환 7} 8fn main() { 9 let (lo, hi) = min_max(&[3,1,4,1,5]); // 구조 분해 10 println!("min={lo} max={hi}"); 11 let pair: (i32, String) = (42, "hi".to_string()); 12 println!("{} {}", pair.0, pair.1); // 인덱스 접근 13}
1fn sum(nums: &[i32]) -> i32 { // &[T] = 슬라이스 뷰 2 nums.iter().sum() 3} 4fn main() { 5 let v = vec![1, 2, 3, 4, 5]; 6 println!("{}", sum(&v)); // 15 7 println!("{}", sum(&v[1..4])); // 9 8 let arr = [10, 20, 30]; 9 println!("{}", sum(&arr)); // 60 10 let s: &[i32] = &v[0..3]; 11 println!("{:?}", s); // [1, 2, 3] 12}
&[T]는 메모리 연속 구간의 뷰(view). Vec, 배열 어디서든 만들 수 있음.함수 인자로
&Vec<T> 대신 &[T]를 받으면 더 유연 — Java의 List vs int[]보다 통합된 방식.접근 제한자 — public/private vs pub
pub 필요.1public class Main { 2 static class Person { 3 public String name; // 어디서든 4 protected int age; // 패키지 + 서브클래스 5 String dept; // package-private (기본) 6 private String secret; // 이 클래스만 7 public String getName() { return name; } 8 } 9 public static void main(String[] args) { 10 Person p = new Person(); 11 p.name = "Simba"; 12 System.out.println(p.getName()); // Simba 13 } 14}
1pub struct Person { 2 pub name: String, // 공개 3 age: u32, // private (기본) 4 secret: String, // private 5} 6impl Person { 7 pub fn name(&self) -> &str { &self.name } 8 fn helper(&self) { } // private 9} 10pub(crate) fn internal() {} // crate 내부만 11fn main() { 12 let p = Person { 13 name: "Simba".to_string(), 14 age: 5, secret: "roar".to_string(), 15 }; 16 println!("{}", p.name()); // Simba 17}
| ☕ Java | Rust | 범위 |
|---|---|---|
public | pub | 어디서든 |
| package-private (기본) | pub(crate) | 같은 크레이트 내 |
protected | 없음 (상속 없음) | — |
private | 생략 = private | 현재 모듈만 |
📁 파일 분리로 이해하는 pub / private / pub(crate)
같은 파일 안에서는 private 필드에도 접근 가능하기 때문에
pub/private 차이가 잘 보이지 않습니다.
파일을 나누면 person.rs는 정의/캡슐화,
main.rs는 사용하는 쪽이 되어
무엇이 공개되고 숨겨졌는지 명확히 보입니다.
├─ main.rs ← 사용하는 쪽
└─ person.rs ← 정의/캡슐화하는 쪽
1pub struct Person { 2 pub name: String, // 외부 공개 3 age: u32, // 외부 비공개 (private) 4 secret: String, // 외부 비공개 (private) 5} 6 7impl Person { 8 // 공개 생성자 — private 필드가 있으므로 필수 9 pub fn new(name: &str, age: u32) -> Self { 10 Self { 11 name: name.to_string(), 12 age, 13 secret: String::new(), 14 } 15 } 16 17 // 공개 getter 18 pub fn name(&self) -> &str { &self.name } 19 pub fn age(&self) -> u32 { self.age } 20 21 // private 메서드 — 이 파일 안에서만 호출 가능 22 fn helper(&self) { 23 println!("helper 실행"); 24 } 25 26 // 공개 메서드이지만 내부에서 private 사용 가능 27 pub fn print_info(&self) { 28 self.helper(); 29 println!("name={}, age={}", 30 self.name, self.age); 31 } 32} 33 34// crate 내부에서만 호출 가능 — 외부 비공개 35pub(crate) fn internal_logic() { 36 println!("crate 내부 전용 함수"); 37}
1mod person; 2use person::Person; 3 4fn main() { 5 let p = Person::new("Simba", 5); 6 7 // ✅ pub 필드 — 직접 접근 가능 8 println!("name = {}", p.name); 9 10 // ✅ pub 메서드 — 호출 가능 11 println!("name() = {}", p.name()); 12 println!("age() = {}", p.age()); 13 14 // ✅ pub(crate) — 같은 crate 안이므로 가능 15 person::internal_logic(); 16 17 // ✅ pub 메서드 (내부에서 private 사용) 18 p.print_info(); 19 20 // ❌ private 필드 — 직접 접근 불가 21 // println!("{}", p.age); 22 // println!("{}", p.secret); 23 // ❌ private 메서드 — 호출 불가 24 // p.helper(); 25}
| 키워드 | 접근 범위 | 용도 |
|---|---|---|
pub |
어디서든 | 공개 API · 타입 · 생성자 |
| 생략 (private) | 현재 모듈 안에서만 | 내부 구현 · 보조 함수 |
pub(crate) |
같은 crate 안 | 내부 공통 로직 · 외부 비공개 |
person.rs는 정의/캡슐화,
main.rs는 사용하는 쪽이 되어
무엇이 공개되고 숨겨졌는지 명확히 보임.pub struct가 없으면 외부에서 타입 자체를 쓸 수 없음 —
use person::Person 불가.private 필드는 공개 getter(
pub fn age())로 우회 접근.pub(crate)는 라이브러리 내부 공통 로직 — 외부 사용자에게 숨김.
배열 · 리스트 초기화 비교
1import java.util.*; 2public class Main { 3 public static void main(String[] args) { 4 int[] arr = {1, 2, 3}; 5 int[] arr2 = new int[5]; // 0으로 초기화 6 List<Integer> list = new ArrayList<>(); 7 list.add(1); list.add(2); 8 List<String> names = 9 List.of("Alice", "Bob"); // 불변 10 int[][] grid = new int[3][3]; 11 System.out.println( 12 arr.length + " " + list.size()); // 3 2 13 } 14}
1fn main() { 2 let arr = [1, 2, 3]; // [i32; 3] 고정 3 let arr2 = [0; 5]; // [0,0,0,0,0] 4 let mut list: Vec<i32> = Vec::new(); 5 list.push(1); list.push(2); 6 let names = vec!["Alice", "Bob"]; // 매크로 7 let grid: Vec<Vec<i32>> = 8 (0..3).map(|_| vec![0; 3]).collect(); 9 println!("{} {}", arr.len(), list.len()); // 3 2 10 let _ = (arr2, names, grid); 11}
String 메서드 비교 — 가장 자주 쓰는 것들
1public class Main { 2 public static void main(String[] args) { 3 String s = " Hello, Rust! "; 4 s.length(); // 16 5 s.trim(); // "Hello, Rust!" 6 s.toUpperCase(); // " HELLO, RUST! " 7 s.contains("Rust"); // true 8 s.replace("Rust","Java"); 9 s.split(","); 10 s.substring(2, 7); // "Hello" 11 s.indexOf("Rust"); // 9 12 String.valueOf(42); // "42" 13 Integer.parseInt("42"); // 42 14 System.out.println(s.trim()); // Hello, Rust! 15 } 16}
1fn main() { 2 let s = " Hello, Rust! "; 3 println!("{}", s.len()); // 16 (바이트 수) 4 println!("{}", s.trim()); // Hello, Rust! 5 println!("{}", s.to_uppercase()); // HELLO, RUST! 6 println!("{}", s.contains("Rust")); // true 7 let r = s.replace("Rust", "Java"); 8 println!("{}", r); // Hello, Java! 9 let parts: Vec<_> = s.split(',').collect(); 10 println!("{:?}", parts); // [" Hello", " Rust! "] 11 println!("{}", &s[2..7]); // Hello (슬라이스) 12 println!("{:?}", s.find("Rust")); // Some(9) 13 println!("{}", 42.to_string()); // 42 14 let n: i32 = "42".parse().unwrap(); 15 println!("{}", n); // 42 16}
| ☕ Java | Rust | 차이 |
|---|---|---|
length() | len() | Rust는 바이트 수 (UTF-8 주의) |
toUpperCase() | to_uppercase() | snake_case |
substring(s,e) | &s[s..e] | Rust는 바이트 인덱스 슬라이스 |
indexOf() | find() | Rust는 Option<usize> 반환 |
String.valueOf(n) | n.to_string() | 모든 타입 가능 |
Integer.parseInt(s) | s.parse::<i32>() | Rust는 Result 반환 |
if let · while let — Optional.ifPresent 대응
Optional.ifPresent()와 유사하지만 Rust는 언어 레벨에서 지원.if let Some(x) = opt { } = match opt { Some(x) => { }, _ => {} }의 축약
1import java.util.Optional; 2public class Main { 3 public static void main(String[] args) { 4 Optional<String> name = 5 Optional.of("Simba"); 6 // ifPresent = if let Some() 7 name.ifPresent(n -> 8 System.out.println("Name: " + n)); 9 // ifPresentOrElse = if let ... else 10 name.ifPresentOrElse( 11 n -> System.out.println(n), 12 () -> System.out.println("none")); 13 // while let 대응: 없음 (Iterator로 대체) 14 Optional<Integer> val = 15 Optional.of(3); 16 while (val.isPresent()) { 17 System.out.println(val.get()); 18 val = Optional.empty(); 19 } 20 } 21}
1fn main() { 2 let name: Option<String> = 3 Some("Simba".to_string()); 4 // if let — Some일 때만 실행 5 if let Some(n) = &name { 6 println!("이름: {}", n); // Simba 7 } 8 // if let ... else 9 if let Some(n) = &name { 10 println!("{}", n); 11 } else { 12 println!("없음"); 13 } 14 // while let — None이 될 때까지 반복 15 let mut stack = vec![1, 2, 3]; 16 while let Some(top) = stack.pop() { 17 println!("{}", top); // 3, 2, 1 18 } 19} 20
| ☕ Java | Rust | 설명 |
|---|---|---|
opt.ifPresent(f) | if let Some(x) = opt | 값 있을 때만 실행 |
opt.ifPresentOrElse(f,g) | if let Some(x) = opt { } else { } | 없을 때 분기 |
| 없음 | while let Some(x) = iter.next() | None까지 반복 |
switch vs match — 패턴 매칭 비교
1public class Main { 2 public static void main(String[] args) { 3 int day = 3; 4 String name = switch (day) { 5 case 1 -> "Mon"; 6 case 2 -> "Tue"; 7 case 3 -> "Wed"; 8 default -> "Other"; // 생략 가능 → 런타임 버그 위험 9 }; 10 System.out.println(name); // 수 11 } 12}
1fn main() { 2 let day = 3u32; 3 let name = match day { 4 1 => "월", 5 2 => "화", 6 3 => "수", 7 _ => "기타", // 누락 시 컴파일 에러 8 }; 9 println!("{}", name); // 수 10}
switch는 default 생략 가능 → 런타임 버그.Rust
match는 모든 경우 커버 강제 — 컴파일 타임에 완전성 보장.try/catch/finally vs Result · ? 연산자
1import java.io.*; 2import java.nio.file.*; 3public class Main { 4 static String readFile(String path) 5 throws IOException { 6 try { 7 return new String( 8 Files.readAllBytes(Path.of(path))); 9 } catch (IOException e) { 10 System.err.println("Error: " + e.getMessage()); 11 throw e; 12 } finally { 13 System.out.println("finally: always runs"); 14 } 15 } 16 public static void main(String[] args) { 17 try { 18 String content = readFile("test.txt"); 19 System.out.println(content); 20 } catch (IOException e) { 21 System.out.println("File not found: " + e.getMessage()); 22 } 23 } 24}
1use std::{fs, io}; 2fn read_file(path: &str) 3 -> Result<String, io::Error> { 4 // ? = 에러 즉시 상위로 전파 5 let content = fs::read_to_string(path)?; 6 Ok(content) 7} 8fn main() { 9 // 파일이 없으면 Err, 있으면 Ok 10 match read_file("test.txt") { 11 Ok(c) => println!("{}", c), 12 Err(e) => println!("파일 없음: {}", e), 13 } 14 // finally 대응: Drop (스코프 끝 자동 실행) 15}
| ☕ Java | Rust | 설명 |
|---|---|---|
throws IOException | -> Result<T, E> | 에러 가능성 시그니처 명시 |
catch(E e) { throw e; } | ? 연산자 | 에러 즉시 상위 전파 |
finally { } | Drop trait | 스코프 종료 시 자동 실행 |
| unchecked (RuntimeException) | panic!() | 복구 불가 오류 |
interface default 메서드 vs trait 기본 구현
1public class Main { 2 interface Drawable { 3 String shape(); // 필수 구현 4 default void draw() { // 선택적 오버라이드 5 System.out.println("Draw: " + shape()); 6 } 7 } 8 static class Circle implements Drawable { 9 public String shape() { return "Circle"; } 10 // draw()는 default 사용 11 } 12 public static void main(String[] args) { 13 Drawable c = new Circle(); 14 c.draw(); // 그리기: 원 15 } 16}
1trait Drawable { 2 fn shape(&self) -> &str; // 필수 구현 3 fn draw(&self) { // 기본 구현 4 println!("그리기: {}", self.shape()); 5 } 6} 7struct Circle; 8impl Drawable for Circle { 9 fn shape(&self) -> &str { "원" } 10 // draw()는 기본 구현 사용 11} 12fn main() { 13 let c = Circle; 14 c.draw(); // 그리기: 원 15}
null 처리 실전 패턴 — Optional vs Option
1import java.util.Optional; 2public class Main { 3 static Optional<String> findUser(int id) { 4 return id == 42 5 ? Optional.of("Alice") : Optional.empty(); 6 } 7 public static void main(String[] args) { 8 Optional<String> opt = findUser(42); 9 opt.orElse("unknown"); // default 10 opt.orElseGet(() -> "lazy"); // lazy default 11 opt.orElseThrow(); // throw if empty 12 opt.map(String::toUpperCase) 13 .filter(s -> s.length() > 3) 14 .ifPresent(System.out::println); // ALICE 15 } 16}
1fn find_user(id: u32) -> Option<String> { 2 if id == 42 { 3 Some("Alice".to_string()) 4 } else { None } 5} 6fn main() { 7 let opt = find_user(42); 8 opt.as_deref() 9 .unwrap_or("unknown"); // Alice 10 opt.map(|s| s.to_uppercase()) 11 .filter(|s| s.len() > 3) 12 .map(|s| println!("{}", s)); 13}
| Java Optional | Rust Option |
|---|---|
Optional.of(v) | Some(v) |
Optional.empty() | None |
opt.orElse(v) | opt.unwrap_or(v) |
opt.orElseGet(f) | opt.unwrap_or_else(f) |
opt.map(f) | opt.map(f) |
opt.isPresent() | opt.is_some() |
opt.ifPresent(f) | if let Some(v) = opt { } |
instanceof 패턴 매칭 (Java 16+) vs if let · match
instanceof Pattern은 타입 검사 + 캐스트를 한 번에 처리합니다.Rust는
if let / match로 동일한 패턴을 더 강력하게 지원합니다.1public class Main { 2 sealed interface Shape permits Circle, Rect {} 3 record Circle(double r) implements Shape {} 4 record Rect(double w, double h) implements Shape {} 5 static double area(Shape s) { 6 if (s instanceof Circle c) 7 return Math.PI * c.r() * c.r(); 8 else if (s instanceof Rect rc) 9 return rc.w() * rc.h(); 10 throw new IllegalArgumentException(); 11 } 12 public static void main(String[] args) { 13 System.out.println(area(new Circle(3.0))); 14 System.out.println(area(new Rect(4.0, 5.0))); 15 } 16}
1enum Shape { 2 Circle { r: f64 }, 3 Rect { w: f64, h: f64 }, 4} 5fn area(s: &Shape) -> f64 { 6 // match: 완전성 강제, 타입 분해 동시에 7 match s { 8 Shape::Circle { r } => std::f64::consts::PI * r * r, 9 Shape::Rect { w, h } => w * h, 10 } 11} 12fn main() { 13 let c = Shape::Circle { r: 3.0 }; 14 let r = Shape::Rect { w: 4.0, h: 5.0 }; 15 println!("{:.2}", area(&c)); // 28.27 16 println!("{:.2}", area(&r)); // 20.00 17} 18
| Java (21+) | Rust | 설명 |
|---|---|---|
instanceof Circle c | if let Shape::Circle{r} = s | 타입 검사 + 분해 |
switch(s){ case Circle c -> } | match s { Circle{r} => } | 완전성 강제 |
sealed interface | enum | 닫힌 타입 계층 |
record (불변 데이터 클래스) | struct + #[derive] | 데이터 보유 타입 |
trait · impl Trait for Type — interface + implements 대응
interface Sound → trait Sound | class Lion implements Sound → impl Sound for Lion가장 큰 차이: Rust는 struct와 impl이 분리, 상속 없음, 외부 타입에도 trait 구현 가능.
1public class Main { 2 // 1. interface 정의 3 interface Sound { 4 String makeSound(); 5 default void loud() { 6 System.out.println(makeSound().toUpperCase()); 7 } 8 } 9 // 2. class에서 implements 10 static class Lion implements Sound { 11 private String name; 12 public Lion(String n) { this.name = n; } 13 public String makeSound() { return "Roar"; } 14 } 15 // 3. 사용 16 public static void main(String[] args) { 17 Sound a = new Lion("Simba"); // 자동 업캐스팅 18 System.out.println(a.makeSound()); // Roar 19 } 20}
1// 1. trait 정의 2trait Sound { 3 fn make_sound(&self) -> String; 4 fn loud(&self) { // 기본 구현 5 println!("{}", self.make_sound().to_uppercase()); 6 } 7} 8// 2. struct 선언 (데이터) 9struct Lion { name: String } 10// 3. impl Trait for Type (계약 이행) 11impl Sound for Lion { 12 fn make_sound(&self) -> String { 13 format!("{}: Roar", self.name) 14 } 15} 16fn main() { 17 let a = Lion { name: "Simba".to_string() }; 18 println!("{}", a.make_sound()); // Simba: Roar 19 a.loud(); // SIMBA: ROAR 20}
&dyn Trait · Vec<&dyn Trait> — 동적 디스패치 기본
Rust는
&dyn Sound 또는 Box<dyn Sound>로 명시해야 동적 디스패치.1import java.util.*; 2public class Main { 3 interface Sound { String makeSound(); } 4 static class Lion implements Sound { 5 String name; Lion(String n){name=n;} 6 public String makeSound(){return name+": Roar~";} 7 } 8 static class Duck implements Sound { 9 String name; Duck(String n){name=n;} 10 public String makeSound(){return name+": Quack!";} 11 } 12 public static void main(String[] args) { 13 // Java: 인터페이스 타입으로 쓰면 자동 동적 디스패치 14 Sound a = new Lion("Simba"); // 동적 디스패치 15 Sound b = new Duck("Donald"); // 동적 디스패치 16 System.out.println(a.makeSound()); // vtable 조회 17 // 이종 컬렉션 18 List<Sound> zoo = new ArrayList<>(); 19 zoo.add(new Lion("Simba")); 20 zoo.add(new Duck("Donald")); 21 for (Sound s : zoo) System.out.println(s.makeSound()); 22 } 23}
1trait Sound { fn make_sound(&self) -> String; } 2struct Lion { name: String } 3struct Duck { name: String } 4impl Sound for Lion { 5 fn make_sound(&self) -> String { 6 format!("{}: 으르렁~", self.name) } 7} 8impl Sound for Duck { 9 fn make_sound(&self) -> String { 10 format!("{}: 꽥꽥!", self.name) } 11} 12fn main() { 13 // &dyn: 참조로 동적 디스패치 (fat pointer) 14 let lion = Lion { name: "Simba".to_string() }; 15 let a: &dyn Sound = &lion; // 명시 필수! 16 a.make_sound(); // vtable 조회 17 // 이종 컬렉션 — Box<dyn> 소유 18 let zoo: Vec<Box<dyn Sound>> = vec![ 19 Box::new(Lion { name: "Simba".to_string() }), 20 Box::new(Duck { name: "Donald".to_string() }), 21 ]; 22 for s in &zoo { 23 println!("{}", s.make_sound()); 24 } 25}
| ☕ Java | Rust | 설명 |
|---|---|---|
Sound a = new Lion() | let a: &dyn Sound = &lion | 동적 디스패치 — Rust는 명시 필수 |
List<Sound> | Vec<&dyn Sound> / Vec<Box<dyn Sound>> | 이종 컬렉션 |
| 참조 8B | fat pointer 16B (data + vtable) | Rust dyn은 포인터가 2배 |
| 자동 | dyn 키워드 명시 | Rust는 정적/동적 구분 명시적 |
제네릭 기본 — <T extends Sound> vs <T: Sound>
<T extends Sound>: 컴파일 후 타입 소거 → 런타임에 Sound로 처리 (동적)Rust
<T: Sound>: 컴파일 시 타입별 전용 코드 생성 (단형화) → 런타임 오버헤드 없음1import java.util.*; 2public class Main { 3 interface Sound { String makeSound(); } 4 static class Lion implements Sound { 5 final String name; Lion(String n){name=n;} 6 public String makeSound(){return name+": Roar~";} 7 } 8 static class Duck implements Sound { 9 final String name; Duck(String n){name=n;} 10 public String makeSound(){return name+": Quack!";} 11 } 12 // <T extends Sound> — 컴파일 후 타입 소거 13 static <T extends Sound> void play(T animal) { 14 System.out.println(animal.makeSound()); 15 } 16 // 호출 — 런타임에 vtable 조회 17 public static void main(String[] args) { 18 play(new Lion("Simba")); // play(Sound) 1개만 19 play(new Duck("Donald")); // 같은 바이트코드 20 } 21}
1trait Sound { fn make_sound(&self) -> String; } 2struct Lion { name: String } 3struct Duck { name: String } 4impl Sound for Lion { 5 fn make_sound(&self) -> String { 6 format!("{}: 으르렁~", self.name) 7 } 8} 9impl Sound for Duck { 10 fn make_sound(&self) -> String { 11 format!("{}: 꽥꽥!", self.name) 12 } 13} 14// <T: Sound> — 컴파일 시 타입별 코드 생성 15fn play<T: Sound>(animal: &T) { 16 println!("{}", animal.make_sound()); 17} 18// 컴파일러 자동 생성: play__Lion, play__Duck ... 19fn main() { 20 let l = Lion { name: "Simba".to_string() }; 21 let d = Duck { name: "Donald".to_string() }; 22 play(&l); play(&d); 23}
| 항목 | ☕ Java <T extends Sound> | Rust <T: Sound> |
|---|---|---|
| 컴파일 방식 | 타입 소거 → Sound로 통일 | 단형화 → 타입별 코드 복제 |
| 런타임 디스패치 | vtable 조회 (동적) | 직접 CALL (정적, 0 오버헤드) |
| 여러 bound | T extends A & B | T: A + B 또는 where 절 |
| 바이너리 크기 | 작음 (코드 1개) | 클 수 있음 (타입별 코드) |
enum — Option · Result · 대수적 타입
출처: Rust Book §6 · std::option · std::result
Rust enum: 각 variant가 다른 타입의 데이터를 직접 담음 — 대수적 데이터 타입(ADT).
Option<T> = null의 안전한 대체 | Result<T,E> = 예외 없는 에러 처리
enum 기본 — 데이터를 담는 열거형
1public class Main { 2 // Java enum: 상수 + 데이터 (필드+생성자 필요) 3 enum Shape { 4 CIRCLE, RECT, TRIANGLE; 5 public boolean hasCorners() { 6 return this != CIRCLE; 7 } 8 } 9 // Java: 데이터를 담으려면 필드+생성자 필요 10 enum Option2 { 11 SOME, NONE; 12 Object value; // 타입 불안전! 13 } 14 public static void main(String[] args) { 15 Shape s = Shape.CIRCLE; 16 String msg = switch (s) { 17 case CIRCLE -> "Circle"; 18 case RECT -> "Rect"; 19 case TRIANGLE -> "Triangle"; 20 }; 21 System.out.println(msg); 22 } 23}
1enum Shape { 2 Circle { radius: f64 }, 3 Rect { w: f64, h: f64 }, 4 Triangle(f64, f64, f64), 5 Point, 6} 7fn area(s: &Shape) -> f64 { 8 match s { 9 Shape::Circle { radius } => 10 std::f64::consts::PI * radius * radius, 11 Shape::Rect { w, h } => w * h, 12 Shape::Triangle(a, b, c) => { 13 let s = (a + b + c) / 2.0; 14 (s*(s-a)*(s-b)*(s-c)).sqrt() 15 } 16 Shape::Point => 0.0, 17 } 18} 19fn main() { 20 let shapes = vec![ 21 Shape::Circle { radius: 3.0 }, 22 Shape::Rect { w: 4.0, h: 5.0 }, 23 Shape::Triangle(3.0, 4.0, 5.0), 24 ]; 25 for s in &shapes { 26 println!("{:.2}", area(s)); 27 } 28}
| 항목 | ☕ Java enum | Rust enum |
|---|---|---|
| 데이터 담기 | 필드+생성자 필요 | variant에 직접 담음 |
| variant 종류 | 동일한 타입만 | 각각 다른 타입 가능 |
| 패턴 매칭 | switch (Java 21+) | match (완전성 강제) |
| 대표 enum | 없음 | Option<T>, Result<T,E> |
Option<T> — null의 안전한 대체
enum Option<T> { Some(T), None }값이 있을 수도 없을 수도 있을 때 사용. Java의
null을 컴파일타임에 강제 처리.
1import java.util.Optional; 2public class Main { 3 // Java: null 또는 Optional — 강제 아님 4 static Optional<Double> divide(double a, double b) { 5 return b == 0 ? Optional.empty() 6 : Optional.of(a / b); 7 } 8 public static void main(String[] args) { 9 divide(10, 3) 10 .ifPresentOrElse( 11 v -> System.out.println(String.format("%.2f", v)), 12 () -> System.out.println("Division error")); 13 // Java: null 직접 반환도 가능 → NullPointerException 위험 14 Double unsafe = null; // 컴파일 OK, 런타임 NPE 위험 15 } 16}
1// Option: null 없는 Rust의 핵심 2// enum Option{{ Some(T), None }} 3fn divide(a: f64, b: f64) -> Option<f64> { 4 if b == 0.0 { None } else { Some(a / b) } 5} 6fn main() { 7 // 안전한 나눗셈 8 match divide(10.0, 3.0) { 9 Some(v) => println!("결과: {:.2}", v), 10 None => println!("나누기 불가"), 11 } 12 // ? 연산자로 체이닝 13 let result = divide(10.0, 2.0) 14 .map(|v| v * 2.0) 15 .filter(|&v| v > 5.0) 16 .unwrap_or(0.0); 17 println!("체이닝: {}", result); // 10 18}
Result<T, E> — 예외 없는 에러 처리
enum Result<T,E> { Ok(T), Err(E) }에러를 반환값으로 표현. 컴파일러가 처리를 강제 — 무시하면 컴파일 경고/에러.
1import java.io.*; 2public class Main { 3 // Java: checked exception으로 에러 표현 4 static int parse(String s) throws NumberFormatException { 5 return Integer.parseInt(s); 6 } 7 public static void main(String[] args) { 8 try { 9 int n = parse("42"); 10 System.out.println(n * 2); // 84 11 } catch (NumberFormatException e) { 12 System.err.println("Error: " + e.getMessage()); 13 } 14 // Java: 예외 무시 가능 → 런타임 폭발 15 int risky = Integer.parseInt("abc"); 16 } 17}
1// Result: 에러를 타입으로 표현 2// enum Result{{ Ok(T), Err(E) }} 3fn parse_double(s: &str) -> Result<i32, String> { 4 s.parse::<i32>() 5 .map_err(|e| format!("파싱 실패: {}", e)) 6 .map(|n| n * 2) 7} 8fn main() { 9 // match로 명시적 처리 — 무시 불가 10 match parse_double("42") { 11 Ok(n) => println!("결과: {}", n), // 84 12 Err(e) => eprintln!("에러: {}", e), 13 } 14 // ? 연산자: Err이면 즉시 반환 15 let n = parse_double("abc").unwrap_or(0); 16 println!("기본값: {}", n); // 0 17}
| 항목 | ☕ Java (throws) | Rust (Result) |
|---|---|---|
| 에러 표현 | throws Exception | 반환 타입 Result<T,E> |
| 에러 무시 | 가능 → 런타임 폭발 | 컴파일 경고/에러 |
| 에러 전파 | throws 재선언 | ? 한 글자 |
| finally 대응 | finally { } | Drop (스코프 끝) |
enum 상세 — BookStatus 예제 · Java switch 비교
Rust enum: 각 variant가 서로 다른 타입의 데이터를 직접 담음 (대수적 데이터 타입, ADT)
1public class Main { 2 enum BookStatus { AVAILABLE, BORROWED, LOST; 3 public boolean isAvailable() { return this == AVAILABLE; } 4 } 5 public static void main(String[] args) { 6 BookStatus s = BookStatus.AVAILABLE; 7 System.out.println(s.isAvailable()); // true 8 String msg = switch (s) { 9 case AVAILABLE -> "Available"; 10 case BORROWED -> "Borrowed"; 11 case LOST -> "Lost"; 12 }; 13 System.out.println(msg); 14 } 15}
1enum BookStatus { 2 Available, 3 Borrowed { due_date: String }, 4 Lost(String), 5} 6 7fn main() { 8 let s = BookStatus::Borrowed { 9 due_date: "2025-01-01".to_string(), 10 }; 11 match s { 12 BookStatus::Available => { 13 println!("대출 가능"); 14 } 15 BookStatus::Borrowed { due_date } => { 16 println!("반납일: {}", due_date); 17 } 18 BookStatus::Lost(reason) => { 19 println!("분실: {}", reason); 20 } 21 } 22} 23
| 처리 방법 | Java Optional | Rust Option |
|---|---|---|
| 패턴 매칭 | 없음 | match Some(v) / None |
| 간결한 처리 | 없음 | if let Some(v) = opt |
| 값 추출 | get() | .unwrap() |
| 기본값 | orElse(val) | .unwrap_or(val) |
| 변환 | map(fn) | .map(|v|..) |
| 체이닝 | flatMap(fn) | .and_then(|v|..) |
| 조건 필터 | 없음 | .filter(|v|..) |
| ? 연산자 | 없음 | ? (None → 즉시 반환) |
| 기본 타입값 | 없음 | .unwrap_or_default() |
Option 상세 — 6가지 처리 패턴 · Java Optional 전체 비교
enum Option<T> { Some(T), None }null은 런타임에야 NPE 발생. Rust Option<T>은 컴파일타임에 처리 강제.무시하면 컴파일 에러 → NPE 원천 차단.
1import java.util.Optional; 2class Main { 3 static class Book { 4 String title; Book(String t) { title = t; } 5 } 6 static Optional<Book> findBook(String t) { 7 return "Clean Code".equals(t) 8 ? Optional.of(new Book(t)) 9 : Optional.empty(); 10 } 11 public static void main(String[] args) { 12 findBook("Clean Code").ifPresent( b -> System.out.println(b.title)); 13 Book b = findBook("Unknown").orElseGet( () -> new Book("Default")); 14 System.out.println(b.title); 15 String t = findBook("Unknown").map( bk -> bk.title).orElse("none"); 16 System.out.println(t); 17 System.out.println( findBook("Clean Code").isPresent()); 18 System.out.println( findBook("Unknown").isEmpty()); 19 } 20}
1struct Book { 2 title: String, 3} 4impl Book { 5 fn new(t: &str) -> Self { 6 Book { title: t.to_string() } 7 } 8} 9 10fn find(t: &str) -> Option<Book> { 11 if t == "Clean Code" { 12 Some(Book::new(t)) 13 } else { 14 None 15 } 16} 17 18fn main() { 19 // 1. match 20 match find("Clean Code") { 21 Some(b) => println!("match: {}", b.title), 22 None => println!("없음"), 23 } 24 25 // 2. if let 26 if let Some(b) = find("Clean Code") { 27 println!("if let: {}", b.title); 28 } 29 30 // 3. unwrap (None → panic) 31 let b = find("Clean Code").unwrap(); 32 println!("unwrap: {}", b.title); 33 34 // 4. unwrap_or_else 35 let title = find("없는책") 36 .map(|b| b.title) 37 .unwrap_or_else(|| "기본값".to_string()); 38 println!("unwrap_or: {}", title); 39 40 // 5. map 41 let len: Option<usize> = find("Clean Code") 42 .map(|b| b.title.len()); 43 println!("map: {:?}", len); // Some(10) 44 45 // 6. is_some / is_none 46 println!("{}", find("Clean Code").is_some()); 47 println!("{}", find("없는책").is_none()); 48} 49
Option 처리 방법 9가지
1fn find(t: &str) -> Option<String> { 2 if t == "Clean Code" { 3 Some(t.to_string()) 4 } else { 5 None 6 } 7} 8 9fn main() { 10 // 1. unwrap_or — 기본값 직접 지정 11 let t = find("없는책") 12 .unwrap_or("기본값".to_string()); 13 println!("unwrap_or: {}", t); 14 15 // 2. map_or — map + 기본값 한번에 16 let len = find("Clean Code") 17 .map_or(0, |s| s.len()); 18 println!("map_or: {}", len); // 10 19 20 // 3. map_or_else — 기본값도 클로저로 21 let msg = find("없는책") 22 .map_or_else( 23 || "없음".to_string(), 24 |s| format!("찾음: {}", s), 25 ); 26 println!("map_or_else: {}", msg); 27 28 // 4. and_then — Option 체이닝 29 let upper = find("Clean Code") 30 .and_then(|s| { 31 if s.len() > 5 { 32 Some(s.to_uppercase()) 33 } else { None } 34 }); 35 println!("and_then: {:?}", upper); 36 37 // 5. or_else — None일 때 대체 Option 38 let fallback = find("없는책") 39 .or_else(|| find("Clean Code")); 40 println!("or_else: {:?}", fallback); 41 42 // 6. filter — 조건 불만족 시 None 43 let filtered = find("Clean Code") 44 .filter(|s| s.starts_with("Clean")); 45 println!("filter: {:?}", filtered); 46 47 // 7. ? 연산자 — None이면 즉시 None 반환 48 fn get_upper(t: &str) -> Option<String> { 49 let s = find(t)?; 50 Some(s.to_uppercase()) 51 } 52 // Some("CLEAN CODE") / None 53 println!("{:?}", get_upper("Clean Code")); 54 println!("{:?}", get_upper("없는책")); 55 56 // 8. as_ref — 소유권 유지하며 참조 57 let opt: Option<String> = find("Clean Code"); 58 let opt_ref: Option<&String> = opt.as_ref(); 59 println!("as_ref: {:?}", opt_ref); 60 println!("원본: {:?}", opt); // 소유권 유지 61 62 // 9. take — 값 꺼내고 None으로 63 let mut opt2 = find("Clean Code"); 64 let taken = opt2.take(); 65 println!("take: {:?}", taken); 66 println!("after: {:?}", opt2); // None 67}
Result 상세 — 커스텀 에러 · ? 연산자 · 체이닝
enum Result<T,E> { Ok(T), Err(E) }throws 선언 무시 가능, unchecked는 전파 안 됨.Rust Result: 반환값으로 에러 표현, 무시하면 컴파일 경고/에러,
?로 즉시 전파.
1import java.io.*; 2import java.nio.file.*; 3public class Main { 4 static String readFile(String path) 5 throws IOException { 6 return Files.readString(Path.of(path)); 7 } 8 static int divide(int a, int b) { 9 if (b == 0) 10 throw new ArithmeticException("Division by zero"); 11 return a / b; 12 } 13 public static void main(String[] args) { 14 try { 15 String s = readFile("book.txt"); 16 System.out.println(s); 17 } catch (IOException e) { 18 System.err.println("Error: " + e.getMessage()); 19 } finally { 20 System.out.println("always runs"); 21 } 22 } 23}
1use std::num::ParseIntError; 2use std::fmt; 3 4// 커스텀 에러 타입 5#[derive(Debug)] 6enum AppError { 7 Parse(ParseIntError), 8 NegativeNumber(i32), 9} 10impl fmt::Display for AppError { 11 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 12 match self { 13 AppError::Parse(e) => write!(f, "Parse error: {}", e), 14 AppError::NegativeNumber(n) => 15 write!(f, "Negative: {}", n), 16 } 17 } 18} 19impl From<ParseIntError> for AppError { 20 fn from(e: ParseIntError) -> Self { AppError::Parse(e) } 21} 22 23// ? 연산자 — Err이면 즉시 반환 24fn parse_positive(s: &str) -> Result<u32, AppError> { 25 let n: i32 = s.parse()?; // ParseIntError → AppError::Parse 26 if n < 0 { 27 return Err(AppError::NegativeNumber(n)); 28 } 29 Ok(n as u32) 30} 31 32fn main() { 33 // match로 명시적 처리 34 match parse_positive("42") { 35 Ok(n) => println!("OK: {}", n), 36 Err(e) => println!("Error: {}", e), 37 } 38 39 // unwrap_or_else 40 let n = parse_positive("abc") 41 .unwrap_or_else(|e| { 42 println!("Recover: {}", e); 43 0u32 44 }); 45 println!("Recovered: {}", n); 46 47 // map + and_then 체이닝 48 let doubled = parse_positive("5") 49 .map(|n| n * 2); 50 println!("doubled: {:?}", doubled); // Ok(10) 51 52 // 에러 변환 53 let result = parse_positive("-3"); 54 println!("{:?}", result); // Err(NegativeNumber(-3)) 55 println!("is_ok: {}", result.is_ok()); 56 println!("is_err: {}", result.is_err()); 57} 58
| 항목 | ☕ Java | Rust |
|---|---|---|
| 에러 표현 | throws Exception | 반환 타입 Result<T,E> |
| 에러 무시 | 가능 (런타임 오류) | 컴파일 에러 |
| 에러 전파 | throws 재선언 | ? 한 글자 |
| 커스텀 에러 | extends Exception | enum MyError |
타입 시스템 — 추론 · 변환 · 숫자 · String · match
출처: Rust Book §3.2 · Rust Reference — Type System
타입 추론 · turbofish — Java var 비교
var: 지역변수만, 필드·매개변수 불가, 우측 값에서만 추론Rust
let: 클로저 인수·제네릭 포함 전방위 추론, 지연 추론 가능turbofish
::<>: 추론 불가할 때 "parse::<i32>()" 처럼 타입 명시
1public class Main { 2 public static void main(String[] args) { 3 // Java 10+: var — 지역변수만, 필드/매개변수 불가 4 var x = 42; // int 추론 5 var list = new java.util.ArrayList<String>(); 6 // Java: 타입별 메서드 이름으로 해결 7 int n = Integer.parseInt("42"); 8 double d = Double.parseDouble("3.14"); 9 // Java에는 turbofish 없음 — 제네릭 메서드 명시 10 java.util.Collections.<String>emptyList(); 11 System.out.println(n + " " + d); 12 } 13}
1use std::collections::HashMap; 2fn main() { 3 // 기본 추론 — 컴파일러가 타입 결정 4 let x = 42; // i32 기본 5 let x = 42u8; // suffix로 명시 6 let x: u8 = 42; // 어노테이션으로 명시 7 // _ placeholder — 일부만 명시 8 let v: Vec<_> = (0..).take(5) 9 .map(|n| n * 2).collect(); 10 // turbofish ::<> — 추론 불가할 때 타입 명시 11 let n = "42".parse::<i32>().unwrap(); 12 let v = (0..).take(5).collect::<Vec<i32>>(); 13 let sz = std::mem::size_of::<String>(); // 반드시 turbofish 14 // 지연 추론 — 나중에 확정 15 let mut map = HashMap::new(); // 타입 미확정 16 map.insert("key", 42i32); // HashMap<&str,i32> 확정 17 println!("{} {} {:?}", n, sz, v); 18}
| 상황 | ☕ Java | Rust |
|---|---|---|
| 지역 변수 추론 | var x = 42 | let x = 42 |
| 필드/매개변수 | 불가 (타입 명시 필수) | 불가 (타입 명시 필수) |
| 부분 명시 | 없음 | Vec<_> — _ 자리만 추론 |
| turbofish | Collections.<String>emptyList() (드묾) | parse::<i32>() (자주 사용) |
| 지연 추론 | 없음 | 나중 사용 시점에서 확정 |
타입 변환 — as · From · Into · TryFrom
as: Java (int)x 대응 — 항상 성공, 데이터 손실 가능From/Into: 안전한 변환 (실패 없음) — String::from(), .into()TryFrom/TryInto: 실패 가능 변환 — Result 반환
1public class Main { 2 public static void main(String[] args) { 3 // Java: 명시적 캐스팅 (데이터 손실 가능) 4 double d = 3.99; 5 int n = (int) d; // 3 (소수점 버림) 6 long l = n; // 암묵적 확장 7 // 문자열 변환 8 String s = String.valueOf(42); 9 int i = Integer.parseInt("42"); 10 // 실패 가능 변환 11 try { 12 int x = Integer.parseInt("abc"); 13 } catch (NumberFormatException e) { 14 System.out.println("failed"); 15 } 16 System.out.println(n + " " + s + " " + i); 17 } 18}
1use std::convert::TryFrom; 2fn main() -> Result<(), Box<dyn std::error::Error>> { 3 // as — 명시적 캐스팅 (항상 성공, 손실 가능) 4 let d: f64 = 3.99; 5 let n = d as i32; // 3 (소수점 버림) 6 let l = n as i64; // 확장 7 let b = 200u32 as u8; // 200 (wrap) 8 // From / Into — 안전한 변환 9 let _s = String::from("hello"); 10 let _s: String = "hello".into(); 11 let _n = i64::from(42i32); 12 // TryFrom — 실패 가능 변환 13 let ok = i8::try_from(100i32); 14 let err = i8::try_from(300i32); 15 // 문자열 변환 16 let _s = 42.to_string(); 17 let m: i32 = "42".parse()?; 18 println!("{} {} {} {:?} {:?}", n, l, b, ok, err); 19 println!("parse: {}", m); 20 Ok(()) 21}
| 방식 | ☕ Java | Rust | 실패 시 |
|---|---|---|---|
| 명시적 캐스트 | (int)x | x as i32 | wrap/truncate |
| 안전 변환 | 없음 | From/Into | 컴파일 에러 |
| 실패 가능 | Integer.parseInt() + catch | TryFrom/TryInto | Err(e) |
| 문자열→수 | Integer.parseInt(s) | s.parse::<i32>() | 각각 예외/Err |
| 수→문자열 | String.valueOf(n) | n.to_string() | 항상 성공 |
숫자 타입 — i8~i128 · u8~u128 · f32/f64 · 박싱 없음
int/Integer 박싱 구분, 오버플로 조용히 wrapRust: 박싱 없음 (항상 값 타입), 오버플로 debug=panic / release=wrap
suffix:
42u8, 3.14f32 — Java의 L, f보다 더 세분화
1public class Main { 2 public static void main(String[] args) { 3 // Java: 원시 타입 8개 + 박싱 4 byte b = 127; // 8bit 5 short s = 32767; // 16bit 6 int i = 2_147_483_647; // 32bit 7 long l = 9_999_999_999L; // 64bit L필수 8 float f = 3.14f; // 32bit f필수 9 double d = 3.14; // 64bit 기본 10 // Java: int/Integer 박싱 구분 11 Integer boxed = 42; // 자동 박싱 → 힙 12 int unboxed = boxed; // 자동 언박싱 13 // 오버플로: 조용히 wrap 14 int overflow = Integer.MAX_VALUE + 1; // -2147483648 15 System.out.println(overflow); 16 } 17}
1fn main() { 2 // Rust: 부호있는 정수 i8~i128, 없는 u8~u128 3 let b: i8 = 127; // 8bit -128..127 4 let s: i16 = 32_767; // 16bit 5 let i: i32 = 2_147_483_647; // 32bit 기본 6 let l: i64 = 9_999_999_999; // 64bit 7 let u: usize = 42; // 포인터 크기 8 let f: f32 = 3.14f32; // suffix 필수 9 let d: f64 = 3.14; // 64bit 기본 10 // 박싱 없음 — int/Integer 구분 없음 11 let x: i32 = 42; // 항상 스택값 12 // 오버플로: debug=panic, release=wrap 13 // 명시적 처리 14 let wrapped = i32::MAX.wrapping_add(1); // -2147483648 15 let checked = i32::MAX.checked_add(1); // None 16 println!("{} {:?}", wrapped, checked); 17}
| ☕ Java | Rust | 크기 | 범위 |
|---|---|---|---|
byte | i8 | 8bit | -128 ~ 127 |
short | i16 | 16bit | -32768 ~ 32767 |
int | i32 | 32bit | ±2억 |
long | i64 | 64bit | ±9경 |
| 없음 | u8/u16/u32/u64 | 8~64bit | 부호 없음 |
| 없음 | usize/isize | 포인터 크기 | 인덱스에 사용 |
float | f32 | 32bit | IEEE 754 |
double | f64 | 64bit | IEEE 754 (기본) |
int/Integer 구분 | 없음 (항상 값) | — | 박싱 오버헤드 없음 |
String vs &str
1class Main { 2 public static void main(String[] args) { 3 // Java: String은 힙, 불변 4 String title = "Clean Code"; 5 System.out.println(title.length()); // 10 6 // 가변 문자열 7 StringBuilder sb = new StringBuilder("Clean"); 8 sb.append(" Code"); 9 System.out.println(sb); // Clean Code 10 System.out.println(sb.length()); // 10 11 } 12}
1fn main() { 2 let slice: &str = "Clean Code"; 3 let mut owned: String = String::from("Clean"); 4 owned.push_str(" Code"); 5 let r: &str = &owned; 6 println!("{} {} {}", slice, owned, r); 7} 8
| Java String | Rust String | Rust &str | |
|---|---|---|---|
| 위치 | 힙 (항상) | 힙 (ptr+len+cap) | 어디서든 (참조) |
| 소유권 | GC 관리 | 소유 (Drop) | 없음 (빌림) |
| 크기 | 참조 8B | 24B | 16B (ptr+len) |
match — 패턴 7가지
1// Java switch — default 없어도 컴파일됨 2int pages = 450; 3switch (pages / 100) { 4 case 0: System.out.println("Thin"); break; 5 case 1: 6 case 2: System.out.println("Normal"); break; 7 case 3: 8 case 4: System.out.println("Thick"); break; 9 // default 없음 → 컴파일 OK, 버그 위험! 10} 11// Java 14+ switch expression 12String size = switch (pages / 100) { 13 case 0 -> "Thin"; 14 case 1, 2 -> "Normal"; 15 case 3, 4 -> "Thick"; 16 default -> "Very Thick"; // default 필수 17}; 18System.out.println(size); // 두꺼움 19// OR 패턴 20String day = "Sat"; 21String kind = switch (day) { 22 case "Sat", "Sun" -> "Weekend"; 23 default -> "Weekday"; 24}; 25// if (null check) — Rust if let Some에 대응 26String title = "Clean Code"; // 직접 값 사용 27if (title != null) { 28 System.out.println("Found: " + title); 29}
1fn main() { 2 let pages: u32 = 450; 3 let size = match pages { 4 0..=100 => "얇음", 5 101..=300 => "보통", 6 301..=500 => "두꺼움", 7 _ => "매우 두꺼움", 8 }; 9 println!("{}", size); 10 let grade = match 85u32 { 11 90..=100 => "A", 12 80..=89 => "B", 13 70..=79 => "C", 14 _ => "F", 15 }; 16 println!("{}", grade); 17 enum Msg { Move { x: i32, y: i32 }, Write(String), Quit } 18 let msg = Msg::Move { x: 10, y: 20 }; 19 match msg { 20 Msg::Move { x, y } => println!("Move: ({}, {})", x, y), 21 Msg::Write(t) => println!("Write: {}", t), 22 Msg::Quit => println!("Quit"), 23 } 24 match 7i32 { 25 x if x < 0 => println!("neg: {}", x), 26 x if x == 0 => println!("zero"), 27 x if x % 2 == 0 => println!("even: {}", x), 28 x => println!("odd: {}", x), 29 } 30 let kind = match "Sat" { 31 "Sat" | "Sun" => "Weekend", 32 _ => "Weekday", 33 }; 34 println!("{}", kind); 35 match (0, -2) { 36 (0, 0) => println!("origin"), 37 (x, 0) | (0, x) => println!("axis: {}", x), 38 (x, y) => println!("({}, {})", x, y), 39 } 40 if let Some(v) = Some(42) { 41 println!("val: {}", v); 42 } 43 let mut stack = vec![1i32, 2, 3]; 44 while let Some(top) = stack.pop() { 45 println!("pop: {}", top); 46 } 47}
| 패턴 | Rust | ☕ Java | 차이 |
|---|---|---|---|
| 범위 | 0..=100 => | 없음 | Rust만 |
| 완전성 | 컴파일 에러 | 없음 | Rust가 안전 |
| 값 반환 | 항상 | Java 14+만 | Rust는 표현식 |
| if 가드 | x if x>0 | 없음 | Rust만 |
| 튜플 매칭 | (0,y) | 없음 | Rust만 |
| if let | if let Some(v)=opt | 없음 | Rust만 |
| while let | while let Some(v)=opt | 없음 | Rust만 |
소유권 · 빌림 · 라이프타임
출처: Rust Book §4Rust Reference — Memory
소유권 이동 vs 참조 복사
1public class Main { 2 static class Book { 3 String title; 4 Book(String t) { title = t; } 5 Book deepCopy() { return new Book(title); } 6 } 7 public static void main(String[] args) { 8 Book a = new Book("Clean Code"); 9 Book b = a; // 참조 복사 — 같은 객체! 10 b.title = "Modified"; 11 // Rust: let b = a; → a 무효 (E0382) 12 System.out.println(a.title); // "Modified" 13 14 // 깊은 복사 15 Book c = a.deepCopy(); 16 c.title = "Another"; 17 System.out.println(a.title); // "Modified" 18 } 19}
1struct Book { title: String } 2impl Book { 3 fn new(t: &str) -> Self { 4 Book { title: t.to_string() } 5 } 6} 7 8// 불변 빌림 — &Book 9fn read(book: &Book) { 10 println!("{}", book.title); 11 // book.title = "X"; // 컴파일 에러! 수정 불가 12} 13 14// 가변 빌림 — &mut Book 15fn stamp(book: &mut Book) { 16 book.title.push_str(" [도장]"); 17} 18 19fn main() { 20 // ── 소유권 이동 ──────────────────────────── 21 let a = Book::new("Clean Code"); 22 let b = a; // 소유권 이동 (move) 23 // println!("{}", a.title); // E0382: 이미 이동됨! 24 println!("{}", b.title); // OK 25 26 // ── 불변 빌림 ───────────────────────────── 27 let c = Book::new("Refactoring"); 28 read(&c); // &c = c를 빌림 29 read(&c); // OK — 여러 번 빌릴 수 있음 30 println!("{}", c.title); // c는 여전히 유효 31 32 // ── 가변 빌림 ───────────────────────────── 33 let mut d = Book::new("SICP"); 34 stamp(&mut d); 35 println!("{}", d.title); // "SICP [도장]" 36 // &mut 빌림 중에는 다른 빌림 불가 (안전 보장) 37} 38
스코프 종료 시 자동 Drop
출처: std::ops::Drop
빌림 규칙
라이프타임
Clone · Copy
1public class Main { 2 static class Book implements Cloneable { 3 String title; 4 int pages; 5 6 Book(String t, int p) { 7 title = t; 8 pages = p; 9 } 10 @Override 11 public Book clone() { 12 try { 13 return (Book) super.clone(); 14 } catch (CloneNotSupportedException e) { 15 throw new RuntimeException(e); 16 } 17 } 18 @Override 19 public String toString() { 20 return title + "(" + pages + "p)"; 21 } 22 } 23 public static void main(String[] args) { 24 Book b1 = new Book("Clean Code", 431); 25 Book b2 = b1; // 참조 복사 → 같은 객체 26 Book b3 = b1.clone(); // 깊은 복사 → 새 객체 27 b2.title = "Modified"; 28 // b1, b2 같은 객체 29 System.out.println(b1.title); // "Modified" 30 // b3는 별도 객체 31 System.out.println(b3.title); // "Clean Code" 32 // Rust: #[derive(Clone)] 한 줄로 해결 33 } 34} 35
1#[derive(Debug, Clone)] 2struct Book { 3 title: String, 4 pages: u32, 5} 6 7#[derive(Debug, Clone, Copy)] 8struct Point { 9 x: i32, 10 y: i32, 11} 12 13fn main() { 14 // Clone — 명시적 깊은 복사 15 let b1 = Book { 16 title: "Clean Code".to_string(), 17 pages: 431, 18 }; 19 let b2 = b1.clone(); // 힙 데이터 복사 20 println!("{:?}", b1); // b1 여전히 유효 21 println!("{:?}", b2); 22 23 // Copy — 자동 비트 복사 24 let p1 = Point { x: 1, y: 2 }; 25 let p2 = p1; // Copy → p1도 유효! 26 println!("{:?}", p1); // OK 27 println!("{:?}", p2); 28 29 // String은 Copy 불가 (힙 소유) 30 // let s1 = String::from("hello"); 31 // let s2 = s1; // move → s1 무효 32} 33
| 항목 | ☕ Java | Rust |
|---|---|---|
| 변수 기본 | 가변 | 불변 (mut 명시) |
| 함수 전달 | 참조 복사 | 소유권 이동 or 빌림 |
| 동시 수정 | 제한 없음 | 가변 빌림 1개만 |
| 메모리 해제 | GC (런타임) | Drop (컴파일타임) |
| null | NPE 가능 | null 없음 — Option<T> |
| 힙 할당 | new 항상 힙 | Box::new() 명시 시만 |
가변 빌림 &mut — Java에 없는 명시적 구분
1import java.util.*; 2public class Main { 3 // Java: 참조로 전달하면 내부 수정 가능 4 // 불변/가변 구분 없음 (final은 재할당만 막음) 5 static void addItem(List<String> list) { 6 list.add("new item"); // 수정 OK — 컴파일러 무관 7 } 8 static void readOnly(final List<String> list) { 9 list.add("item"); // final이어도 내부 수정 가능! 10 // list = new ArrayList<>(); // 이것만 에러 11 } 12 public static void main(String[] args) { 13 List<String> v = new ArrayList<>(); 14 v.add("a"); 15 addItem(v); 16 System.out.println(v); // [a, new item] 17 } 18}
1// &mut: 수정 허용, &: 읽기만 2fn add_item(list: &mut Vec<String>) { 3 list.push("new item".to_string()); 4} 5fn read_only(list: &Vec<String>) { 6 // list.push(...); // 컴파일 에러! &는 수정 불가 7 println!("{:?}", list); 8} 9fn main() { 10 let mut v = vec!["a".to_string()]; 11 add_item(&mut v); // &mut 명시 필요 12 read_only(&v); // &만으로 읽기 13}
1fn main() { 2 let mut s = String::from("hello"); 3 let r1 = &s; // 불변 빌림 1 4 let r2 = &s; // 불변 빌림 2 — OK 5 let r3 = &mut s; // ❌ error[E0502]: &와 &mut 동시 불가 6 println!("{} {} {}", r1, r2, r3); 7} 8// error[E0502]: cannot borrow `s` as mutable 9// because it is also borrowed as immutable
Box<T> · Rc · Arc — Java new = 항상 힙, Rust = 기본 스택
출처: Rust Book §15.1Rust Book §15.4JVM Spec §2.5
new는 항상 힙. 언어 차원에서 스택 배치 불가.Rust:
struct는 기본 스택. Box::new()로 명시해야만 힙.출처: JVM Spec §2.5 · Rust Book §15.1
1public class Main { 2 static class Book { 3 String title; 4 int pages; 5 Book(String t, int p) { 6 title = t; 7 pages = p; 8 } 9 @Override 10 public String toString() { 11 return "'" + title 12 + "' (" + pages + "p)"; 13 } 14 } 15 public static void main(String[] args) { 16 // 기본 타입 → 스택 (값 직접 저장) 17 int x = 42; // 스택: 4B 18 boolean b = true; // 스택: 1B 19 20 // 객체 → 항상 힙 (언어 차원 선택 불가) 21 Book book = new Book("Clean Code", 431); 22 // Object Header(12~16B) 23 // + title(ref 8B) 24 // + pages(int 4B) 25 26 System.out.println("Stack: " + x + ", " + b); 27 System.out.println("Heap(always): " + book); 28 // Rust: struct Book → 스택 (기본) 29 // Rust: Box::new(Book{..}) → 힙 (명시) 30 } 31} 32
1#[derive(Debug)] 2struct Book { 3 title: String, 4 pages: u32, 5} 6impl Book { 7 fn new(t: &str, p: u32) -> Self { 8 Book { 9 title: t.to_string(), 10 pages: p, 11 } 12 } 13} 14 15trait Printable { 16 fn print_info(&self); 17} 18impl Printable for Book { 19 fn print_info(&self) { 20 println!( 21 "'{}' ({}p)", 22 self.title, 23 self.pages, 24 ); 25 } 26} 27 28fn main() { 29 // 스택에 직접 (기본) 30 let book = Book::new("Clean Code", 431); 31 println!("스택: {:?}", book); 32 33 // Box::new() → 힙에 명시적 할당 34 let boxed = Box::new( 35 Book::new("Refactoring", 448) 36 ); 37 println!("힙: {:?}", boxed); 38 boxed.print_info(); // 자동 역참조 39 40 // Box<dyn Trait> — 동적 디스패치 41 let d: Box<dyn Printable> = Box::new( 42 Book::new("SICP", 657) 43 ); 44 d.print_info(); 45 46 println!( 47 "Box 포인터: {}B, Book 직접: {}B", 48 std::mem::size_of::<Box<Book>>(), 49 std::mem::size_of::<Book>(), 50 ); 51 // 스코프 끝 → 자동 Drop 52} 53
| 항목 | ☕ Java | Rust |
|---|---|---|
| 기본 타입 | 스택 (값 직접) | 스택 (값 직접) |
| 객체/struct | 항상 힙 (new) | 기본 스택 |
| 힙 강제 | new Book() | Box::new(Book{{..}}) |
| Object Header | 12~16B (강제) | 없음 |
| 메모리 해제 | GC | Drop (스코프 끝) |
Box가 필요한 3가지 상황
| 상황 | 예시 | 이유 |
|---|---|---|
| dyn Trait 반환 | fn f() -> Box<dyn Sound> | 크기를 컴파일타임에 모름 |
| 재귀 자료구조 | struct Node {{ next: Box<Node> }} | 무한 크기 방지 |
| 큰 데이터 이동 | Box::new(large_data) | 스택 오버플로 방지 |
Rc<T> · Arc<T> — 공유 소유권 (Java GC 대응)
출처: Rust Book §15.4 Rcstd::sync::Arc
Rc 또는 Arc를 명시적으로 사용해야 합니다.| 상황 | ☕ Java | Rust |
|---|---|---|
| 힙에 하나, 단독 소유 | Sound a = new Lion() | Box<dyn Sound> |
| 여러 곳이 공유 (단일 스레드) | Sound a = new Lion() (GC 공유) | Rc<dyn Sound> |
| 여러 스레드가 공유 | volatile / synchronized | Arc<dyn Sound> |
| 빌림 (소유권 없음) | 없음 (Java는 항상 참조) | &dyn Sound |
1import java.util.*; 2public class Main { 3 public static void main(String[] args) { 4 // Java: GC가 알아서 공유 관리 5 String lion = "Simba"; 6 List<String> zoo1 = new ArrayList<>(); 7 List<String> zoo2 = new ArrayList<>(); 8 zoo1.add(lion); // 두 리스트가 같은 객체 참조 9 zoo2.add(lion); // GC가 수명 관리 10 System.out.println(zoo1.get(0)); // Simba 11 } 12}
1use std::rc::Rc; 2fn main() { 3 let lion = Rc::new("Simba"); 4 let zoo1 = Rc::clone(&lion); // 참조 카운트 +1 5 let zoo2 = Rc::clone(&lion); // 참조 카운트 +1 6 // 카운트 = 3. 모두 drop 되면 해제. 7 println!("{} refs", Rc::strong_count(&lion)); // 3 8} // 스코프 끝 → 카운트 0 → Drop
1use std::sync::Arc; 2use std::thread; 3fn main() { 4 let lion = Arc::new("Simba"); 5 let lion2 = Arc::clone(&lion); 6 let handle = thread::spawn(move || { 7 println!("thread: {}", lion2); // 다른 스레드에서 접근 8 }); 9 handle.join().unwrap(); 10 println!("main: {}", lion); 11}
Rc는 단일 스레드 전용 — 멀티 스레드에서 사용 시 컴파일 에러.Arc는 원자적(atomic) 참조 카운트로 스레드 안전. 약간의 성능 오버헤드 있음.Java는 GC가 두 경우를 모두 투명하게 처리하지만, Rust는 명시적 선택이 필요.
클로저 · 람다
출처: Rust Book §13.1
1import java.util.function.*; 2public static void main(String[] args) { 3 Function<Integer, Integer> doubler = x -> x * 2; 4 Function<Integer, Integer> addOne = x -> x + 1; 5 System.out.println(doubler.apply(5)); // 10 6 System.out.println(addOne.apply(5)); // 6 7 8 Predicate<String> isLong = s -> s.length() > 5; 9 System.out.println(isLong.test("Rust")); // false 10 11 BiFunction<Integer, Integer, Integer> add = 12 (a, b) -> a + b; 13 System.out.println(add.apply(3, 4)); // 7 14 15 // 캡처 (effectively final만 가능) 16 String prefix = "Book: "; 17 Function<String, String> label = t -> prefix + t; 18 System.out.println(label.apply("Clean Code")); 19 // prefix = "X"; // 에러 20}
1fn apply<F: Fn(i32) -> i32>(f: F, x: i32) -> i32 { f(x) } 2fn main() { 3 let double = |x| x * 2; 4 let add_one = |x: i32| x + 1; 5 println!("{}", apply(double, 5)); 6 println!("{}", apply(add_one, 5)); 7 let mut count = 0i32; 8 let mut inc = || { count += 1; count }; 9 println!("{} {}", inc(), inc()); 10} 11
| 트레이트 | 호출 횟수 | 캡처 방식 | Java 대응 |
|---|---|---|---|
Fn | 여러 번 | 불변 참조 | Function<T,R> |
FnMut | 여러 번 | 가변 참조 | Consumer<T> |
FnOnce | 1번만 | 소유권 이동 | 없음 |
Rust 클로저는 &(불변), &mut(가변), move(소유권) 세 가지로 명시 가능.
캡처 방식 3가지 — SVG로 이해
Fn · FnMut · FnOnce — 3종 완전 비교
Fn: 불변 캡처 — 몇 번이든 호출 가능. Java Function<T,R> 대응FnMut: 가변 캡처 — 몇 번이든 호출 가능하지만 mut 필요. Java Consumer<T> 대응FnOnce: 소유권 이동 — 단 1번만 호출. Java에 없음 (move 의미론)
1import java.util.function.*; 2public class Main { 3 // Fn 대응: Function<T,R> — 여러 번 호출 4 static int callFn(Function<Integer,Integer> f, int x) { 5 return f.apply(x); 6 } 7 // FnMut 대응: Consumer<T> — 내부 상태 변경 8 static void callFnMut(Runnable f) { f.run(); } 9 // FnOnce 대응: Java에 없음 — 1번만 실행 보장 불가 10 public static void main(String[] args) { 11 int base = 10; 12 // effectively final만 캡처 가능 13 Function<Integer,Integer> add = 14 x -> x + base; 15 System.out.println(callFn(add, 5)); // 15 16 System.out.println(callFn(add, 3)); // 13 17 // 내부 상태 변경: int[]로 우회 18 int[] cnt = {0}; 19 Runnable inc = () -> cnt[0]++; 20 inc.run(); inc.run(); 21 System.out.println(cnt[0]); // 2 22 } 23}
1// Fn — 불변 캡처, 여러 번 호출 가능 2fn call_fn<F: Fn(i32) -> i32>(f: &F, x: i32) -> i32 { 3 f(x) 4} 5// FnMut — 가변 캡처, 여러 번 호출 가능 6fn call_fn_mut<F: FnMut() -> i32>(mut f: F) -> i32 { 7 f() + f() // 두 번 호출 8} 9// FnOnce — 소유권 이동, 1번만 호출 10fn call_once<F: FnOnce() -> String>(f: F) -> String { 11 f() // 1번만 — 이후 f 사용 불가 12} 13fn main() { 14 let base = 10i32; 15 // Fn: 불변 참조 캡처 16 let add = |x| x + base; 17 println!("{}", call_fn(&add, 5)); // 15 18 println!("{}", call_fn(&add, 3)); // 13 19 // FnMut: 가변 참조 캡처 (mut 필수) 20 let mut count = 0i32; 21 let inc = || { count += 1; count }; 22 println!("{}", call_fn_mut(inc)); // 1+2=3 23 // FnOnce: 소유권 이동 (move) 24 let name = String::from("Rust"); 25 let greet = move || format!("Hello, {}!", name); 26 println!("{}", call_once(greet)); // Hello, Rust! 27 // greet(greet); // ❌ 이미 소비됨 28} 29
| 트레이트 | 캡처 방식 | 호출 횟수 | mut 필요 | Java 대응 |
|---|---|---|---|---|
Fn | &T 불변 참조 | 무제한 | 불필요 | Function<T,R> |
FnMut | &mut T 가변 참조 | 무제한 | 필요 | Consumer<T> (우회) |
FnOnce | 소유권 이동 | 1번 | 불필요 | 없음 |
1fn main() { 2 let mut count = 0; 3 // count를 &mut 캡처 → FnMut 4 let mut inc = || { 5 count += 1; 6 count 7 }; 8 println!("{}", inc()); // 1 9 println!("{}", inc()); // 2 10 println!("{}", inc()); // 3 11}
1fn consume<F: FnOnce() -> String>(f: F) -> String { 2 f() // 1번만 호출 가능 3} 4fn main() { 5 let name = String::from("Rust"); 6 // move: name 소유권이 클로저 안으로 7 let greet = move || format!("Hello, {}!", name); 8 // println!("{}", name); // ❌ name 이미 이동됨 9 println!("{}", consume(greet)); 10}
고차 함수 — 클로저를 인자/반환값으로
1import java.util.function.*; 2import java.util.*; 3import java.util.stream.*; 4public class Main { 5 // 함수를 인자로 받는 메서드 6 static <T> List<T> myFilter( 7 List<T> list, Predicate<T> pred) { 8 return list.stream() 9 .filter(pred) 10 .collect(Collectors.toList()); 11 } 12 // 함수를 반환하는 메서드 13 static Function<Integer,Integer> multiplier(int n) { 14 return x -> x * n; 15 } 16 public static void main(String[] args) { 17 List<Integer> nums = List.of(1,2,3,4,5); 18 var evens = myFilter(nums, x -> x > 3); System.out.println(evens); // [4, 5] 19 Function<Integer,Integer> x3 = multiplier(3); 20 System.out.println(x3.apply(7)); // 21 21 } 22}
1// impl Fn: 클로저를 인자로 2fn my_filter<T, F>(v: Vec<T>, pred: F) -> Vec<T> 3where F: Fn(&T) -> bool { 4 v.into_iter().filter(|x| pred(x)).collect() 5} 6// impl Fn: 클로저를 반환 7fn multiplier(n: i32) -> impl Fn(i32) -> i32 { 8 move |x| x * n // n을 move 캡처 9} 10fn main() { 11 let nums = vec![1,2,3,4,5]; 12 let evens = my_filter(nums, |x| x % 2 == 0); 13 let triple = multiplier(3); 14 println!("{:?}", evens); // [2, 4] 15 println!("{}", triple(7)); // 21 16}
| 트레이트 | 호출 | 캡처 | Java 대응 | 주요 용도 |
|---|---|---|---|---|
Fn | 여러 번 | &T (불변) | Function<T,R> | 일반 변환, 필터 |
FnMut | 여러 번 | &mut T (가변) | Consumer<T> | 누적, 카운터 |
FnOnce | 1번만 | T (소유권) | 없음 | 스레드 전달, 소비 |
Fn ⊂ FnMut ⊂ FnOnce — Fn을 구현하면 FnMut, FnOnce도 자동 구현.함수 인자로
FnOnce를 받으면 세 가지 모두 수용 가능 (가장 넓은 계약).출처: Rust Reference — Closure Types
컬렉션 · 함수형
출처: Rust Book §8Rust Book §13.2
⑬ Vec — Java ArrayList
1import java.util.*; 2import java.util.stream.*; 3public class Main { 4 public static void main(String[] args) { 5 List<String> v = new ArrayList<>( 6 List.of( 7 "Clean Code", "Refactoring", "SICP")); 8 v.add("DDIA"); // push 9 v.add(0, "DDD"); // insert(0) 10 String last = v.get(v.size()-1); 11 v.remove(v.size()-1); // pop() 12 v.stream() 13 .filter(s -> s.length() > 4) 14 .map(String::toUpperCase) 15 .forEach(System.out::println); 16 System.out.println(last); 17 } 18}
1fn main() { 2 // ─── 생성 ────────────────────────────────────── 3 let mut shelf: Vec<String> = Vec::new(); 4 // vec! 매크로로 초기화 5 let mut v = vec![ 6 "Clean Code".to_string(), 7 "Refactoring".to_string(), 8 "SICP".to_string(), 9 ]; 10 11 // ─── 추가 / 삭제 ────────────────────────────── 12 v.push("DDIA".to_string()); // add(e) — 끝에 추가 13 v.insert(0, "DDD".to_string()); // add(0, e) — 앞에 삽입 14 let last = v.pop(); // remove(n-1) — 끝 제거 15 println!("pop: {:?}", last); 16 println!("크기: {}", v.len()); // size() 17 18 // ─── 접근 ────────────────────────────────────── 19 println!("첫 번째: {}", v[0]); // get(0) 대응 20 println!("안전 접근: {:?}", v.get(100)); // None 반환 21 22 // ─── 순회 ────────────────────────────────────── 23 for title in &v { // for (String t : list) 24 print!("{} / ", title); 25 } 26 println!(); 27 28 // 인덱스와 함께 29 for (i, title) in v.iter().enumerate() { 30 println!("[{}] {}", i, title); 31 } 32 33 // ─── 함수형 체인 ────────────────────────────── 34 let long_titles: Vec<&str> = v.iter() 35 .filter(|t| t.len() > 4) 36 .map(|t| t.as_str()) 37 .collect(); 38 println!("긴 제목: {:?}", long_titles); 39 40 // ─── 정렬 ────────────────────────────────────── 41 v.sort(); 42 println!("정렬: {:?}", v); 43 v.sort_by(|a, b| b.cmp(a)); // 역순 44 println!("역순: {:?}", v); 45 46 // ─── 불변 컬렉션 패턴 ───────────────────────── 47 let fixed = vec![1i32, 2, 3]; // mut 없음 → 수정 불가 48 println!("contains 3: {}", fixed.contains(&3)); 49} 50
⑭ HashMap
1import java.util.*; 2public class Main { 3 public static void main(String[] args) { 4 Map<String,String> lib = new HashMap<>(); 5 lib.put("isbn-001", "Clean Code"); 6 lib.put("isbn-002", "Refactoring"); 7 lib.getOrDefault("isbn-999", "none"); 8 lib.putIfAbsent("isbn-003", "DDIA"); 9 lib.forEach((k, v) -> System.out 10 .println(k + " → " + v)); 11 // 빈도 계산 12 Map<String,Integer> cnt = new HashMap<>(); 13 String[] ws = {"a","b","a"}; for (String w : ws) 14 cnt.merge(w, 1, Integer::sum); 15 System.out.println(cnt); 16 } 17}
1use std::collections::HashMap; 2 3#[derive(Debug, Clone)] 4struct Book { 5 title: String, 6 pages: u32, 7} 8impl Book { 9 fn new(t: &str, p: u32) -> Self { 10 Book { title: t.to_string(), pages: p } 11 } 12} 13 14fn main() { 15 let mut library: HashMap<String, Book> = HashMap::new(); 16 17 // ─── 추가 ────────────────────────────────────── 18 library.insert( 19 "isbn-001".to_string(), 20 Book::new("Clean Code", 431), 21 ); 22 library.insert( 23 "isbn-002".to_string(), 24 Book::new("Refactoring", 448), 25 ); 26 27 // ─── 조회 ────────────────────────────────────── 28 // get → Option<&V> 29 if let Some(book) = library.get("isbn-001") { 30 println!("찾음: {}", book.title); 31 } 32 // get_or_insert — 없으면 삽입 (Entry API) 33 library.entry("isbn-003".to_string()) 34 .or_insert_with(|| Book::new("SICP", 657)); 35 36 // ─── 삭제 / 존재 확인 ────────────────────────── 37 println!("exists: {}", library.contains_key("isbn-001")); 38 library.remove("isbn-001"); 39 println!("after remove: {}", library.len()); // 2 40 41 // ─── 순회 ────────────────────────────────────── 42 for (isbn, book) in &library { 43 println!("{} → {}", isbn, book.title); 44 } 45 46 // ─── 함수형 ──────────────────────────────────── 47 let titles: Vec<&str> = library.values() 48 .map(|b| b.title.as_str()) 49 .collect(); 50 println!("제목들: {:?}", titles); 51 52 // ─── 카운터 패턴 ────────────────────────────── 53 let text = "hello world hello rust world"; 54 let mut counts: HashMap<&str, u32> = HashMap::new(); 55 for word in text.split_whitespace() { 56 *counts.entry(word).or_insert(0) += 1; 57 } 58 println!("word counts: {:?}", counts); 59} 60
⑮ Iterator 체인 비교
1import java.util.*; 2import java.util.stream.*; 3 4public static void main(String[] args) { 5 List<Integer> nums = List.of(1, 2, 3, 4, 5); 6 7 // Stream API — 지연 평가 (Rust Iterator) 8 int sum = nums.stream() 9 .filter(n -> n % 2 == 0) 10 .map(n -> n * 2) 11 .reduce(0, Integer::sum); 12 System.out.println(sum); // 12 13 14 List<String> names = nums.stream() 15 .map(n -> "book-" + n) 16 .collect(Collectors.toList()); 17 System.out.println(names); 18 19 boolean any = nums.stream() 20 .anyMatch(n -> n > 4); 21 long cnt = nums.stream() 22 .filter(n -> n > 2) 23 .count(); 24 System.out.println(any + " " + cnt); 25}
1fn main() { 2 let nums = [1i32, 2, 3, 4, 5]; 3 4 // filter → map → sum (지연 평가) 5 let sum: i32 = nums.iter() 6 .filter(|&&n| n % 2 == 0) 7 .map(|&n| n * 2) 8 .sum(); 9 println!("{}", sum); // 12 10 11 // collect → Vec<String> 12 let names: Vec<String> = nums.iter() 13 .map(|n| format!("book-{}", n)) 14 .collect(); 15 println!("{:?}", names); 16 17 // any / count 18 let any_big = nums.iter().any(|&n| n > 4); 19 let cnt = nums.iter() 20 .filter(|&&n| n > 2) 21 .count(); 22 println!("any={} cnt={}", any_big, cnt); 23} 24
iter() · iter_mut() · into_iter() — Java for-each 비교
iter(): &T — 불변 참조 반복, 원본 유지iter_mut(): &mut T — 가변 참조 반복, 원본 수정 가능into_iter(): T — 소유권 이동, 반복 후 원본 소비
1import java.util.*; 2public class Main { 3 public static void main(String[] args) { 4 List<String> names = new ArrayList<>( 5 List.of("Simba", "Donald", "Nagini")); 6 // iter() 대응: for-each (읽기만) 7 for (String n : names) 8 System.out.println(n); 9 // iter_mut() 대응: ListIterator (수정) 10 ListIterator<String> it = 11 names.listIterator(); 12 while (it.hasNext()) { 13 String s = it.next(); 14 it.set(s.toUpperCase()); // 수정 15 } 16 System.out.println(names); 17 // into_iter() 대응: 없음 (Java는 항상 참조) 18 // Java는 소유권 이동 개념 없음 → GC가 관리 19 List<String> consumed = names; 20 System.out.println(consumed); 21 System.out.println(names); // 여전히 OK 22 } 23}
1fn main() { 2 let mut names = vec![ 3 "Simba".to_string(), 4 "Donald".to_string(), 5 "Nagini".to_string(), 6 ]; 7 // iter() → &String, 원본 유지 8 for n in names.iter() { // n: &String 9 print!("{} ", n); 10 } 11 println!(); 12 // iter_mut() → &mut String, 원본 수정 13 for n in names.iter_mut() { // n: &mut String 14 *n = n.to_uppercase(); 15 } 16 println!("{:?}", names); 17 // into_iter() → String, 소유권 이동 18 for n in names.into_iter() { // n: String 19 println!("{}", n); 20 } 21 // println!("{:?}", names); // ❌ 소유권 이동됨 22} 23
| 메서드 | 반환 타입 | 원본 상태 | Java 대응 |
|---|---|---|---|
iter() | &T | 유지 | for (T x : list) |
iter_mut() | &mut T | 수정 가능 | ListIterator.set() |
into_iter() | T | 소비됨 | 없음 (GC가 관리) |
| Java Stream | Rust Iterator | 설명 |
|---|---|---|
stream() | .iter() | 불변 이터레이터 |
filter(pred) | .filter(|x|..) | 조건 필터링 |
map(fn) | .map(|x|..) | 변환 |
collect(toList()) | .collect() | 소비→컬렉션 |
reduce(0,sum) | .sum() | 합산 |
anyMatch() | .any(|x|..) | 하나라도 참 |
count() | .count() | 개수 |
flatMap() | .flat_map() | 중첩 해제 |
collect(), sum())이 호출될 때까지 실제 연산이 실행되지 않습니다.제네릭 · trait bound · struct 메서드
출처: Rust Book §10.1Rust Book §10.2
struct 메서드
1public class Main { 2 static class Rectangle { 3 private double width, height; 4 5 static Rectangle of(double w, double h) { 6 Rectangle r = new Rectangle(); 7 r.width = w; 8 r.height = h; 9 return r; 10 } 11 static Rectangle square(double s) { 12 return of(s, s); 13 } 14 double area() { 15 return width * height; 16 } 17 double perimeter() { 18 return 2 * (width + height); 19 } 20 boolean isSquare() { 21 return Math.abs(width - height) 22 < 1e-10; 23 } 24 // &mut self 대응 — Java는 구분 없음 25 Rectangle scale(double f) { 26 width *= f; 27 height *= f; 28 return this; 29 } 30 @Override 31 public String toString() { 32 return String.format( 33 "%.1fx%.1f (area=%.1f)", 34 width, height, area()); 35 } 36 } 37 public static void main(String[] args) { 38 var r = Rectangle.of(3.0, 4.0); 39 System.out.println(r.area()); // 12 40 r.scale(2.0); 41 System.out.println(r); // 6.0x8.0 42 System.out.println( 43 Rectangle.square(5.0)); // 5.0x5.0 44 } 45} 46
1#[derive(Debug)] 2struct Rectangle { 3 width: f64, 4 height: f64, 5} 6 7impl Rectangle { 8 // 연관 함수 (Java static factory) 9 fn new(w: f64, h: f64) -> Self { 10 Rectangle { width: w, height: h } 11 } 12 fn square(s: f64) -> Self { 13 Rectangle { width: s, height: s } 14 } 15 16 // &self — 불변 메서드 (Java 일반 메서드) 17 fn area(&self) -> f64 { 18 self.width * self.height 19 } 20 fn perimeter(&self) -> f64 { 21 2.0 * (self.width + self.height) 22 } 23 fn is_square(&self) -> bool { 24 (self.width - self.height).abs() 25 < f64::EPSILON 26 } 27 28 // &mut self — 가변 메서드 (Java는 구분 없음) 29 fn scale(&mut self, f: f64) { 30 self.width *= f; 31 self.height *= f; 32 } 33 34 // self — 소유권 소비 (Java에 없음) 35 fn describe(self) -> String { 36 format!( 37 "{:.1}x{:.1} (area={:.1})", 38 self.width, 39 self.height, 40 self.width * self.height, 41 ) 42 } 43} 44 45fn main() { 46 let mut r = Rectangle::new(3.0, 4.0); 47 println!("{}", r.area()); // 12 48 println!("{}", r.perimeter()); // 14 49 println!("{}", r.is_square()); // false 50 r.scale(2.0); 51 println!("{:?}", r); 52 let s = Rectangle::square(5.0); 53 println!("{}", s.describe()); 54} 55
| 수신자 | Rust | Java 대응 |
|---|---|---|
| 없음 | fn new() | static T of() |
&self | 읽기 전용 | 일반 메서드 |
&mut self | 수정 가능 | 일반 메서드 (구분 없음) |
self | 소유권 소비 (1회만) | 없음 |
제네릭 (Generics)
1import java.util.*; 2public class Main { 3 // 제네릭 — 타입 소거 → 런타임 동적 디스패치 4 static <T extends Comparable<T>> T largest( 5 List<T> list) { 6 T max = list.get(0); 7 for (T item : list) { 8 if (item.compareTo(max) > 0) max = item; 9 } 10 return max; 11 } 12 static class Pair<T extends Comparable<T>> { 13 final T first, second; 14 Pair(T f, T s) { first=f; second=s; } 15 T larger() { 16 return first.compareTo(second) >= 0 17 ? first : second; 18 } 19 } 20 public static void main(String[] args) { 21 System.out.println( 22 largest(List.of(34,50,25,100,65))); // 100 23 var p = new Pair<>(5, 10); 24 System.out.println(p.larger()); // 10 25 // Java 제네릭 = 타입 소거 → 동적 디스패치 26 // Rust 제네릭 = 단형화 → 정적 디스패치 27 } 28}
1fn largest<T: PartialOrd>(list: &[T]) -> &T { 2 let mut l = &list[0]; 3 for item in list { 4 if item > l { l = item; } 5 } 6 l 7} 8 9struct Pair<T> { 10 first: T, 11 second: T, 12} 13impl<T: std::fmt::Display + PartialOrd> Pair<T> { 14 fn new(f: T, s: T) -> Self { 15 Pair { first: f, second: s } 16 } 17 fn cmp_display(&self) { 18 if self.first >= self.second { 19 println!("first={}", self.first); 20 } else { 21 println!("second={}", self.second); 22 } 23 } 24} 25 26fn main() { 27 let nums = vec![34i32, 50, 25, 100, 65]; 28 println!("{}", largest(&nums)); // 100 29 30 let p = Pair::new(5i32, 10); 31 p.cmp_display(); // "second=10" 32} 33
| 항목 | Java 제네릭 | Rust 제네릭 |
|---|---|---|
| 구현 | 타입 소거 → 런타임 Object | 단형화 → 컴파일타임 구체 함수 |
| vtable | 있음 (동적) | 없음 (직접 CALL) |
| bound 문법 | <T extends Sound> | <T: Sound> |
trait bound · where · impl Trait 반환
1import java.util.*; 2public class Main { 3 // 제네릭 — 타입 소거 → 런타임 동적 디스패치 4 static <T extends Comparable<T>> T largest( 5 List<T> list) { 6 T max = list.get(0); 7 for (T item : list) { 8 if (item.compareTo(max) > 0) max = item; 9 } 10 return max; 11 } 12 static class Pair<T extends Comparable<T>> { 13 final T first, second; 14 Pair(T f, T s) { first=f; second=s; } 15 T larger() { 16 return first.compareTo(second) >= 0 17 ? first : second; 18 } 19 } 20 public static void main(String[] args) { 21 System.out.println( 22 largest(List.of(34,50,25,100,65))); // 100 23 var p = new Pair<>(5, 10); 24 System.out.println(p.larger()); // 10 25 // Java 제네릭 = 타입 소거 → 동적 디스패치 26 // Rust 제네릭 = 단형화 → 정적 디스패치 27 } 28}
1use std::fmt; 2fn print_info<T: fmt::Display + fmt::Debug>(item: &T) { 3 println!("Display: {}", item); 4 println!("Debug: {:?}", item); 5} 6fn complex<T, U>(t: &T, u: &U) -> String 7where T: fmt::Display + Clone, U: fmt::Debug + Clone { 8 format!("{} {:?}", t, u) 9} 10fn make_adder(x: i32) -> impl Fn(i32) -> i32 { move |y| x + y } 11fn main() { 12 print_info(&42i32); 13 println!("{}", complex(&"hello", &vec![1,2,3])); 14 let add5 = make_adder(5); 15 println!("{}", add5(3)); 16}
fn make_adder(x: i32) -> impl Fn(i32) -> i32구체 타입을 숨기면서 정적 디스패치 유지. vtable 없음.
출처: Rust Reference — impl Trait
라이프타임 (Lifetimes) — Java 비교
출처: Rust Book §10.3Rust Reference — ElisionRust Book — Elision Rules
라이프타임은 참조가 유효한 범위를 나타내며, 컴파일러가 자동으로 추론합니다.
Java는 GC가 참조를 관리하므로 이 개념이 없습니다.
출처: Rust Book §10.3 — Lifetime Syntax
핵심 규칙 — dangling pointer 차단
참조 변수가 가리키고 있는 동안 GC는 객체를 해제하지 않습니다.
개발자가 라이프타임을 신경 쓸 필요 없지만,
GC pause · 메모리 오버헤드 · NPE 위험이 있습니다.
참조는 원본 데이터보다 오래 살 수 없습니다.
이를 어기면 E0597 컴파일 에러.
GC 없이 dangling pointer를 원천 차단합니다.
라이프타임 명시가 필요한 경우
명시가 필요한 경우: ① 여러 참조를 받아 참조를 반환하는 함수 ② struct에 참조 필드를 저장할 때
출처: Rust Reference — Lifetime Elision
1public class Main { 2 // Java: GC가 참조를 살려두므로 3 // 라이프타임 개념이 없음 4 static String dangling() { 5 StringBuilder sb = new StringBuilder("local"); 6 return sb.toString(); // String 반환 (복사) 7 // Rust: 참조(&str) 반환 시 컴파일 에러! 8 // dangling pointer 원천 차단 9 } 10 11 // Java: 참조가 살아있는 한 GC가 보존 12 static String outer; 13 14 static void lifetime_demo() { 15 String local = "hello world"; 16 outer = local; // OK — GC가 살려둠 17 // Rust: outer = &local; 은 에러! 18 // local의 라이프타임이 함수 끝에 끝남 19 } 20 21 // Java: 메서드 간 참조 전달 — 자동 22 static String firstWord(String s) { 23 int space = s.indexOf(' '); 24 if (space < 0) return s; 25 return s.substring(0, space); 26 // Rust: &str 반환 시 라이프타임 명시 필요 27 // fn first_word(s: &str) -> &str 28 // (여기선 생략 가능) 29 } 30 31 public static void main(String[] args) { 32 System.out.println(dangling()); 33 lifetime_demo(); 34 System.out.println(outer); 35 System.out.println( 36 firstWord("hello world")); 37 // Java: 라이프타임 신경 쓸 필요 없음 38 // 대신 GC pause + NPE 위험 39 } 40} 41
1// ── 1. 라이프타임이 필요한 이유 2// 컴파일러가 둘 중 어느 참조를 반환할지 모름 3// → 'a 로 "둘 다 최소한 'a 동안 유효" 명시 4fn longest<'a>( 5 x: &'a str, 6 y: &'a str, 7) -> &'a str { 8 if x.len() > y.len() { x } else { y } 9} 10 11// ── 2. struct에 참조 저장 12struct Excerpt<'a> { 13 // 'a: Excerpt가 살아있는 동안 14 // part 원본도 살아있어야 함 15 part: &'a str, 16} 17impl<'a> Excerpt<'a> { 18 fn announce( 19 &self, 20 ann: &str, 21 ) -> &str { 22 println!("주의: {}", ann); 23 self.part 24 } 25} 26 27// ── 3. 생략(elision) — 컴파일러가 자동 추론 28// 아래 두 함수는 라이프타임이 동일 29fn first_word(s: &str) -> &str { 30 match s.find(' ') { 31 Some(i) => &s[..i], 32 None => s, 33 } 34} 35// 명시 버전 (위와 완전히 동일) 36fn first_word_explicit<'a>( 37 s: &'a str, 38) -> &'a str { 39 match s.find(' ') { 40 Some(i) => &s[..i], 41 None => s, 42 } 43} 44 45// ── 4. 'static — 프로그램 전체 수명 46fn static_demo() { 47 // 문자열 리터럴 → 바이너리에 포함 → 'static 48 let s: &'static str = "항상 살아있음"; 49 println!("{}", s); 50} 51 52fn main() { 53 // longest 예제 54 let s1 = String::from("long string"); 55 { 56 let s2 = String::from("xyz"); 57 let result = longest(&s1, &s2); 58 println!("longest: {}", result); 59 } // s2 Drop → result도 여기까지만 유효 60 61 // struct 예제 62 let novel = String::from( 63 "Call me Ishmael. Some years ago...", 64 ); 65 let first = novel.split('.').next().unwrap(); 66 let e = Excerpt { part: first }; 67 println!("{}", e.announce("중요!")); 68 69 // 생략 예제 70 let sentence = String::from("hello world"); 71 println!("{}", first_word(&sentence)); 72 println!("{}", first_word_explicit(&sentence)); 73 74 static_demo(); 75} 76
라이프타임 생략(Elision) — 컴파일러 자동 추론
1// 라이프타임 생략(elision) — 컴파일러 자동 추론 2// 아래 세 함수는 모두 동일한 라이프타임을 가짐 3 4// 생략 버전 (컴파일러가 'a 자동 추론) 5fn first_word(s: &str) -> &str { 6 let bytes = s.as_bytes(); 7 for (i, &b) in bytes.iter().enumerate() { 8 if b == b' ' { 9 return &s[0..i]; 10 } 11 } 12 &s[..] 13} 14 15// 명시 버전 (위와 동일) 16fn first_word_explicit<'a>( 17 s: &'a str, 18) -> &'a str { 19 let bytes = s.as_bytes(); 20 for (i, &b) in bytes.iter().enumerate() { 21 if b == b' ' { 22 return &s[0..i]; 23 } 24 } 25 &s[..] 26} 27 28fn main() { 29 let s = String::from("hello world"); 30 // 생략 버전 31 let w1 = first_word(&s); 32 println!("{}", w1); // "hello" 33 // 명시 버전 34 let w2 = first_word_explicit(&s); 35 println!("{}", w2); // "hello" 36} 37
| 규칙 | 설명 | 예시 |
|---|---|---|
| 규칙 1 | 입력 참조가 하나면 출력에 같은 라이프타임 | fn f(s: &str) -> &str |
| 규칙 2 | &self/&mut self가 있으면 출력에 self 라이프타임 | fn f(&self) -> &str |
| 나머지 | 위 규칙 미적용 시 명시 필요 | fn longest<'a>(x: &'a str, ...) -> &'a str |
'static — 프로그램 전체 수명
| 종류 | 예시 | 설명 |
|---|---|---|
| 문자열 리터럴 | let s: &'static str = "hello"; | 바이너리에 포함 → 항상 유효 |
| 정적 변수 | static MAX: u32 = 100; | 프로그램 전체 수명 |
| trait bound | T: 'static | 소유권 있는 타입 또는 &'static만 허용 |
| 항목 | ☕ Java | Rust |
|---|---|---|
| 메모리 관리 | GC (런타임) | 라이프타임 (컴파일타임) |
| dangling pointer | GC가 방지 | 컴파일 에러로 방지 |
| 런타임 비용 | GC pause 있음 | 없음 (0 비용) |
| null 참조 | NPE 가능 | Option<T> (강제) |
struct에 참조 필드 — 라이프타임 명시 필수
1public class Main { 2 // Java: GC가 참조 수명을 자동 관리 3 // 원본이 GC되면 참조도 무효 — 런타임에서야 알 수 있음 4 static class Excerpt { 5 private final String part; // String 자체를 복사해 저장 6 public Excerpt(String text) { 7 this.part = text.split("\\\\.")[0]; 8 } 9 public String getPart() { return part; } 10 } 11 // Java는 &str 슬라이스 개념이 없어 12 // 항상 String 복사본을 만들어야 함 13 public static void main(String[] args) { 14 Excerpt e = new Excerpt("Hello. World."); 15 System.out.println(e.getPart()); // Hello 16 } 17}
1// 참조 필드를 가진 struct는 라이프타임 필수 2struct Excerpt<'a> { 3 part: &'a str, // 복사 없이 슬라이스 참조 4} 5impl<'a> Excerpt<'a> { 6 fn announce(&self) { 7 println!("발췌: {}", self.part); 8 } 9} 10fn main() { 11 let novel = String::from("Call me Ishmael. Some years ago..."); 12 let first = novel.split('.').next().unwrap(); 13 let ex = Excerpt { part: first }; // 복사 없음! 14 ex.announce(); // novel보다 ex가 먼저 drop됨 → OK 15} // novel drop → ex는 이미 없으므로 안전
Excerpt<'a>의 의미: "이 struct 인스턴스는 'a가 유효한 동안만 존재 가능"Java는 항상 String을 복사해 저장 — Rust는 복사 없이 원본 슬라이스를 안전하게 참조 가능.
제네릭 · 템플릿 — Java · C++ · Rust 비교
C++17 기준 · Java 21 기준 · Rust 1.82 기준 — Template Instantiation / Type Erasure / Monomorphization 완전 비교
① 왜 제네릭/템플릿이 필요한가
타입마다 같은 로직을 반복하는 문제를 해결하기 위해 등장했다. 세 언어 모두 <T> 문법을 사용하지만 컴파일 타임 처리 방식이 근본적으로 다르다.
1class PrintInt { 2 static void print(int v) { 3 System.out.println("int: " + v); 4 } 5} 6class PrintStr { 7 static void print(String v) { 8 System.out.println("str: " + v); 9 } 10} 11public class Main { 12 public static void main(String[] args) { 13 PrintInt.print(42); 14 PrintStr.print("hello"); 15 // Problem: duplicate code per type 16 // Solution: generics 17 } 18}
1#include <iostream> 2#include <string> 3 4void print_int(int v) { 5 std::cout << "int: " << v << "\n"; 6} 7void print_str(const std::string& v) { 8 std::cout << "str: " << v << "\n"; 9} 10 11// Solution: template 12template<typename T> 13void print_any(const T& v) { 14 std::cout << "val: " << v << "\n"; 15} 16 17int main() { 18 print_int(42); 19 print_str("hello"); 20 print_any(42); 21 print_any(std::string("hello")); 22 return 0; 23}
1fn print_i32(v: i32) { 2 println!("i32: {}", v); 3} 4fn print_str(v: &str) { 5 println!("str: {}", v); 6} 7// Solution: generic function 8fn print_any<T: std::fmt::Display>(v: T) { 9 println!("val: {}", v); 10} 11fn main() { 12 print_i32(42); 13 print_str("hello"); 14 print_any(42); 15 print_any("hello"); 16 print_any(3.14f64); 17}
② Box<T> — Template Instantiation vs Type Erasure vs Monomorphization
상자 = 실제 생성된 객체 (new 할 때마다 힙에 생성)
C++: Stack<int>용 공장 따로 + Stack<string>용 공장 따로 → 타입마다 공장이 다름
Java: Stack(Object) 공장 하나만 → 공장은 하나, 상자는 여러 개 생성 가능
Rust: Stack<i32>용 공장 따로 + Stack<String>용 공장 따로 → C++와 동일
오해 주의: Java도 객체(상자)는 여러 개 만든다. 차이는 공장 코드가 몇 개냐의 차이다.
Stack<int> 첫 사용 → int 전용 공장 생성 (바이너리에 1회만)
Stack<int> 두번째 new → 공장 재사용, 객체만 새로 생성
Stack<string> 첫 사용 → string 전용 공장 별도 생성
공장(클래스 코드) = 타입당 1개 상자(객체) = new 할 때마다 1개
C++17 Template Instantiation: Box<int> 사용 시 컴파일러가 int 전용 독립 코드를 생성한다. Box<string>은 또 다른 독립 코드다 (cppreference §Template instantiation). 개념 설명용으로 Box_int라 부르지만 실제 이름은 아니다.
Java Type Erasure: Box<Integer>와 Box<String>은 컴파일 후 하나의 Box(Object)로 합쳐진다 (JLS §4.6). 런타임에 T 정보는 사라진다.
Rust Monomorphization: C++와 동일하게 컴파일 타임에 타입별 독립 코드를 생성한다 (The Rust Book §10.1). 런타임 오버헤드 없음.
1class Box<T> { 2 private T value; 3 Box(T v) { this.value = v; } 4 T get() { return value; } 5 @Override 6 public String toString() { 7 return "Box(" + value + ")"; 8 } 9} 10public class Main { 11 public static void main(String[] args) { 12 Box<Integer> a = new Box<>(42); 13 Box<String> b = new Box<>("hello"); 14 // Type Erasure: both become Box(Object) 15 // at runtime - no separate classes 16 System.out.println(a.get()); // 42 17 System.out.println(b.get()); // hello 18 System.out.println( 19 a.getClass() == b.getClass()); // true 20 } 21}
1#include <iostream> 2#include <string> 3 4template<typename T> 5class Box { 6public: 7 T value; 8 Box(T v) : value(v) {} 9 T get() const { return value; } 10}; 11// Template Instantiation: 12// Box<int> -> separate int-specific code 13// Box<string> -> separate string-specific code 14 15int main() { 16 Box<int> a(42); 17 Box<std::string> b("hello"); 18 std::cout << a.get() << "\n"; // 42 19 std::cout << b.get() << "\n"; // hello 20 // a and b are completely different types 21 return 0; 22}
1struct Box<T> { 2 value: T, 3} 4impl<T: std::fmt::Display> Box<T> { 5 fn new(v: T) -> Self { Box { value: v } } 6 fn get(&self) -> &T { &self.value } 7} 8// Monomorphization: 9// Box<i32> -> i32-specific code at compile time 10// Box<String> -> String-specific code at compile time 11// Same result as C++ Template Instantiation 12fn main() { 13 let a = Box::new(42i32); 14 let b = Box::new(String::from("hello")); 15 println!("{}", a.get()); // 42 16 println!("{}", b.get()); // hello 17}
Java → 하나의 클래스로 처리 (바이너리 크기 불변, boxing/casting 비용 발생)
② 컴파일: javac가 T를 소거 → Box { Object value; } 로 변환 (Type Erasure)
③ JVM Method Area: Box.push(Object), Box.pop() 코드 딱 1개씩만 존재
④ 런타임 new: Box<Integer> a = new Box<>(42) → 힙에 Box 객체 생성 { value: Object }
⑤ get() 호출: Object 반환 → javac가 자동으로 (Integer) 캐스팅 삽입
핵심: T는 컴파일 타임 타입 체크 도구. 런타임에는 T 정보가 존재하지 않음
JVM Method Area (공장): Stack.push(Object) ← 하나만 존재 / push(Integer) → 없음
힙 (Heap): s1 → Stack 객체 #1 s2 → Stack 객체 #2
결론: 코드(공장) 1개, 객체(상자) 2개 → Java Type Erasure의 핵심
③ 함수 템플릿 · 제네릭 메서드
함수/메서드에도 타입 파라미터를 적용할 수 있다. Java는 객체에 > 연산자를 사용할 수 없어 Comparable 제약이 필요하다. Rust는 PartialOrd trait bound로 명시한다.
1class Util { 2 // Generic method: <T extends Comparable<T>> 3 // Cannot use > on objects, must use compareTo 4 public static <T extends Comparable<T>> 5 T maxValue(T a, T b) { 6 return (a.compareTo(b) > 0) ? a : b; 7 } 8} 9public class Main { 10 public static void main(String[] args) { 11 System.out.println( 12 Util.maxValue(3, 7)); // 7 13 System.out.println( 14 Util.maxValue(2.1, 5.4)); // 5.4 15 System.out.println( 16 Util.maxValue("apple", 17 "banana")); // banana 18 } 19}
1#include <iostream> 2#include <string> 3 4// Template function: T must support operator> 5// No explicit constraint in C++17 6// Compile error occurs at point of use if T 7// does not support operator> 8template<typename T> 9T max_value(T a, T b) { 10 return (a > b) ? a : b; 11} 12 13int main() { 14 std::cout << max_value(3, 7) 15 << "\n"; // 7 16 std::cout << max_value(2.1, 5.4) 17 << "\n"; // 5.4 18 std::cout << max_value( 19 std::string("apple"), 20 std::string("banana")) 21 << "\n"; // banana 22 return 0; 23}
1use std::fmt::Display; 2 3// Rust: trait bound PartialOrd + Display 4// Explicit constraint at declaration site 5fn max_value<T: PartialOrd + Display>( 6 a: T, b: T 7) -> T { 8 if a > b { a } else { b } 9} 10 11fn main() { 12 println!("{}", max_value(3i32, 7)); 13 println!("{}", max_value(2.1f64, 5.4)); 14 println!("{}", max_value("apple", "banana")); 15 // Monomorphization: separate code per type 16 // Zero runtime overhead 17}
④ 타입 제한 — C++17 static_assert · Java extends · Rust trait bound
C++17: concept은 C++20부터. C++17에서는 static_assert + std::is_arithmetic으로 타입을 제한한다. 제약 위반 시 컴파일 에러.
Java: T extends Number로 선언 시점에 상한 경계를 지정한다 (JLS §4.4). unbounded T는 Object로 소거, bounded T extends Number는 Number로 소거.
Rust: T: Add + Display + Copy처럼 trait bound를 impl 블록에 명시한다 (The Rust Book §10.2).
1// Java: T extends Number constrains at declaration 2class NumberBox<T extends Number> { 3 private T value; 4 NumberBox(T v) { this.value = v; } 5 double asDouble() { 6 return value.doubleValue(); 7 } 8 @Override 9 public String toString() { 10 return "NumberBox(" + value + ")"; 11 } 12} 13public class Main { 14 public static void main(String[] args) { 15 NumberBox<Integer> a = 16 new NumberBox<>(10); 17 NumberBox<Double> b = 18 new NumberBox<>(3.14); 19 System.out.println(a.asDouble()); // 10.0 20 System.out.println(b.asDouble()); // 3.14 21 // NumberBox<String> c = 22 // new NumberBox<>("x"); // compile error 23 } 24}
1#include <iostream> 2#include <type_traits> 3 4// C++17: static_assert for type constraint 5// (concept available in C++20 only) 6template<typename T> 7class NumberBox { 8 static_assert( 9 std::is_arithmetic<T>::value, 10 "T must be arithmetic type"); 11public: 12 T value; 13 NumberBox(T v) : value(v) {} 14 double asDouble() const { 15 return static_cast<double>(value); 16 } 17}; 18 19int main() { 20 NumberBox<int> a(10); 21 NumberBox<double> b(3.14); 22 std::cout << a.asDouble() << "\n"; // 10 23 std::cout << b.asDouble() << "\n"; // 3.14 24 // NumberBox<std::string> c("x"); 25 // -> static_assert fails at compile time 26 return 0; 27}
1// Rust: trait bound constrains at declaration 2// std::ops::Add ensures T supports + operator 3use std::ops::Add; 4use std::fmt::Display; 5 6struct NumberBox<T> { 7 value: T, 8} 9impl<T: Add<Output = T> + Display + Copy> 10NumberBox<T> { 11 fn new(v: T) -> Self { 12 NumberBox { value: v } 13 } 14 fn doubled(&self) -> T { 15 self.value + self.value 16 } 17} 18fn main() { 19 let a = NumberBox::new(10i32); 20 let b = NumberBox::new(3.14f64); 21 println!("{}", a.doubled()); // 20 22 println!("{}", b.doubled()); // 6.28 23 // Constraint checked at compile time 24}
⑤ Primitive 타입 처리 · 메모리 구조
C++/Rust: primitive 타입을 제네릭 파라미터로 직접 사용 가능. vector<int>/Vec<i32>는 값을 힙 버퍼에 직접 저장한다 (boxing 없음).
Java: Box<int> 불가 (JLS §4.5.1). Wrapper class 필수. Integer 각각이 힙 객체 → boxing overhead 발생.
메모리 도식에서 Stack 구조체/객체는 스택에 선언한 경우 기준. 내부 버퍼(vector/Vec/ArrayList)는 힙에 위치.
1import java.util.ArrayList; 2import java.util.List; 3 4// Java: cannot use primitive types directly 5// int, double -> must use wrapper classes 6class Container<T> { 7 private List<T> items = new ArrayList<>(); 8 void add(T v) { items.add(v); } 9 T get(int i) { return items.get(i); } 10 int size() { return items.size(); } 11} 12public class Main { 13 public static void main(String[] args) { 14 // Container<int> -> compile error 15 Container<Integer> a = new Container<>(); 16 Container<Double> b = new Container<>(); 17 a.add(10); // autoboxing: int -> Integer 18 a.add(20); 19 b.add(3.14); // autoboxing: double -> Double 20 System.out.println(a.get(0)); // 10 21 System.out.println(b.get(0)); // 3.14 22 // Integer objects stored on heap (boxing) 23 } 24}
1#include <iostream> 2#include <vector> 3 4// C++: primitive types work directly 5// No boxing overhead 6template<typename T> 7class Container { 8 std::vector<T> items; 9public: 10 void add(T v) { items.push_back(v); } 11 T get(int i) const { return items[i]; } 12 size_t size() const { return items.size(); } 13}; 14 15int main() { 16 Container<int> a; // int stored directly 17 Container<double> b; // double stored directly 18 a.add(10); 19 a.add(20); 20 b.add(3.14); 21 std::cout << a.get(0) << "\n"; // 10 22 std::cout << b.get(0) << "\n"; // 3.14 23 // Values stored directly in vector buffer 24 // No boxing, no heap allocation per element 25 return 0; 26}
1// Rust: primitive types work directly 2// No boxing overhead (same as C++) 3struct Container<T> { 4 items: Vec<T>, 5} 6impl<T: std::fmt::Display> Container<T> { 7 fn new() -> Self { 8 Container { items: Vec::new() } 9 } 10 fn add(&mut self, v: T) { 11 self.items.push(v); 12 } 13 fn get(&self, i: usize) -> &T { 14 &self.items[i] 15 } 16} 17fn main() { 18 let mut a: Container<i32> = Container::new(); 19 let mut b: Container<f64> = Container::new(); 20 a.add(10i32); // i32 stored directly in Vec 21 a.add(20i32); 22 b.add(3.14f64); 23 println!("{}", a.get(0)); // 10 24 println!("{}", b.get(0)); // 3.14 25 // No boxing, values in heap buffer directly 26}
⑥ 런타임 타입 정보
Java: Type Erasure로 List<String>과 List<Integer>는 런타임에 같은 클래스 (getClass() == getClass() → true). T가 무엇인지 런타임에 알 수 없다.
C++: Template Instantiation으로 타입별 독립 코드 → typeid(Stack<int>) != typeid(Stack<string>).
Rust: Monomorphization으로 C++와 동일. TypeId::of::<Vec<i32>>() != TypeId::of::<Vec<String>>().
1import java.util.ArrayList; 2import java.util.List; 3 4public class Main { 5 public static void main(String[] args) { 6 List<String> a = new ArrayList<>(); 7 List<Integer> b = new ArrayList<>(); 8 a.add("hello"); 9 b.add(42); 10 // Type Erasure: at runtime both are 11 // just ArrayList - T info is gone 12 System.out.println( 13 a.getClass() == b.getClass()); // true 14 System.out.println( 15 a.getClass().getName()); 16 // java.util.ArrayList 17 // Cannot do: if (a instanceof List<String>) 18 // T is erased at runtime 19 } 20}
1#include <iostream> 2#include <typeinfo> 3#include <vector> 4#include <string> 5 6int main() { 7 std::vector<int> a; 8 std::vector<std::string> b; 9 a.push_back(42); 10 b.push_back("hello"); 11 // Template Instantiation: each type generates 12 // separate code -> truly different types 13 bool same = (typeid(a) == typeid(b)); 14 std::cout << std::boolalpha 15 << same << "\n"; // false 16 std::cout << typeid(a).name() << "\n"; 17 std::cout << typeid(b).name() << "\n"; 18 return 0; 19}
1use std::any::TypeId; 2 3fn main() { 4 // Monomorphization: each type generates 5 // separate code -> different TypeIds 6 let id_i32 = TypeId::of::<Vec<i32>>(); 7 let id_str = TypeId::of::<Vec<String>>(); 8 println!("{}", id_i32 == id_str); // false 9 10 // Unlike Java, type info is preserved 11 // at compile time (no type erasure) 12 println!("Vec<i32> != Vec<String>"); 13 // Each has separate monomorphized code 14 // in the binary, just like C++ 15}
javac 실제 변환: Integer x = (Integer) s1.pop(); ← 캐스팅 자동 삽입
정상: s1에 Integer push → (Integer) 캐스팅 성공
에러: s1에 String push 후 Integer로 꺼내면 → ClassCastException
컴파일 타임 타입 체크로 런타임 에러를 방지하는 것이 Java 제네릭의 핵심 목적.
⑦ 실전 — Stack<T> 완전 구현
앞에서 배운 개념을 모두 적용한 완전한 Stack 구현. C++는 std::move로 불필요한 복사를 제거. Rust는 pop()이 Option<T>를 반환해 빈 스택 접근을 컴파일 타임에 강제 처리한다.
1import java.util.ArrayList; 2import java.util.List; 3import java.util.EmptyStackException; 4 5class Stack<T> { 6 private List<T> data = new ArrayList<>(); 7 void push(T v) { data.add(v); } 8 T pop() { 9 if (data.isEmpty()) 10 throw new EmptyStackException(); 11 return data.remove(data.size() - 1); 12 } 13 T peek() { 14 if (data.isEmpty()) 15 throw new EmptyStackException(); 16 return data.get(data.size() - 1); 17 } 18 boolean isEmpty() { return data.isEmpty(); } 19 int size() { return data.size(); } 20} 21public class Main { 22 public static void main(String[] args) { 23 Stack<Integer> s1 = new Stack<>(); 24 s1.push(10); s1.push(20); s1.push(30); 25 System.out.println(s1.pop()); // 30 26 System.out.println(s1.peek()); // 20 27 Stack<String> s2 = new Stack<>(); 28 s2.push("a"); s2.push("b"); 29 System.out.println(s2.pop()); // b 30 } 31}
1#include <iostream> 2#include <vector> 3#include <string> 4#include <stdexcept> 5 6template<typename T> 7class Stack { 8 std::vector<T> data; 9public: 10 void push(T v) { 11 data.push_back(std::move(v)); 12 } 13 T pop() { 14 if (data.empty()) 15 throw std::runtime_error( 16 "Stack is empty"); 17 T v = std::move(data.back()); 18 data.pop_back(); 19 return v; 20 } 21 const T& peek() const { 22 if (data.empty()) 23 throw std::runtime_error( 24 "Stack is empty"); 25 return data.back(); 26 } 27 bool empty() const { return data.empty(); } 28 size_t size() const { return data.size(); } 29}; 30 31int main() { 32 Stack<int> s1; 33 s1.push(10); s1.push(20); s1.push(30); 34 std::cout << s1.pop() << "\n"; // 30 35 std::cout << s1.peek() << "\n"; // 20 36 Stack<std::string> s2; 37 s2.push("a"); s2.push("b"); 38 std::cout << s2.pop() << "\n"; // b 39 return 0; 40}
1struct Stack<T> { 2 data: Vec<T>, 3} 4impl<T: std::fmt::Display> Stack<T> { 5 fn new() -> Self { 6 Stack { data: Vec::new() } 7 } 8 fn push(&mut self, v: T) { 9 self.data.push(v); 10 } 11 fn pop(&mut self) -> Option<T> { 12 self.data.pop() 13 } 14 fn peek(&self) -> Option<&T> { 15 self.data.last() 16 } 17 fn is_empty(&self) -> bool { 18 self.data.is_empty() 19 } 20 fn size(&self) -> usize { 21 self.data.len() 22 } 23} 24fn main() { 25 let mut s1: Stack<i32> = Stack::new(); 26 s1.push(10); s1.push(20); s1.push(30); 27 println!("{}", s1.pop().unwrap()); // 30 28 println!("{}", s1.peek().unwrap()); // 20 29 let mut s2: Stack<String> = Stack::new(); 30 s2.push(String::from("a")); 31 s2.push(String::from("b")); 32 println!("{}", s2.pop().unwrap()); // b 33}
⑧ Rust — 정적 디스패치 vs 동적 디스패치 (&dyn · Fat Pointer)
Rust는 C++처럼 정적 디스패치(Monomorphization)가 기본이지만, &dyn Trait을 통해 Java처럼 동적 디스패치도 선택 가능하다.&dyn Trait은 Fat Pointer로 구현된다 (The Rust Reference §Trait Objects): data pointer(8B) + vtable pointer(8B) = 16B. vtable에는 destructor, size, alignment, 메서드 포인터가 저장된다.
이종 컬렉션(Vec<Box<dyn Shape>>)은 동적 디스패치로만 가능하다. 개발자가 명시적으로 선택한다.
1use std::fmt::Display; 2 3trait Shape { 4 fn area(&self) -> f64; 5 fn name(&self) -> &str; 6} 7struct Circle { radius: f64 } 8struct Rect { width: f64, height: f64 } 9 10impl Shape for Circle { 11 fn area(&self) -> f64 { 12 std::f64::consts::PI * self.radius 13 * self.radius 14 } 15 fn name(&self) -> &str { "Circle" } 16} 17impl Shape for Rect { 18 fn area(&self) -> f64 { 19 self.width * self.height 20 } 21 fn name(&self) -> &str { "Rect" } 22} 23 24// Static dispatch: monomorphization 25// Compiler generates separate code per type 26// Zero runtime overhead 27fn print_area<T: Shape>(shape: &T) { 28 println!("{}: area = {:.2}", 29 shape.name(), shape.area()); 30} 31 32fn main() { 33 let c = Circle { radius: 3.0 }; 34 let r = Rect { width: 4.0, height: 5.0 }; 35 print_area(&c); // Circle: area = 28.27 36 print_area(&r); // Rect: area = 20.00 37}
1trait Shape { 2 fn area(&self) -> f64; 3 fn name(&self) -> &str; 4} 5struct Circle { radius: f64 } 6struct Rect { width: f64, height: f64 } 7 8impl Shape for Circle { 9 fn area(&self) -> f64 { 10 std::f64::consts::PI * self.radius 11 * self.radius 12 } 13 fn name(&self) -> &str { "Circle" } 14} 15impl Shape for Rect { 16 fn area(&self) -> f64 { 17 self.width * self.height 18 } 19 fn name(&self) -> &str { "Rect" } 20} 21 22// Dynamic dispatch: fat pointer (&dyn Trait) 23// data ptr (8B) + vtable ptr (8B) = 16B 24// vtable resolved at runtime 25fn print_area_dyn(shape: &dyn Shape) { 26 println!("{}: area = {:.2}", 27 shape.name(), shape.area()); 28} 29 30fn main() { 31 // Heterogeneous collection: only with dyn 32 let shapes: Vec<Box<dyn Shape>> = vec![ 33 Box::new(Circle { radius: 3.0 }), 34 Box::new(Rect { width: 4.0, 35 height: 5.0 }), 36 ]; 37 for s in &shapes { 38 print_area_dyn(s.as_ref()); 39 } 40 // Circle: area = 28.27 41 // Rect: area = 20.00 42}
<T: Trait>): 타입이 컴파일 타임에 확정, 이종 컬렉션 불필요 → 성능 최우선동적 (
&dyn Trait): 런타임에 타입 결정, 이종 컬렉션 필요 → 유연성 우선C++는
virtual으로 동적, Rust는 명시적 dyn으로 동적을 선택① String vs &str — 소유권과 뷰
Java는 String 하나뿐이지만 Rust는 소유하는 String과
빌리는 &str이 명확히 구분된다.
C++17의 string_view와 유사하지만 Rust는 잘못된 사용을 컴파일 타임에 차단한다.
String::from("hello")&str: 슬라이스(뷰) · 소유권 없음 · 불변 ·
"hello" 또는 &ownedJava의
String은 Rust의 String에 대응 — 단 Java는 GC, Rust는 소유권Java에는
&str에 해당하는 개념이 없음 — 모든 String이 힙 객체C++의
string_view가 &str과 유사하지만 C++은 dangling을 런타임에서야 발견1// Java: String 하나만 존재, GC 관리 2// 문자열 리터럴은 String Pool에 캐시됨 3public class Main { 4 public static void main(String[] args) { 5 // 리터럴 → String Pool (같은 객체) 6 String s1 = "hello"; 7 String s2 = "hello"; 8 System.out.println(s1 == s2); // true (Pool) 9 10 // new → 힙에 새 객체 11 String s3 = new String("hello"); 12 System.out.println(s1 == s3); // false (다른 주소) 13 System.out.println( 14 s1.equals(s3)); // true 15 16 // 연결 → 새 String 생성 17 String s4 = s1 + " world"; 18 System.out.println(s4); // hello world 19 } 20}
1#include <iostream> 2#include <string> 3#include <string_view> 4using namespace std; 5 6int main() { 7 // std::string: 힙 할당, 소유권 있음 8 string owned = "hello"; 9 10 // string_view: 뷰, 소유권 없음 (C++17) 11 string_view view1 = owned; // String → 뷰 12 string_view view2 = "world"; // 리터럴 → 뷰 13 14 cout << owned << endl; // hello 15 cout << view1 << endl; // hello 16 cout << view2 << endl; // world 17 18 // owned 변경 → view1 무효화 위험! 19 // (Rust는 컴파일 타임에 차단) 20 owned += "!"; 21 // view1 사용 = UB (미정의 동작) 22 return 0; 23}
1fn main() { 2 // String: 힙 할당, 소유권 있음, 가변 3 let mut owned: String = String::from("hello"); 4 5 // &str: 슬라이스(뷰), 소유권 없음, 불변 6 let literal: &str = "world"; // 정적 메모리 7 let view: &str = &owned; // owned의 뷰 8 9 println!("{} {}", owned, literal); // hello world 10 println!("view: {}", view); // hello 11 12 // owned 변경 시 컴파일러가 view 사용 차단 13 owned.push_str("!"); 14 // println!("{}", view); // 컴파일 에러! 15 // owned 변경됨 → view 무효 16 17 println!("owned: {}", owned); // hello! 18}
② 메모리 구조 — 실제로 어떻게 저장되나
String과 &str의 내부 구조를 이해하면 언제 복사가 발생하고 언제 발생하지 않는지, 왜 &str이 더 효율적인 파라미터인지 알 수 있다.
C++ string: SSO — 15자 이하 스택 저장, 초과 시 ptr+len+cap 힙 할당
Rust String: ptr(8B)+len(8B)+cap(8B) = 24B 스택 | 실제 데이터는 힙
Rust &str: ptr(8B)+len(8B) = 16B fat pointer | 소유권 없음, 복사 없음
핵심: &str 슬라이스는 복사 없이 기존 메모리를 가리키는 뷰 — 매우 효율적
1// Java String 메모리 구조 2// Object Header(12B) + 필드 + 내부 byte[] 참조 3public class Main { 4 public static void main(String[] args) { 5 String s = "hello"; 6 // 힙: [ Header 12B | coder | hash | value → ] 7 // ↓ 8 // byte[] {h,e,l,l,o} 9 10 System.out.println(s.length()); // 5 11 System.out.println(s.charAt(0)); // h 12 13 // Java 9+: Latin-1(1B/char) 또는 UTF-16(2B/char) 14 // 슬라이스 없음 — 항상 새 String 생성 15 String sub = s.substring(1, 3); // "el" 새 객체 16 System.out.println(sub); // el 17 } 18}
1#include <iostream> 2#include <string> 3#include <string_view> 4using namespace std; 5 6int main() { 7 // SSO(Small String Optimization): 15자 이하 스택 8 string small = "hi"; // 스택 직접 저장 9 string large = "hello world this is long string"; 10 11 // string: ptr(8B)+len(8B)+cap(8B) 또는 SSO 버퍼 12 cout << small.size() << endl; // 2 13 cout << large.size() << endl; // 31 14 15 // string_view: ptr(8B)+len(8B) = 16B 고정 16 string_view sv = large; 17 cout << sv.size() << endl; // 31 18 cout << sv.substr(0, 5) << endl; // hello (뷰) 19 return 0; 20}
1use std::mem; 2 3fn main() { 4 let owned = String::from("hello"); 5 let slice: &str = "hello"; 6 7 // String: { ptr(8B), len(8B), cap(8B) } = 24B 8 // 데이터는 힙에 저장 9 println!("String 크기: {}B", 10 mem::size_of::<String>()); // 24 11 12 // &str: { ptr(8B), len(8B) } = 16B (fat pointer) 13 // 데이터를 소유하지 않음 — 어딘가를 가리킬 뿐 14 println!("&str 크기: {}B", 15 mem::size_of::<&str>()); // 16 16 17 // 슬라이스: 복사 없이 뷰 생성 18 let hello: &str = &owned[0..5]; // 뷰 (0비용) 19 println!("슬라이스: {}", hello); // hello 20 21 // String → &str: Deref coercion (0비용) 22 let s: &str = &owned; 23 println!("뷰: {}", s); // hello 24}
③ 함수 파라미터 — &str vs String 선택 기준
Rust 함수 설계에서 파라미터를 &str로 받으면
String · &str · 슬라이스 모두 인자로 전달할 수 있다 (Deref coercion).
새 문자열을 생성해서 반환할 때는 String을 반환한다.
파라미터: String — 함수 내부에서 소유권 필요 시 (저장, 수정, 이동)
반환: String — 새로 생성한 문자열 반환 (
format! 등)반환: &str — 입력의 일부를 반환 시 (라이프타임 자동 추론 or 명시)
Java는 항상 String 파라미터 — 내부적으로 참조 전달이지만 슬라이스 불가
1// Java: 항상 String — &str 개념 없음 2public class Main { 3 // String 파라미터 — 참조 전달 (복사 없음) 4 static int countChar(String s, char c) { 5 int count = 0; 6 for (char ch : s.toCharArray()) { 7 if (ch == c) count++; 8 } 9 return count; 10 } 11 12 // String 반환 — 새 객체 또는 기존 참조 13 static String greet(String name) { 14 return "Hello, " + name + "!"; // 새 String 15 } 16 17 public static void main(String[] args) { 18 String s = "hello world"; 19 System.out.println(countChar(s, 'l')); // 3 20 System.out.println( 21 greet("Rust")); // Hello, Rust! 22 } 23}
1#include <iostream> 2#include <string> 3#include <string_view> 4using namespace std; 5 6// string_view 권장: String, 리터럴, 슬라이스 모두 수용 7int countChar(string_view s, char c) { 8 int count = 0; 9 for (char ch : s) { 10 if (ch == c) count++; 11 } 12 return count; 13} 14 15// 새 문자열 생성 시 string 반환 16string greet(string_view name) { 17 return string("Hello, ") + string(name) + "!"; 18} 19 20int main() { 21 string owned = "hello world"; 22 cout << countChar(owned, 'l') << endl; // 3 23 cout << countChar("hello", 'l') 24 << endl; // 2 (리터럴) 25 cout << greet("Rust") << endl; // Hello, Rust! 26 return 0; 27}
1// Rust: 파라미터는 &str 권장 (가장 유연) 2fn count_char(s: &str, c: char) -> usize { 3 s.chars().filter(|&ch| ch == c).count() 4} 5 6// 새 문자열 생성 시 String 반환 7fn greet(name: &str) -> String { 8 format!("Hello, {}!", name) 9} 10 11fn main() { 12 let owned = String::from("hello world"); 13 let literal = "hello"; 14 15 // String → &str 자동 변환 (Deref coercion) 16 println!("{}", count_char(&owned, 'l')); // 3 17 // &str 그대로 전달 18 println!("{}", count_char(literal, 'l')); // 2 19 // 슬라이스도 전달 가능 20 println!("{}", count_char(&owned[0..5], 'l')); // 2 21 22 println!("{}", greet("Rust")); // Hello, Rust! 23 println!("{}", greet(&owned)); 24}
④ 구조체 필드와 라이프타임
구조체에 문자열 참조(&str)를 필드로 저장하면
라이프타임 파라미터가 필요하다. Java는 GC가 처리하고
C++은 dangling 위험이 있지만 Rust는 컴파일 타임에 안전을 보장한다.
struct Parser<'a> { input: &'a str }'a = 'input이 Parser보다 오래 살아야 한다'는 컴파일러와의 계약C++의
string_view 필드와 동일한 개념 — 단 C++은 위반 시 UB, Rust는 컴파일 에러간단하게 하려면 필드를
String으로 — 소유권 가져오면 라이프타임 불필요Java는 GC가 자동 관리 — 수명 걱정 없음
1// Java: 구조체 필드에 String — GC 자동 관리 2public class Parser { 3 private String input; // 소유, GC 관리 4 5 public Parser(String input) { 6 this.input = input; // 참조 저장 7 } 8 9 // substring → 새 String (항상 안전) 10 public String getWord(int start, int end) { 11 return input.substring(start, end); 12 } 13 14 public static void main(String[] args) { 15 Parser p = new Parser("hello world"); 16 System.out.println(p.getWord(0, 5)); // hello 17 System.out.println(p.getWord(6, 11)); // world 18 // GC가 모두 처리 — 수명 걱정 없음 19 } 20}
1#include <iostream> 2#include <string> 3#include <string_view> 4using namespace std; 5 6// C++: string_view 필드 — 원본 수명 의존 (위험!) 7struct Parser { 8 string_view input; // 뷰 저장 — 원본이 살아있어야 함 9 10 Parser(string_view s) : input(s) {} 11 12 string_view getWord(size_t start, size_t end) { 13 return input.substr(start, end - start); 14 } 15}; 16 17int main() { 18 string text = "hello world"; 19 Parser p(text); // text가 p보다 오래 살아야 함 20 cout << p.getWord(0, 5) << endl; // hello 21 cout << p.getWord(6, 11) << endl; // world 22 // text 소멸 후 p 사용 = dangling (UB) 23 return 0; 24}
1// Rust: &str 필드 → 라이프타임 명시 필수 2struct Parser<'a> { 3 input: &'a str, // 'a: input이 Parser보다 오래 살아야 4} 5 6impl<'a> Parser<'a> { 7 fn new(input: &'a str) -> Self { 8 Parser { input } 9 } 10 11 // 반환값도 'a 수명 — input의 일부를 반환 12 fn get_word(&self, 13 start: usize, end: usize) -> &str { 14 &self.input[start..end] 15 } 16} 17 18fn main() { 19 let text = String::from("hello world"); 20 let p = Parser::new(&text); 21 22 println!("{}", p.get_word(0, 5)); // hello 23 println!("{}", p.get_word(6, 11)); // world 24 // text보다 p가 먼저 소멸 → 안전 25 // text 소멸 후 p 사용 → 컴파일 에러로 차단 26}
⑤ 라이프타임 파라미터 — 컴파일러와의 계약
여러 참조를 받아 그 중 하나를 반환하는 함수에서 라이프타임 파라미터가 필수다. 컴파일러는 반환값이 어떤 입력에서 왔는지 알아야 메모리 안전을 보장할 수 있다. C++은 이를 추적하지 않아 dangling 위험이 항상 존재한다.
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str→ 반환값의 수명 = x와 y 중 짧은 것 — 컴파일러에게 '이 참조는 이만큼 산다' 알림
Java: String 반환 → 새 객체 또는 GC 관리 → 수명 문제 없음
C++: string_view 반환 → 수명 추적 안 함 → dangling 위험 (런타임 버그)
Rust: 잘못된 수명 → 컴파일 에러 → 런타임 버그 불가능
1// Java: 반환값이 String이므로 수명 문제 없음 2public class Main { 3 // 더 긴 문자열 반환 4 static String longest(String x, String y) { 5 return x.length() >= y.length() ? x : y; 6 } 7 8 public static void main(String[] args) { 9 String s1 = "long string is here"; 10 String result; 11 { 12 String s2 = "short"; 13 // s2가 스코프 벗어나도 GC가 보호 14 result = longest(s1, s2); 15 } 16 // 안전 — GC가 result 유지 17 System.out.println(result); 18 } 19}
1#include <iostream> 2#include <string_view> 3using namespace std; 4 5// C++: 반환 string_view — 수명 추적 안 함! 6string_view longest(string_view x, string_view y) { 7 return x.size() >= y.size() ? x : y; 8} 9 10int main() { 11 string s1 = "long string is here"; 12 string_view result; 13 { 14 string s2 = "short"; 15 result = longest(s1, s2); 16 } // s2 소멸! 17 // result가 s2를 가리킬 경우 dangling 18 // 컴파일러가 감지하지 못함 → 런타임 버그 19 cout << result << endl; // UB 가능 20 return 0; 21}
1// Rust: 라이프타임으로 컴파일 타임에 안전 보장 2// 'a = x와 y 중 짧은 수명, 반환값도 그 수명 3fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { 4 if x.len() >= y.len() { x } else { y } 5} 6 7fn main() { 8 let s1 = String::from("long string is here"); 9 let result; 10 { 11 let s2 = String::from("short"); 12 result = longest(&s1, &s2); 13 println!("{}", result); // OK — s2 살아있음 14 } // s2 소멸 15 16 // println!("{}", result); // 컴파일 에러! 17 // s2 소멸 → result 사용 불가 (컴파일 타임 차단) 18 19 // s1만 비교 시 안전 20 let r2 = longest(&s1, "tiny"); 21 println!("{}", r2); // long string is here 22}
⑥ 실전 — 라이프타임 + 트레이트 바운드
라이프타임과 트레이트 바운드를 조합하면 강력하고 안전한 제네릭 함수를 만들 수 있다.
where 절을 사용하면 복잡한 바운드를 분리해서 가독성을 높일 수 있다.
fn announce<'a, T, M>(item: &'a T, msg: M) -> &'a str where T: Summarize, M: Displaywhere 절: 복잡한 바운드를 함수 시그니처에서 분리 → 가독성 향상
라이프타임 + 트레이트:
T: Display + 'a = T가 Display를 구현하고 'a보다 오래 살아야 함반환값
&'a str: item에서 빌린 참조 → item이 살아있는 동안만 유효Java는 String 반환으로 항상 안전 — 단 슬라이스 반환 불가 (새 객체만)
1// Java: 제네릭 + 인터페이스 + String 반환 2interface Summarize { 3 String summary(); 4} 5 6class Article implements Summarize { 7 private String title; 8 private String content; 9 10 Article(String title, String content) { 11 this.title = title; 12 this.content = content; 13 } 14 15 public String summary() { 16 return title + ": " + content.substring(0, 20); 17 } 18} 19 20public class Main { 21 static <T extends Summarize> 22 void announce(T item, String msg) { 23 System.out.println( 24 "[" + msg + "] " + item.summary()); 25 } 26 27 public static void main(String[] args) { 28 Article a = new Article( 29 "Rust", "Fast and safe language!"); 30 announce(a, "공지"); 31 } 32}
1#include <iostream> 2#include <string> 3#include <string_view> 4using namespace std; 5 6struct Article { 7 string title; 8 string content; 9 10 string_view summary() const { 11 // 첫 20자 반환 (뷰) 12 size_t len = min(content.size(), (size_t)20); 13 return string_view(content).substr(0, len); 14 } 15}; 16 17// C++20 concept + string_view 18template<typename T, typename Msg> 19void announce(const T& item, Msg msg) { 20 cout << "[" << msg << "] " 21 << item.title << ": " 22 << item.summary() << endl; 23} 24 25int main() { 26 Article a{"Rust", "Fast and safe language!"}; 27 announce(a, "공지"); 28 return 0; 29}
1use std::fmt::Display; 2 3trait Summarize { 4 fn summary(&self) -> &str; 5} 6 7struct Article { 8 title: String, 9 content: String, 10} 11 12impl Summarize for Article { 13 fn summary(&self) -> &str { 14 // content의 일부를 뷰로 반환 (0비용) 15 let end = self.content.len().min(20); 16 &self.content[..end] 17 } 18} 19 20// 라이프타임 + 트레이트 바운드 + where 절 21fn announce<'a, T, M>(item: &'a T, msg: M) -> &'a str 22where 23 T: Summarize, 24 M: Display, 25{ 26 println!("[{}] {}: ", msg, item.summary()); 27 item.summary() 28} 29 30fn main() { 31 let a = Article { 32 title: String::from("Rust"), 33 content: String::from( 34 "Fast and safe language!"), 35 }; 36 let summary = announce(&a, "공지"); 37 println!("반환된 요약: {}", summary); 38}
상속 없는 Rust
출처: Rust Book §10.2Rust Reference — Traits
① 상속이 하는 일 → Rust · Go · C++ 대체 방법
| Java 상속의 역할 | ☕ Java | Rust | Go | C++ |
|---|---|---|---|---|
| 데이터 공유 | extends Animal | 컴포지션 (필드 포함) | 임베딩 (자동 위임) | : public Animal |
| 코드 재사용 | 부모 메서드 상속 | trait 기본 구현 | 임베딩 메서드 | 부모 메서드 상속 |
| 다형성 강제 | abstract 메서드 | trait 필수 메서드 | interface 메서드 | 순수 가상 함수 |
| 다중 상속 | 불가 (interface만 가능) | 여러 trait 구현 ✅ | 여러 interface ✅ | 다중 상속 (복잡) ⚠️ |
② impl 블록 = Go 리시버 = Java 클래스 메서드
impl 블록으로 분리, Go는 리시버 함수로 분리한다.&self = 읽기전용 (Go: 값 리시버 b Book) | &mut self = 수정가능 (Go: 포인터 리시버 b *Book) | self = 소유권 소비 (Go에 없음)1public class Main { 2 static class Book { 3 private String title; 4 private int pages; 5 public Book(String t, int p) { 6 title = t; pages = p; 7 } 8 public String getTitle() { return title; } 9 public void addPage() { pages++; } 10 } 11 public static void main(String[] args) { 12 Book b = new Book("Clean Code", 430); 13 b.addPage(); 14 System.out.println(b.getTitle()); // Clean Code 15 } 16}
1struct Book { title: String, pages: u32 } 2impl Book { 3 // &self = Go 값 리시버 (읽기) 4 fn title(&self) -> &str { 5 &self.title 6 } 7 // &mut self = Go *T 리시버 (수정) 8 fn add_page(&mut self) { 9 self.pages += 1; 10 } 11 // self = 소유권 소비 (Go에 없음) 12 fn destroy(self) { 13 println!("dropped: {}", self.title) 14 } 15} 16fn main() { 17 let mut b = Book { 18 title: "Clean Code".to_string(), 19 pages: 430, 20 }; 21 b.add_page(); 22 println!("{}", b.title()); 23 b.destroy(); 24}
| 수신자 | ☕ Java | Rust | Go | 의미 |
|---|---|---|---|---|
| 읽기 | this | &self | func (b Book) | 불변 접근 |
| 수정 | this | &mut self | func (b *Book) | 가변 접근 |
| 소비 | 없음 | self | 없음 | 소유권 이전 후 drop |
| 정적 | static | self 없는 연관 함수 | 패키지 함수 | 인스턴스 불필요 |
③ trait = interface + 추가 기능 — why_trait
1public class Main { 2 // Java: implements는 클래스 선언 시점에만 가능 3 interface Sound { 4 String makeSound(); 5 default void loudSound() { 6 String s = makeSound() 7 .toUpperCase() + "!!!"; 8 System.out.println(s); 9 } 10 } 11 // String에 Sound를 구현하고 싶어도 불가! 12 // String은 java.lang — 수정 불가, 컴파일 에러 13 // Sound s = "hello"; // ✗ 14 static class Lion implements Sound { 15 public String makeSound() { return "Roar"; } 16 } 17 public static void main(String[] args) { 18 Sound s = new Lion(); 19 s.loudSound(); // 으르렁!!! 20 } 21}
1trait Sound { 2 fn make_sound(&self) -> String; 3 fn loud_sound(&self) { // 기본 구현 4 let s = self.make_sound().to_uppercase(); 5 println!("{}!!!", s); 6 } 7} 8// 기존 타입 i32에 나중에 구현 가능! 9impl Sound for i32 { 10 fn make_sound(&self) -> String { 11 format!("Number: {}", self) 12 } 13} 14fn main() { 15 42.loud_sound(); // 숫자 42!!! 16}
| 표현 | 의미 | Java 대응 |
|---|---|---|
impl Lion { fn name(&self) } | Lion 고유 메서드 블록 | class 내부 일반 메서드 |
impl Sound for Lion { ... } | Sound trait 계약 이행 | implements Sound + @Override |
예: 표준 라이브러리의
Display를 표준 라이브러리의 Vec에 별도 구현 ❌출처: Rust Reference — Orphan Rules
| 역할 | ☕ Java | Rust | Go | C++ |
|---|---|---|---|---|
| 데이터 묶음 | class | struct | struct | struct/class |
| 메서드 | class 내부 | impl 블록 | 리시버 함수 | class 내부 |
| 인터페이스 | interface | trait | interface | 순수 가상 함수 |
| 구현 선언 | implements | impl Trait for Type | 묵시적 | : public Base |
| 데이터 상속 | extends | 없음 (컴포지션) | 임베딩 | : public Base |
| 외부 타입 확장 | 불가 | 블랭킷 impl | 불가 | 불가 |
4개 언어 비교 — Java · Rust · Go · C++
1// Java: 데이터 + 메서드가 class 안에 2public class Book { 3 private final String title; 4 private final int pages; 5 Book(String t, int p) { 6 this.title = t; 7 this.pages = p; 8 } 9 public String summary() { 10 return title + " (" + pages + "p)"; 11 } 12 public static void main(String[] args) { 13 Book b = new Book("Clean Code", 431); 14 System.out.println(b.summary()); 15 } 16}
1// Rust: 데이터(struct)와 메서드(impl) 분리 2struct Book { title: String, pages: u32 } 3 4impl Book { 5 fn summary(&self) -> String { 6 format!("{} ({}p)", self.title, self.pages) 7 } 8} 9fn main() { 10 let b = Book { 11 title: "Clean Code".to_string(), 12 pages: 431, 13 }; 14 println!("{}", b.summary()); 15}
1// Go: struct + 리시버 함수 분리 2package main 3import "fmt" 4type Book struct { 5 title string 6 pages int 7} 8// 값 리시버: (b Book) — &self 대응, 복사본 9func (b Book) Summary() string { 10 return fmt.Sprintf("%s (%dp)", b.title, b.pages) 11} 12// 포인터 리시버: (b *Book) — &mut self 대응, 수정 가능 13func (b *Book) AddPage() { 14 b.pages++ 15} 16// interface 묵시적 구현 (implements 없음!) 17type Summarizer interface { 18 Summary() string 19} 20func main() { 21 b := Book{title: "Clean Code", pages: 431} 22 b.AddPage() 23 fmt.Println(b.Summary()) // Clean Code (432p) 24 var s Summarizer = b 25 fmt.Println(s.Summary()) 26}
1// C++: class 내부 (Java와 유사) 2// virtual 키워드로 동적 디스패치 선택 3#include <iostream> 4#include <string> 5class Book { 6private: 7 std::string title; 8 int pages; 9public: 10 Book(std::string t, int p) : title(t), pages(p) {} 11 // const = 불변 (Rust &self와 유사) 12 std::string Summary() const { 13 return title + " (" + std::to_string(pages) + "p)"; 14 } 15}; 16int main() { 17 Book b("Clean Code", 431); 18 std::cout << b.Summary() << std::endl; 19 return 0; 20}
Go interface vs Rust dyn
trait vs interface
1interface Sound { 2 String makeSound(); 3 default void loudSound() { 4 System.out.println( makeSound().toUpperCase() + "!!!"); 5 } 6} 7class Lion implements Sound { 8 private final String name; 9 Lion(String n) { this.name = n; } 10 @Override public String makeSound() { 11 return name + ": Roar"; 12 } 13} 14public class Main { 15 public static void main(String[] args) { 16 Lion l = new Lion("Simba"); 17 System.out.println(l.makeSound()); 18 l.loudSound(); 19 } 20}
1trait Sound { 2 fn make_sound(&self) -> String; 3} 4trait Announce { 5 fn announce(&self); 6} 7 8// blanket: Sound 구현 → Announce 자동! 9impl<T: Sound> Announce for T { 10 fn announce(&self) { 11 println!("[Announce] {}", self.make_sound()); 12 } 13} 14 15struct Book { title: String } 16impl Book { 17 fn new(t: &str) -> Self { 18 Book { title: t.to_string() } 19 } 20} 21impl Sound for Book { 22 fn make_sound(&self) -> String { 23 format!("Book '{}' sound", self.title) 24 } 25} 26struct Lion { name: String } 27impl Sound for Lion { 28 fn make_sound(&self) -> String { 29 format!("{}: Roar", self.name) 30 } 31} 32 33fn main() { 34 let book = Book::new("Clean Code"); 35 let lion = Lion { name: "Simba".into() }; 36 // Sound 구현 → Announce 자동 획득 37 book.announce(); 38 lion.announce(); 39 // 표준 라이브러리 실제 예: 40 // impl<T: Display> ToString for T 41 // Display 구현 → .to_string() 자동 42} 43
impl<T: Display> ToString for T → Display 구현 시 .to_string() 자동.출처: Rust Reference — Implementations
상속 대신 컴포지션 — Java extends vs Rust 필드 포함
1public class Main { 2 abstract static class Animal { 3 protected String name; 4 public Animal(String name) { this.name = name; } 5 public abstract String sound(); 6 public String describe() { 7 return name + ": " + sound(); 8 } 9 } 10 static class Dog extends Animal { 11 private String breed; 12 public Dog(String name, String breed) { 13 super(name); this.breed = breed; 14 } 15 public String sound() { return "Woof"; } 16 } 17 public static void main(String[] args) { 18 Animal d = new Dog("Rex", "Labs"); 19 System.out.println(d.describe()); // Rex: 멍멍 20 } 21}
1struct AnimalInfo { name: String } 2 3trait Animal { 4 fn info(&self) -> &AnimalInfo; 5 fn sound(&self) -> &'static str; 6 // 기본 구현 (Java default 메서드와 동일) 7 fn describe(&self) -> String { 8 format!("{}: {}", self.info().name, self.sound()) 9 } 10} 11struct Dog { info: AnimalInfo, breed: String } 12impl Animal for Dog { 13 fn info(&self) -> &AnimalInfo { &self.info } 14 fn sound(&self) -> &'static str { "Woof" } 15} 16fn main() { 17 let d = Dog { 18 info: AnimalInfo { name: "Rex".to_string() }, 19 breed: "Husky".to_string(), 20 }; 21 println!("{}", d.describe()); // Rex: Woof 22}
| Java (상속) | Rust (컴포지션) | 차이 |
|---|---|---|
extends Animal | 필드로 info: AnimalInfo | 데이터 재사용 방식 |
super(name) | 필드 초기화 직접 | 생성 방식 |
abstract 메서드 | trait 필수 메서드 | 계약 정의 |
default 메서드 | trait 기본 구현 | 동일 역할 |
| 다중 상속 불가 | 여러 trait 구현 가능 | Rust가 더 유연 |
Rust는 이를 분리: 데이터 재사용 → 컴포지션(필드 포함), 다형성 → trait.
결과적으로 더 유연하고 예측 가능한 코드 구조를 만들 수 있음.
출처: Rust Book §17.1 — OO 특성
impl vs dyn — 5가지 비교 + blanket impl
출처: Rust Book §18.2
1import java.util.*; 2public class Main { 3 interface Sound { 4 String makeSound(); 5 default void loudSound() { 6 System.out.println( 7 makeSound().toUpperCase() 8 + "!!!"); 9 } 10 } 11 record Lion(String name) 12 implements Sound { 13 public String makeSound() { 14 return name + ": Roar"; 15 } 16 } 17 record Duck(String name) 18 implements Sound { 19 public String makeSound() { 20 return name + ": Quack"; 21 } 22 public void loudSound() { 23 System.out.println( 24 "(quietly) " + makeSound()); 25 } 26 } 27 // 타입 소거 → 컴파일 후 play(Sound a) 28 // → 항상 동적 디스패치! 29 static <T extends Sound> void play(T a) { 30 System.out.println( 31 "[generic] " + a.makeSound()); 32 } 33 static void holdConcert(List<Sound> zoo) { 34 System.out.println("=== Chorus ==="); 35 for (Sound a : zoo) { 36 a.loudSound(); // vtable 조회 37 } 38 } 39 static Sound makeAnimal(String kind) { 40 return switch (kind) { 41 case "lion" -> new Lion("Simba"); 42 default -> new Duck("Donald"); 43 }; 44 } 45 public static void main(String[] args) { 46 var lion = new Lion("Simba"); 47 var duck = new Duck("Donald"); 48 play(lion); 49 play(duck); 50 holdConcert(List.of(lion, duck)); 51 Sound a = makeAnimal("lion"); 52 System.out.println(a.makeSound()); 53 } 54} 55
1trait Sound { 2 fn make_sound(&self) -> String; 3 fn loud_sound(&self) { 4 println!( 5 "{}!!!", 6 self.make_sound().to_uppercase(), 7 ); 8 } 9} 10 11#[derive(Clone)] 12struct Lion { name: String } 13#[derive(Clone)] 14struct Duck { name: String } 15 16impl Sound for Lion { 17 fn make_sound(&self) -> String { 18 format!("{}: Roar", self.name) 19 } 20} 21impl Sound for Duck { 22 fn make_sound(&self) -> String { 23 format!("{}: Quack", self.name) 24 } 25 fn loud_sound(&self) { 26 println!("(quietly) {}", self.make_sound()); 27 } 28} 29 30// 정적 — impl Trait 31fn play_impl(a: &impl Sound) { 32 println!("[impl] {}", a.make_sound()); 33} 34// 동적 — dyn Trait 35fn play_dyn(a: &dyn Sound) { 36 println!("[dyn] {}", a.make_sound()); 37} 38 39// 이종 컬렉션 — dyn만 가능 40fn hold_concert(zoo: &[Box<dyn Sound>]) { 41 for a in zoo { a.loud_sound(); } 42} 43 44// impl Trait 반환 — 정적 45fn make_static() -> impl Sound { 46 Lion { name: "Simba".to_string() } 47} 48// Box<dyn Trait> 반환 — 동적 필수 49fn make_dynamic(kind: &str) -> Box<dyn Sound> { 50 match kind { 51 "lion" => Box::new(Lion { 52 name: "Simba".into(), 53 }), 54 _ => Box::new(Duck { 55 name: "Donald".into(), 56 }), 57 } 58} 59 60fn main() { 61 let lion = Lion { name: "Simba".into() }; 62 let duck = Duck { name: "Donald".into() }; 63 64 play_impl(&lion); play_impl(&duck); 65 play_dyn(&lion); play_dyn(&duck); 66 67 let zoo: Vec<Box<dyn Sound>> = vec![ 68 Box::new(lion.clone()), 69 Box::new(duck.clone()), 70 ]; 71 hold_concert(&zoo); 72 73 println!("{}", make_static().make_sound()); 74 println!("{}", make_dynamic("lion").make_sound()); 75} 76
| Rust impl (정적) | Rust dyn (동적) | ☕ Java | |
|---|---|---|---|
| 결정 시점 | 컴파일 타임 | 런타임 | 런타임 |
| vtable | 없음 | 있음 (16B) | 있음 (헤더) |
| 인라인 | 가능 | 불가 | JIT |
| 이종 컬렉션 | 불가 | 가능 | 가능 |
| 반환 타입 | impl Trait | Box<dyn Trait> | 인터페이스 타입 |
blanket 구현
1trait Sound { 2 fn make_sound(&self) -> String; 3} 4trait Announce { 5 fn announce(&self); 6} 7 8// blanket: Sound 구현 → Announce 자동! 9impl<T: Sound> Announce for T { 10 fn announce(&self) { 11 println!("[Announce] {}", self.make_sound()); 12 } 13} 14 15struct Book { title: String } 16impl Book { 17 fn new(t: &str) -> Self { 18 Book { title: t.to_string() } 19 } 20} 21impl Sound for Book { 22 fn make_sound(&self) -> String { 23 format!("Book '{}' sound", self.title) 24 } 25} 26struct Lion { name: String } 27impl Sound for Lion { 28 fn make_sound(&self) -> String { 29 format!("{}: Roar", self.name) 30 } 31} 32 33fn main() { 34 let book = Book::new("Clean Code"); 35 let lion = Lion { name: "Simba".into() }; 36 // Sound 구현 → Announce 자동 획득 37 book.announce(); 38 lion.announce(); 39 // 표준 라이브러리 실제 예: 40 // impl<T: Display> ToString for T 41 // Display 구현 → .to_string() 자동 42} 43
정적 vs 동적 디스패치 — 한눈에
출처: Rust Reference — Trait Objects
- 컴파일 타임에 호출 함수 확정
- 문법:
impl Trait또는<T: Trait> - 단형화: 타입별 함수 복사본 생성
- vtable 없음 → 직접 CALL → 최고 성능
- 이종 컬렉션 불가
- Java에 없음 — Rust·C++ 템플릿만
- 런타임에 vtable 조회 후 호출
- 문법:
dyn Trait - fat pointer = data_ptr(8B) + vtable_ptr(8B) = 16B
- 간접 CALL 오버헤드
- 이종 컬렉션 가능
- Java의 모든 인터페이스 호출이 여기에 해당
| 항목 | 정적 (impl/제네릭) | 동적 (dyn) | ☕ Java |
|---|---|---|---|
| 결정 시점 | 컴파일 타임 | 런타임 | 런타임 |
| vtable | 없음 | 있음 | 있음 |
| 인라인 | 가능 | 어려움 | JIT |
| 이종 컬렉션 | 불가 | 가능 | 가능 |
| 포인터 크기 | 보통 8B | fat ptr 16B | ref 8B (헤더에 vtable) |
정적 디스패치 — 단형화 (Java에 없음)
<T extends Sound> → 타입 소거 → play(Sound a) → vtable 조회.Rust
<T: Sound> → 단형화 → play__Lion, play__Duck → 직접 CALL.1import java.util.List; 2// Java 정적 디스패치 대응 예제 3// Java는 정적 디스패치가 없음 — 항상 vtable 조회 4public class Main { 5 interface Sound { 6 String makeSound(); 7 } 8 record Lion(String name) implements Sound { 9 public String makeSound() { 10 return name + ": Roar"; 11 } 12 } 13 record Duck(String name) implements Sound { 14 public String makeSound() { 15 return name + ": Quack"; 16 } 17 } 18 19 // Java 제네릭 = 타입 소거 → 런타임에 Sound로 변환 → vtable! 20 // Rust <T: Sound>와 달리 단형화가 일어나지 않음 21 static <T extends Sound> void playGeneric(T animal) { 22 // 컴파일 후: play(Sound animal) — T 소거됨 23 System.out.println("[dynamic] " + animal.makeSound()); 24 } 25 26 // 인터페이스 타입 — 명시적 동적 디스패치 27 static void playInterface(Sound animal) { 28 System.out.println("[dynamic] " + animal.makeSound()); 29 } 30 31 public static void main(String[] args) { 32 var simba = new Lion("Simba"); 33 var donald = new Duck("Donald"); 34 35 // Java는 항상 동적 — Lion, Duck 모두 같은 코드 경로 36 playGeneric(simba); // vtable 조회 37 playGeneric(donald); // vtable 조회 38 playInterface(simba); // vtable 조회 39 playInterface(donald); // vtable 조회 40 41 // 이종 컬렉션 — Java는 자동 (Rust dyn과 동일) 42 List<Sound> zoo = List.of(simba, donald); 43 zoo.forEach(a -> 44 System.out.println(a.makeSound())); 45 } 46} 47
1trait Sound { 2 fn make_sound(&self) -> String; 3} 4 5struct Lion { name: String } 6struct Duck { name: String } 7 8impl Sound for Lion { 9 fn make_sound(&self) -> String { 10 format!("{}: Roar", self.name) 11 } 12} 13impl Sound for Duck { 14 fn make_sound(&self) -> String { 15 format!("{}: Quack", self.name) 16 } 17} 18 19// impl Trait → 단형화 (정적 디스패치) 20// 컴파일 후: 21// play__Lion(&Lion) 생성 22// play__Duck(&Duck) 생성 23fn play_impl(animal: &impl Sound) { 24 // 직접 CALL — vtable 없음! 25 println!( 26 "[static] {}", 27 animal.make_sound(), 28 ); 29} 30 31// 제네릭 문법 — 위와 완전히 동일 32fn play_generic<T: Sound>(animal: &T) { 33 println!( 34 "[static] {}", 35 animal.make_sound(), 36 ); 37} 38 39// dyn — 동적 디스패치 (비교용) 40fn play_dynamic(animal: &dyn Sound) { 41 // vtable 조회 → 간접 CALL 42 println!( 43 "[dynamic] {}", 44 animal.make_sound(), 45 ); 46} 47 48fn main() { 49 let simba = Lion { name: "Simba".into() }; 50 let donald = Duck { name: "Donald".into() }; 51 52 // 정적: 타입별 별도 함수로 컴파일됨 53 play_impl(&simba); // play__Lion 54 play_impl(&donald); // play__Duck 55 56 play_generic(&simba); // 동일 57 play_generic(&donald); 58 59 // 동적: vtable 조회 60 play_dynamic(&simba); 61 play_dynamic(&donald); 62 63 // 정적은 이종 컬렉션 불가! 64 // let v: Vec<impl Sound> = ... // 에러 65} 66
| 항목 | Java 제네릭 | Rust impl/제네릭 |
|---|---|---|
| 구현 | 타입 소거 → play(Sound a) | 단형화 → play__Lion, play__Duck |
| vtable | 있음 (동적) | 없음 (직접 CALL) |
| 인라인 | JIT만 | 컴파일타임 가능 |
| 이종 컬렉션 | List<Sound> 가능 | 불가 → dyn 사용 |
동적 디스패치 — dyn Trait · vtable · fat pointer
출처: Rust Reference — Trait ObjectsRust Book §18.2
1import java.util.List; 2interface Sound { 3 String makeSound(); 4 default void loudSound() { 5 System.out.println( " " + makeSound().toUpperCase() + "!!!"); 6 } 7} 8record Lion(String name) implements Sound { 9 public String makeSound() { return name + ": Roar"; } 10} 11record Duck(String name) implements Sound { 12 public String makeSound() { return name + ": Quack"; } 13 public void loudSound() { 14 System.out.println(" (quietly) " + makeSound()); 15 } 16} 17record Snake(String name) implements Sound { 18 public String makeSound() { return name + ": Hiss"; } 19} 20public class Main { 21 static void holdConcert(List<Sound> zoo) { 22 System.out.println(" === Animal Chorus ==="); 23 for (Sound a : zoo) { a.loudSound(); } 24 } 25 public static void main(String[] args) { 26 var simba = new Lion("Simba"); 27 var donald = new Duck("Donald"); 28 var nagini = new Snake("Nagini"); 29 List<Sound> zoo = List.of(simba, donald, nagini); 30 holdConcert(zoo); 31 Sound a = switch ("lion") { 32 case "lion" -> new Lion("Simba"); 33 default -> new Duck("Donald"); 34 }; 35 System.out.println(" " + a.makeSound()); 36 } 37}
1trait Sound { 2 fn make_sound(&self) -> String; 3 fn loud_sound(&self) { 4 println!( 5 " {}!!!", 6 self.make_sound().to_uppercase(), 7 ); 8 } 9} 10 11struct Lion { name: String } 12struct Duck { name: String } 13struct Snake { name: String } 14 15impl Sound for Lion { 16 fn make_sound(&self) -> String { 17 format!("{}: Roar", self.name) 18 } 19} 20impl Sound for Duck { 21 fn make_sound(&self) -> String { 22 format!("{}: Quack", self.name) 23 } 24 fn loud_sound(&self) { 25 println!( 26 " (quietly) {}", 27 self.make_sound(), 28 ); 29 } 30} 31impl Sound for Snake { 32 fn make_sound(&self) -> String { 33 format!("{}: Hiss", self.name) 34 } 35} 36 37// 정적 디스패치 — 단형화 38fn play_impl(a: &impl Sound) { 39 println!("[impl] {}", a.make_sound()); 40} 41// 동적 디스패치 — vtable 조회 42fn play_dyn(a: &dyn Sound) { 43 println!("[dyn] {}", a.make_sound()); 44} 45 46// Box<dyn Sound> — 런타임 타입 결정 47fn make_animal(kind: &str) -> Box<dyn Sound> { 48 match kind { 49 "lion" => Box::new(Lion { 50 name: "Simba".into(), 51 }), 52 "duck" => Box::new(Duck { 53 name: "Donald".into(), 54 }), 55 _ => Box::new(Snake { 56 name: "Nagini".into(), 57 }), 58 } 59} 60 61fn hold_concert(zoo: &[Box<dyn Sound>]) { 62 println!(" === Animal Chorus ==="); 63 for a in zoo { 64 // fat ptr → vtable → 함수 포인터 65 a.loud_sound(); 66 } 67} 68 69fn main() { 70 let lion = Lion { name: "Simba".into() }; 71 let duck = Duck { name: "Donald".into() }; 72 73 // 정적 (단형화 — 직접 CALL) 74 play_impl(&lion); 75 play_impl(&duck); 76 77 // 동적 (vtable — 간접 CALL) 78 play_dyn(&lion); 79 play_dyn(&duck); 80 81 // 이종 컬렉션 — dyn만 가능 82 let zoo: Vec<Box<dyn Sound>> = vec![ 83 make_animal("lion"), 84 make_animal("duck"), 85 make_animal("snake"), 86 ]; 87 hold_concert(&zoo); 88} 89
fat pointer 구조 — JVM vs Rust
출처: Rust Reference — Trait Objects
호출 흐름 비교 — 정적 vs 동적 vs Java
출처: Rust Reference
단, 이는 런타임 최적화이고 언어 차원의 보장이 아님. Rust 정적 디스패치는 컴파일 타임에 항상 보장.
| 단계 | Rust 정적 | Rust 동적 | ☕ Java |
|---|---|---|---|
| 1 | 컴파일 시 T=Lion 확정 | fat ptr에서 vtable_ptr 로드 | 참조에서 Klass Word 로드 |
| 2 | Lion::make_sound 주소 확정 | vtable[n] (make_sound) 로드 | vtable에서 makeSound 로드 |
| 3 | 직접 CALL (주소 고정) | 간접 CALL | 간접 CALL |
| 4 | 인라인 가능 | 인라인 불가 | JIT devirtualize |
| 비용 | 0 (컴파일타임) | 메모리 로드 2회 + 간접 점프 | 메모리 로드 2회 + 간접 점프 |
| 항목 | Java 참조 8B | Rust fat pointer 16B |
|---|---|---|
| 포인터 크기 | 8B (단일) | 16B (data+vtable) |
| vtable 위치 | 객체 헤더 (Klass Pointer) | fat pointer (vtable_ptr) |
| 객체 오버헤드 | Header 12~16B (강제) | 없음 (struct에 vptr 없음) |
| null 안전 | NPE 가능 | Option<Box<dyn Sound>> |
메모리 비교 — JVM vs Rust
출처: JVM Spec §2Rust Reference — Memory
① Java JVM 객체 메모리 구조
② Rust 메모리 구조
| 항목 | ☕ Java (HotSpot 64bit) | Rust |
|---|---|---|
| 스택 | 기본 타입, 참조(8B) | 기본 타입, struct 전체 |
| 힙 | 모든 객체 (new) | Box::new() 명시 시만 |
| 객체 헤더 | 12~16B (Mark Word + Klass Ptr) | 없음 |
| vtable 위치 | Klass Pointer → Method table | fat pointer vtable_ptr |
| 메모리 해제 | GC (STW pause) | Drop (컴파일타임) |
| null 안전 | NPE 가능 | Option<T> |
• Klass Pointer (4~8B): 클래스 메타데이터 (vtable 포함)
• Java 17+ 기본: 압축 OOP → Klass 4B → 헤더 합계 12B
출처: JVM Spec §2.5
C++ 비교 (참고)
virtual 명시 시 동적. Rust는 기본 정적, dyn 명시 시 동적.Rust는 C++와 같은 방향이지만 키워드를
virtual 대신 dyn으로 통일하고, vtable을 객체 안이 아닌 fat pointer에 분리.| 항목 | ☕ Java | C++ | Rust |
|---|---|---|---|
| vtable 위치 | 객체 헤더 (Klass) | 객체 내부 (vptr) | fat pointer (struct에 없음) |
| 기본 디스패치 | 동적 (항상) | 정적 (virtual 시 동적) | 정적 (dyn 시 동적) |
| 메모리 해제 | GC | 수동 / RAII | 소유권 Drop |
| null 안전 | 없음 (NPE) | 없음 (dangling) | Option<T> |
| 객체 오버헤드 | Header 12~16B | vptr 8B | 없음 |
전체 예제 — Main.java / main.rs
Main.java: javac 21 검증 완료 (HashMap·Stream·제네릭·feed·status 포함) | main.rs: rustc 1.75 검증 완료 (정적+동적·Box<dyn>·HashMap·Iterator·feed·status 포함)
1// Main.java — javac 21 검증 완료 2import java.util.ArrayList; 3import java.util.List; 4import java.util.Map; 5import java.util.HashMap; 6import java.util.stream.Collectors; 7 8// ── Step 1: 데이터 클래스 ──────────────── 9class Lion { 10 private final String name; 11 private final int age; 12 private boolean hungry; 13 public Lion(String name, int age) { 14 this.name = name; 15 this.age = age; 16 this.hungry = true; 17 } 18 public String getName() { return name; } 19 public String status() { 20 return name + " (" + age + " y.o.) — " 21 + (hungry ? "Hungry" : "Full"); 22 } 23 public void feed() { 24 if (hungry) { 25 System.out.println( 26 " [Lion] " + name + " fed."); 27 hungry = false; 28 } else { 29 System.out.println( 30 " [Lion] " + name + " already full."); 31 } 32 } 33 @Override public String toString() { 34 return "Lion{" + name + "}"; 35 } 36} 37class Duck { 38 private final String name; 39 private final int age; 40 private int feathers; 41 public Duck(String name, int age) { 42 this.name = name; 43 this.age = age; 44 this.feathers = 1000; 45 } 46 public String getName() { return name; } 47 @Override public String toString() { 48 return "Duck{" + name + "}"; 49 } 50} 51class Snake { 52 private final String name; 53 private final int age; 54 private final boolean isVenomous; 55 public Snake(String name, int age, boolean v) { 56 this.name = name; 57 this.age = age; 58 this.isVenomous = v; 59 } 60 public String getName() { return name; } 61 @Override public String toString() { 62 return "Snake{" + name + "}"; 63 } 64} 65 66// ── Step 2 & 3: interface + implements ── 67interface Sound { 68 String makeSound(); 69 default void loudSound() { 70 System.out.println( 71 " " + makeSound().toUpperCase() 72 + "!!!"); 73 } 74 static String describe(Sound s) { 75 return "Sound: " + s.makeSound(); 76 } 77} 78class LionSound extends Lion implements Sound { 79 public LionSound(String name, int age) { 80 super(name, age); 81 } 82 @Override public String makeSound() { 83 return getName() + ": Roar~"; 84 } 85} 86class DuckSound extends Duck implements Sound { 87 public DuckSound(String name, int age) { 88 super(name, age); 89 } 90 @Override public String makeSound() { 91 return getName() + ": Quack!"; 92 } 93 @Override public void loudSound() { 94 System.out.println( 95 " (quietly) " + makeSound()); 96 } 97} 98class SnakeSound extends Snake implements Sound { 99 public SnakeSound( 100 String name, int age, boolean v) { 101 super(name, age, v); 102 } 103 @Override public String makeSound() { 104 return getName() + ": Hiss!!!"; 105 } 106} 107 108// ── 정적 메서드 ────────────────────────── 109class Zoo { 110 // Java 제네릭 = 타입 소거 → 동적 디스패치 111 // Rust <T: Sound> = 단형화 → 정적 디스패치 112 public static <T extends Sound> 113 void playGeneric(T animal) { 114 System.out.println( 115 " [generic] " + animal.makeSound()); 116 } 117 public static void holdConcert( 118 List<Sound> animals) { 119 System.out.println(" === Animal Chorus ==="); 120 for (Sound a : animals) { 121 a.loudSound(); // 동적 디스패치 122 } 123 } 124 public static Sound makeAnimal(String kind) { 125 return switch (kind) { 126 case "lion" -> new LionSound("Simba", 5); 127 case "duck" -> new DuckSound("Donald", 3); 128 default -> 129 new SnakeSound("Nagini", 8, false); 130 }; 131 } 132 public static Map<String, Sound> buildZoo() { 133 Map<String, Sound> zoo = new HashMap<>(); 134 zoo.put("simba", 135 new LionSound("Simba", 5)); 136 zoo.put("donald", 137 new DuckSound("Donald", 3)); 138 zoo.put("nagini", 139 new SnakeSound("Nagini", 8, false)); 140 return zoo; 141 } 142 public static void main(String[] args) { 143 System.out.println("=".repeat(55)); 144 System.out.println( 145 " Java Zoo — all calls are dynamic dispatch"); 146 System.out.println("=".repeat(55)); 147 148 LionSound simba = new LionSound("Simba", 5); 149 DuckSound donald = new DuckSound("Donald", 3); 150 SnakeSound nagini = 151 new SnakeSound("Nagini", 8, false); 152 153 System.out.println( 154 "\n1. Generic <T extends Sound>:"); 155 playGeneric(simba); 156 playGeneric(donald); 157 playGeneric(nagini); 158 159 System.out.println( 160 "\n2. Heterogeneous List<Sound>:"); 161 List<Sound> zoo = new ArrayList<>(); 162 zoo.add(simba); 163 zoo.add(donald); 164 zoo.add(nagini); 165 holdConcert(zoo); 166 167 System.out.println( 168 "\n3. Conditional return makeAnimal():"); 169 for (String kind : 170 List.of("lion", "duck", "snake")) { 171 Sound a = makeAnimal(kind); 172 System.out.println(" " + a.makeSound()); 173 } 174 175 System.out.println("\n4. HashMap:"); 176 Map<String, Sound> zooMap = buildZoo(); 177 zooMap.forEach((name, sound) -> 178 System.out.println( 179 " " + name + " → " + sound.makeSound())); 180 181 System.out.println("\n5. Stream API:"); 182 zoo.stream() 183 .filter(a -> a.makeSound().contains("Roar")) 184 .map(Sound::describe) 185 .forEach(s -> System.out.println(" " + s)); 186 187 System.out.println("\n6. Feeding:"); 188 System.out.println(" Status: " + simba.status()); 189 simba.feed(); 190 simba.feed(); 191 192 System.out.println( 193 "\n Done: Java GC frees memory"); 194 System.out.println("=".repeat(55)); 195 } 196}
1// main.rs — rustc 1.75 검증 완료 2use std::collections::HashMap; 3 4#[derive(Debug, Clone)] 5struct Lion { 6 name: String, 7 age: u32, 8 hungry: bool, 9} 10#[derive(Debug, Clone)] 11struct Duck { 12 name: String, 13 age: u32, 14 feathers: u32, 15} 16#[derive(Debug, Clone)] 17struct Snake { 18 name: String, 19 age: u32, 20 is_venomous: bool, 21} 22 23impl Lion { 24 fn new(name: &str, age: u32) -> Self { 25 Lion { name: name.to_string(), age, hungry: true } 26 } 27 fn name(&self) -> &str { &self.name } 28 fn status(&self) -> String { 29 format!( 30 "{} ({} y.o.) — {}", 31 self.name, 32 self.age, 33 if self.hungry { "Hungry" } else { "Full" } 34 ) 35 } 36 fn feed(&mut self) { 37 if self.hungry { 38 println!(" [Lion] {} fed.", self.name); 39 self.hungry = false; 40 } else { 41 println!(" [Lion] {} already full.", self.name); 42 } 43 } 44} 45impl Duck { 46 fn new(name: &str, age: u32) -> Self { 47 Duck { name: name.to_string(), age, feathers: 1000 } 48 } 49 fn name(&self) -> &str { &self.name } 50} 51impl Snake { 52 fn new(name: &str, age: u32, v: bool) -> Self { 53 Snake { name: name.to_string(), age, is_venomous: v } 54 } 55 fn name(&self) -> &str { &self.name } 56} 57 58trait Sound { 59 fn make_sound(&self) -> String; 60 fn loud_sound(&self) { 61 println!(" {}!!!", self.make_sound().to_uppercase()); 62 } 63} 64 65impl Sound for Lion { 66 fn make_sound(&self) -> String { 67 format!("{}: Roar~", self.name) 68 } 69} 70impl Sound for Duck { 71 fn make_sound(&self) -> String { 72 format!("{}: Quack!", self.name) 73 } 74 fn loud_sound(&self) { 75 println!(" (quietly) {}", self.make_sound()); 76 } 77} 78impl Sound for Snake { 79 fn make_sound(&self) -> String { 80 if self.is_venomous { 81 format!("{}: Hiss!!!", self.name) 82 } else { 83 format!("{}: Sssss...", self.name) 84 } 85 } 86} 87 88// 정적 디스패치 — 단형화 (Java에 없음) 89fn play_static<T: Sound>(a: &T) { 90 println!(" {}", a.make_sound()); 91} 92 93// &impl Sound — play_static과 동일 (syntactic sugar) 94fn play_impl(a: &impl Sound) { 95 println!(" {}", a.make_sound()); 96} 97 98// 동적 디스패치 — Java holdConcert와 동일 99fn hold_concert(animals: &[&dyn Sound]) { 100 println!(" === Animal Chorus ==="); 101 for animal in animals { 102 // fat ptr → vtable → 함수 포인터 103 animal.loud_sound(); 104 } 105} 106 107fn make_animal(kind: &str) -> Box<dyn Sound> { 108 match kind { 109 "lion" => Box::new(Lion::new("Simba", 5)), 110 "duck" => Box::new(Duck::new("Donald", 3)), 111 _ => Box::new(Snake::new("Nagini", 8, false)), 112 } 113} 114 115fn main() { 116 println!("{}", "=".repeat(60)); 117 println!(" Rust Zoo — static dispatch by default"); 118 println!("{}", "=".repeat(60)); 119 120 let simba = Lion::new("Simba", 5); 121 let donald = Duck::new("Donald", 3); 122 let nagini = Snake::new("Nagini", 8, false); 123 124 // ── 정적 디스패치 ────────────────────────── 125 // Java에 없는 개념: 컴파일타임 단형화 126 println!("\n Static dispatch (not in Java):"); 127 play_static(&simba); // play__Lion 직접 CALL 128 play_static(&donald); // play__Duck 직접 CALL 129 play_impl(&nagini); 130 131 // ── 동적 디스패치 ────────────────────────── 132 // Java의 기본 동작과 동일 133 println!("\n Dynamic dispatch (like Java List<Sound>):"); 134 let animals: Vec<&dyn Sound> = vec![ 135 &simba, &donald, &nagini, 136 ]; 137 hold_concert(&animals); // fat ptr → vtable → 함수 138 139 // ── Box<dyn Sound> ───────────────────────── 140 println!("\n Box<dyn Sound> (like Java new Lion() similar):"); 141 let animal = make_animal("lion"); 142 println!(" {}", animal.make_sound()); 143 144 // ── HashMap (Java와 동일한 기능) ─────────── 145 println!("\n HashMap:"); 146 let mut zoo_map: HashMap<&str, &str> = HashMap::new(); 147 zoo_map.insert("simba", "lion"); 148 zoo_map.insert("donald", "duck"); 149 zoo_map.insert("nagini", "snake"); 150 for (name, kind) in &zoo_map { 151 println!(" {} → {}", name, kind); 152 } 153 154 // ── Iterator 체인 (Java Stream에 대응) ───── 155 println!("\n Iterator chain (like Java Stream):"); 156 let names: Vec<&str> = vec!["simba", "donald", "nagini"]; 157 let lions: Vec<&&str> = names.iter() 158 .filter(|&&n| n == "simba") 159 .collect(); 160 println!(" Lions: {:?}", lions); 161 162 // ── 먹이 주기 ───────────────────────────── 163 println!("\n Feeding:"); 164 let mut simba2 = simba.clone(); 165 println!(" Status: {}", simba2.status()); 166 simba2.feed(); 167 simba2.feed(); // 이미 배부름 168 169 println!("\n Done: scope ends → auto Drop (no GC)"); 170 println!("{}", "=".repeat(60)); 171} 172
class Lion {
String name;
String makeSound() { ... }
}
struct Lion { name: String }
impl Lion {
fn make_sound(&self) { ... }
}
<T extends Sound> void play(T a) // 타입 소거 → vtable 조회!
fn play(a: &impl Sound) // 정적 fn play(a: &dyn Sound) // 동적
// Java — GC (비결정적)
Lion simba = new Lion("Simba");
// 언젠가 GC가 해제 (STW)
// Rust — Drop (즉시)
let simba = Lion::new("Simba");
} // 스코프 끝 → 즉시 Drop!
// Java List<Sound> zoo = new ArrayList<>(); zoo.add(new Lion(...)); // 자동 Sound a = null; // NPE 위험!
// Rust let zoo: Vec<Box<dyn Sound>> = vec![...]; // null 없음! let a: Option<Box<dyn Sound>> = None;
| 항목 | ☕ Main.java | main.rs |
|---|---|---|
| 데이터+메서드 | class 내부 | struct + impl 분리 |
| 정적 디스패치 | 없음 (타입 소거→동적) | &impl Sound (단형화) |
| 동적 디스패치 | 항상 (vtable) | &dyn Sound (명시) |
| 메모리 해제 | GC (런타임) | Drop (스코프 끝) |
| null 안전 | NPE 가능 | null 없음, Option<T> |
핵심 용어 사전 — Rust 용어 ↔ Java 대응
출처: Rust ReferenceThe Rust BookJLS SE21
| Rust 용어 | ☕ Java 대응 | 설명 |
|---|---|---|
trait | interface | 메서드 시그니처 집합. Rust trait은 기존 타입에 외부에서 추가 가능 (Java 불가 — orphan rule) |
impl Trait for Type | class T implements I | 계약 이행. Rust는 클래스 외부 별도 블록, Java는 클래스 선언부에 명시 |
dyn Trait | (Java 기본 동작) | 동적 디스패치 명시. Java는 항상 동적이므로 별도 키워드 없음 |
&dyn Trait | Interface ref = new Type() | fat pointer (16B) = data_ptr + vtable_ptr. Java 참조는 단순 포인터 8B |
Box<dyn Trait> | Interface ref = new Type() | 힙 단독 소유. Java는 GC 해제, Rust는 스코프 끝에 즉시 Drop |
Rc<T> | GC 참조 공유 (단일 스레드) | 단일 스레드 공유 소유권. 참조 카운트 방식. Arc와 달리 스레드 안전 아님 |
Arc<T> | GC 참조 공유 (멀티 스레드) | 원자적 참조 카운트. 멀티 스레드 안전. Java의 volatile 참조와 유사 |
| 단형화 (Monomorphization) | ❌ 없음 (타입 소거가 반대) | 제네릭을 타입별 전용 코드로 컴파일 타임에 복제. Java는 런타임에 타입 소거 |
| fat pointer (와이드 포인터) | ❌ 없음 | data_ptr (8B) + vtable_ptr (8B) = 16B. dyn Trait에만 존재. Java 참조는 8B 단순 포인터 |
| vtable | JVM 내부 vtable | 동일 개념. Java는 객체 헤더(클래스 포인터)에 내장, Rust는 fat pointer에만 존재 |
DST (!Sized) | ❌ 없음 | 컴파일 타임에 크기 불확정 타입. Java는 모든 객체가 참조 = 8B 고정 |
| 소유권 (Ownership) | GC 관리 참조 | 값의 유일한 주인. 스코프 종료 시 즉시 Drop. Java GC는 비결정적 해제 |
빌림 (&T / &mut T) | 참조 전달 (Java 기본) | 소유권 없이 읽기/쓰기 접근. 컴파일러가 유효성 보장 (댕글링 포인터 불가) |
라이프타임 ('a) | ❌ 없음 (GC가 대신) | 참조의 유효 범위를 컴파일 타임에 명시. GC 없는 메모리 안전의 핵심 |
Sized trait | Java primitive 타입 (크기 확정) | 컴파일 타임에 크기가 확정되는 타입이 자동 구현하는 marker trait. Rust의 모든 기본 타입은 Sized. Java 참조 타입은 항상 포인터(8B)라 개념이 다름 |
DST (!Sized) | ❌ 없음 (Java는 참조로 숨김) | 크기가 컴파일 타임에 불확정인 타입. dyn Sound, [i32], str이 해당. 반드시 포인터 뒤에서만 사용 가능 |
| trait object | Sound a = new Lion() (자동) | dyn Sound처럼 dyn 뒤에 trait 이름이 붙은 타입. 구체 타입을 숨기고 trait 메서드만 호출 가능한 불투명 타입. Java는 dyn 없이 자동으로 동작 |
| vtable (가상 메서드 테이블) | JVM 내부 vtable (객체 헤더) | trait 메서드의 함수 포인터 배열. 컴파일 타임에 확정, 바이너리 .rodata에 저장. Java는 객체 헤더(class word)가 vtable을 가리킴. Rust는 fat pointer의 vtable_ptr이 가리킴 |
| type erasure (타입 소거) | Java 제네릭의 동작 방식 | Java 제네릭은 컴파일 후 타입 파라미터 T가 제거되어 런타임에 T가 무엇인지 모름. Rust <T: Trait>는 반대 — 단형화로 타입별 코드 생성. dyn Trait만 타입 소거와 유사 |
핵심 정리 — Java ↔ Rust 대응
| ☕ Java | Rust | 설명 |
|---|---|---|
class | struct + impl | 데이터와 메서드 분리 |
interface | trait | 메서드 계약 + 기본 구현 |
implements | impl Trait for Type | trait 구현 |
new Book() | Book {{ .. }} | 생성 (Rust는 스택 기본) |
null | None / Option<T> | 없음 표현 — 컴파일타임 강제 |
try/catch | Result<T,E> + ? | 에러 처리 — 무시 불가 |
List<Sound> | Vec<&dyn Sound> | 이종 컬렉션 |
<T extends Sound> | <T: Sound> | Java=타입소거(동적), Rust=단형화(정적) |
| GC | 소유권 + Drop | 메모리 해제 — Rust는 컴파일타임 |
| 동적 디스패치 (항상) | dyn Trait (명시) | vtable 조회 |
| 없음 | impl Trait | 정적 디스패치 — 단형화 |
| 참조 8B (vtable은 헤더) | fat pointer 16B (data+vtable) | 포인터 구조 차이 |