Java 개발자를 위한 Rust 입문
Java로 오랫동안 일해 왔다면, Rust를 배우는 데 필요한 건 이미 대부분 갖추고 있어요. 여러분은 타입으로 생각하는 데 익숙하고, 제네릭, 인터페이스, 람다, 스트림 파이프라인을 알고 있어요. Rust에도 이게 다 있어요. 트레이트는 기본 메서드가 있는 인터페이스와 아주 비슷하고, 반복자는 스트림처럼 읽히고, Cargo는 Maven이나 Gradle이 하던 일을 훨씬 적은 설정으로 해 줘요.
새로운 건 JVM이 대신해 주던 모든 일이에요. Rust에는 가비지 컬렉터도 가상 머신도 없어요. 코드는 네이티브 바이너리로 컴파일되고, 메모리를 언제 해제할지는 컴파일러가 프로그램을 실행하기 전에 소유권(ownership)과 대여(borrowing)를 검사해서 정해요. 또 Rust에는 null이 없고(Option을 써요), 예외도 없고(Result를 써요), 클래스 상속도 없어요. 공부 시간은 대부분 이 네 가지 변화에 쓰게 될 거예요.
Java 개발자는 왜 Rust를 배울까요? 보통 이런 이유 중 하나예요. 메모리 사용량이 적고 일정해야 하며 GC 멈춤이 없어야 하는 서비스, JVM 없이 바로 시작해야 하는 커맨드 라인 도구와 서버리스 함수, Java 앱에서 네이티브 코드로 옮기고 싶은 뜨거운 경로(Rust는 JNI나 새로운 Foreign Function & Memory API로 Java에서 호출할 수 있어요), 아니면 그냥 Rust가 왜 “가장 사랑받는 언어” 설문에서 계속 1위를 하는지 궁금해서예요. 이 페이지는 Java에서 알던 것을 Rust와 짝지어 보여 줘서, 정말 다른 부분에 집중할 수 있게 도와드려요.
C++, Go, Python과의 일반적인 비교가 궁금하다면 Rust vs C++, Go, Python을 보세요. .NET도 알고 있다면 C# 개발자를 위한 Rust 페이지도 따로 있어요.
언어 개념: Java에서 Rust로
섹션 제목: “언어 개념: Java에서 Rust로”이 표를 회화 사전처럼 써 보세요. 마지막 열은 Rust 쪽을 설명하는 이 사이트의 강좌로 연결돼요.
| Java | Rust | 더 배우기 |
|---|---|---|
필드와 메서드가 있는 class |
데이터는 struct, 메서드는 impl 블록 |
구조체 |
생성자와 new |
Self를 반환하는 평범한 함수. 관례상 이름이 new예요 |
구조체 |
interface |
trait |
트레이트 |
인터페이스의 default 메서드 |
트레이트의 기본 메서드 (똑같이 동작해요) | 트레이트 |
abstract class, extends, super |
상속은 없어요. 공통 동작은 기본 메서드가 있는 트레이트로, 공통 데이터는 컴포지션(구조체 안에 구조체)으로 만들어요 | 트레이트, 디자인 패턴 |
여러 클래스를 담는 List<Shape> |
Vec<Box<dyn Shape>> (트레이트 객체) |
트레이트 객체 |
sealed interface + record + 패턴 switch (Java 17/21) |
배리언트가 데이터를 담는 enum + match |
열거형, match |
필드와 생성자가 있는 enum |
배리언트마다 서로 다른 데이터를 담을 수 있는 enum |
열거형 |
record Point(int x, int y) |
#[derive(Debug, Clone, PartialEq)] struct Point { x: i32, y: i32 } |
구조체, 자주 쓰는 트레이트 |
toString(), equals(), hashCode(), compareTo() |
Display / Debug, PartialEq / Eq, Hash, PartialOrd / Ord (보통 derive로) |
자주 쓰는 트레이트 |
==(같은 객체)와 .equals()(같은 값) |
==는 항상 값을 비교해요. “참조 비교” 함정은 없어요 |
연산자 |
null과 Optional<T> |
Option<T>: Some(value) 아니면 None |
Option |
orElse, orElseGet, orElseThrow, map, flatMap, ifPresent |
unwrap_or, unwrap_or_else, ok_or / expect, map, and_then, if let Some(...) |
Option, if let / while let |
검사 예외(checked exception)와 throws |
반환 타입의 Result<T, E> + ? 연산자 |
Result, 사용자 정의 에러 |
진짜 버그를 알리는 비검사 예외(IllegalStateException) |
panic! (스레드를 멈춰요) |
panic |
예외 체이닝 (new ConfigException(msg, cause)) |
map_err, 또는 thiserror의 #[source]와 anyhow의 .context(...) |
anyhow와 thiserror |
try-with-resources와 AutoCloseable |
Drop: 주인이 스코프를 벗어나면 자동으로 실행돼요 |
스코프, 스마트 포인터 |
| 가비지 컬렉터 | 소유권과 대여, 컴파일할 때 검사 | 소유권, 대여 |
final 지역 변수 |
그냥 let (기본이 불변). 바꾸려면 let mut |
변수 |
static final 상수 |
const |
변수 |
가변 static 필드, 싱글턴 |
LazyLock이나 OnceLock을 쓴 static. 바뀌어야 하면 Mutex로 감싸요 |
공유 상태 |
<T extends Comparable<T>>가 붙은 제네릭 |
트레이트 바운드가 붙은 제네릭: T: Ord 또는 where T: Ord |
제네릭 |
타입 소거, List<int> 대신 List<Integer> |
단형화(monomorphization): 제네릭 코드가 타입마다 컴파일되고, Vec<i32>는 평범한 숫자를 저장해요 |
제네릭, 고급 제네릭 |
람다, Function<T, R>, Supplier<T>, Consumer<T> |
클로저. Fn, FnMut, FnOnce 트레이트로 표현해요 |
클로저, Fn, FnMut, FnOnce |
스트림: filter, map, reduce, collect |
반복자 어댑터: filter, map, fold, sum, collect |
반복자 |
Iterable<T>와 Iterator<T> |
IntoIterator와 Iterator (메서드는 Option을 반환하는 next 하나) |
사용자 정의 반복자 |
parallelStream() |
rayon 크레이트의 par_iter() |
동시성 |
Thread, ExecutorService |
std::thread::spawn과 std::thread::scope. 풀에는 rayon이나 tokio |
동시성 |
synchronized, ReentrantLock |
보호할 데이터를 감싸는 Mutex<T> |
공유 상태 |
AtomicInteger, AtomicLong |
AtomicI32, AtomicI64, AtomicUsize |
공유 상태 |
스레드 사이의 BlockingQueue |
채널 (std::sync::mpsc) |
채널 |
CompletableFuture, 가상 스레드 |
async fn과 .await. tokio 같은 런타임이 실행해요 |
async/await |
String(불변)과 StringBuilder |
&str(빌려 온 텍스트 뷰)과 String(소유하고, 늘릴 수 있음) |
문자열 |
char (UTF-16 단위 하나) |
char (어떤 유니코드 문자든, 4바이트). 문자열은 UTF-8이에요 |
문자열 |
int, long, double과 박싱된 Integer |
i32, i64, f64, 그리고 부호 없는 u8부터 u128까지. 박싱은 없어요 |
데이터 타입 |
| 조용히 넘어가는 정수 오버플로 | debug 빌드에서는 패닉. checked_add, wrapping_add, saturating_add로 동작을 골라요 |
정수 오버플로 |
(int) x 같은 캐스트 |
숫자에는 as, 안전한 변환에는 From / TryFrom |
타입 변환, 타입 변환 (From, Into) |
BigDecimal, BigInteger |
rust_decimal과 num-bigint 크레이트 |
데이터 타입 |
패키지와 import |
모듈(mod)과 use |
모듈 |
public, private, protected, package-private |
기본은 비공개. pub과 pub(crate). protected는 없어요 |
모듈 |
| JAR / Maven 아티팩트 | 크레이트(라이브러리 또는 바이너리) | Cargo와 크레이트 |
애너테이션 (@Override, Lombok의 @Data) |
#[derive(Debug)], #[test] 같은 어트리뷰트 |
매크로 |
| 리플렉션, 런타임 애너테이션 처리 | 런타임에는 거의 없어요. 대신 매크로가 컴파일할 때 코드를 만들어요 | 절차적 매크로 |
| 메서드 오버로딩 | 오버로딩은 없어요. 이름을 다르게 하거나 제네릭 매개변수를 써요 | 함수 |
instanceof 검사와 다운캐스트 |
열거형에 match (Any로 다운캐스트할 수도 있지만 드물어요) |
패턴 매칭 |
| JNI, Foreign Function & Memory API | extern "C" 함수 |
FFI (C 호출하기) |
컬렉션
섹션 제목: “컬렉션”Java 컬렉션 프레임워크는 std::collections에 깔끔하게 대응돼요. 가장 큰 차이는, Rust 컬렉션은 절대 null을 반환하지 않는다는 거예요. 조회하면 Option을 돌려줘요.
| Java | Rust | 더 배우기 |
|---|---|---|
ArrayList<T> |
Vec<T> |
벡터 |
int[], String[] |
컴파일할 때 길이를 알면 [T; N], 아니면 Vec<T>. 어느 쪽이든 슬라이스 &[T]로 넘겨요 |
배열, 슬라이스 |
List.of(...) (수정 불가) |
배열, 또는 &Vec<T> / &[T]로만 건네주는 Vec (공유 참조로는 아무도 바꿀 수 없어요) |
대여 |
HashMap<K, V> |
HashMap<K, V> (get은 Option<&V>를 반환해요) |
HashMap |
map.merge(word, 1, Integer::sum) |
*map.entry(word).or_insert(0) += 1 |
HashMap |
TreeMap<K, V> |
BTreeMap<K, V> (키가 정렬된 상태로 유지돼요) |
HashMap |
LinkedHashMap<K, V> (넣은 순서) |
indexmap 크레이트의 IndexMap |
|
HashSet<T>, TreeSet<T> |
HashSet<T>, BTreeSet<T> |
HashSet과 BTreeMap |
ArrayDeque<T> |
VecDeque<T> |
VecDeque와 BinaryHeap |
Stack<T>, 또는 스택으로 쓰는 Deque |
push / pop을 쓰는 Vec<T> |
벡터 |
PriorityQueue<T> (작은 것부터) |
BinaryHeap<T> (큰 것부터 나와요. 작은 것부터 꺼내려면 Reverse로 감싸요) |
VecDeque와 BinaryHeap |
LinkedList<T> |
LinkedList<T>도 있지만, 거의 항상 Vec이나 VecDeque가 더 빨라요 |
VecDeque와 BinaryHeap |
ConcurrentHashMap<K, V> |
Mutex<HashMap<K, V>>, 또는 dashmap 크레이트 |
공유 상태 |
list.sort(Comparator.comparing(...)) |
v.sort_by_key(...) 또는 v.sort_by(...) |
정렬과 검색 |
list.removeIf(...) |
v.retain(...) (조건에 맞는 항목을 남겨요) |
벡터 |
Java의 HashMap처럼 Rust의 HashMap에도 정해진 순서가 없고, 실행할 때마다 순서가 달라질 수 있어요. 정렬된 출력이 필요하면 BTreeMap을 쓰거나 먼저 키를 정렬하세요.
도구: Maven과 Gradle에서 Cargo로
섹션 제목: “도구: Maven과 Gradle에서 Cargo로”Java 개발자 대부분이 금방 Cargo에 반해요. 도구 하나, 설정 형식 하나, 플러그인 XML은 없어요. 빌드, 의존성, 테스트, 문서, 포매팅, 린트가 모두 한곳에서 나와요.
| Java | Rust | 참고 |
|---|---|---|
| Maven / Gradle | cargo |
빌드, 실행, 테스트, 의존성 추가, 배포까지. Cargo와 크레이트 참고 |
pom.xml / build.gradle.kts |
Cargo.toml |
짧고, 평범한 프로젝트에는 빌드 스크립트가 필요 없어요 |
| Gradle 의존성 잠금 | Cargo.lock |
자동으로 만들어지고 갱신돼요 |
| 멀티 모듈 빌드 | Cargo 워크스페이스 | 맨 위의 Cargo.toml 하나가 멤버 크레이트를 나열해요 |
| Maven Central | crates.io | 공개된 모든 크레이트의 API 문서는 docs.rs(영어)에 있어요 |
<dependency> 추가 |
cargo add serde |
|
mvn archetype:generate / gradle init |
cargo new my_app (라이브러리는 cargo new --lib) |
|
mvn compile / mvn test / mvn package |
cargo build / cargo test / cargo build --release |
debug 빌드는 실행 속도가 훨씬 느려요. 성능은 release로 재세요 |
java -jar app.jar |
./target/release/app |
네이티브 바이너리 하나예요. 서버에 JVM을 설치할 필요가 없어요 |
| JDK 버전, SDKMAN!, 툴체인 | rustup과 rust-toolchain.toml 파일 |
Rust 도구와 개발 환경 참고 |
javac |
rustc |
Cargo가 대신 호출해 줘서 직접 실행할 일은 드물어요 |
| JShell | 브라우저의 Rust Playground(영어), 또는 evcxr_repl |
|
| Checkstyle, SpotBugs, PMD, Error Prone | cargo clippy |
공식 린트 수백 개 |
| google-java-format, Spotless | cargo fmt |
표준 스타일 하나. 중괄호 위치로 다툴 일이 없어요 |
| IntelliJ IDEA, Eclipse | JetBrains RustRover, 또는 VS Code + rust-analyzer | rust-analyzer가 자동 완성, 정의로 이동, 실시간 에러 표시를 해 줘요 |
| Javadoc | 마크다운으로 쓰는 /// 문서 주석 + cargo doc |
문서 안의 코드 예제가 테스트로 실행돼요. Rust 문서화 참고 |
| JUnit 5 | 내장된 #[test]와 cargo test (더 빠른 실행기로 cargo nextest가 인기예요) |
Rust 테스트 참고 |
@ParameterizedTest |
rstest |
|
| Mockito | mockall, 또는 트레이트를 구현한 직접 만든 가짜 객체 |
테스트 더 알아보기 참고 |
| AssertJ / Hamcrest | assert!, assert_eq!, 읽기 쉬운 diff에는 pretty_assertions |
|
| WireMock, Testcontainers | wiremock, testcontainers |
아이디어는 같고, Rust 버전이에요 |
| JaCoCo | cargo llvm-cov 또는 cargo tarpaulin |
|
| JMH | criterion |
성능 참고 |
| JFR, async-profiler, VisualVM | cargo flamegraph (cargo install flamegraph로 설치), perf |
튜닝할 GC나 JIT가 없어요 |
| GraalVM native-image | 필요 없어요 | 모든 Rust 빌드는 이미 네이티브 코드예요 |
작은 Cargo.toml을 보여 드릴게요. 같은 의존성 세 개를 넣으려면 pom.xml이 얼마나 길어지는지 비교해 보세요.
[package]name = "orders-api"version = "0.1.0"edition = "2021"
[dependencies]serde = { version = "1", features = ["derive"] }serde_json = "1"tokio = { version = "1", features = ["full"] }라이브러리와 프레임워크: “그럼 대신 뭘 쓰나요?”
섹션 제목: “라이브러리와 프레임워크: “그럼 대신 뭘 쓰나요?””Java 팀이라면 모두 가장 먼저 묻는 질문이에요. “우리는 Spring Boot와 Hibernate를 쓰는데, Rust 버전은 뭐예요?” Rust에는 Spring이 없어요. 모든 걸 다 가져다주는 프레임워크 하나 대신, 한 가지 일에 집중하는 크레이트 몇 개를 조합해요. 가장 가까운 것들을 정리해 볼게요.
웹과 네트워크
섹션 제목: “웹과 네트워크”| Java | Rust | 참고 |
|---|---|---|
| Spring Boot (Spring MVC / WebFlux) | axum (프로젝트: Axum으로 웹 API에서 직접 만들어 봐요), actix-web, Rocket | axum과 actix-web은 실무에서 널리 쓰여요 |
@RestController + @GetMapping |
평범한 async fn 핸들러를 가리키는 axum 라우트 |
애너테이션도 클래스패스 스캔도 없어요. 라우트는 코드로 연결해요 |
| 서블릿 필터, Spring 인터셉터 | tower 레이어와 tower-http 크레이트(CORS, 압축, 트레이싱, 타임아웃) |
axum은 tower를 그대로 써요 |
Spring 의존성 주입 (@Autowired) |
axum의 공유 State, 평범한 생성자, 트레이트 |
아래쪽의 DI 행을 참고하세요 |
| springdoc-openapi | utoipa |
|
| Thymeleaf | askama (컴파일할 때 템플릿을 검사해요) 또는 tera |
|
| Spring WebSocket | axum의 WebSocket(ws 기능), 또는 tokio-tungstenite |
|
| Spring for GraphQL | async-graphql |
|
| gRPC Java | tonic (protobuf는 prost와 함께) |
|
java.net.http.HttpClient, OkHttp, WebClient |
reqwest |
기본은 비동기이고, 블로킹 클라이언트도 선택할 수 있어요 |
| Netty | tokio + hyper |
|
| Spring Security (JWT, 비밀번호 해싱) | jsonwebtoken, argon2 |
모든 걸 갖춘 보안 프레임워크는 없어요 |
데이터와 메시징
섹션 제목: “데이터와 메시징”| Java | Rust | 참고 |
|---|---|---|
| Hibernate / JPA / Spring Data JPA | SeaORM: 엔티티, 관계, 마이그레이션을 지원하는 비동기 ORM | JPA와 가장 비슷한 느낌이에요. sqlx 위에 만들어졌어요 |
| 엄격한 타입을 중시하는 Hibernate 사용 | Diesel: ORM이자 쿼리 빌더예요. 쿼리 타입을 컴파일할 때 검사해요 | 성숙한 라이브러리(2.x). 기본은 동기 방식이에요(diesel-async도 있어요) |
JDBC, JdbcTemplate, jOOQ, MyBatis |
sqlx: 순수 SQL을 쓰고, query! 매크로가 컴파일할 때 실제 데이터베이스에 대고 검사해요 |
비동기, 아주 인기 있음, ORM은 아니에요 |
| Flyway / Liquibase | sqlx migrate (sqlx-cli), diesel migration, 또는 refinery |
|
| Jedis / Lettuce (Redis) | redis |
|
| Kafka 클라이언트, Spring Kafka | rdkafka (librdkafka 기반) |
|
| RabbitMQ Java 클라이언트 | lapin |
|
| AWS SDK for Java | 공식 AWS SDK for Rust: aws-config, aws-sdk-s3, … |
JPA 엔티티가 좋았다면 SeaORM부터 써 보세요. SQL을 직접 쓰는 게 좋았다면(jOOQ와 MyBatis 쪽 사람들) sqlx를 써 보세요. 컴파일 타임 보장을 가장 강하게 원한다면 Diesel을 써 보세요. 세 가지 모두 동작하는 코드와 함께 데이터베이스에서 볼 수 있어요.
매일 쓰는 기본 부품
섹션 제목: “매일 쓰는 기본 부품”| Java | Rust | 참고 |
|---|---|---|
| Jackson / Gson | serde + serde_json |
사실상 표준이에요. TOML, YAML 등도 다뤄요. Serde로 JSON 다루기 참고 |
| JAXB, Jackson XML | quick-xml (serde와 쓰려면 serialize 기능을 켜요) |
|
| SLF4J + Logback / Log4j 2 | tracing (+ tracing-subscriber), 또는 log + env_logger |
로깅 참고 |
| Lombok | 내장 derive(Debug, Clone, PartialEq, Default, …). 빌더는 bon이나 derive_builder |
바이트코드 트릭은 없어요. 모두 평범한 매크로예요 |
| MapStruct | From / Into 구현 |
직접 작성하고, 컴파일러가 검사해 줘요. 타입 변환 (From, Into) 참고 |
| Spring IoC 컨테이너 | 보통은 아무것도 안 써요. 의존성은 생성자로 넘기고, 테스트에서 바꿔 끼울 부분은 트레이트로 만들어요 | DI 컨테이너를 쓰는 Rust 프로젝트는 드물어요 |
Bean Validation (@NotNull, @Size) |
validator 또는 garde (derive 기반 검증) |
@NotNull은 필요 없어요. 값이 없을 수 있는 곳에만 Option을 써요 |
| Resilience4j | backon, 또는 tower의 retry와 timeout 미들웨어 |
|
| Caffeine (캐싱) | moka |
|
| Guava, Apache Commons | 대부분 표준 라이브러리로 충분하고, 그 밖에 itertools |
|
java.time |
chrono 또는 jiff |
날짜와 시간 참고 |
| picocli | clap |
프로젝트: clap으로 CLI 도구 참고 |
@Scheduled, Quartz |
tokio-cron-scheduler |
|
| Micrometer | metrics, 또는 opentelemetry |
|
| Jakarta Mail | lettre |
데스크톱, 게임, Android, 상호 운용
섹션 제목: “데스크톱, 게임, Android, 상호 운용”| Java | Rust | 참고 |
|---|---|---|
| JavaFX / Swing | Tauri (웹 UI + Rust 백엔드), Slint, egui, Iced |
Tauri 2와 Slint 1.x가 가장 안정적이에요. GUI 앱 참고 |
| LibGDX, jMonkeyEngine | Bevy, macroquad |
Bevy는 1.0 이전이라 릴리스마다 API가 바뀌어요. 게임 개발 참고 |
| Android 앱 코드 | UI는 Kotlin이나 Java로 두고, 공통 로직을 Android NDK로 빌드한 Rust 라이브러리에 넣어요 | Mozilla의 uniffi가 Kotlin 바인딩을 만들어 줘요 |
| Java에서 네이티브 코드 호출 | extern "C" 함수를 공개하고 FFM API(JDK 22+)로 호출하거나, jni 크레이트로 JNI 함수를 작성해요 |
FFI (C 호출하기) 참고 |
나란히 보기: 추상 클래스는 트레이트로
섹션 제목: “나란히 보기: 추상 클래스는 트레이트로”전형적인 Java 클래스 계층이에요. 추상 메서드 하나와 공통 메서드 하나가 있는 추상 기본 클래스예요.
abstract class Employee { protected final String name; Employee(String name) { this.name = name; }
abstract int monthlyPay();
String payslip() { return name + " earns " + monthlyPay() + " a month"; }}
class Salaried extends Employee { private final int yearly; Salaried(String name, int yearly) { super(name); this.yearly = yearly; } @Override int monthlyPay() { return yearly / 12; }}
class Contractor extends Employee { private final int hours, rate; Contractor(String name, int hours, int rate) { super(name); this.hours = hours; this.rate = rate; } @Override int monthlyPay() { return hours * rate; }}
List<Employee> staff = List.of(new Salaried("Ada", 60000), new Contractor("Linus", 120, 50));for (Employee e : staff) System.out.println(e.payslip());Rust에는 extends가 없어요. 공통 메서드는 트레이트의 기본 메서드로 옮기고, 구조체마다 자기 데이터를 가져요.
trait Employee { fn name(&self) -> &str; fn monthly_pay(&self) -> u32;
// 기본 메서드: 추상 클래스에 있던 구체적인 payslip()과 같아요. fn payslip(&self) -> String { format!("{}의 월급: {}", self.name(), self.monthly_pay()) }}
struct Salaried { name: String, yearly: u32,}
struct Contractor { name: String, hours: u32, rate: u32,}
impl Employee for Salaried { fn name(&self) -> &str { &self.name } fn monthly_pay(&self) -> u32 { self.yearly / 12 }}
impl Employee for Contractor { fn name(&self) -> &str { &self.name } fn monthly_pay(&self) -> u32 { self.hours * self.rate }}
fn main() { // Box<dyn Employee>가 "어떤 Employee를 가리키는 참조" 역할을 해요. let staff: Vec<Box<dyn Employee>> = vec![ Box::new(Salaried { name: "Ada".to_string(), yearly: 60_000 }), Box::new(Contractor { name: "Linus".to_string(), hours: 120, rate: 50 }), ];
for e in &staff { println!("{}", e.payslip()); }}실행 결과:
Ada의 월급: 5000Linus의 월급: 6000트레이트는 필드를 가질 수 없어서, protected name은 구조체마다 구현하는 작은 name() 메서드가 돼요. 처음에는 타이핑이 늘어난 것 같지만, 문제 하나를 통째로 없애 줘요. 깨지기 쉬운 기본 클래스도, 깜빡하기 쉬운 super 호출도, 스크롤해야 하는 깊은 계층도 없어요. Java 8의 인터페이스 default 메서드를 안다면, Rust가 동작을 공유하는 방법은 이미 아는 거예요. 그리고 타입의 종류가 정해져 있다면(Java 17의 sealed interface처럼), 트레이트보다 열거형과 match가 더 간단할 때가 많아요.
나란히 보기: Optional은 Option으로
섹션 제목: “나란히 보기: Optional은 Option으로”Java의 Optional은 큰 발전이었지만 반환값에만 쓰이고, map.get()이나 아무 필드나 여전히 null일 수 있어요.
static Optional<String> findEmail(int userId) {}
for (int id : new int[] {1, 2}) { System.out.println(findEmail(id).map(String::toUpperCase).orElse("no email on file"));}findEmail(1).ifPresent(email -> System.out.println("sending to " + email));Rust의 Option은 API가 거의 같지만, 컬렉션 조회를 포함해서 값이 없을 수 있는 모든 곳에 쓰여요. 그리고 그 뒤에 숨은 null도 없어요.
fn find_email(user_id: u32) -> Option<String> { if user_id == 1 { } else { None }}
fn main() { for id in [1, 2] { // map + orElseGet let shown = find_email(id) .map(|email| email.to_uppercase()) .unwrap_or_else(|| "등록된 이메일이 없어요".to_string()); println!("{shown}"); }
// ifPresent if let Some(email) = find_email(1) { println!("보내는 중: {email}"); }
// flatMap은 and_then이라고 불러요 let domain = find_email(1).and_then(|email| email.split_once('@').map(|(_, d)| d.to_string())); println!("{domain:?}");}실행 결과:
등록된 이메일이 없어요보내는 중: [email protected]Some("example.com")None을 처리하기 전에는 컴파일러가 Option<String>에 .to_uppercase()를 호출하지 못하게 해서, NullPointerException이란 게 아예 존재하지 않아요. Option에는 추가 비용도 없어요. Option<&T>는 평범한 참조와 크기가 같아요. 도우미 메서드는 Option 페이지에서 배워 보세요.
나란히 보기: 검사 예외는 Result로
섹션 제목: “나란히 보기: 검사 예외는 Result로”검사 예외의 핵심에는 좋은 아이디어가 있었어요. 시그니처가 무엇이 잘못될 수 있는지 알려 준다는 거예요. 전형적인 Java 코드예요.
class InsufficientFundsException extends Exception { InsufficientFundsException(int needed) { super("need " + needed + " more"); }}
static int withdraw(int balance, int amount) throws InsufficientFundsException { if (amount > balance) throw new InsufficientFundsException(amount - balance); return balance - amount;}
static int pay(int balance, int amount) throws InsufficientFundsException { int left = withdraw(balance, amount); System.out.println("paid " + amount + ", " + left + " left"); return left;}
try { pay(100, 80); pay(50, 80); }catch (InsufficientFundsException e) { System.out.println("error: " + e.getMessage()); }Rust는 그 좋은 아이디어는 살리고 나머지는 버려요. 에러는 반환 타입의 일부이고(Result<u32, BankError>), ?가 throws의 역할, 즉 “이게 실패했으면 에러를 호출한 쪽으로 넘겨줘”를 해요. 에러는 보통 enum이라서, 타입 하나에 작업이 실패할 수 있는 모든 경우를 나열할 수 있어요.
use std::fmt;
#[derive(Debug)]enum BankError { InvalidAmount, InsufficientFunds { needed: u32 },}
// Display가 getMessage() 역할을 해요.impl fmt::Display for BankError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { BankError::InvalidAmount => write!(f, "금액은 0보다 커야 해요"), BankError::InsufficientFunds { needed } => write!(f, "{needed}만큼 더 필요해요"), } }}
fn withdraw(balance: u32, amount: u32) -> Result<u32, BankError> { if amount == 0 { return Err(BankError::InvalidAmount); } if amount > balance { return Err(BankError::InsufficientFunds { needed: amount - balance }); } Ok(balance - amount)}
fn pay(balance: u32, amount: u32) -> Result<u32, BankError> { let left = withdraw(balance, amount)?; // 예외를 "던지는" 메서드를 호출하는 것과 같아요 println!("{amount} 지불, {left} 남음"); Ok(left)}
fn main() { for (balance, amount) in [(100, 80), (50, 80), (100, 0)] { if let Err(e) = pay(balance, amount) { println!("에러: {e}"); } }}실행 결과:
80 지불, 20 남음에러: 30만큼 더 필요해요에러: 금액은 0보다 커야 해요무엇이 사라졌는지 보세요. try 블록도, 스택 되감기도, 람다를 통과시키려고 RuntimeException으로 감싼 예외도 없어요. 에러는 평범한 값이라서 클로저와 반복자 체인 안에서도 문제없이 동작해요. ?는 함수가 일찍 반환할 수 있는 모든 지점을 눈에 보이게 표시해 줘요. Display를 손으로 쓰는 건 금방 지겨워지는데, thiserror 크레이트가 대신 만들어 주고, anyhow는 애플리케이션에 모든 에러를 담는 타입을 제공해요(anyhow와 thiserror 참고). 비검사 예외로 알리던 진짜 버그에는 Rust의 panic!이 있어요.
나란히 보기: 스트림은 반복자로
섹션 제목: “나란히 보기: 스트림은 반복자로”groupingBy 컬렉터를 쓰는 스트림 파이프라인이에요.
record Order(String customer, String city, int total) {}
List<Order> orders = List.of( new Order("Ada", "London", 120), new Order("Grace", "New York", 80), new Order("Linus", "Helsinki", 200), new Order("Ken", "New York", 150));
List<String> big = orders.stream() .filter(o -> o.total() > 100) .sorted(Comparator.comparingInt(Order::total).reversed()) .map(Order::customer) .toList();System.out.println(String.join(", ", big));
Map<String, Integer> byCity = orders.stream() .collect(Collectors.groupingBy(Order::city, TreeMap::new, Collectors.summingInt(Order::total)));byCity.forEach((city, sum) -> System.out.println(city + ": " + sum));Rust에서는 .iter()가 .stream() 역할을 하고, 어댑터 이름도 익숙해요.
use std::cmp::Reverse;use std::collections::BTreeMap;
struct Order { customer: String, city: String, total: u32,}
fn order(customer: &str, city: &str, total: u32) -> Order { Order { customer: customer.to_string(), city: city.to_string(), total }}
fn main() { let orders = vec![ order("Ada", "London", 120), order("Grace", "New York", 80), order("Linus", "Helsinki", 200), order("Ken", "New York", 150), ];
// filter + toList let mut big: Vec<&Order> = orders.iter().filter(|o| o.total > 100).collect(); // sorted(...reversed()): 정렬은 반복자 안이 아니라 Vec에서 해요. big.sort_by_key(|o| Reverse(o.total)); // map let names: Vec<&str> = big.iter().map(|o| o.customer.as_str()).collect(); println!("{}", names.join(", "));
// groupingBy(city, TreeMap::new, summingInt(total)) let mut by_city: BTreeMap<&str, u32> = BTreeMap::new(); for o in &orders { *by_city.entry(o.city.as_str()).or_insert(0) += o.total; } for (city, sum) in &by_city { println!("{city}: {sum}"); }}실행 결과:
Linus, Ken, AdaHelsinki: 200London: 120New York: 230도시 이름은 BTreeMap이 알파벳순으로 정렬하기 때문에 영어로 두었어요.
스트림처럼 Rust 반복자도 게을러서(lazy), 무언가가 소비할 때(collect, sum, for 반복문)에만 실행돼요. 스트림과 달리 Collectors 클래스는 없어요. collect()가 여러분이 요청한 타입(Vec, HashMap, String, …)을 보고 그걸 만들어 줘요. 그룹으로 묶을 때는 entry API를 쓰는 평범한 반복문이 보통의 답이고, groupingBy가 그립다면 itertools 크레이트가 도우미를 더해 줘요. 그리고 반복자 체인은 손으로 쓴 반복문과 똑같은 기계어로 컴파일되고, 중간에 int를 박싱하는 일도 없어요. 더 자세한 내용은 반복자와 정렬과 검색에서 읽어 보세요.
나란히 보기: 타입 소거와 단형화
섹션 제목: “나란히 보기: 타입 소거와 단형화”Java 제네릭은 컴파일할 때만 존재해요. 런타임에는 T가 지워지기 때문에 new T()라고 쓸 수 없고, 박싱하지 않고는 int를 List에 넣을 수 없어요.
static <T> List<T> filled(int n) { List<T> list = new ArrayList<>(); for (int i = 0; i < n; i++) list.add(new T()); // 컴파일되지 않아요: T가 지워졌어요 return list;}
List<int> ids; // 이것도 컴파일되지 않아요: List<Integer>가 필요해요Rust는 제네릭 함수를 쓰는 타입마다 따로 복사본을 컴파일해요(이걸 단형화(monomorphization)라고 해요). 그래서 함수는 T가 정확히 무엇인지 알고, T::default()를 호출할 수 있어요.
use std::mem::size_of;
// "T는 기본값이 있고 복제할 수 있어야 해요"fn filled<T: Default + Clone>(n: usize) -> Vec<T> { vec![T::default(); n]}
fn main() { let ids: Vec<i32> = filled(3); let names: Vec<String> = filled(2); let flags: Vec<bool> = filled(2);
println!("{ids:?} {names:?} {flags:?}");
// 박싱이 없어요. Vec<i32>는 4바이트짜리 평범한 숫자를 나란히 저장해요. println!("i32 하나는 {}바이트를 차지해요", size_of::<i32>()); println!("bool 하나는 {}바이트를 차지해요", size_of::<bool>());}실행 결과:
[0, 0, 0] ["", ""] [false, false]i32 하나는 4바이트를 차지해요bool 하나는 1바이트를 차지해요대가도 있어요. 코드 복사본이 많아지면 바이너리가 커지고 컴파일 시간이 길어져요. 대신 모든 호출이 직접 호출이고 인라인될 수 있어요. (Java처럼) 공유하는 복사본 하나를 원할 때는 Box<dyn Trait> 같은 트레이트 객체를 쓰세요. 콜론 뒤의 바운드(T: Default + Clone)가 Rust판 <T extends ...>예요. 제네릭과 고급 제네릭을 참고하세요.
나란히 보기: synchronized는 Arc와 Mutex로
섹션 제목: “나란히 보기: synchronized는 Arc와 Mutex로”스레드 네 개가 공유 카운터를 증가시켜요. Java에서는 락이 객체에 붙어 있고, synchronized를 기억하는 건 여러분의 몫이에요.
class Counter { private int count = 0; synchronized void increment() { count++; } synchronized int get() { return count; }}
Counter counter = new Counter();List<Thread> threads = new ArrayList<>();for (int i = 0; i < 4; i++) { Thread t = new Thread(() -> { for (int j = 0; j < 1000; j++) counter.increment(); }); t.start(); threads.add(t);}for (Thread t : threads) t.join();System.out.println("count = " + counter.get());Rust에서는 Mutex가 숫자를 소유해요. 먼저 잠그지 않고는 숫자에 닿을 방법이 없어요. Arc(원자적으로 참조 개수를 세는 포인터)는 여러 스레드가 뮤텍스를 공유할 수 있게 해 줘요.
use std::sync::{Arc, Mutex};use std::thread;
fn main() { let counter = Arc::new(Mutex::new(0)); let mut handles = Vec::new();
for _ in 0..4 { // 스레드마다 같은 Mutex를 가리키는 자기 핸들을 받아요. let counter = Arc::clone(&counter); handles.push(thread::spawn(move || { for _ in 0..1000 { // lock()은 가드를 반환해요. 가드가 drop되면 잠금이 풀려요. *counter.lock().unwrap() += 1; } })); }
for handle in handles { handle.join().unwrap(); }
println!("count = {}", *counter.lock().unwrap());}실행 결과:
count = 4000Java 개발자가 좋아하는 부분이 여기 있어요. Java에서 synchronized를 깜빡하면 코드는 컴파일되고, 가끔씩 틀린 숫자가 나와요. Rust에서는 평범한 i32나 Vec을 스레드 사이에서 공유하면서 바꾸려고 하면 프로그램이 컴파일되지 않아요. 데이터 경쟁은 타입 시스템이 잡아 줘요(Send와 Sync 트레이트를 통해서요). 카운터 하나만 있다면 AtomicInteger처럼 AtomicUsize가 뮤텍스보다 가벼워요. 동시성과 공유 상태를 참고하세요.
Java 개발자가 자주 걸려 넘어지는 부분
섹션 제목: “Java 개발자가 자주 걸려 넘어지는 부분”Java 개발자는 누구나 같은 곳에서 걸려요. 미리 알고 있으면 훨씬 수월해요.
-
함수에 값을 넘기면 이동해요. Java에서 객체를 넘기면 참조가 넘어가고, 그 뒤에도 변수를 계속 쓸 수 있어요. Rust에서
String이나Vec을 값으로 넘기면 함수 안으로 이동해요.fn greet(name: String) {println!("Hello, {name}!");}fn main() {let name = String::from("Ada");greet(name);greet(name);}error[E0382]: use of moved value: `name`--> src/main.rs:8:11|6 | let name = String::from("Ada");| ---- move occurs because `name` has type `String`, which does not implement the `Copy` trait7 | greet(name);| ---- value moved here8 | greet(name);| ^^^^ value used here after move컴파일러 메시지는 영어로 나와요. “
name의 값은 7번째 줄에서 이동했는데, 8번째 줄에서 이동한 값을 다시 쓰려고 했다”는 뜻이에요. 보통은 함수가 빌리게 해서 해결해요.fn greet(name: &str)로 만들고greet(&name)으로 호출하세요. 소유권을 먼저 읽어 보세요. 이 규칙을 설명하는 페이지예요. 이 에러는 E0382에서 자세히 다뤄요. -
대여 검사기(borrow checker). 읽는 쪽(
&T)은 여럿이어도 되고, 아니면 쓰는 쪽(&mut T)이 딱 하나여야 해요. 둘을 동시에 가질 수는 없어요. 부모를 다시 가리키는 자식 객체나, 여러 객체가 공유 객체 하나를 들고 바꾸는 것처럼 Java에서는 평범한 객체 그래프도 스마트 포인터나 다른 설계가 필요해요. 처음 몇 주는 답답할 수 있어요. 그래도 정말 쉬워져요. -
상속도,
protected도,super도 없어요. 트레이트로 클래스 계층을 다시 만들고 싶은 유혹이 들 거예요. 먼저 열거형과 컴포지션을 써 보세요. 보통 그쪽이 더 잘 맞아요. -
==는 값을 비교해요. 이번에는 좋은 소식이에요. 두String에a == b를 하면 텍스트를 비교해서, 고전적인==와.equals()버그가 일어날 수 없어요. 직접 만든 구조체에서는PartialEq를 derive하거나 구현해야==가 동작해요. -
BinaryHeap은 가장 큰 항목을 꺼내요. Java의PriorityQueue는 가장 작은 것부터 줘요. Rust에서 최소 힙처럼 쓰려면 항목을std::cmp::Reverse로 감싸세요. VecDeque와 BinaryHeap을 참고하세요. -
정수 오버플로는 조용히 넘어가지 않아요. Java에서
Integer.MAX_VALUE + 1은 조용히 값이 한 바퀴 돌아요. Rust의 debug 빌드에서는 패닉이 나요.checked_add,wrapping_add,saturating_add로 의도를 밝히세요. 정수 오버플로를 참고하세요. -
String과&str, 그리고charAt이 없어요. Rust 문자열은 UTF-8이라서s[0]은 컴파일되지 않아요. 글자를 하나씩 보려면s.chars()를 쓰세요. 함수는 보통&str을 받고, 구조체는 보통String을 저장해요. 문자열을 참고하세요. -
메서드 오버로딩이 없어요. 매개변수만 다른
print메서드 두 개를 만들 수 없어요. 이름을 다르게 하거나(print,print_with_color) 제네릭 매개변수를 쓰세요. -
런타임 마법이 없어요. 클래스패스 스캔도,
@Autowired도, 메서드 주위에 트랜잭션을 덧붙이는 프록시도 없어요. 모든 건 코드에서 명시적으로 연결되고, 매크로는 컴파일할 때 실행돼요. 타이핑은 늘지만, 무슨 일이 일어나는지 언제나 눈으로 볼 수 있어요. -
null도 예외도 없어요. 처음에는 여기저기Option과Result를 쓰는 게 일이 늘어난 것처럼 느껴져요. 몇 주 지나면NullPointerException도, 예상치 못한 스택 트레이스도 한동안 못 봤다는 걸 알게 될 거예요. 배우는 동안.unwrap()을 써도 괜찮지만, 실제 코드에서 이건 Rust판 “그냥 죽게 둬”예요. -
컴파일 시간. 처음부터 빌드하면 모든 의존성을 소스에서 가져와 컴파일해서
javac보다 느려요. 증분 빌드는 훨씬 빠르고, 코드를 고치는 동안 가장 많이 실행하게 될 건cargo check(코드 생성을 건너뛰어요)예요. 반대쪽 끝에 좋은 소식도 있어요. JVM 워밍업 없이 프로그램이 바로 시작돼요. -
async에는 런타임이 필요해요. 가상 스레드는 없어요. 실행기(executor)가 돌려 주기 전까지
async fn은 아무 일도 하지 않아서, 웹 프로젝트는 tokio를 추가하고main에#[tokio::main]을 붙여요. async/await를 참고하세요.
Java 개발자를 위한 학습 경로
섹션 제목: “Java 개발자를 위한 학습 경로”이 사이트를 순서대로 읽을 필요는 없어요. 이 경로는 Java가 이미 가르쳐 준 건 건너뛰고, Rust가 다른 부분에 시간을 써요.
- Rust 시작하기와 Rust 도구와 개발 환경: Rust를 설치하고, 에디터를 고르고,
cargo new를 실행해요. - 변수와 데이터 타입: 가볍게 훑어보면서
let mut과 숫자 타입 위주로 익혀요. - 소유권, 대여, 슬라이스 순서로: 여러분에게 가장 중요한 페이지예요. 천천히 읽으세요.
- 문자열:
String과&str. - 구조체와 열거형, 그다음 match와 패턴 매칭.
- Option과 Result, 그다음 사용자 정의 에러와 anyhow와 thiserror.
- 트레이트, 제네릭, 자주 쓰는 트레이트, 트레이트 객체: 여러분의 인터페이스와 추상 클래스예요.
- 클로저와 반복자: 여러분의 람다와 스트림이에요.
- 벡터, HashMap, VecDeque와 BinaryHeap.
- 모듈, Cargo와 크레이트, 테스트.
- 라이프타임과 스마트 포인터.
- 동시성, 공유 상태, 채널, async/await.
- Serde로 JSON 다루기, 로깅, 데이터베이스, 그다음 웹 API 프로젝트를 만들어 봐요.
- 기존 Java 앱을 빠르게 하고 싶다면 FFI (C 호출하기)를 읽고, Rust가
extern "C"함수를 어떻게 공개하는지 보세요.
연습 문제
섹션 제목: “연습 문제”두 문제 모두 Java 코드를 주고, 그걸 Rust로 옮기는(포팅하는) 문제예요.
1. record와 스트림 파이프라인 옮기기
섹션 제목: “1. record와 스트림 파이프라인 옮기기”이 코드는 합격한 학생, 만점자가 있는지, 그리고 최고 점수를 출력해요. 힌트: 반복자에는 any(anyMatch와 같아요)와 max(OptionalInt처럼 Option을 반환해요)가 있어요. Collectors.joining 대신 Vec<&str>로 모은 다음 .join(", ")을 호출하세요.
record Student(String name, int score) {}
List<Student> students = List.of( new Student("Ada", 91), new Student("Brian", 42), new Student("Grace", 100), new Student("Ken", 67));
String passed = students.stream() .filter(s -> s.score() >= 50) .map(Student::name) .collect(Collectors.joining(", "));boolean perfect = students.stream().anyMatch(s -> s.score() == 100);int best = students.stream().mapToInt(Student::score).max().orElse(0);
System.out.println("passed: " + passed);System.out.println("perfect score: " + perfect);System.out.println("best: " + best);정답 보기
#[derive(Debug, Clone, PartialEq)]struct Student { name: String, score: u32,}
fn student(name: &str, score: u32) -> Student { Student { name: name.to_string(), score }}
fn main() { let students = vec![ student("Ada", 91), student("Brian", 42), student("Grace", 100), student("Ken", 67), ];
let passed: Vec<&str> = students .iter() .filter(|s| s.score >= 50) .map(|s| s.name.as_str()) .collect(); let perfect = students.iter().any(|s| s.score == 100); let best = students.iter().map(|s| s.score).max().unwrap_or(0);
println!("합격: {}", passed.join(", ")); println!("만점자 있음: {perfect}"); println!("최고 점수: {best}");}실행 결과:
합격: Ada, Grace, Ken만점자 있음: true최고 점수: 100derive 덕분에 Java record가 공짜로 주던 기능을 그대로 얻어요. 디버그 문자열({:?}), 복사(.clone()), 값 비교(==)요. student 도우미 함수가 record의 생성자 역할을 해요. Java 스트림과 달리 students.iter()는 몇 번이든 호출할 수 있어요. 호출할 때마다 같은 Vec 위에 새 반복자를 만들기 때문이에요.
2. 원인이 있는 검사 예외 옮기기
섹션 제목: “2. 원인이 있는 검사 예외 옮기기”이 메서드는 설정 파일에서 port=<number> 형식의 줄을 읽어요. 검사 예외 ConfigException을 던지고, Integer.parseInt에서 나온 NumberFormatException을 감싸요. read_port가 Result<u16, String>을 반환하도록 Rust로 옮겨 보세요. 힌트: line.split_once('=')는 Option<(&str, &str)>를 반환해요. .ok_or_else(...)로 Result로 바꾸세요. .parse::<u16>()는 Result를 반환하고, .map_err(...)가 그 에러를 여러분의 메시지로 바꿔 줘요. 예외 하나를 다른 예외로 감싸는 것과 똑같아요.
class ConfigException extends Exception { ConfigException(String message) { super(message); }}
static int readPort(String line) throws ConfigException { String[] parts = line.split("=", 2); if (parts.length != 2 || !parts[0].trim().equals("port")) throw new ConfigException("expected port=<number>, got '" + line + "'"); try { return Integer.parseInt(parts[1].trim()); } catch (NumberFormatException e) { throw new ConfigException("bad port number: " + parts[1].trim()); }}
for (String line : List.of("port = 8080", "port=eighty", "host=localhost")) { try { System.out.println("port is " + readPort(line)); } catch (ConfigException e) { System.out.println("config error: " + e.getMessage()); }}정답 보기
fn read_port(line: &str) -> Result<u16, String> { let expected = || format!("port=<숫자> 형식이어야 해요. 받은 값: '{line}'");
let (key, value) = line.split_once('=').ok_or_else(expected)?; if key.trim() != "port" { return Err(expected()); }
let value = value.trim(); value .parse::<u16>() .map_err(|_| format!("잘못된 포트 번호: {value}"))}
fn main() { for line in ["port = 8080", "port=eighty", "host=localhost"] { match read_port(line) { Ok(port) => println!("포트: {port}"), Err(e) => println!("설정 에러: {e}"), } }}실행 결과:
포트: 8080설정 에러: 잘못된 포트 번호: eighty설정 에러: port=<숫자> 형식이어야 해요. 받은 값: 'host=localhost'throws ConfigException 절은 Result 반환 타입이 됐고, parseInt를 감싼 try/catch는 map_err 하나가 됐고, throw는 Err(...)가 됐어요. 포트에 u16을 쓰면 작은 덤이 따라와요. 65535보다 큰 수는 parse가 알아서 거부하는데, Java의 int는 그렇게 해 주지 않아요. 작은 expected 클로저는 orElseThrow(() -> ...)의 supplier처럼, 필요할 때만 에러 메시지를 만들어요.
- Java의 많은 부분을 그대로 가져갈 수 있어요. 제네릭, 인터페이스(트레이트로), 람다(클로저로), 스트림 파이프라인(반복자로)까지요.
- 크게 달라지는 건 이거예요. 가비지 컬렉터 대신 소유권,
null대신Option, 예외 대신Result와?, 상속 대신 트레이트와 컴포지션. - 제네릭은 타입마다 컴파일돼요(단형화). 그래서 타입 소거도 없고, 숫자를 박싱하지도 않아요.
- 스레드 사이에서 공유하는 데이터는 (
Arc로 감싼)Mutex안에 살아요. 잠그는 걸 깜빡하면 경쟁 상태가 아니라 컴파일 에러가 나요. - Cargo가 Maven이나 Gradle을 대신해요. clippy와 rustfmt가 Checkstyle, SpotBugs, 포매터를 대신하고, 테스트는 기본으로 들어 있어요.
- Spring Boot 대신 axum이나 actix-web, JPA 대신 SeaORM이나 Diesel, JDBC와 jOOQ 대신 sqlx를 살펴보고, Jackson 대신 serde를 쓰세요.
이해도 점검
섹션 제목: “이해도 점검”이해도 점검
1. Java 추상 클래스에 추상 메서드 하나와, 그걸 호출하는 구체 메서드 하나가 있어요. Rust에서는 보통 무엇으로 바꿀까요?
2. Java의 PriorityQueue는 가장 작은 항목을 먼저 반환해요. Rust에서 BinaryHeap::pop은 무엇을 반환할까요?
3. Java에서는 new T()를 쓸 수 없는데, Rust 제네릭 함수는 왜 T::default()를 호출할 수 있을까요?
4. 평범한 Vec을 스레드 사이에서 공유하면서 Mutex 없이 항목을 추가하려고 해요. Rust에서는 어떻게 될까요?
점수: 0 / 4
더 알아보기
섹션 제목: “더 알아보기”- The Rust Programming Language 한국어판: 공식 책의 한국어 번역이에요. Java 개발자에게는 4장(소유권), 9장(에러 처리), 10장(제네릭과 트레이트), 16장(동시성)이 가장 중요해요.
- std::collections (영어):
Vec,VecDeque,HashMap,BTreeMap등 중에서 무엇을 고를지 알려 주는 표준 라이브러리의 공식 안내예요. - JEP 454: Foreign Function & Memory API (영어): JNI 코드를 쓰지 않고 Java에서 네이티브 라이브러리(Rust로 만든 것도 포함)를 호출하는 JDK 22의 기능이에요.
- docs.rs의 jni 크레이트 (영어): JNI의 Rust 바인딩이에요. 오래된 JDK에서 Rust를 호출하거나 Java를 다시 호출해야 할 때 써요.
- Are We Web Yet? (영어): Rust의 웹 프레임워크, 데이터베이스 라이브러리 등을 정리한 커뮤니티 사이트예요. 여러분의 Spring Boot 스택을 대신할 것을 찾을 때 좋아요.