이 글은 Claude Opus 4.5 을 이용해 초안이 작성되었으며, 이후 퇴고를 거쳤습니다.

이 마지막 섹션에서는 실제 Rust 프로젝트를 구성하고 관리하는 방법을 학습합니다. Cargo 심화, 모듈 시스템, 테스트, 그리고 Go 코드를 Rust로 포팅할 때 유용한 패턴들을 다룹니다.


10.1 Cargo 심화#

Cargo.toml 상세 설정#

[package]
name = "myapp"
version = "0.1.0"
edition = "2021"
authors = ["Your Name <you@example.com>"]
description = "A sample Rust application"
license = "MIT"
repository = "https://github.com/you/myapp"
keywords = ["cli", "tool"]
categories = ["command-line-utilities"]

[dependencies]
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1", features = ["full"] }

[dev-dependencies]
criterion = "0.5"  # 벤치마크용

[build-dependencies]
cc = "1.0"  # 빌드 스크립트용

[features]
default = ["json"]
json = ["serde_json"]
full = ["json", "yaml"]

[[bin]]
name = "myapp"
path = "src/main.rs"

[[bin]]
name = "mytool"
path = "src/bin/mytool.rs"

[profile.release]
opt-level = 3
lto = true

의존성 관리#

[dependencies]
# 버전 지정
serde = "1.0"           # ^1.0.0 (1.x.x)
serde = "=1.0.104"      # 정확히 1.0.104
serde = ">=1.0, <2.0"   # 범위

# Git 의존성
mylib = { git = "https://github.com/user/mylib" }
mylib = { git = "https://github.com/user/mylib", branch = "dev" }
mylib = { git = "https://github.com/user/mylib", tag = "v1.0.0" }
mylib = { git = "https://github.com/user/mylib", rev = "abc123" }

# 로컬 경로
mylib = { path = "../mylib" }

# 선택적 의존성
serde_json = { version = "1.0", optional = true }

# 피처 플래그
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }

Go modules와 비교#

# Go: go.mod
module github.com/user/myapp

go 1.21

require (
    github.com/gin-gonic/gin v1.9.0
    github.com/lib/pq v1.10.0
)
# Rust: Cargo.toml
[package]
name = "myapp"
version = "0.1.0"
edition = "2021"

[dependencies]
axum = "0.7"
sqlx = { version = "0.7", features = ["postgres"] }
측면 Go Cargo
의존성 파일 go.mod Cargo.toml
락 파일 go.sum Cargo.lock
저장소 분산 (GitHub 등) 중앙 (crates.io)
버전 관리 Git 태그 SemVer

피처 플래그#

[features]
default = ["std"]
std = []
alloc = []
json = ["serde", "serde_json"]
full = ["json", "yaml", "toml"]
// 조건부 컴파일
#[cfg(feature = "json")]
pub mod json {
    pub fn parse_json(s: &str) -> Value { /* ... */ }
}

#[cfg(feature = "std")]
use std::collections::HashMap;

#[cfg(not(feature = "std"))]
use alloc::collections::BTreeMap as HashMap;

10.2 모듈 시스템#

파일 시스템과 모듈 구조#

myapp/
├── Cargo.toml
└── src/
    ├── main.rs          # 바이너리 루트
    ├── lib.rs           # 라이브러리 루트
    ├── config.rs        # config 모듈
    ├── utils/
    │   ├── mod.rs       # utils 모듈
    │   ├── helpers.rs   # utils::helpers
    │   └── format.rs    # utils::format
    └── handlers/
        ├── mod.rs
        ├── user.rs
        └── post.rs

lib.rs:

pub mod config;
pub mod utils;
pub mod handlers;

pub use config::Config;

utils/mod.rs:

pub mod helpers;
pub mod format;

pub use helpers::*;

가시성 제어#

// pub: 어디서든 접근 가능
pub fn public_fn() {}

// pub(crate): 같은 크레이트 내에서만
pub(crate) fn crate_fn() {}

// pub(super): 부모 모듈에서만
pub(super) fn parent_fn() {}

// pub(in path): 특정 경로에서만
pub(in crate::utils) fn utils_fn() {}

// 기본: private
fn private_fn() {}

use와 경로#

// 절대 경로
use crate::utils::helpers;
use crate::config::Config;

// 상대 경로
use self::helpers;      // 같은 모듈
use super::config;      // 부모 모듈

// 외부 크레이트
use std::collections::HashMap;
use serde::{Serialize, Deserialize};

// 별칭
use std::collections::HashMap as Map;
use std::io::Result as IoResult;

// glob import (주의해서 사용)
use std::collections::*;

// 중첩
use std::{
    collections::{HashMap, HashSet},
    io::{self, Read, Write},
};

Go 패키지 시스템과 비교#

// Go: 디렉토리 = 패키지
// myapp/utils/helpers.go
package utils

func Helper() {}

// 사용
import "myapp/utils"
utils.Helper()
// Rust: 명시적 모듈 선언
// src/utils/helpers.rs
pub fn helper() {}

// src/utils/mod.rs
pub mod helpers;

// src/lib.rs
pub mod utils;

// 사용
use myapp::utils::helpers;
helpers::helper();

워크스페이스#

여러 크레이트를 하나의 저장소에서 관리:

my_workspace/
├── Cargo.toml           # 워크스페이스 루트
├── crates/
│   ├── mylib/
│   │   ├── Cargo.toml
│   │   └── src/lib.rs
│   ├── myapp/
│   │   ├── Cargo.toml
│   │   └── src/main.rs
│   └── mytool/
│       ├── Cargo.toml
│       └── src/main.rs

워크스페이스 Cargo.toml:

[workspace]
members = [
    "crates/mylib",
    "crates/myapp",
    "crates/mytool",
]

[workspace.dependencies]
serde = "1.0"
tokio = "1"

멤버 Cargo.toml:

[package]
name = "myapp"
version = "0.1.0"

[dependencies]
mylib = { path = "../mylib" }
serde.workspace = true
tokio.workspace = true

10.3 테스트#

유닛 테스트#

// src/lib.rs
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

#[cfg(test)]
mod tests {
    use super::*;
    
    #[test]
    fn test_add() {
        assert_eq!(add(2, 3), 5);
    }
    
    #[test]
    fn test_add_negative() {
        assert_eq!(add(-1, 1), 0);
    }
    
    #[test]
    #[should_panic(expected = "overflow")]
    fn test_overflow() {
        // 패닉이 발생해야 통과
        panic!("overflow");
    }
    
    #[test]
    #[ignore]  // cargo test -- --ignored로 실행
    fn expensive_test() {
        // 시간이 오래 걸리는 테스트
    }
}

통합 테스트#

myapp/
├── src/
│   └── lib.rs
└── tests/
    ├── integration_test.rs
    └── common/
        └── mod.rs

tests/integration_test.rs:

use myapp;

#[test]
fn test_full_workflow() {
    let result = myapp::process("input");
    assert!(result.is_ok());
}

테이블 드리븐 테스트#

Go 스타일의 테이블 드리븐 테스트:

// Go
func TestAdd(t *testing.T) {
    tests := []struct {
        a, b, want int
    }{
        {1, 2, 3},
        {0, 0, 0},
        {-1, 1, 0},
    }
    for _, tt := range tests {
        got := Add(tt.a, tt.b)
        if got != tt.want {
            t.Errorf("Add(%d, %d) = %d, want %d", tt.a, tt.b, got, tt.want)
        }
    }
}
// Rust
#[cfg(test)]
mod tests {
    use super::*;
    
    #[test]
    fn test_add() {
        let cases = vec![
            (1, 2, 3),
            (0, 0, 0),
            (-1, 1, 0),
        ];
        
        for (a, b, expected) in cases {
            assert_eq!(add(a, b), expected, "add({}, {}) failed", a, b);
        }
    }
}

// 또는 매크로 사용
macro_rules! test_cases {
    ($($name:ident: $value:expr,)*) => {
        $(
            #[test]
            fn $name() {
                let (a, b, expected) = $value;
                assert_eq!(add(a, b), expected);
            }
        )*
    }
}

test_cases! {
    add_positive: (1, 2, 3),
    add_zero: (0, 0, 0),
    add_negative: (-1, 1, 0),
}

Go 테스트와의 비교#

측면 Go Rust
테스트 파일 *_test.go #[cfg(test)] 모듈
테스트 함수 func TestXxx(t *testing.T) #[test] fn xxx()
실행 go test cargo test
벤치마크 func BenchmarkXxx(b *testing.B) criterion 또는 nightly
커버리지 go test -cover cargo tarpaulin

10.4 빌드와 릴리스#

빌드 프로파일#

# Cargo.toml

[profile.dev]
opt-level = 0
debug = true

[profile.release]
opt-level = 3
lto = true
codegen-units = 1
panic = "abort"
strip = true

[profile.bench]
opt-level = 3
debug = true

크로스 컴파일#

# 타겟 추가
rustup target add x86_64-unknown-linux-musl
rustup target add aarch64-apple-darwin
rustup target add x86_64-pc-windows-gnu

# 빌드
cargo build --release --target x86_64-unknown-linux-musl

.cargo/config.toml:

[target.x86_64-unknown-linux-musl]
linker = "x86_64-linux-musl-gcc"

[target.aarch64-unknown-linux-gnu]
linker = "aarch64-linux-gnu-gcc"

Go와 비교:

# Go: 매우 간단
GOOS=linux GOARCH=amd64 go build
GOOS=darwin GOARCH=arm64 go build
GOOS=windows GOARCH=amd64 go build

바이너리 크기 최적화#

[profile.release]
opt-level = "z"      # 크기 최적화
lto = true           # Link Time Optimization
codegen-units = 1    # 단일 코드 생성 단위
panic = "abort"      # 패닉 시 즉시 종료
strip = true         # 심볼 제거
# 추가 스트리핑
strip target/release/myapp

# UPX 압축 (선택사항)
upx --best target/release/myapp

10.5 unsafe Rust#

unsafe가 필요한 경우#

  1. 원시 포인터 역참조
  2. unsafe 함수/메서드 호출
  3. 가변 정적 변수 접근
  4. unsafe 트레이트 구현
  5. FFI (외부 함수 인터페이스)
// 원시 포인터
let mut num = 5;
let r1 = &num as *const i32;
let r2 = &mut num as *mut i32;

unsafe {
    println!("r1: {}", *r1);
    *r2 = 10;
}

// unsafe 함수
unsafe fn dangerous() {
    // 위험한 작업
}

unsafe {
    dangerous();
}

FFI와 C 연동#

// C 함수 호출
extern "C" {
    fn abs(input: i32) -> i32;
    fn strlen(s: *const std::os::raw::c_char) -> usize;
}

fn main() {
    unsafe {
        println!("abs(-5) = {}", abs(-5));
    }
}

// Rust 함수를 C에서 호출 가능하게
#[no_mangle]
pub extern "C" fn rust_function(x: i32) -> i32 {
    x * 2
}

Go의 cgo와 비교:

// Go
/*
#include <stdlib.h>
*/
import "C"

func main() {
    result := C.abs(-5)
    fmt.Println(result)
}

10.6 실전 설계 패턴#

Builder 패턴#

#[derive(Debug)]
struct Server {
    host: String,
    port: u16,
    timeout: u64,
    tls: bool,
}

#[derive(Default)]
struct ServerBuilder {
    host: String,
    port: u16,
    timeout: u64,
    tls: bool,
}

impl ServerBuilder {
    fn new() -> Self {
        Self {
            host: String::from("localhost"),
            port: 8080,
            timeout: 30,
            tls: false,
        }
    }
    
    fn host(mut self, host: &str) -> Self {
        self.host = host.to_string();
        self
    }
    
    fn port(mut self, port: u16) -> Self {
        self.port = port;
        self
    }
    
    fn timeout(mut self, timeout: u64) -> Self {
        self.timeout = timeout;
        self
    }
    
    fn tls(mut self, tls: bool) -> Self {
        self.tls = tls;
        self
    }
    
    fn build(self) -> Server {
        Server {
            host: self.host,
            port: self.port,
            timeout: self.timeout,
            tls: self.tls,
        }
    }
}

fn main() {
    let server = ServerBuilder::new()
        .host("example.com")
        .port(443)
        .tls(true)
        .build();
}

Type State 패턴#

컴파일 타임에 상태 전이를 검증:

// 상태 타입
struct Draft;
struct Published;

struct Post<State> {
    title: String,
    content: String,
    _state: std::marker::PhantomData<State>,
}

impl Post<Draft> {
    fn new(title: &str) -> Self {
        Post {
            title: title.to_string(),
            content: String::new(),
            _state: std::marker::PhantomData,
        }
    }
    
    fn add_content(&mut self, content: &str) {
        self.content.push_str(content);
    }
    
    fn publish(self) -> Post<Published> {
        Post {
            title: self.title,
            content: self.content,
            _state: std::marker::PhantomData,
        }
    }
}

impl Post<Published> {
    fn print(&self) {
        println!("{}: {}", self.title, self.content);
    }
}

fn main() {
    let mut post = Post::<Draft>::new("Hello");
    post.add_content("World");
    
    // post.print();  // 컴파일 에러! Draft에는 print 없음
    
    let published = post.publish();
    published.print();  // OK
    
    // published.add_content("!");  // 컴파일 에러! Published에는 add_content 없음
}

Newtype 패턴#

// 타입 안전성 강화
struct UserId(u64);
struct OrderId(u64);

fn get_user(id: UserId) -> User { /* ... */ }
fn get_order(id: OrderId) -> Order { /* ... */ }

// 컴파일 에러 방지
let user_id = UserId(42);
let order_id = OrderId(42);
get_user(user_id);     // OK
// get_user(order_id);  // 컴파일 에러!

// 외부 타입에 트레이트 구현
struct Wrapper(Vec<String>);

impl std::fmt::Display for Wrapper {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        write!(f, "[{}]", self.0.join(", "))
    }
}

10.7 Go 코드를 Rust로 포팅하기#

구조체 변환#

// Go
type User struct {
    ID       int64
    Name     string
    Email    string
    IsActive bool
}

func NewUser(name, email string) *User {
    return &User{
        Name:     name,
        Email:    email,
        IsActive: true,
    }
}

func (u *User) Deactivate() {
    u.IsActive = false
}
// Rust
struct User {
    id: i64,
    name: String,
    email: String,
    is_active: bool,
}

impl User {
    fn new(name: &str, email: &str) -> Self {
        User {
            id: 0,
            name: name.to_string(),
            email: email.to_string(),
            is_active: true,
        }
    }
    
    fn deactivate(&mut self) {
        self.is_active = false;
    }
}

인터페이스 → 트레이트#

// Go
type Reader interface {
    Read(p []byte) (n int, err error)
}

type Writer interface {
    Write(p []byte) (n int, err error)
}

type ReadWriter interface {
    Reader
    Writer
}
// Rust
trait Reader {
    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize>;
}

trait Writer {
    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize>;
}

// 트레이트 조합
trait ReadWriter: Reader + Writer {}

// 또는 blanket implementation
impl<T: Reader + Writer> ReadWriter for T {}

에러 처리#

// Go
func readFile(path string) ([]byte, error) {
    data, err := os.ReadFile(path)
    if err != nil {
        return nil, fmt.Errorf("failed to read %s: %w", path, err)
    }
    return data, nil
}
// Rust
fn read_file(path: &str) -> Result<Vec<u8>, std::io::Error> {
    std::fs::read(path)
}

// 또는 컨텍스트 추가
use anyhow::{Context, Result};

fn read_file(path: &str) -> Result<Vec<u8>> {
    std::fs::read(path)
        .with_context(|| format!("failed to read {}", path))
}

동시성#

// Go
func process(items []string) []Result {
    results := make([]Result, len(items))
    var wg sync.WaitGroup
    
    for i, item := range items {
        wg.Add(1)
        go func(i int, item string) {
            defer wg.Done()
            results[i] = processItem(item)
        }(i, item)
    }
    
    wg.Wait()
    return results
}
// Rust with rayon
use rayon::prelude::*;

fn process(items: &[String]) -> Vec<Result> {
    items
        .par_iter()
        .map(|item| process_item(item))
        .collect()
}

// 또는 tokio
async fn process(items: Vec<String>) -> Vec<Result> {
    let futures: Vec<_> = items
        .into_iter()
        .map(|item| tokio::spawn(async move { process_item(&item) }))
        .collect();
    
    let results = futures::future::join_all(futures).await;
    results.into_iter().map(|r| r.unwrap()).collect()
}

10.8 생태계와 크레이트 추천#

웹 프레임워크#

크레이트 특징 Go 대응
axum 타입 안전, 모듈러 chi, echo
actix-web 고성능, 액터 모델 fiber
rocket 매크로 기반, 사용 쉬움 gin

CLI#

[dependencies]
clap = { version = "4", features = ["derive"] }
use clap::Parser;

#[derive(Parser)]
#[command(name = "myapp")]
#[command(about = "A CLI application")]
struct Cli {
    /// Input file
    #[arg(short, long)]
    input: String,
    
    /// Verbose mode
    #[arg(short, long, default_value_t = false)]
    verbose: bool,
}

fn main() {
    let cli = Cli::parse();
    println!("Input: {}", cli.input);
}

직렬화#

[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
serde_yaml = "0.9"
toml = "0.8"

로깅#

[dependencies]
tracing = "0.1"
tracing-subscriber = "0.3"
use tracing::{info, warn, error, instrument};

#[instrument]
fn process(id: u32) {
    info!("Processing {}", id);
}

fn main() {
    tracing_subscriber::fmt::init();
    process(42);
}

데이터베이스#

크레이트 특징
sqlx 컴파일 타임 쿼리 검증, 비동기
diesel ORM, 타입 안전
sea-orm ActiveRecord 스타일
// sqlx 예제
use sqlx::postgres::PgPoolOptions;

#[tokio::main]
async fn main() -> Result<(), sqlx::Error> {
    let pool = PgPoolOptions::new()
        .max_connections(5)
        .connect("postgres://user:pass@localhost/db").await?;
    
    let row: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM users")
        .fetch_one(&pool)
        .await?;
    
    println!("User count: {}", row.0);
    Ok(())
}

10.9 요약#

프로젝트 구조 비교#

# Go                          # Rust
myapp/                        myapp/
├── go.mod                    ├── Cargo.toml
├── go.sum                    ├── Cargo.lock
├── main.go                   ├── src/
├── internal/                 │   ├── main.rs
│   └── pkg/                  │   ├── lib.rs
├── pkg/                      │   └── modules/
└── cmd/                      ├── tests/
    └── myapp/                └── benches/

개발 워크플로우#

작업 Go Rust
새 프로젝트 go mod init cargo new
의존성 추가 go get cargo add
빌드 go build cargo build
테스트 go test ./... cargo test
린트 golangci-lint cargo clippy
포맷 go fmt cargo fmt
문서 go doc cargo doc

학습 다음 단계#

  1. 실습 프로젝트: CLI 도구, 웹 서버, 시스템 유틸리티
  2. 커뮤니티 참여: Rust 포럼, Discord, Reddit
  3. 심화 학습: The Rust Programming Language (The Book)
  4. 오픈소스 기여: 관심 있는 Rust 프로젝트에 기여

연습 문제#

  1. 프로젝트 설정: 워크스페이스로 라이브러리와 바이너리 크레이트를 포함하는 프로젝트를 설정하세요.

  2. 모듈 구조: 3개 이상의 모듈로 구성된 라이브러리를 만들고 적절한 가시성을 설정하세요.

  3. 테스트 작성: 유닛 테스트, 통합 테스트, 문서 테스트를 모두 포함하는 라이브러리를 작성하세요.

  4. CLI 도구: clap을 사용해 파일 처리 CLI 도구를 만드세요.

  5. Go 포팅: 간단한 Go 프로젝트를 Rust로 포팅하세요.

  6. 크로스 컴파일: 여러 플랫폼용 바이너리를 빌드하는 스크립트를 작성하세요.


마무리#

Go 개발자로서 Rust를 배우는 여정을 완료하셨습니다!

Rust는 처음에는 컴파일러와 싸우는 것처럼 느껴질 수 있지만, 일단 소유권 시스템에 익숙해지면 더 안전하고 효율적인 코드를 작성할 수 있습니다.

핵심 기억사항:

  • 소유권은 Rust의 핵심이자 강점
  • 컴파일러 에러 메시지를 친구로 삼으세요
  • Go의 단순함과 Rust의 안전성은 서로 다른 트레이드오프
  • 둘 다 훌륭한 언어 - 상황에 맞게 선택하세요

Happy Rusting! 🦀