Rust와 Java는 둘 다 "메모리 안전"을 말하지만 방법이 정반대다. Java는 실행 중에 가비지 컬렉터가 안 쓰는 객체를 찾아 치우고, Rust는 컴파일할 때 소유권 규칙으로 언제 메모리를 해제할지 정해 둔다. 이 차이가 메모리 사용량, 지연의 예측 가능성, 시작 시간, 개발 속도까지 갈라놓는다. 이 글은 두 언어의 메모리 관리 방식과 실행 모델을 비교하고, 어떤 서비스에 무엇이 맞는지 판단 기준을 정리한 것이다. 성능 수치는 워크로드마다 크게 달라 일반 숫자를 들지 않고, 차이가 생기는 이유와 재는 방법을 중심으로 쓴다.

한눈에 비교
| 기준 | Rust | Java |
|---|---|---|
| 메모리 해제 | 컴파일 시 정해진 시점(스코프 끝)에 즉시 | 실행 중 GC가 판단해 나중에 |
| 안전성 보장 | 소유권·빌림 검사기(컴파일 오류) | 런타임 검사, GC, 예외 |
| 실행 방식 | 기계어로 미리 컴파일(AOT) | 바이트코드 → 실행 중 JIT 컴파일 |
| 시작·예열 | 즉시 최고 성능 | JIT 예열 시간 필요 |
| 메모리 사용 | 데이터 크기에 가깝다 | 객체 헤더, GC 여유 공간, JVM 기본 사용량이 더해진다 |
| 지연 예측성 | GC 멈춤 없음 | GC에 따라 멈춤(ZGC로 크게 줄일 수 있음) |
| 동시성 안전 | 데이터 경쟁을 컴파일 시 차단(Send, Sync) |
메모리 모델 + 개발자 규율, 가상 스레드 |
| 생산성·생태계 | 학습 곡선이 가파르다 | 엔터프라이즈 생태계와 인력이 풍부하다 |
Rust: 소유권으로 해제 시점을 정한다
Rust의 값에는 주인이 하나 있다. 주인 변수가 스코프를 벗어나면 그 자리에서 메모리가 해제된다(Drop).
fn handle(req: Request) -> Response {
let buf = Vec::with_capacity(4096); // 힙 할당
let user = load_user(&req); // &: 소유권을 넘기지 않고 빌려 준다
render(&user, buf) // buf의 소유권이 render로 이동
} // user는 여기서 즉시 해제, buf는 render가 책임
- 빌림 규칙: 여러 곳에서 읽기 전용으로 빌리거나, 한 곳에서만 쓰기로 빌릴 수 있다. 둘을 동시에 할 수 없다. 이 규칙이 해제된 메모리 사용과 데이터 경쟁을 컴파일 단계에서 막는다.
- 공유가 필요할 때: 참조 카운트(
Rc, 스레드 간에는Arc)를 쓴다. 이 경우만 실행 중 비용이 든다. 순환 참조는 자동으로 풀리지 않아Weak로 끊어야 한다. - 비용 없는 추상화: 제네릭은 타입별 코드로 펼쳐지고(단형화), 반복자 체인도 손으로 쓴 반복문 수준으로 컴파일된다.
Java: GC가 치우고, JIT가 다듬는다
Java는 객체를 마음껏 만들고, 도달할 수 없게 된 객체를 GC가 나중에 회수한다.
- 세대 가설: 대부분의 객체는 금방 죽는다. 젊은 영역을 자주, 오래된 영역을 드물게 회수해 비용을 줄인다.
- 할당이 싸다: 스레드별 할당 버퍼(TLAB)에서 포인터를 밀기만 하면 되므로 할당 자체는 매우 빠르다. 비용은 나중에 GC로 낸다.
- JIT 최적화: 실행 중 많이 쓰이는 코드를 프로파일 정보를 보며 컴파일한다. 인라이닝, 탈출 분석(메서드 밖으로 나가지 않는 객체는 힙 할당을 없애기도 한다) 같은 최적화가 실행 데이터를 근거로 이뤄진다. 그래서 충분히 예열된 Java는 많은 작업에서 네이티브 코드와 차이가 작다.
- 비용: 객체마다 헤더가 붙고, GC가 일할 여유 공간이 필요하며, JVM 자체가 쓰는 메모리(메타스페이스, JIT 코드 캐시, 스레드 스택)가 더해진다.
데이터 배치가 성능을 가른다
같은 "점 100만 개 목록"도 메모리에 놓이는 모양이 다르다.

- Java의
ArrayList<Point>는 참조의 배열이고, 각Point객체는 힙 여기저기에 헤더와 함께 놓인다. 순회할 때마다 포인터를 따라가며 캐시 미스가 생긴다. - Rust의
Vec<Point>는 값이 연속으로 붙어 있다. 순회가 캐시 친화적이고 메모리도 데이터 크기에 가깝다. - Java에서도 원시 타입 배열(
double[] xs, ys)로 나눠 담으면 연속 배치를 얻을 수 있다. 값 객체를 연속으로 담으려는 Project Valhalla가 진행 중이다.
수치 계산, 대량 데이터 처리, 게임·미디어처럼 데이터를 촘촘히 훑는 일에서 Rust가 유리한 큰 이유가 이것이다.
실행 성능을 비교할 때 주의할 점
- 예열을 고려한다. Java는 첫 몇 초~몇 분의 성능과 안정 상태 성능이 다르다. 짧게 실행되는 CLI·서버리스 함수는 Rust(또는 GraalVM 네이티브 이미지)가 유리하다.
- 지연 분포를 본다. 평균 처리량이 비슷해도 P99는 GC 영향이 드러난다.
- 도구를 맞춘다. Java는 JMH, Rust는 criterion처럼 예열·반복·통계를 처리하는 벤치마크 도구를 쓴다. 단순 반복문 시간 재기는 JIT 최적화나 컴파일러 제거 때문에 틀린 결론을 낸다.
- 같은 알고리즘·같은 자료구조로 비교한다. 언어 차이보다 구현 차이가 더 크게 나오는 경우가 많다.
- 메모리를 함께 잰다. 같은 처리량에서 필요한 메모리(컨테이너 크기)는 비용으로 직결된다.
무엇을 고를까
| 상황 | 추천 |
|---|---|
| 업무 규칙이 많은 엔터프라이즈 시스템, 팀이 Java에 익숙 | Java |
| 지연 예측성이 중요한 게이트웨이·프록시·실시간 처리 | Rust 검토 |
| 메모리가 비용인 대량 인스턴스, 엣지·임베디드 | Rust |
| 짧게 실행되는 CLI·함수 | Rust 또는 GraalVM 네이티브 이미지 |
| 기존 Java 시스템의 핫스팟 하나 | 먼저 Java 안에서 개선(자료구조, 할당), 그래도 부족하면 그 부분만 분리 |
전면 교체보다 경계가 분명한 구성 요소(파서, 압축, 암호화, 고성능 프록시)를 Rust로 만들고 Java와 프로세스·네트워크로 연결하는 방식이 위험이 작다.
자주 하는 실수
- 예열 없이 Java를 잰다. JIT 전의 인터프리터 성능을 비교하게 된다.
- 평균 처리량만 비교한다. GC 멈춤과 메모리 사용량 차이가 빠진다.
- Rust를 쓰면 무조건 빠르다고 본다.
clone()남발과 불필요한Arc<Mutex<…>>는 Rust도 느리게 만든다. - Java의 객체 배치를 신경 쓰지 않는다. 데이터 중심 코드에서 객체 배열은 캐시 미스를 부른다.
- 팀 역량을 빼고 고른다. 빌림 검사기와 싸우는 기간도 일정이다.
점검표
- 비교 목적(처리량, 지연, 메모리, 시작 시간)을 먼저 정했다
- Java는 JMH로 예열을 포함해, Rust는 criterion으로 쟀다
- 같은 알고리즘·자료구조로 구현했다
- P99 지연과 메모리 사용량을 함께 기록했다
- 데이터 중심 코드의 메모리 배치를 확인했다
- 교체 범위를 경계가 분명한 구성 요소로 제한했다
- 팀의 학습 비용과 운영 생태계를 비용에 넣었다
Rust는 메모리를 컴파일러가 관리해 예측 가능한 성능과 적은 메모리를 주고, Java는 GC와 JIT가 관리해 생산성과 안정 상태 성능을 준다. 어느 쪽이 빠른가보다 우리 서비스가 지연, 메모리, 개발 속도 중 무엇에 돈을 내는지가 선택을 정한다.
댓글 0개
아직 댓글이 없습니다. 첫 댓글을 남겨보세요.