Go to Rust: #1. 시작하기 - Rust 소개와 환경 설정
이 글은 Claude Opus 4.5 을 이용해 초안이 작성되었으며, 이후 퇴고를 거쳤습니다.
Go 개발자로서 새로운 언어를 배우려면 먼저 그 언어가 왜 존재하는지, 어떤 문제를 해결하려고 하는지 이해하는 것이 중요합니다. 이 섹션에서는 Rust의 설계 철학을 Go와 비교하며 이해하고, 개발 환경을 설정한 뒤 첫 프로젝트를 생성해봅니다.
1.1 Rust는 왜 배워야 하는가?#
Rust의 설계 철학#
Rust는 Mozilla Research에서 시작되어 2015년에 1.0이 릴리스된 시스템 프로그래밍 언어입니다. Rust의 핵심 목표는 세 가지입니다:
- 안전성(Safety): 메모리 안전성을 컴파일 타임에 보장
- 속도(Speed): C/C++에 필적하는 성능
- 동시성(Concurrency): 데이터 레이스 없는 동시성 프로그래밍
Go도 비슷한 시기(2009년)에 등장했지만, 설계 목표가 다릅니다:
| 측면 | Go | Rust |
|---|---|---|
| 주요 목표 | 단순성, 빠른 컴파일, 쉬운 동시성 | 안전성, 성능, 제로 비용 추상화 |
| 메모리 관리 | 가비지 컬렉션 (GC) | 소유권 시스템 (컴파일 타임) |
| 학습 곡선 | 완만함 | 가파름 (특히 소유권) |
| 런타임 | 런타임 포함 (GC, 스케줄러) | 최소 런타임 (거의 없음) |
| 추상화 수준 | 실용적 단순함 | 고수준 추상화 가능 |
Go vs Rust: 설계 목표의 차이#
Go의 철학: “Less is more”
- 언어 기능을 최소화하여 학습과 유지보수를 쉽게
- 암묵적 인터페이스 구현으로 유연성 제공
- 고루틴과 채널로 동시성을 쉽게
- 빠른 컴파일 속도 우선
Rust의 철학: “Zero-cost abstractions”
- 고수준 추상화를 사용해도 성능 손실 없음
- 컴파일러가 최대한 많은 오류를 잡아줌
- 명시적인 것을 선호 (암묵적 동작 최소화)
- 안전성을 위해 학습 비용 감수
// Go: 간결하고 읽기 쉬움
func sum(numbers []int) int {
total := 0
for _, n := range numbers {
total += n
}
return total
}
// Rust: 더 명시적이지만 더 많은 보장
fn sum(numbers: &[i32]) -> i32 {
numbers.iter().sum()
}
// 또는 Go 스타일로 작성할 수도 있음
fn sum_imperative(numbers: &[i32]) -> i32 {
let mut total = 0;
for n in numbers {
total += n;
}
total
}
Rust가 빛나는 영역#
1. 시스템 프로그래밍
- 운영체제, 디바이스 드라이버, 임베디드 시스템
- Linux 커널에 Rust 지원 추가됨
- Windows도 일부 컴포넌트를 Rust로 재작성
2. WebAssembly
- Rust는 WebAssembly의 1급 지원 언어
wasm-pack,wasm-bindgen등 풍부한 도구- 브라우저에서 네이티브 성능
3. 고성능 네트워크 서비스
- Discord: Go에서 Rust로 마이그레이션하여 지연 시간 개선
- Cloudflare: 엣지 컴퓨팅에 Rust 사용
- AWS: Firecracker (서버리스 VM) Rust로 개발
4. 암호화 및 보안
- 메모리 안전성이 보안에 직결
- 버퍼 오버플로우, use-after-free 등 원천 차단
5. CLI 도구
ripgrep(grep 대체): Go의 어떤 구현보다 빠름fd(find 대체),bat(cat 대체)- 단일 바이너리 배포 (Go와 동일한 장점)
언제 Go를, 언제 Rust를 선택할 것인가#
Go를 선택해야 할 때:
- 빠른 개발과 프로토타이핑이 중요할 때
- 팀의 학습 곡선을 최소화해야 할 때
- 마이크로서비스, API 서버 개발
- DevOps 도구 개발
- GC 일시 정지가 문제되지 않는 경우
Rust를 선택해야 할 때:
- 예측 가능한 지연 시간이 필요할 때 (GC 없음)
- 메모리 사용량을 정밀하게 제어해야 할 때
- C/C++ 수준의 성능이 필요할 때
- 시스템 레벨 프로그래밍
- WebAssembly 타겟
- 장기 실행 프로세스에서 메모리 관리가 중요할 때
1.2 개발 환경 설정#
rustup을 통한 설치#
Rust는 rustup이라는 도구체인 관리자를 사용합니다. Go의 버전 관리가 수동적인 것과 달리, rustup은 여러 버전을 쉽게 관리할 수 있습니다.
# macOS / Linux
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# Windows는 rustup-init.exe 다운로드
# https://rustup.rs 방문
# 설치 확인
rustc --version
cargo --version
rustup 주요 명령어:
# 최신 버전으로 업데이트
rustup update
# 설치된 도구체인 확인
rustup show
# nightly 버전 설치 (실험적 기능 사용 시)
rustup install nightly
# 기본 도구체인 변경
rustup default stable
# 프로젝트별 도구체인 설정
rustup override set nightly
Cargo: Rust의 빌드 시스템이자 패키지 매니저#
Cargo는 Go의 go 명령어와 go mod를 합친 것과 비슷합니다. 빌드, 테스트, 의존성 관리를 모두 담당합니다.
Go vs Cargo 명령어 비교:
| 작업 | Go | Cargo |
|---|---|---|
| 새 프로젝트 생성 | go mod init |
cargo new |
| 빌드 | go build |
cargo build |
| 실행 | go run . |
cargo run |
| 테스트 | go test ./... |
cargo test |
| 의존성 추가 | go get |
cargo add (cargo-edit) |
| 포맷팅 | go fmt |
cargo fmt |
| 린트 | go vet / golangci-lint |
cargo clippy |
| 문서 생성 | go doc |
cargo doc |
| 릴리스 빌드 | go build -ldflags="-s -w" |
cargo build --release |
IDE 설정#
VS Code + rust-analyzer (권장):
# VS Code 확장 설치
# 1. rust-analyzer (공식 권장)
# 2. Even Better TOML (Cargo.toml 편집용)
# 3. Error Lens (인라인 에러 표시)
# 4. CodeLLDB (디버깅용)
settings.json 권장 설정:
{
"rust-analyzer.checkOnSave.command": "clippy",
"rust-analyzer.inlayHints.typeHints.enable": true,
"rust-analyzer.inlayHints.parameterHints.enable": true,
"[rust]": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "rust-lang.rust-analyzer"
}
}
JetBrains RustRover:
- JetBrains의 Rust 전용 IDE
- IntelliJ 플랫폼 기반
- GoLand 사용자라면 익숙한 환경
유용한 Cargo 확장 도구#
# 필수 도구들
rustup component add clippy # 린터
rustup component add rustfmt # 포매터
# 유용한 cargo 확장
cargo install cargo-edit # cargo add/rm/upgrade
cargo install cargo-watch # 파일 변경 감지 자동 빌드
cargo install cargo-expand # 매크로 확장 보기
cargo install cargo-audit # 보안 취약점 검사
cargo install cargo-outdated # 의존성 업데이트 확인
cargo-watch 활용:
# Go의 air나 reflex처럼 파일 변경 시 자동 실행
cargo watch -x run
# 테스트 자동 실행
cargo watch -x test
# 여러 명령 조합
cargo watch -x check -x test -x run
1.3 첫 번째 Rust 프로젝트#
Hello, World!#
# 새 프로젝트 생성
cargo new hello_rust
cd hello_rust
# 프로젝트 구조
# hello_rust/
# ├── Cargo.toml
# └── src/
# └── main.rs
Go 프로젝트와 비교:
# Go 프로젝트 # Rust 프로젝트
myproject/ myproject/
├── go.mod ├── Cargo.toml
├── go.sum ├── Cargo.lock
├── main.go └── src/
└── pkg/ └── main.rs
└── util/
└── util.go
Cargo.toml (Go의 go.mod에 해당):
[package]
name = "hello_rust"
version = "0.1.0"
edition = "2021"
[dependencies]
# 여기에 의존성 추가
src/main.rs:
fn main() {
println!("Hello, world!");
}
Go와 비교해봅시다:
// Go
package main
import "fmt"
func main() {
fmt.Println("Hello, world!")
}
// Rust
fn main() {
println!("Hello, world!");
}
차이점:
- Rust는
package선언 불필요 (파일 구조로 결정) println!은 함수가 아닌 매크로 (!가 매크로 표시)import대신use(필요할 때)- 세미콜론 필수 (대부분의 경우)
빌드와 실행#
# 개발 빌드 (최적화 없음, 빠른 컴파일)
cargo build
# 실행 파일: target/debug/hello_rust
# 실행
cargo run
# 릴리스 빌드 (최적화, 느린 컴파일)
cargo build --release
# 실행 파일: target/release/hello_rust
# 문법 검사만 (빌드보다 빠름)
cargo check
Go와 빌드 속도 비교:
- Go: 매우 빠른 컴파일 (설계 목표)
- Rust: 상대적으로 느린 컴파일 (많은 검사와 최적화)
하지만 cargo check를 활용하면 개발 중 피드백 루프를 빠르게 유지할 수 있습니다.
라이브러리 프로젝트#
Go에서 main 패키지와 라이브러리 패키지가 다르듯, Rust도 바이너리와 라이브러리를 구분합니다.
# 라이브러리 프로젝트 생성
cargo new mylib --lib
# 구조
# mylib/
# ├── Cargo.toml
# └── src/
# └── lib.rs # main.rs 대신 lib.rs
바이너리 + 라이브러리 혼합:
myproject/
├── Cargo.toml
└── src/
├── main.rs # 바이너리 진입점
└── lib.rs # 라이브러리 루트
1.4 REPL과 빠른 실험 환경#
Rust Playground#
play.rust-lang.org에서 브라우저로 Rust를 실험할 수 있습니다.
기능:
- Stable/Beta/Nightly 버전 선택
- 코드 공유 링크 생성
- ASM/LLVM IR/MIR 출력 확인
- Clippy/Miri 실행
cargo run –example#
프로젝트에 예제 코드를 포함할 수 있습니다:
myproject/
├── Cargo.toml
├── src/
│ └── lib.rs
└── examples/
├── basic.rs
└── advanced.rs
# 특정 예제 실행
cargo run --example basic
# 모든 예제 나열
cargo run --example
evcxr: Rust REPL#
Python이나 Node.js처럼 대화형 환경을 원한다면:
# 설치
cargo install evcxr_repl
# 실행
evcxr
>> let x = 42;
>> x * 2
84
>> :type x
i32
Jupyter Notebook 커널도 있습니다:
cargo install evcxr_jupyter
evcxr_jupyter --install
1.5 Go 개발자가 알아야 할 Rust 생태계#
crates.io vs pkg.go.dev#
| 측면 | Go (pkg.go.dev) | Rust (crates.io) |
|---|---|---|
| 중앙 저장소 | 없음 (분산) | 있음 (중앙 집중) |
| 버전 관리 | Git 태그 기반 | SemVer 엄격 적용 |
| 문서 | pkg.go.dev | docs.rs (자동 생성) |
| 이름 충돌 | 경로로 구분 | 고유 이름 필요 |
필수 크레이트 소개#
1. serde - 직렬화/역직렬화
Go의 encoding/json과 비슷하지만 더 유연합니다.
[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug)]
struct User {
name: String,
age: u32,
#[serde(rename = "emailAddress")] // Go의 `json:"emailAddress"`와 동일
email: String,
}
fn main() {
let user = User {
name: "Alice".to_string(),
age: 30,
email: "alice@example.com".to_string(),
};
// 직렬화
let json = serde_json::to_string(&user).unwrap();
println!("{}", json);
// 역직렬화
let parsed: User = serde_json::from_str(&json).unwrap();
println!("{:?}", parsed);
}
2. tokio - 비동기 런타임
Go의 고루틴과 달리, Rust는 비동기 런타임이 별도입니다.
[dependencies]
tokio = { version = "1", features = ["full"] }
#[tokio::main]
async fn main() {
let result = fetch_data().await;
println!("{}", result);
}
async fn fetch_data() -> String {
// 비동기 작업
"data".to_string()
}
3. anyhow & thiserror - 에러 처리
[dependencies]
anyhow = "1.0" # 애플리케이션용
thiserror = "1.0" # 라이브러리용
// anyhow: Go의 fmt.Errorf와 비슷
use anyhow::{Context, Result};
fn read_config() -> Result<String> {
std::fs::read_to_string("config.toml")
.context("Failed to read config file")?; // 컨텍스트 추가
Ok("config".to_string())
}
// thiserror: 커스텀 에러 타입 정의
use thiserror::Error;
#[derive(Error, Debug)]
enum MyError {
#[error("Invalid input: {0}")]
InvalidInput(String),
#[error("IO error")]
Io(#[from] std::io::Error),
}
4. clap - CLI 파싱
Go의 flag 패키지나 cobra와 비슷합니다.
[dependencies]
clap = { version = "4", features = ["derive"] }
use clap::Parser;
#[derive(Parser, Debug)]
#[command(name = "myapp")]
#[command(about = "A sample CLI application")]
struct Args {
/// Name of the user
#[arg(short, long)]
name: String,
/// Verbose mode
#[arg(short, long, default_value_t = false)]
verbose: bool,
}
fn main() {
let args = Args::parse();
println!("Hello, {}!", args.name);
}
5. tracing - 로깅과 추적
Go의 log 패키지보다 구조화된 로깅을 제공합니다.
[dependencies]
tracing = "0.1"
tracing-subscriber = "0.3"
use tracing::{info, warn, error, instrument};
#[instrument] // 함수 진입/종료 자동 로깅
fn process_request(user_id: u32) {
info!(user_id, "Processing request");
// ...
warn!("Something might be wrong");
}
fn main() {
// 구독자 초기화
tracing_subscriber::fmt::init();
process_request(42);
}
문서화 문화#
Rust 커뮤니티는 문서화를 매우 중시합니다:
- docs.rs: 모든 크레이트의 문서가 자동 생성
- 예제 코드: 문서 내 예제가 테스트로 실행됨
- README 중심이 아님: API 문서가 주요 문서
/// 두 숫자를 더합니다.
///
/// # Examples
///
/// ```
/// let result = mylib::add(2, 3);
/// assert_eq!(result, 5);
/// ```
///
/// # Panics
///
/// 오버플로우 시 패닉합니다 (debug 모드에서).
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
# 문서 생성 및 브라우저에서 열기
cargo doc --open
1.6 요약: Go에서 Rust로 전환 시 마인드셋#
받아들여야 할 변화#
-
컴파일러와 싸우기: Rust 컴파일러는 까다롭지만, 통과하면 런타임 에러가 크게 줄어듭니다.
-
소유권 사고방식: “이 데이터를 누가 소유하는가?“를 항상 생각해야 합니다.
-
명시성: Go보다 더 많은 것을 명시적으로 작성합니다.
-
느린 컴파일: 대신 런타임에 발견할 버그를 컴파일 타임에 잡습니다.
Go 경험이 도움되는 부분#
- 정적 타입: 타입 시스템에 익숙함
- 단일 바이너리: 배포 방식 유사
- 패키지 관리: 개념적으로 유사
- 에러 처리: 명시적 에러 처리 문화
다음 단계#
다음 섹션에서는 Rust의 기본 문법과 타입 시스템을 Go와 비교하며 학습합니다. 변수 선언, 기본 타입, 함수, 제어 흐름 등 익숙한 개념들이 Rust에서는 어떻게 다른지 알아봅니다.
연습 문제#
-
환경 설정: rustup을 설치하고
cargo new hello_rust로 프로젝트를 생성해보세요. -
빌드 비교: 같은 Hello World 프로그램을 Go와 Rust로 작성하고, 바이너리 크기와 빌드 시간을 비교해보세요.
-
Playground: play.rust-lang.org에서 다음 코드를 실행해보세요:
fn main() { let numbers = vec![1, 2, 3, 4, 5]; let sum: i32 = numbers.iter().sum(); println!("Sum: {}", sum); } -
문서 탐색: docs.rs에서
serde크레이트의 문서를 탐색해보세요.