Go to Rust: #10. 프로젝트 관리와 실전 패턴
이 글은 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가 필요한 경우#
- 원시 포인터 역참조
- unsafe 함수/메서드 호출
- 가변 정적 변수 접근
- unsafe 트레이트 구현
- 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 |
학습 다음 단계#
- 실습 프로젝트: CLI 도구, 웹 서버, 시스템 유틸리티
- 커뮤니티 참여: Rust 포럼, Discord, Reddit
- 심화 학습: The Rust Programming Language (The Book)
- 오픈소스 기여: 관심 있는 Rust 프로젝트에 기여
연습 문제#
-
프로젝트 설정: 워크스페이스로 라이브러리와 바이너리 크레이트를 포함하는 프로젝트를 설정하세요.
-
모듈 구조: 3개 이상의 모듈로 구성된 라이브러리를 만들고 적절한 가시성을 설정하세요.
-
테스트 작성: 유닛 테스트, 통합 테스트, 문서 테스트를 모두 포함하는 라이브러리를 작성하세요.
-
CLI 도구: clap을 사용해 파일 처리 CLI 도구를 만드세요.
-
Go 포팅: 간단한 Go 프로젝트를 Rust로 포팅하세요.
-
크로스 컴파일: 여러 플랫폼용 바이너리를 빌드하는 스크립트를 작성하세요.
마무리#
Go 개발자로서 Rust를 배우는 여정을 완료하셨습니다!
Rust는 처음에는 컴파일러와 싸우는 것처럼 느껴질 수 있지만, 일단 소유권 시스템에 익숙해지면 더 안전하고 효율적인 코드를 작성할 수 있습니다.
핵심 기억사항:
- 소유권은 Rust의 핵심이자 강점
- 컴파일러 에러 메시지를 친구로 삼으세요
- Go의 단순함과 Rust의 안전성은 서로 다른 트레이드오프
- 둘 다 훌륭한 언어 - 상황에 맞게 선택하세요
Happy Rusting! 🦀
Read other posts