Funtion, Method and Associated Functions

Function은 특정 구조체나 인스턴스와 관계없이 독립적으로 정의되는데, Method는 구조체(또는 enum) 인스턴스의 상태를 읽거나 변경하는 동작을 구현하며, Assosiated Functions는 특정 타입과 연결되어 있지만 해당 타입의 인스턴스와는 독립적인 함수를 의미합니다.

1. 함수와 메소드의 차이점

가. 함수 (Function)

  • 특정 구조체나 인스턴스와 관계없이 독립적으로 정의됩니다.
  • fn 키워드를 사용해 선언하며, 어디서든 호출할 수 있습니다.
  • 호출 시 add(3, 5)처럼 함수 이름만 사용합니다.
fn add(a: i32, b: i32) -> i32 {
    a + b
}

나. 메소드 (Method)

  • 구조체(struct), 열거형(enum) 등 특정 타입의 impl 블록 안에서 정의됩니다.
  • 첫 번째 파라미터로 반드시 self, &self, &mut self 중 하나를 받습니다.
  • 인스턴스를 통해서만 호출할 수 있으며, rect.area()처럼 점(.) 연산자를 사용합니다.
struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
    }
}

이처럼 Rust에서는 함수는 독립성, 메소드는 인스턴스와의 종속성이 가장 큰 차이입니다.

2. 함수와 메소드의 차이점

가. 함수(Function)

(1) 장점

  • 독립적이어서 다양한 곳에서 재사용하기 쉽고, 유닛 테스트가 간편합니다.
  • 간단한 연산이나 유틸리티 작업에 적합합니다.
  • 이름만으로 호출하므로 사용이 직관적입니다.

(2)

  • 구조체 등 데이터 타입과 직접적으로 연관된 행동을 묶어 표현하기 어렵습니다.
  • 많은 함수가 생기면 네임스페이스 충돌이나 코드의 구조적 복잡도가 증가할 수 있습니다.
  • 데이터 캡슐화와 추상화에서 약점이 있습니다.

나. 메소드(Method)

(1)장점

  • 데이터와 행위의 결합(캡슐화): 구조체의 로직과 동작을 impl 블록에 모아, 객체지향적 코드를 짤 수 있습니다.
  • 가독성: 인스턴스.메소드(…) 형태로 표현해 자연스럽고 읽기 쉬운 코드를 만듭니다.
  • 확장성: 타입별로만 동작을 추가하거나 오버라이드하는 데 용이합니다.

(2) 단점

  • 인스턴스가 있어야만 호출 가능합니다. 즉, 독립적으로 동작할 수 없는 경우가 많습니다.
  • 객체의 상태 변경이 필요하다면 &mut self 등으로 가변 참조를 써야 하므로, 소유권·빌림 규칙에 주의를 기울여야 합니다.
  • 단순 기능(예: 수학 연산 등)에는 불필요하게 복잡합니다.
구분장점단점
함수독립성, 재사용성, 간결함, 테스트 용이성데이터 캡슐화 불가, 네임스페이스 충돌 위험
메소드데이터와 로직 결합, 캡슐화, 가독성, 타입별 동작 확장 용이인스턴스 필요, 소유권·참조 규칙 신경써야, 불필요한 복잡성 가능

Rust에서는 함수는 독립적이고 범용적인 동작에, 메소드는 인스턴스와 연관된 행동에 사용하는 것이 일반적입니다.

2. 메소드와 연관 함수의 차이점

가. 메소드

  • 메서드는 반드시 첫 번째 인자로 self, &self 또는 &mut self를 받습니다.
  • 구조체 인스턴스를 통해 점(.) 연산자로 호출합니다.
struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
    }
}

fn main() {
    let rect = Rectangle {
        width: 30,
        height: 50,
    };  

    // 메소드 호출
    println!("사각형의 면적: {}", rect.area());
}

나. 연관 함수

  • 연관 함수는 self를 인자로 받지 않습니다.
  • 타입 이름을 통해 이중 콜론(::) 연산자로 호출합니다.
  • 주로 생성자 역할(예: new)로 사용되고, 구조체 등의 유틸리티 함수로도 사용됩니다.

(예제 1 – 생성자)

struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn new(width: u32, height: u32) -> Rectangle {
        Rectangle { width, height }
    }
}

fn main() {
    // 연관 함수 사용
    let rect = Rectangle::new(30, 50);
    println!("사각형의 면적: {}", rect.area());    
}

fn new의 반환 타입이 Rectange 구조체입니다.

(예제 2 – 유틸리티 함수)

struct Circle {
    radius: f64,
}

impl Circle {
    // 연관 함수 (유틸리티 함수)
    fn area(radius: f64) -> f64 {
        std::f64::consts::PI * radius * radius
    }
}

fn main() {
    // 연관 함수 호출 (유틸리티 함수)
    let circle_area = Circle::area(5.0);
    println!("Circle area: {}", circle_area);
}

메소드와 달리 Circle::area(5.0) 처럼 ::을 붙여 호출합니다.

위 코드를 실행하면
Circle area: 78.53981633974483라고 처리 결과가 제대로 표시되지만

“구조체 Circle안의 radius 필드가 사용되지 않았다”는 경고가 표시됩니다.


다시 말해 연관 함수의 인수 radius는 이름은 같지만 구조체의 radius가 아닙니다. 또한 Circle이란 인스턴스를 생성하지 않고도 원의 면적을 구했습니다.

3. 메소드와 연관 함수의 장단점

가. 메서드

(1) 사용 목적

  • 구조체(혹은 enum) 인스턴스의 **상태(필드)**를 읽거나 변경하는 동작을 구현합니다.
  • 객체의 행동을 정의하고, 객체의 데이터와 직접 상호작용합니다.

(2) 장점

  • 캡슐화: 인스턴스의 데이터를 안전하게 다루며, 객체의 상태를 직접 관리할 수 있습니다.
  • 가독성:rect.area()처럼 객체의 행동을 자연스럽게 표현할 수 있습니다.
  • 유지보수성: 객체의 동작이 구조체 내부에 모여 있어 코드 관리가 용이합니다.

나. 연관 함수

(1) 사용 목적

  • 구조체의 생성자 역할(예: new)이나, 인스턴스와 무관한 동작(예: 유틸리티 함수)을 구현합니다.
  • 인스턴스 없이 타입 자체에 대해 동작하는 기능을 제공합니다.

(2) 장점

  • 유연성: 인스턴스가 없어도 타입 이름으로 직접 호출할 수 있습니다(Rectangle::new() 등).
  • 명확성: 생성자나 특정 타입 관련 기능을 명확하게 분리할 수 있습니다.
  • 재사용성: 인스턴스와 무관한 기능을 여러 곳에서 활용할 수 있습니다.

다. 요약

  • 메서드는 객체의 상태와 밀접하게 연관된 동작을 구현하며, 객체지향적 설계와 데이터 캡슐화에 강점을 가집니다.
  • 연관 함수는 객체 생성이나 타입 자체와 관련된 기능을 제공하며, 인스턴스가 필요 없는 동작을 분리해 코드의 명확성과 재사용성을 높입니다.

Rust의 Slice와 컬렉션 참조

Rust의 slice(슬라이스)는 배열, 벡터, 문자열 등 메모리상에 연속적으로 저장된 데이터의 일부 구간을 참조하는 타입이고, 컬렉션 참조는 컬렉션(배열 포함) 전체에 대한 참조입니다. 그러나, 모두 소유권을 갖지 않고 데이터를 참조하는 것은 같지만, 그 목적과 내부 구조, 사용 방식에 중요한 차이가 있습니다.

1. 슬라이스(Slice)

가.기본 개념

  • 슬라이스는 배열, 벡터, 문자열 등 메모리상에 연속적으로 저장된 데이터의 일부 구간을 참조하는 타입으로, &[T] 또는 &str(문자열 슬라이스)와 같이 참조 형태로 표현된다.
  • 슬라이스는 원본 데이터의 소유권을 이동시키지 않으며, 원본 데이터가 유효할 때만 사용할 수 있다.
  • 슬라이스는 연속된 메모리 블록의 시작 주소와 길이 정보를 함께 저장하는 fat pointer(두 개의 값: 포인터 + 길이)이다.
  • 슬라이스는 항상 연속적인 데이터만 참조할 수 있습니다.

나.문법과 사용법

  • 슬라이스는 [start..end] 범위 문법을 사용합니다.
    • start는 포함, end는 포함하지 않습니다(즉, 반 열린 구간).
      let a = [1, 2, 3, 4, 5];
      let slice = &a[1..4]; // [2, 3, 4]
  • start나 end를 생략하면 처음이나 끝을 의미합니다.
    &a[..3] → 처음부터 3번째 전까지
    &a[2..] → 2번째부터 끝까지(끝 포함)
    &a[..] → 전체

다. 문자열 슬라이스

  • 문자열 슬라이스는 String이나 문자열 리터럴의 일부를 참조하며 타입은 &str 입니다.
  • 문자열 일부 참조
    let s = String::from(“hello world”);
    let hello = &s[0..5]; // “hello”
    let world = &s[6..11]; // “world”
  • 문자열 리터럴 
    let s = “hello, world”;     // &’static str
    let part: &str = &s[0..5]; // “hello” 부분만 참조
    “hello” 자체도 &str 타입의 슬라이스이므로, 문자열 리터럴의 일부도 참조할 수 있습니다.

라. 특징과 주의점

  • 슬라이스는 참조이므로, 원본 데이터가 유효해야 한다.
    원본 데이터가 변경(예: String의 clear 등)되면 기존 슬라이스는 무효화되어 컴파일 에러가 발생합다.
  • 슬라이스는 소유권과 라이프타임 개념을 이해하는 데 중요한 역할을 합니다. -> 아래 ‘3. 슬라이스와 라이프타임’에서 설명
  • 슬라이스는 함수의 파라미터로 자주 사용됩니다.
fn main() {
    let s = "hello, world"; 
    println!("First two elements: {:?}", first_two(s));
}

fn first_two(slice: &str) -> &str {
    if slice.len() < 2 {
        panic!("Slice must have at least two elements");
    }
    &slice[0..2]
}

위 코드에서 s는 문자열 리터럴인 문자열 슬라이스이므로 함수로 넘길 때는 &를 안붙여도 되지만(붙여도 됨), 함수에서 받을 때는 타입을 &str으로 지정하고, 반환도 &slice[0..2]이므로&str 타입으로 지정해야 합니다.

실행 결과는 First two elements: “he”입니다.

2. 컬렉션 참조

가. 개념

  • 컬렉션 참조는 컬렉션 전체에 대한 참조입니다. 예를 들어, &Vec, &String, &[T; N] 등입니다. 여기서 &[T; N]은 배열 전체에 대한 참조입니다. 엄격하게 말하면 배열은 컬렉션은 아니지만 “배열 전체를 참조한다”는 측면에서 컬렉션 참조에 포함해서 설명합니다.
  • 이 참조는 해당 컬렉션 타입의 모든 메서드와 속성을 사용할 수 있게 해줍니다.
  • 컬렉션 참조는 그 자체가 소유한 데이터 전체를 가리키며, 슬라이스처럼 부분만을 가리키지는 않습니다.
  • 예시:
    let v = vec![1, 2, 3];
    let r = &v; // Vec 전체에 대한 참조

벡터 컬렉션 참조의 간단한 예시는 다음과 같습니다.

fn main() {
let v = vec![100, 32, 57]; // Vec<i32> 생성
// 벡터에 대한 불변 참조를 사용하여 각 요소 출력
for i in &v {
println!("{}", i);
}
}
  • 여기서 &v는 Vec 타입의 벡터에 대한 불변 참조(&Vec)입니다.
  • for i in &v 구문은 벡터의 각 요소에 대한 참조(&i32)를 순회하며 안전하게 접근합니다.
  • 벡터 자체를 소유하지않고 참조만 하므로, 함수 인자나 반복문에서 자주 활용됩니다.

나. 예제

fn print_array(arr: &[i32; 3]) {
println!("{:?}", arr);
}

fn main() {
let data = [10, 20, 30];
print_array(&data); // &[i32; 3] 타입의 참조 전달
}
  • 위 예제에서 print_array 함수는 크기가 3인 i32 배열에 대한 참조(&[i32; 3])를 인자로 받습니다.
  • &data는 [i32; 3] 타입 배열 전체에 대한 참조입니다.

다. 슬라이스와 주요 차이점 비교

구분슬라이스
(&[T], &str)
컬렉션 참조 (&Vec<T>, &String)
대상컬렉션의 일부(연속 구간)컬렉션 전체
내부 구조포인터 + 길이 (fat pointer)포인터 (컬렉션 구조체 전체)
메서드 사용슬라이스 관련 메서드만 사용 가능컬렉션의 모든 메서드 사용 가능(벡터의 len(), push() 등
용도부분 참조, 함수 인자 등전체 참조, 컬렉션의 메서드 활용
동적 크기길이가 런타임에 결정됨컬렉션 타입에 따라 다름
예시&arr[1..3], &vec[..], &s[2..]&vec, &arr, &String

&[T; N]은 배열 전체를 참조하고, &[T]는 슬라이스로, 배열의 일부 구간(혹은 전체)을 참조할 수 있습니다.
예를 들어, &arr[1..3]은 &[T] 타입(슬라이스)이지만, &arr은 &[T; N] 타입(컬렉션 참조)입니다. 이처럼 &[T; N]은 정확히 N개 원소를 가진 배열 전체에 대한 참조입니다.

또한 &str과 &String은 문자열 슬라이스와 문자열 참조로 다르지만, &String이 &str으로 자동 변환이 가능하기 때문에 함수 인자로는 &str이 더 범용적이고 권장됩니다.

3. 슬라이스와 라이프타임

가. 개념

Rust에서 라이프타임(lifetime)은 참조가 유효한 기간을 명확하게 지정하는 개념으로, 참조의 유효성에 직접적인 영향을 미칩니다. 라이프타임이 중요한 이유와 그 영향은 다음과같습니다.

  • 댕글링 참조 방지
    라이프타임의 가장 큰 목적은 댕글링 참조(dangling reference)를 방지하는 것입니다. 댕글링 참조란, 이미 스코프를 벗어나 소멸된 데이터를 참조하는 상황을 의미합니다. Rust는 각 참조자의 라이프타임을 추적하여, 참조가 원본 데이터보다 오래 살아남을 수 없도록 컴파일 타임에 강제합니다.
  • 안전한 메모리 접근 보장
    라이프타임을 통해 참조자가 항상 유효한 메모리만 가리키도록 보장합니다. 예를 들어, 아래와 같은 코드는 컴파일되지 않습니다.

    let r; // r의 라이프타임 시작
    {
    let x = 5; // x의 라이프타임
    r = &x; // r이 x를 참조
    } // x의 라이프타임 종료, r은 더 이상 유효하지 않음
    println!(“{}”, r); // 컴파일 에러!
    Rust는 위 코드에서 r이 x보다 오래 살아남으려 하므로 컴파일 에러를 발생시킵니다.
  • 함수와 구조체에서의 명확한 라이프타임 관계
    여러 참조가 얽히는 함수나 구조체에서는, 각각의 참조가 얼마나 오래 유효해야 하는지 명확히 지정해야 합니다. 라이프타임 파라미터를 명시함으로써, 함수가 반환하는 참조가 입력 참조자 중 어느 것과 연관되어 있는지 컴파일러가 알 수 있습니다.
  • 암묵적 추론과 명시적 지정
    대부분의 경우 Rust는 라이프타임을 자동으로 추론하지만, 여러 참조가 얽히거나 복잡한 관계가 있을 때는 명시적으로 라이프타임을 지정해야 합니다. 이를 통해 참조 유효성에 대한 컴파일러의 보장이 더욱 강력해집니다.

나. 예제 1

Rust에서 슬라이스의 라이프타임이 중요한 이유를 보여주는 대표적인 예제는, 두 개의 문자열 슬라이스 중 더 긴 쪽을 반환하는 함수입니다. 이 예제를 통해 슬라이스의 라이프타임을 명확히 지정하지 않으면 컴파일 에러가 발생하고, 올바르게 지정하면 안전하게 참조를 반환할 수 있음을 알 수 있습니다.

fn main() {
let string1 = String::from("abcd");
let string2 = "xyz";
let result = longest(string1.as_str(), string2);
println!("The longest string is {}", result);
}

// 라이프타임 명시가 반드시 필요!
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
  • 이 함수 시그니처에서 ‘a라는 라이프타임 파라미터를 명시했습니다.
  • 이 뜻은, x와 y 모두 최소한 ‘a만큼 살아 있어야 하며, 반환되는 슬라이스도 ‘a만큼 살아 있어야 한다는 의미입니다.
  • 실제로 함수가 호출될 때, Rust는 x와 y의 라이프타임 중 더 짧은 쪽에 맞춰 반환값의 라이프타임을 제한합니다. 따라서, ‘a와 ‘b를 사용해서
    fn longest<‘a, ‘b>(x: &’a str, y: &’b str) -> &’a str
    라고 하면 x가 반환될 수도 있고 y가 반환될 수도 있기 때문에 에러가 발생합니다.
  • 또한 라이프타임을 명시하지 않아도, Rust는 반환되는 참조가 입력 참조자 중 어느 쪽과 연관되어 있는지 알 수 없어 컴파일 오류가 발생합니다. 따라서, 라이프타임을 ‘a로 통일한 것입니다.

이처럼, 슬라이스의 라이프타임을 명확히 지정하지 않으면 댕글링 참조가 생길 수 있고, Rust는 이를 컴파일 타임에 방지합니다.
따라서 라이프타임 명시는 슬라이스가 안전하게 사용될 수 있는 핵심적인 장치입니다.

위 코드에서 as_str()은 String 타입을 &str 타입으로 변환하는 메소드입니다.

다. 예제2

fn main() {
let result;
{
let string1 = String::from("abc");
let string2 = String::from("xyz");
result = longest(string1.as_str(), string2.as_str());
// string1, string2는 여기서 소멸
}
println!("The longest string is {}", result); // 컴파일 에러!
}
  • 위 코드는 result가 내부 스코프의 데이터인 string1 또는 string2의 슬라이스를 참조하게 되므로, 컴파일 에러를 발생시켜 댕글링 참조를 차단합니다.

Rust의 Array와 Vector 비교

Rust에서 array와 vector는 모두 여러 개의 값을 순차적으로 저장하는 자료구조입니다. 하지만 두 타입은 메모리 관리, 크기, 사용 목적 등에서 중요한 차이점이 있습니다. 이 글에서는 각 자료구조의 특징, 사용법, 예제, 그리고 언제 어떤 것을 선택해야 하는지에 대해 자세히 알아보겠습니다.

1. Array(배열)

1.1. 기본 개념

  • 고정 크기: 배열은 선언 시 크기가 고정되며, 이후 변경할 수 없습니다.
  • 동일 타입: 배열의 모든 원소는 같은 타입이어야 합니다.
  • 스택 메모리: 배열은 스택에 저장되어 빠른 접근이 가능합니다.

1.2. 배열 선언과 사용 예제

fn main() {
let arr: [i32; 5] = [1, 2, 3, 4, 5];
println!("첫 번째 원소: {}", arr[0]);
println!("배열의 길이: {}", arr.len());

// 배열 반복
for elem in arr.iter() {
println!("{}", elem);
}
}
  • [i32; 5]는 5개의 i32 타입 값을 가지는 배열을 의미합니다.
  • 배열의 크기는 타입의 일부이므로, arr은 항상 5개의 요소만 가질 수 있습니다.
  • arr.iter()로 반복(소유권 이전이 안되며, arr.iter()를 &arr로 바꿔도 됨)해서 요소를 elem에 할당한 후 그 값을 화면에 표시합니다.

    전체적인 출력 결과는 아래와 같습니다.

1.3. 배열의 주요 특징

  • 정적 크기: 크기가 컴파일 타임에 결정됩니다.
  • 빠른 접근: 인덱스를 통한 접근이 가능합니다.
  • 복사: 크기가 작을 경우, 배열 전체가 복사될 수 있습니다.

1.4. 배열의 한계

  • 크기를 동적으로 변경할 수 없습니다.
  • 대용량 데이터에는 적합하지 않을 수 있습니다.

2. Vector(벡터)

2.1. 기본 개념

  • 동적 크기: 벡터는 런타임에 크기를 자유롭게 변경할 수 있습니다.
  • 동일 타입: 모든 원소는 동일한 타입이어야 합니다.
  • 힙 메모리: 벡터는 힙에 저장되어 대용량 데이터 처리에 적합합니다.

2.2. 벡터 선언과 사용 예제

fn main() {
let mut vec: Vec<i32> = Vec::new();
vec.push(10);
vec.push(20);
vec.push(30);

println!("두 번째 원소: {}", vec[1]);
println!("벡터의 길이: {}", vec.len());

// 벡터 반복
for elem in &vec {
println!("{}", elem);
}

// 벡터에서 값 제거
vec.pop();
println!("마지막 원소 제거 후: {:?}", vec);
}
  • Vec::new()로 빈 벡터를 생성하면 type을 추론할 수 없으므로 Vec 다음에 <i32>가 반드시 있어야 합니다.
  • push로 값을 추가하고, pop으로 마지막 값을 제거합니다.
  • 벡터는 mut로 선언해야 원소 추가/삭제가 가능합니다.
  • vec 요소를 소유권 이전없이 참조로 하나씩 꺼내서 elem에 넣은 다음 출력합니다. 배열과 마찬가지로 &vec을 vec.iter()로 바꿔도 됩니다.

    전체적인 출력은 아래와 같습니다.
    두 번째 원소: 20(인덱스가 0부터 시작하기 때문에 1이 두번째 원소가 됩니다)
    벡터의 길이: 3
    10
    20
    30
    마지막 원소 제거 후: [10, 20]

2.3. 벡터의 주요 특징

  • 가변 크기: 필요에 따라 자동으로 크기가 늘어남.
  • 유연성: 다양한 상황에 맞게 사용할 수 있음.
  • 힙 저장: 대용량 데이터에도 적합.

2.4. 벡터의 한계

  • 배열에 비해 약간의 오버헤드가 발생할 수 있음.
  • 힙에 저장되므로 스택보다 접근 속도가 느릴 수 있음.

3. Array vs Vector 비교

특징Array(배열)Vector(벡터)
크기고정동적
선언 위치스택
선언 방식[T; N]Vec<T>
값 추가/제거불가가능 (push, pop 등)
반복.iter().iter()
사용 예시소규모, 고정 데이터대규모, 가변 데이터

4. 실전 예제: 배열과 벡터 변환

4.1. 배열을 벡터로 변환

fn main() {
let arr = [1, 2, 3];
let vec = arr.to_vec();
println!("{:?}", vec); // [1, 2, 3]
}

4.2. 벡터를 배열로 변환

  • 벡터의 길이가 고정되어 있고, 크기를 알고 있을 때만 가능합니다.
fn main() {
let vec = vec![4, 5, 6];
let arr: [i32; 3] = vec.try_into().unwrap();
println!("{:?}", arr); // [4, 5, 6]
}

5. 벡터의 다양한 메서드

fn main() {
let mut v = vec![1, 2, 3, 4, 5];

// 값 삽입
v.insert(2, 99); // 2번 인덱스에 99 삽입

// 여러 개의 값 추가
v.extend([6, 7, 8, 9]);

// 값 삭제
v.remove(1); // 1번 인덱스 값 제거

// 슬라이스
let slice = &v[1..3];
println!("{:?}", slice);

// 반복자 메서드
let doubled: Vec<i32> = v.iter().map(|x| x * 2).collect();
println!("{:?}", doubled);
}
  • v.insert(2, 99)
    => 2번 인덱스에 99를 추가하니 벡터 v는 [1, 2, 99, 3, 4, 5]가 됩니다.
  • v.extend([6, 7, 8, 9]);
    => push는 맨 뒤에 값 하나를 추가하는데, extend를 한 번에 여러 개의 값을 추가할 수 있습니다. 따라서, v는 [1, 2, 99, 3, 4, 5, 6, 7, 8, 9]가 됩니다.
  • v.remove(1);
    => 1번 인덱스인 2를 제거하므로 v는 [1, 99, 3, 4, 5, 6, 7, 8, 9]가 됩니다.
  • let slice = &v[1..3];
    println!(“{:?}”, slice);
    => v 벡터에서 1번 인덱스부터 3미만인 2번 인덱스까지 참조 형식으로 가져오는 slice는 [99, 3]이 됩니다.
    Vector는 일반 포맷인 {}로는 출력이 안되므로 디버그 포맷인 {:?}으로 출력해야 합니다.
  • let doubled: Vec<i32> = v.iter().map(|x| x * 2).collect();
    println!(“{:?}”, doubled);
    => v 벡터의 요소 들에 2를 곱한 후 새로운 벡터로 변환한 후 doubled에 저장합니다. doubled의 타입도 i32타입의 Vector이므로 디버그 포맷으로 출력하면
    [2, 198, 6, 8, 10, 12, 14, 16, 18]가 출력됩니다.

6. 성능 차이의 실제 사례

6.1. 반복문에서의 성능

fn main() {
let arr = [1; 1000000];
let vec = vec![1; 1000000];

let mut sum = 0;
for i in 0..arr.len() {
sum += arr[i];
}

let mut sum2 = 0;
for i in 0..vec.len() {
sum2 += vec[i];
}
}
  • 실행 시간: 배열이 벡터보다 약간 더 빠른 경우가 많습니다.
  • 이유: 배열은 스택에 연속적으로 저장되어 CPU 캐시 효율이 높고, 컴파일러가 최적화를 더 적극적으로 적용할 수 있습니다.

위와 같이 배열의 개수를 1백만개로 숫자가 1인데도 array의 경우 overflow가 발생하므로 1십만으로 바꾸는데, 시간을 체크하는 부분을 추가하고, 천단위 쉼표를 추가하도록 아래와 같이 수정한 후

[Cargo.toml] -num-format 크레이트를 사용하기 위해 필요

[dependencies]
num-format = "0.4"

[main.rs]

use std::time::Instant;
use num_format::{Locale, ToFormattedString};

fn main() {
let arr = [1; 100_000];
let vec = vec![1; 100_000];

// 배열 합계 시간 측정
let start = Instant::now();
let mut sum = 0;
for i in 0..arr.len() {
sum += arr[i];
}
let duration = start.elapsed();
println!("Array sum: {}, elapsed: {:?}", sum.to_formatted_string(&Locale::ko), duration);


// 벡터 합계 시간 측정
let start = Instant::now();
let mut sum2 = 0;
for i in 0..vec.len() {
sum2 += vec[i];
}
let duration = start.elapsed();
println!("Vector sum: {}, elapsed: {:?}", sum2.to_formatted_string(&Locale::ko), duration);
}

실행하면 결과는 아래와 같습니다.

Array sum: 100,000, elapsed: 806.2µs
Vector sum: 100,000, elapsed: 2.3802ms

1㎳가 1000㎲이므로, Vector가 약 3배정도 오래 걸립니다.

[프로그램 설명]

  • Rust의 표준 라이브러리에서 제공하는 std::time::Instant을 사용해 각 합계 연산의 소요 시간을 측정합니다.
  • Instant::now()로 현재 시각을 기록하고, 반복문이 끝난 후 elapsed()로 소요 시간을 구합니다.
  • for i in 0..arr.len()라고 0부터 배열의 길이전까지 i를 반복하면 sum에 arr[i]를 더하도록 했는데, for i in arr.iter()라고 하고, sum += i;이라고 해도 되는데, iter()를 이용한 것이 훨씬 빠릅니다. 특히 Vector 속도가 많이 빨라졌습니다.
    Array sum: 100,000, elapsed: 728.6µs
    Vector sum: 100,000, elapsed: 875.7µs
    ※ 그런데 매번 속도가 다르기 때문에 위 수치가 절대적인 것은 아닙니다. 어느 때는 Vector가 빠른 경우도 있습니다.
  • sum 또는 sum2 다음에 num_format의 ToFormattedString(&Locale::ko)를 추가해서 숫자에 천단위마다 쉼표를 추가합니다.

6.2. 크기 변경 및 데이터 추가

  • 배열은 크기가 고정되어 있어, 데이터 추가/삭제가 불가능합니다.
  • 벡터는 push, pop, extend 등으로 동적으로 크기를 조절할 수 있지만, 이 과정에서 메모리 재할당이 발생할 수 있습니다. 대량의 데이터를 추가할 때는 재할당 오버헤드가 성능 저하 요인이 됩니다.

6.3. 벤치마크 및 실제 사용 조언

  • 고정 크기, 빠른 반복/접근이 필요하다면 배열이 유리합니다.
  • 크기가 가변적이거나, 데이터 추가/삭제가 빈번하다면 벡터가 적합합니다.
  • 대용량 데이터 처리에서 벡터는 힙 할당 및 재할당 비용이 있으므로, 성능이 민감한 경우 벡터의 용량을 미리 예약(with_capacity)하는 것이 좋습니다.

※with_capacity란?

  • Vec::with_capacity는 Rust의 벡터(Vec)를 생성할 때 초기 용량(capacity) 을 미리 지정하는 메서드입니다.
  • with_capacity(n)은 최소 n개의 요소를 저장할 수 있는 공간을 미리 할당한 빈 벡터를 생성합니다.
  • 이렇게 하면, 벡터에 요소를 추가할 때마다 메모리를 재할당하는 비용을 줄일 수 있어 성능이 향상됩니다.
  • 사용 예시
fn main() {
let mut vec = Vec::with_capacity(10);
assert_eq!(vec.len(), 0);
assert!(vec.capacity() >= 10);

for i in 0..10 {
vec.push(i);
}

assert_eq!(vec.len(), 10);
assert!(vec.capacity() >= 10);

// 11번째 push 시 재할당 발생 가능
vec.push(11);
assert_eq!(vec.len(), 11);
}
  • 위 예제에서 vec은 처음부터 10개 이상의 요소를 저장할 수 있도록 메모리를 할당받아, 10개까지는 재할당 없이 push가가능합니다.
  • 11번째 요소를 추가하면 내부 용량을 초과하므로 재할당이 발생할 수 있습니다.

7. 요약 표

구분배열 (Array)벡터 (Vector)
저장 위치스택
크기고정동적
접근 속도매우 빠름빠르나 배열보다 느림
데이터 추가/삭제불가가능
성능반복/접근에서 우위대용량·가변성에 우위
  • 배열은 크기가 고정되고, 반복/접근 성능이 최상입니다.
  • 벡터는 크기 가변성, 데이터 추가/삭제의 유연성이 강점이지만, 힙 할당과 재할당에서 오버헤드가 발생할 수 있습니다.
  • 실제 성능 차이는 데이터 크기와 사용 패턴에 따라 다르므로, 벤치마크를 통해 최적의 자료구조를 선택하는 것이 중요합니다.

구조체와 튜플을 조합한 데이터 모델링

Rust에서 구조체(struct)와 튜플(tuple)을 조합해 복잡한 데이터 모델링을 하는 방법은, 각 자료구조의 장점을 살려 중첩(nesting)하거나, 서로 포함시켜 계층적인 구조를 만드는 것입니다. 이렇게 하면 의미 있는 필드(구조체)와 위치 기반 데이터(튜플)를 효과적으로 결합할 수 있습니다.

실제 프로젝트에서는 구조체로 주요 엔티티(예: 사용자, 상품, 센서 등)를 정의하고,
구조체의 일부 필드를 튜플로 선언해 위치, 좌표, 설정값 등 간단한 데이터를 묶어 표현하거나, 구조체로 선언해서 의미를 명확히 합니다.

1. 구조체 안에 튜플을 포함하는 예시

struct Employee {
name: String,
age: u32,
// (연, 월, 일) 생년월일을 튜플로 표현
birth_date: (u16, u8, u8),
}

fn main() {
let emp = Employee {
name: String::from("Kim"),
age: 28,
birth_date: (1997, 5, 14),
};
println!("{}의 생년월일: {}-{}-{}", emp.name, emp.birth_date.0, emp.birth_date.1, emp.birth_date.2);
}
  • 구조체는 필드의 의미를 명확히하고, 튜플은 간단한 데이터 묶음에 적합합니다.
  • Emplyee라는 구조체를 선언(정의)하면서 필드명과 형식을 지정하는데, birthdate는 생년월일의 연,월,일을 튜플 형식으로 지정한 것입니다.
  • let 문을 이용해서 Employee의 instance를 생성하고, 여기서는 emp, 출력할 때는 emp를 이용해서 emp.필드명 식으로 하면 되는데, 튜플 타입은 emp.필드명 다음에 튜플이므로 index를 붙여서 emp.birth_date.0, .1, .2식으로 표현합니다.
  • 출력값은 Kim의 생년월일: 1997-5-14입니다. 두 자릿수로 출력하려면 {:02}로 수정하면 됩니다. 두 자릿수로 출력하는데, 부족하면 0으로 채우라는 의미입니다.

2. 튜플 안에 구조체를 포함하는 예시

struct Product {
id: u32,
name: String,
}

fn main() {
// (상품, 수량) 형태로 장바구니 항목 표현
let cart_item: (Product, u32) = (
Product { id: 1, name: String::from("Book") },
3,
);
println!("{}: {}개", cart_item.0.name, cart_item.1);
}
  • 튜플로 여러 정보를 임시로 묶되, 각 요소가 구조체라면 의미를 명확히 할 수 있습니다.
  • cart_item을 튜플 형식으로 지정해서 Product와 수량을 받는데, Product를 구조체와 연결해서 id와 name으로 의미를 명확히하는 것입니다.
  • 튜플 속에 구조체가 들어있으므로 출력할 때 cart_item 다음에 인덱스를 적고, 구조체의 필드명을 적어서 표시합니다. 예) cart_item.0.id, cart_item.0.name, cart_item.1
  • 출력 결과는 ‘Book: 3개’입니다.

3. 중첩 구조체와 튜플을 활용한 복합 모델

struct Address {
city: String,
zip: String,
}

// (위도, 경도) 위치 정보를 튜플로 표현
struct Store {
name: String,
address: Address,
location: (f64, f64),
}

fn main() {
let store = Store {
name: "Rust Mart".to_string(),
address: Address {
city: "Seoul".to_string(),
zip: "12345".to_string(),
},
location: (37.5665, 126.9780),
};
println!("{} ({}, {}) - 위치: ({}, {})",
store.name, store.address.city, store.address.zip, store.location.0, store.location.1
);
}
  • Address 구조체를 정의한 다음 Address 구조체를 Store의 address 필드의 type으로 사용하고, Store의 location은 위도와 경도를 튜플 형식으로 정의했습니다.
  • 따라서, Store 구조체의 인스턴스를 만들 때도 address를 Address 구조체로 입력하고, location은 위도와 경도를 튜플 형식으로 입력했습니다.
  • 그리고, 출력할 때는 인스턴스명.필드명인데, address는 구조체이므로 다시 한번 더 필드명을 적어주었고, tuple 타입은 필드명 다음에 인덱스를 추가했습니다.
  • 출력 결과는 ‘Rust Mart (Seoul, 12345) – 위치: (37.5665, 126.978)’입니다.

4. 튜플 구조체와 일반 구조체 조합

struct Point(i32, i32, i32);

struct Sensor {
id: u32,
position: Point,
}

fn main() {
let sensor = Sensor { id: 101, position: Point(10, 20, 30) };
println!("센서 {} 위치: ({}, {}, {})", sensor.id, sensor.position.0, sensor.position.1, sensor.position.2);
}
  • 이번에는 튜플 구조체를 정의한 다음, 일반 구조체의 타입으로 사용한 예입니다.
  • 일반 구조체의 타입이 튜플이냐 아니냐만 다를 뿐 표현하는 방식은 위와 동일합니다.

이처럼 구조체와 튜플을 조합하면 복잡한 데이터도 명확하고 효율적으로 모델링할 수 있습니다.

  • 구조체는 필드의 의미와 계층 구조를,
  • 튜플은 간단한 값 묶음이나 위치 기반 데이터를 담당하게 하여,
  • 코드의 가독성과 확장성을 모두 높일 수 있습니다

5. 튜플 구조체로 타입 구분

struct Point(i32, i32, i32);
struct Color(i32, i32, i32);

fn draw_sphere(center: Point, color: Color) {
// center와 color가 같은 (i32, i32, i32) 구조지만, 타입이 달라 혼동 방지
// This function would contain logic to draw a sphere at the given center
// with the specified color.

println!("Drawing sphere at center: ({}, {}, {}) with color: ({}, {}, {})",
center.0, center.1, center.2,
color.0, color.1, color.2);
}

fn main() {
let center = Point(0, 0, 0);
let color = Color(255, 0, 0); // Red color

draw_sphere(center, color);
}
  • 위와 같이 구조체를 튜플 형식으로 지정하면 draw_sphere 함수에서 입력 타입이 구조체 형식과 맞는지 체크하는데,
  • 아래와 같이 함수의 인수를 튜플 형식으로 지정하면 둘 다 튜플 형식이기 때문에 center 자리에 Point 구조체 타입이 아닌 color 튜플을 넣어도 맞는 타입인지 체크를 못합니다.
  • 튜플 구조체(예: struct Point(i32, i32, i32);)를사용하면,
    동일한 데이터 구조라도 타입별로 구분할 수 있어 실수 방지 및 타입 안전성을 높입니다.
fn draw_sphere(center: (i32, i32, i32), color: (i32, i32, i32)) {
...
}

fn main() {
let center = (0, 0, 0);
let color = (255, 0, 0); // Red color

draw_sphere(color,center);
}

6. 함수 반환값 및 임시 데이터

함수에서 여러 값을 반환할 때 튜플을 사용하고,
이 반환값을 구조체의 필드로 저장하거나, 여러 구조체 인스턴스를 튜플로 묶어 일시적으로 처리할 수 있습니다.

fn min_max(numbers: &[i32]) -> (i32, i32) {
let min = *numbers.iter().min().unwrap();
let max = *numbers.iter().max().unwrap();
(min, max)
}

struct Stats {
min: i32,
max: i32,
}

fn main() {
let numbers = [3, 7, 2, 9, 4];
let (min, max) = min_max(&numbers);

let stats = Stats { min, max };
println!("최솟값: {}, 최댓값: {}", stats.min, stats.max);
}
  • let (min, max) = min_max(&numbers);
    => numbers 배열을 참조로 가져와서 min_max 함수를 처리한 다음 결괏값을 min, max 튜플에 넣고,
  • let stats = Stats { min, max };
    => min과 max를 Stats 구조체에 넣어 stats 인스턴스(또는 변수)를 만듭니다.
    min: min, max: max라고 쓰는 것이 정석이지만 필드명과 변수명이 같기 때문에 필드명만 적으면 됩니다.
  • 그리고, 인스턴스의 min과 max를 출력하는 것입니다.

7. 설정값, 좌표, 범위 등 불변 데이터 관리

고정된 설정값이나 좌표와 같이, 변경되지 않는 데이터는 튜플로 관리하고,
이 값을 구조체의 일부로 포함시켜 사용합니다.

struct DbConfig { 
host: String,
port: u16,
credentials: (String, String), // (username, password)
}

fn main() {
let db_config = DbConfig {
host: String::from("localhost"),
port: 5432,
credentials: (String::from("user"), String::from("password")),
};

println!("DB 호스트: {}", db_config.host);
println!("DB 포트: {}", db_config.port);
println!("DB 사용자명: {}", db_config.credentials.0);
println!("DB 비밀번호: {}", db_config.credentials.1);
}

8. 튜플과 달리 Struct는 메서드와 함께 활용

구조체에 메서드를 구현하여 데이터와 동작을 결합할 수 있습니다.
예를 들어, 2차원 평면상의 점(Point)에 대해 특정 축 위에 있는지 판별하는 메서드를 추가할 수 있습니다.

struct Point(f32, f32);

impl Point {
fn on_x_axis(&self) -> bool {
self.0 == 0.0
}
fn on_y_axis(&self) -> bool {
self.1 == 0.0
}
}

fn main() {
let point = Point(0.0, 0.0);
if point.on_x_axis() && point.on_y_axis() {
println!("원점에 있습니다.");
}
}
  • 구조체에 메서드를 추가해 객체 지향적으로 사용할 수 있습니다.
  • 구조체의 메서드를 만들려면 impl 구조체라고 명명하고, 그 안에서 함수(fn, 메소드)를 작성하는데, 첫번째 인수는 &self, 구조체 자체입니다.
  • fn on_x_axis(&self) -> bool은 구조체를 인수로 받아 bool 형식인 True, False를 반환합니다.
  • self.0 == 0.0
    => 세미콜론으로 끝나지 않으므로 반환값인 표현식으로 첫번째 튜플 값이 0.0인지 비교해서 같다면 True를 반환하고, 아니면 False를 반환하는 것입니다.
  • self.1 == 0.0는 튜플의 두번째 값이 0.0인지 비교하는 것입니다.
  • 출력값은 튜플의 값이 모두 0.0이므로 ‘원점에 있습니다.’입니다.

9. 요약

  • 튜플: 간단한 값 묶음, 여러 값 반환, 임시 데이터에 적합
  • 구조체: 명확한 의미의 데이터 구조, 필드 이름, 가독성·유지보수성 강조
  • 튜플 구조체: 같은 구조이지만 다른 의미의 타입 구분으로 타입 안전성을 강화

fmt::Display trait

Debug는 구조체 정보를 그대로 출력하고, fmt::Display는 원하는 방식으로 표현을 커스터마이징할 수 있게 해줍니다. Debug는 #[derive(Debug)]로 자동 구현이 되고, fmt::Display는 impl fmt::Display를 통해 수동 구현이 필요합니다.

구분DebugDisplay
목적디버깅용 출력사용자 친화적 출력
매크로println!(“{:?}”, x)println!(“{}”, x)
구현 방법#[derive(Debug)]로 자동 구현수동 구현 필요 (impl fmt::Display)
포맷 예시Matrix(1,0, 2.0, 3.0, 4.0)(1.0, 2.0)
(3.0, 4.0)
문자열화format!(“{:?}”, x)x.to_string() 또는
format!(“{}”, x)

1. tuple 구조체

가. 선언

#[derive(Debug)]
struct Matrix(f32, f32, f32, f32);
  • [derive(Debug)]는 Rust에서 자동으로 Debug 트레이트를 구현해 달라는 뜻입니다. 이를 통해 해당 구조체(또는 열거형 등)를 {:?} 형식으로 출력할 수 있게 됩니다.
  • Matrix 구조체는 f32 타입의 값 4개를 가지며, 필드에 이름이 없어서 튜플처럼 순서로 접근 합니다.

나. instance 생성 및 Debug 포맷 출력

fn main() {
    let matrix = Matrix(1.1, 1.2, 2.1, 2.2);
    println!("{:?}", matrix);
}
  • Matrix라는 튜플 구조체의 새 인스턴스(instance)를 생성하는 코드입니다.
  • (1.1, 1.2, 2.1, 2.2) 라는 4개의 f32 값을 순서대로 Matrix 구조체에 넣어 새 변수 matrix에 저장합니다.
  • println!(“{:?}”, matrix);는 Debug 트레잇을 이용해 구조체 내부 값을 출력하는 것입니다.
    출력값은 Matrix(1.1, 1.2, 2.1, 2.2)입니다.

2. fmt::Display 트레잇 구현

위는 Debug 트레잇으로 출력하는 것이고, fmt::Display 트레잇을 통해 다양한 포맷으로 출력할 수 있습니다.

use std::fmt;

impl fmt::Display for Matrix {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        // 원하는 포맷으로 출력 구성
        write!(
            f,
            "( {} {} )\n( {} {} )",
            self.0,self.1, self.2,self.3
        )
    }
}
  • use std::fmt;
    => Rust 표준 라이브러리(std) 안에 있는 fmt 모듈을 현재 스코프(scope)로 가져온다는 뜻입니다.
    fmt는 그 안에 문자열 포맷(formatting) 관련 함수와 트레이트들이 모여있는 모듈(module)입니다.
    use std::fmt;를 하면, 나중에 fmt::Display나 fmt::Formatter 같은 타입이나 트레이트를 쓸 때, std::fmt 전체를 계속 적지 않고 그냥 fmt만 써도 됩니다.

  • impl fmt::Display for Matrix
    => Matrix에 대해 Display 트레이트를 수동으로 구현합니다.
  • fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
    => 출력 형식을 어떻게 할지를 정의하는 함수입니다.
    메소드명은 fmt이고, 인수로 자기 자신인 &self와 가변 참조 형식의 fmt::Fomatter 형식(구조체)의 f를 받고, 반환 형식은 fmt::Result입니다.

    fmt::Result는 Result<(), fmt::Error>의 별칭으로,
    성공하면 Ok(()), 실패하면 Err(fmt::Error)를 반환합니다.
  • write! 매크로의 입력 포맷은 write!(f, “{}”, value)입니다. “{}”라는 포맷에 value를 넣고, 그 결과를 f에 기록합니다.
  • “( {} {} )\n( {} {} )”는 괄호안에 값 2개씩을 넣어 출력하는데, \n은 줄 바꿈을 의미합니다.
  • self.0,self.1, self.2,self.3는 포맷에 들어갈 값을 순서대로 나열한 것입니다.

3. main 함수

fn main() {
    let matrix = Matrix(1.1, 1.2, 2.1, 2.2);

    // Debug 포맷
    println!("{:?}", matrix);

    // Display 포맷
    println!("{}", matrix);
}

위에서 첫번째 println! 매크로는 Debug 트레잇으로 출력하는 것으로 위에서 봤고,

두번째 println! 매크로는 fmt::Display 트레잇으로 출력하는 것으로 2번에서 구현한 바 있습니다.

출력하면 아래와 같이 튜플 구조체의 값이 두 개씩 괄호안에 싸여 두 줄로 표시됩니다.

( 1.1 1.2 )
( 2.1 2.2 )

4. fmt::Formatter의 주요 메서드

가. width(): Option<usize>

  • 출력 시 최소 너비를 설정하는 값입니다.
  • Some(n)이면, 출력 문자열이 최소 n칸은 차지해야 함을 의미합니다.
  • 예: println!(“{:5}”, “hi”) → ” hi” (공백 3개 + hi)

사용 예:

fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let width = f.width().unwrap_or(0);
write!(f, "{:width$}", "hi", width = width)
}

나. precision(): Option<usize>

  • 소수점 이하 자리수 또는 최대 출력 길이를 설정합니다.
  • Some(n)이면, 최대 n자리 또는 n글자까지만 출력합니다.

사용 예:

fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let prec = f.precision().unwrap_or(2);
write!(f, "{:.prec$}", 3.14159, prec = prec)
}
// precision이 2이면 → 3.14

또는 문자열에 적용하면:

write!(f, "{:.5}", "Hello, world!") // → Hello

다.align(): Option<fmt::Alignment>

  • 정렬 방식 설정입니다. 리턴값은 Option이고, Alignment는 열거형입니다.
의미
Some(Left)왼쪽 정렬 (:<)
Some(Right)오른쪽 정렬 (:>)
Some(Center)가운데 정렬 (:^)
None정렬 지정 없음

사용 예:

use std::fmt::{self, Alignment};

fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match f.align() {
Some(Alignment::Left) => write!(f, "왼쪽 정렬됨"),
Some(Alignment::Right) => write!(f, "오른쪽 정렬됨"),
Some(Alignment::Center) => write!(f, "가운데 정렬됨"),
None => write!(f, "기본 정렬"),
}
}

라. Matrix 구조체에 width, precision 적용하기

impl fmt::Display for Matrix {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let w = f.width().unwrap_or(5);
let p = f.precision().unwrap_or(2);
write!(
f,
"( {:>w$.p$} {:>w$.p$} )\n( {:>w$.p$} {:>w$.p$} )",
self.0, self.1, self.2, self.3,
w = w, p = p
)
}
}

사용:

let m = Matrix(1.2345, 12.3456, 123.4567, 1234.5678);
println!("{:8.1}", m); // 최소 너비 8칸, 소수점 아래 1자

출력:

(     1.2    12.3 )
( 123.5 1234.6 )

바. format!와 write!

방식장점
format!() 사용문자열을 먼저 만들어 두기 편함
write!() 직접 사용메모리 절약, 포맷 버퍼 덜 생성함

Rust에서는 둘 다 가능하지만, 성능이나 메모리를 조금 더 아끼고 싶다면 write!()를 직접 쓰는 방식이 더 낫습니다.

5. 튜플 위치 변경(reverse)

// Tuples can be used as function arguments and as return values.
fn reverse(pair: (i32, bool)) -> (bool, i32) {
    // `let` can be used to bind the members of a tuple to variables.
    let (int_param, bool_param) = pair;

    (bool_param, int_param)
}

fn main() {
    let pair = (1, true);
    println!("Pair is {:?}", pair);

    println!("The reversed pair is {:?}", reverse(pair));
}
  • fn reverse(pair: (i32, bool)) -> (bool, i32) {
    => reverse 함수는 pair라는 인수를 받는데, i32와 bool 타입의 튜플이며, 반환 형식은 bool, i32 타입의 튜플입니다.
  • let (int_param, bool_param) = pair;
    => pair라는 튜플에서 값을 튜플 형식으로 받는데 int_param과 bool_param은 튜플의 멤버와 묶입니다.
  • (bool_param, int_param)은 표현식으로 reverse 함수의 return 값입니다. 인수는 i32, bool 타입이었는데, 순서가 바뀌어서 bool, i32 타입으로 반환됩니다.
  • let pair = (1, true);
    => pair란 변수에 1, true를 멤버로 하는 튜플을 대입합니다.
  • println!(“Pair is {:?}”, pair);
    => pair란 튜플을 debug 포맷으로 출력합니다.
  • println!(“The reversed pair is {:?}”, reverse(pair));
    => reverse 함수를 이용해 pair 튜플의 순서를 바꿔서 출력합니다.
  • cargo run으로 실행하면
    입력한 값대로 (1, true)로 출력되고,
    그 다음은 reverse 함수가 적용되어 순서가 바뀌어 (true, 1)로 출력됩니다.
Tuple 필드 reverse

6. 구조체 위치 변경(transpose)

use std::fmt;

#[derive(Debug)]
struct Matrix (f32, f32, f32, f32);

impl fmt::Display for Matrix {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(
            f,
            "( {} {} )\n( {} {} )",
            self.0,self.1, self.2,self.3
        )
    }
}

fn transpose(matrix: Matrix) -> Matrix {
    Matrix(matrix.0, matrix.2, matrix.1, matrix.3)
}

fn main() {
    let matrix = Matrix(1.1, 1.2, 2.1, 2.2);
    println!("{:?}", matrix);
    println!("Matrix:\n{}", matrix);
    println!("Transpose:\n{}", transpose(matrix));
}
fn transpose(matrix: Matrix) -> Matrix {
Matrix(matrix.0, matrix.2, matrix.1, matrix.3)
}
  • fn transpose(matrix: Matrix) -> Matrix
    => 이 부분을 함수 signature라고 부르며,
    의미는 transpose 함수를 만드는데, 인수는 matrix이고, matrix의 타입은 Matrix 구조체입니다. 또한 반환 형식도 Matrix 구조체입니다.
  • { Matrix(matrix.0, matrix.2, matrix.1, matrix.3) }
    => 중괄호 안에 있는 것은 함수 본문으로 세미콜론이 없기 때문에 반환값을 나타내는 표현식입니다.

    Matrix(matrix.0, matrix.2, matrix.1, matrix.3)는 matrix라는 인수의 필드에 접근하는데, 인덱스가 0, 1, 2, 3이지만 순서를 바꾸기 위해 0, 2, 1, 3으로 표시했습니다. 이들 필드를 결합해서 Matrix 구조체를 반환합니다.
    println!("Matrix:\n{}", matrix);
println!("Transpose:\n{}", transpose(matrix));

fmt::Display trait을 이용해 matrix와 transpose(matrix)를 출력합니다.

출력 결과는
Matrix:
( 1.1 1.2 )
( 2.1 2.2 )
Transpose:
( 1.1 2.1 )
( 1.2 2.2 )
입니다.

1.2와 2.1의 위치가 달라졌습니다.

literal과 variable, value, const, static

Rust에서 리터럴(literal)은 소스 코드에 직접 값을 표현한 것이며(예: 5, “hello”, true), 변수(variable)는 프로그램 실행 중에 값을 저장하고 변경할 수 있는 이름이 있는 저장 공간입니다(예: let x = 5;).

1. 리터럴과 변수

가. 리터럴(Literals)

프로그래밍에서 리터럴은 변수(variable)나 상수(const)와 같이 특정 데이터 유형의 값을 직접 나타내는 표현 방식입니다. 예를 들어, 숫자 10, 문자 ‘a’, 문자열 “hello” , true, false 등이 모두 리터럴입니다. 

  • 직접적인 값:리터럴은 프로그램 실행 중에 변경될 수 없는 고정된 값 자체를 의미합니다. 
  • 불변성: 리터럴은 정의된 이후에 그 값을 변경할 수 없습니다.
  • 표현식:때로는 코드에서 특정 값을 계산하는 표현식 자체가 리터럴로 사용되기도 합니다. 예를 들어, (2 + 3)과 같이 계산 결과가 고정된 값으로 나타나는 경우. 

나. 변수(Variables)

  • 이름이 있는 저장소: 변수는 메모리에 값을 저장하는 이름이 붙은 공간입니다.
  • 가변성 또는 불변성: Rust에서는 let x = 5;처럼 불변 변수도 만들 수 있고, let mut x = 5;처럼 가변 변수도 만들 수 있습니다. 가변 변수는 값을 변경할 수 있습니다.
  • 모든 타입 가능: 변수는 기본 타입은 물론, 구조체나 참조 등 다양한 데이터 타입의 값을 저장할 수 있습니다.
  • 데이터 저장과 조작: 변수는 프로그램에서 필요에 따라 데이터를 저장하거나 변경하는 데 사용됩니다.

다. 비교표

항목리터럴(Literal)변수(Variable)
표현 방식값을 직접 표현이름이 있는 저장 공간
변경 가능 여부변경 불가 (immutable)변경 가능 또는 불가능 (mutable / immutable)
※ Rust에서는 기본값이 immutable
저장 방식변수처럼 메모리에 저장되지 않음메모리에 저장됨
사용 목적상수값(변하지 않는 값) 표현데이터 저장 및 조작
예시5, “hello”, truelet x = 5;에서 x
let mut y = “hello”;에서 y

라. 예제 1

fn main() {
let x = 10; // 10은 정수 리터럴
let y = "hello"; // "hello"는 문자열 리터럴

println!("x = {}", x);
println!("y = {}", y);
}

여기서:

  • 10은 숫자 리터럴 (미리 정의된 값)
  • “hello”는 문자열 리터럴 (미리 코드에 써 넣은 값)

즉, 이런 리터럴 값은 코드에서 바로 보이고 바뀌지 않는 상수 같은 값입니다.


마. 예제 2

fn main() {
let a = 5; // 5는 리터럴
let b = a + 3; // 3도 리터럴, b는 변수

println!("b = {}", b); // 출력: b = 8
}
  • 여기서 5와 3은 literal 또는 literal value (미리 정해진 값)로 바뀔 수 없는데,
  • a와 b는 변수로 나중에 바뀔 수 있습니다. 그러나, Rust에서는 mut가 없으면 불변이 기본임

바. 요약

Rust에서 10, “hello”, true 같은 값들은 literal로서
코드에 직접 써 넣은, 미리 정의된 값들입니다.

2. 리터럴과 값(value)

가. Literal (리터럴)

“소스 코드에 직접 쓰인 값”
즉, 고정된 형태의 값을 코드에 명시한 것입니다.

42       // 정수 리터럴
3.14 // 부동소수점 리터럴
"hello" // 문자열 리터럴
true // 불리언 리터럴
'b' // 문자 리터럴

리터럴은 컴파일 타임에 결정되며 변하지 않습니다.


나. Value (값)

프로그램이 실행될 때 메모리에 존재하는 데이터입니다.
리터럴, 변수, 연산 결과 등 다양한 방식으로 생성될 수 있습니다.

let x = 5;            // 리터럴 5를 변수 x에 저장 → value는 5
let y = x + 2; // 연산 결과 7도 value
let s = String::from("hi"); // value는 "hi"라는 힙에 저장된 문자열

value는 runtime의 개념이며, 리터럴은 value를 만들기 위한 한 방법입니다.


다. 비교표

항목LiteralValue
정의코드에 직접 적힌 고정 값실행 중 사용되는 데이터
예시“hello”, 3.14, true변수에 저장된 값, 함수의 반환값 등
생성 시점컴파일 타임런타임
변경 가능성불변변경 가능 (가변 변수에 저장된 경우)
위치코드에 직접 작성됨메모리에 존재

라. 관계 요약

  • 리터럴은 value를 만드는 수단 중 하나입니다.
  • 모든 리터럴은 value지만, 모든 value가 리터럴은 아닙니다.

예:

let a = 1 + 2;  
// 1과 2는 리터럴 → 3은 value (리터럴이지만 연산 결과이기도 함)

let b = a * 2;
// b의 값 6은 value지만 literal은 아님 (런타임 연산 결과)

3. literal과 const, static

구분의미특징
literal코드에 직접 써 넣은 값미리 정의된 값. 변수나 상수에 저장 가능
const컴파일 시점 상수항상 값이 고정, 함수 밖/안 모두 사용 가능, 메모리 없음
static정적 변수 (전역 상수)프로그램 전체에서 공유, 고정 메모리 공간 사용

가. 예제

const CONST_VALUE: i32 = 10;
static STATIC_VALUE: i32 = 20;

fn main() {
let literal = 5; // 5는 literal

println!("literal: {}", literal);
println!("const: {}", CONST_VALUE);
println!("static: {}", STATIC_VALUE);
}

나. 설명

  1. 5는리터럴(literal)
    → 그냥 코드에 직접 쓴 고정된 값
  2. CONST_VALUE는 const
    → 컴파일 시간에 값이 확정됨. 메모리에 저장되지 않음.
    → const는 let처럼 지역적으로도 가능하지만 항상 값이 바뀌지 않음
  3. STATIC_VALUE는 static
    → 프로그램이 끝날 때까지 살아있는 고정 메모리 주소에 저장됨
    → 전역 변수처럼 사용 가능
    → 다만static mut은 unsafe하게 써야 함

다. 비교표

항목값이 고정됨변경 가능성메모리 위치사용 시기
literal스택 또는 인라인코드 내 직접
const없음 (값 인라인)컴파일 시
static❌ (기본)전역 메모리런타임 전체 기간

라. static mut의 대체 수단

static mut COUNTER: i32 = 0;

fn main() {
unsafe {
COUNTER += 1;
println!("counter: {}", COUNTER);
}
}
  • static mut은 동시성 문제 때문에 unsafe로 접근해야 함

Rust의const와 static은 값이 컴파일 타임에 반드시 결정되어야 하는데,

다음처럼 런타임 정보나 복잡한 로직으로 초기화해야 하는 경우에는 문제가 생깁니다:

static MY_STRING: String = String::from("hello"); // ❌ 에러!

🚫String은 컴파일 타임에 생성할 수 없기 때문에 static으로 직접 초기화 불가능

이럴 때 쓰는 게 바로 아래 세 가지입니다:

(1) const fn

  • const fn은 컴파일 타임에 실행 가능한 함수를 정의합니다.
  • const 상수나 const 표현식에서 사용할 수 있음
const fn square(x: i32) -> i32 {
x * x
}

const RESULT: i32 = square(4); // ✅ OK!

📌 단점: const fn은 제한이 많아서, String, Vec, 파일읽기 등은 못 씀


(2) lazy_static (deprecated 경향 있음)

  • 복잡한 값도 런타임 최초 1회 초기화 후 전역처럼 사용 가능
  • macro_use가 필요하고, 내부적으로 unsafe와 Mutex를 씀
#[macro_use]
extern crate lazy_static;

use std::collections::HashMap;

lazy_static! {
static ref CONFIG: HashMap<&'static str, i32> = {
let mut m = HashMap::new();
m.insert("threshold", 10);
m
};
}

fn main() {
println!("threshold = {}", CONFIG["threshold"]);
}

✅ 장점: 복잡한 초기화 가능
⚠️ 단점: 오래된 방식이고 macro 기반, 무거움


(3) once_cell (추천)

  • 최신 Rust 커뮤니티에서는 lazy_static 대신 once_cell을 많이 씁니다.
  • 런타임 최초 1회 초기화, macro 없이, 더 가볍고 안전
use once_cell::sync::Lazy;
use std::collections::HashMap;

static CONFIG: Lazy<HashMap<&'static str, i32>> = Lazy::new(|| {
let mut m = HashMap::new();
m.insert("threshold", 10);
m
});

fn main() {
println!("threshold = {}", CONFIG["threshold"]);
}

✅ 장점: lazy_static보다 깔끔하고 안전
🔒 sync::Lazy: 여러 스레드에서 안전하게 사용 가능
🧵 unsync::Lazy: 단일 스레드용으로 더 빠름


(4) const, static 등 비교표

방식초기화 시점복잡한 타입 가능스레드 안전성비고
const컴파일 타임❌ (기본 타입만)N/A빠르고 간단
static컴파일 타임❌ (기본 타입만)가능전역 메모리 사용
const fn컴파일 타임제한적가능복잡한 초기화 불가능
lazy_static런타임 (1회만)무겁고 오래된 방식
once_cell런타임 (1회만)✅ or ❌최신 방식, 매크로 없이 가능

(5) 요약

  • 간단한 상수는 const
  • 전역 정적 값은 static
  • 복잡한 초기화가 필요한 전역 값은 once_cell::Lazy 추천

Rust 프로젝트 구조 및 배포

Rust 프로젝트가 커짐에 따라 모듈화와 구조화, 그리고 최종적으로 바이너리 또는 라이브러리로 배포하는 과정이 중요해집니다. 이번에는 Rust 프로젝트를 어떻게 구성하고, 빌드하며, 배포하는지에 대해 알아보겠습니다.


1. Rust 프로젝트 구조

Rust는 기본적으로 cargo를 사용하여 프로젝트를 관리합니다. 일반적인 프로젝트 구조는 다음과 같습니다:

my_project/
├── Cargo.toml
├── Cargo.lock
├── src/
│ ├── main.rs
│ └── lib.rs
├── tests/
│ └── integration_test.rs
├── benches/
│ └── benchmark.rs
└── examples/
└── hello.rs

✅ 주요 구성 요소 설명

  • Cargo.toml: 프로젝트의 메타데이터, 의존성 등을 관리하는 설정 파일입니다.
  • src/main.rs: 실행 가능한 바이너리 프로그램의 진입점입니다.
  • src/lib.rs: 라이브러리 crate의 코드가 들어가는 곳입니다. 모듈화된 코드 작성 시 여기에 구현합니다.
  • tests/: 통합 테스트를 위한 디렉터리입니다.
  • benches/: 성능 벤치마크용 테스트가 위치합니다.
  • examples/: 사용 예제를 담는 폴더로, cargo run –example 예제이름으로 실행할 수 있습니다.

2. 모듈과 라이브러리 구조

기본적인 모듈 구성

my_project/
├── Cargo.toml
└── src/
├── main.rs // 실행 가능한 바이너리 프로그램
├── lib.rs // 라이브러리로 정의된 코드
└── utils.rs // lib.rs에서 공개하는 하위 모듈

프로젝트명은 my_project이고, 그 아래 Cargo.toml과 src 폴더가 있고,
src 폴더에 main.rs, lib.rs, utils.rs가 있습니다.

  • main.rs는 fn main()을 통해 프로그램을 실행하는 진입점(entry point)
  • lib.rs는 모듈과 함수들을 정의하고 외부에 노출하는 라이브러리
  • utils.rs는 lib의 서브 모듈로 기능 분리용

가. utils.rs

pub fn add(a: i32, b: i32) -> i32 {
a + b
}
  • add 함수는 두 정수를 더해서 반환하는 간단한 유틸 함수입니다.
  • pub으로 선언했기 때문에 이 함수는 외부 모듈 (lib.rs나 main.rs)에서도 사용할 수 있습니다.

나. lib.rs

pub mod utils;

pub fn greet(name: &str) {
println!("Hello, {}!", name);
}
  • pub mod utils;
    → src/utils.rs 파일을 모듈로 포함함.
    utils 모듈이 공개(pub)되었기 때문에 외부에서 사용 가능.
  • pub fn greet(name: &str)
    → greet 함수도 공개되어 있어서 외부 crate나 main.rs에서 사용할 수 있음.

다. main.rs

use my_project::greet;
use my_project::utils::add;

fn main() {
greet("Rust");
println!("2 + 3 = {}", add(2, 3));
}
  • use my_project::greet;
    → lib.rs에서 정의된 greet 함수를 가져와 사용함.
  • use my_project::utils::add;
    → lib.rs가 공개한 utils 모듈을 통해 add 함수를 가져옴.
  • main() 함수에서는
    greet(“Rust”)를 호출해서 Hello, Rust!를 출력하고,
    add(2, 3)의 결과 5를 출력합니다.

라. Cargo 관점에서

Cargo.toml에는 특별한 설정 없이도 다음이 자동 처리됨:

[package]
name = "my_project"
version = "0.1.0"
edition = "2021"
  • lib.rs는 my_project라는 이름의 라이브러리 크레이트로 인식됨
  • main.rs는 실행 파일용 바이너리 크레이트로 인식됨

따라서 main.rs에서 my_project::를 통해 lib.rs의 함수와 모듈을 가져올 수 있는 것임.


마. 빌드 및 실행

cargo run

실행하면 아래와 같이 출력됩니다.

Hello, Rust!
2 + 3 = 5

사. 요약

파일역할주요 내용
lib.rs라이브러리 크레이트greet 함수 정의, utils 모듈 공개
utils.rs하위 모듈add 함수 정의
main.rs실행 크레이트greet와 add 호출하여 메시지 출력

이 구조는 라이브러리를 재사용하거나 다른 프로젝트에 공유하고 싶을 때 매우 적합하며, 기능 분리모듈화에도 강력한 패턴입니다.


3. 패키지와 워크스페이스

여러 crate를 하나의 프로젝트로 묶고싶다면 workspace 기능을 사용할 수 있습니다.

[workspace]
members = [
"core_lib",
"cli_tool",
]

이 구조에서는 core_lib는 공통 로직을 담고, cli_tool은 실행 바이너리를 담당할 수 있습니다. 모노레포(Monorepo) 방식으로 관리하기에 유리합니다.

Cargo.toml 파일에 [workspace] 섹션을 추가하여 workspace를 정의하는데, members 필드에 각 하위 프로젝트의 경로를 지정합니다.

모노레포는 멀티레포와 대비되는 개념으로, 여러 하위 프로젝트들을 하나의 저장소에서 관리하는 것을 말하며, 이렇게 하면 코드 공유, 재사용, 일관된 빌드 및 테스트 설정 등을 효율적으로 관리할 수 있습니다. 일반적으로 Cargo를 사용하여 모노레포를 구성합니다. 
* 레포는 Repository의 약어

4. 빌드 및 최적화

기본 빌드

cargo build
  • 기본 빌드 모드에서는 target/debug/에 실행 파일이 생성됩니다.

릴리즈 빌드 (최적화 포함)

cargo build --release
  • 릴리즈 모드에서는 최적화가 적용되며, target/release/에 실행 파일이 생성됩니다.
  • 실제 배포 시에는 릴리즈 모드를 사용하는 것이 일반적입니다.

5. 바이너리 배포

Rust는 정적 바이너리 생성을 기본으로 하므로, 종속성 없는 실행 파일을 만들 수 있습니다.

가. 윈도우/리눅스/맥용 빌드

# 리눅스용으로 크로스 컴파일
cargo build --release --target x86_64-unknown-linux-gnu

# 윈도우용
cargo build --release --target x86_64-pc-windows-msvc

cargo build –release –target x86_64-unknown-linux-gnu실행 시
note: the x86_64-unknown-linux-gnu target may not be installed
라고 표시되면
rustup target add x86_64-unknown-linux-gnu를 통해 target을 먼저 설치해야 합니다.

나. 크로스 컴파일

  • cross 크레이트를 사용하면 플랫폼별 크로스 컴파일을 간편하게 할 수 있습니다.
    cargo install cross
    cross build –target x86_64-unknown-linux-gnu –release
  • 위 명령어를 실행하면 docker 또는 podman이 설치되어 있는지 묻습니다. 따라서, docker 등을 설치하고 그 안에서 위 명령을 실행해야 합니다.

다. 실행 파일 위치

  • 기본 빌드: target/debug/프로젝트명
  • 릴리즈 빌드: target/release/프로젝트명

이 파일을 원하는 디렉토리나 서버로 옮기면 됩니다.


6. Crates.io에 라이브러리 배포

만약 자신이 만든 라이브러리를 다른 사람들과 공유하고 싶다면, crates.io에 등록할 수 있습니다.

가. 준비 단계

  • https://crates.io에 계정을 만들고 API Token을 생성합니다.
  • ~/.cargo/credentials에 토큰을 저장합니다.
  • Cargo.toml에 필수 정보를 입력합니다:
[package]
name = "my_crate"
version = "0.1.0"
authors = ["홍길동 <email@example.com>"]
edition = "2021"
license = "MIT OR Apache-2.0"
description = "간단한 수학 라이브러리"
repository = "https://github.com/username/my_crate"

나. 배포 명령

cargo publish
  • 첫 배포 전에는 cargo package로 정상 패키징이 되는지 테스트해보는 것이 좋습니다.

7. CI/CD 통합

CI/CD는 지속적인 통합(Continuous Integration)과 지속적인 제공/배포(Continuous Delivery/Deployment)를 의미하며, 소프트웨어 개발 및 배포 과정을 자동화하여 효율성과 속도를 높이는 방법론입니다. CI/CD는 개발자가 코드 변경 사항을 주기적으로 메인 저장소에 통합하고, 테스트를 거쳐 자동으로 배포될 수 있도록 합니다. 

프로젝트가 커지면 GitHub Actions나 GitLab CI와 같은 CI 도구를 통해 자동으로 테스트와 빌드, 배포까지 설정할 수 있습니다.

예: .github/workflows/rust.yml

name: Rust

on: [push]

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
- run: cargo build --release
- run: cargo test

8. 마무리

Rust 프로젝트는 명확한 구조와 cargo의 강력한 관리 도구 덕분에 규모가 커져도 효율적으로 관리할 수 있습니다. 프로젝트 구조를 잘 나누고, 최적화된 빌드를 통해 빠르고 안정적인 배포 환경을 만드는 것이 중요합니다.

매크로(Macro)와 메타 프로그래밍

Rust는 정적 타입 언어이면서도 매우 강력한 매크로 시스템을 제공합니다. 매크로는 코드를 작성하는 코드를 작성할 수 있게 해 주며, 반복되는 코드의 중복을 줄이고, 컴파일 타임에 코드를 생성하여 성능 저하 없이 유연성을 확보할 수 있습니다.

Rust의 매크로는 크게 두 가지로 나뉩니다:

  • 매크로 by 예시 (macro_rules!)
  • 절차적 매크로 (Procedural Macros)

이번 시간에는 두 매크로의 차이점과 사용법, 그리고 메타 프로그래밍의 개념에 대해 알아보겠습니다.


1. macro_rules! 기본 매크로

Rust에서 가장 널리 사용되는 매크로는 macro_rules!로 작성하는 선언형 매크로(declarative macro)입니다.

가. 예시

macro_rules! say_hello {
() => {
println!("Hello, macro!");
};
}

fn main() {
say_hello!(); // Hello, macro!
}

이 매크로는 함수처럼 보이지만, 실제로는 코드를 치환하는 역할을 합니다. 괄호 안에 아무 인자도 없을때, println! 코드를 삽입하는 구조입니다.

  • macro_rules!는 Rust에서 매크로를 정의하는 키워드입니다. 함수와 비슷해 보이지만, 컴파일 타임에 코드 조각을 만들어주는 메타 프로그래밍 도구입니다.
  • say_hello라는 이름의 매크로를 정의합니다.
    ()는 이 매크로가 인자를 받지 않음을 의미합니다.
    => { … }는 매크로가 어떤 코드로 확장될지를 정의합니다.
    여기서는 println!(“Hello, macro!”);라는 코드를 삽입합니다.
    즉, 이 매크로는 호출되면 println!을 실행하는 코드로 치환됩니다.
  • say_hello!();는 매크로를 호출하는 문법입니다.
    함수 호출과 다르게 ! 기호가 사용됩니다.
  • 결과적으로 Hello, macro!가 출력됩니다.

나. 매크로는 함수보다 더 유연하다.

  • 함수는 특정 타입에 대해 정의되어야 하지만,
  • 매크로는 타입과 관계없이 패턴으로 처리할 수 있습니다.
macro_rules! create_function {
($name:ident) => {
fn $name() {
println!("함수 {} 호출됨", stringify!($name));
}
};
}

create_function!(foo);
create_function!(bar);

fn main() {
foo(); // 함수 foo 호출됨
bar(); // 함수 bar 호출됨
}

위의 매크로는 여러 개의 함수를 자동으로 생성하는 예시입니다.

(1) 매크로 정의

macro_rules! create_function {
($name:ident) => {
fn $name() {
println!("함수 {} 호출됨", stringify!($name));
}
};
}
  • macro_rules! create_function: create_function이라는 이름의 매크로를 정의합니다.
  • ($name:ident) => { … }: 이 매크로는 식별자 하나를 입력 인자로 받아 내부에서 함수 하나를 생성합니다.
    ident: 식별자(identifier) 타입을 의미합니다. 변수명, 함수명 같은 이름을 받을 때 사용합니다.니다.
    • ident: 식별자(identifier) 타입을 의미합니다. 변수명, 함수명 같은 이름을 받을 때 사용합니다.
  • fn $name() { … }: 입력받은식별자 $name을 함수 이름으로 사용하여 새 함수를 만듭니다.
  • stringify!($name): 식별자 $name을 문자열로바꿔줍니다.
    • 예: stringify!(foo) → “foo”

(2) 매크로 호출

create_function!(foo);
create_function!(bar);
  • 이 부분은 위에서 정의한 매크로를 호출하는 부분입니다.
  • create_function!(foo)는 foo를 인수로 받아 아래와 같은 함수를 만들어냅니다:
fn foo() {
println!("함수 foo 호출됨");
}
  • create_function!(bar)도 마찬가지로 bar를 인수로 받아 아래와 같은 함수를 만들어냅니다:
fn bar() {
println!("함수 bar 호출됨");
}

즉, 이 두줄 덕분에 foo와 bar라는 이름의 함수가 자동으로 생성됩니다.


(3) main 함수 – 함수 호출

fn main() {
foo(); // 함수 foo 호출됨
bar(); // 함수 bar 호출됨
}
  • 앞서 매크로를통해 생성된 foo()와 bar() 함수를 호출합니다.
  • 각각의 함수는 다음과 같은 출력을 합니다:
함수 foo 호출됨
함수 bar 호출됨

2. 메타 프로그래밍(Metaprogramming)이란?

Rust에서 매크로를 사용하는 이유는 곧 메타 프로그래밍을 위한 것입니다. 메타 프로그래밍이란 프로그램이 프로그램 코드를 다루거나 생성하는 것을 말합니다.

Rust에서는 컴파일 타임에 코드를 생성하여

  • 코드 반복 제거
  • 에러 감소
  • 성능 저하 없이 추상화 제공

이라는 장점을 누릴 수 있습니다.


3. 절차적 매크로(Procedural Macro)

macro_rules!는 구조가 제한적이므로 복잡한 로직을 처리하기엔 어렵습니다. 이를 해결하기 위해 Rust는 절차적 매크로를 제공합니다. 절차적 매크로는 함수처럼 동작하며, 다음과 같이 세 가지 유형이 있습니다:

  1. Derive 매크로 (#[derive])
  2. Attribute 매크로 (#[route], #[test] 등)
  3. Function-like 매크로 (my_macro!(…) 형태)

가. Derive 매크로

Rust는 많은 표준 트레잇을 #[derive(…)]를 통해 자동으로 구현할 수 있습니다.

#[derive(Debug, Clone, PartialEq)]
struct Point {
x: i32,
y: i32,
}

#[DERIVE(…)]

이 부분은 파생 구현(derive macro)라고 부릅니다. Rust에서는 구조체에 대해 자주 사용하는 trait들을 자동으로 구현할 수 있도록 해줍니다.

* Debug
  • 구조체를 {:?} 형식으로 출력할 수 있게 해줍니다.
  • 예: println!(“{:?}”, p); // 출력: Point { x: 3, y: 5 }
*Clone
  • 구조체를 복사(clone) 할 수 있게 해줍니다.
  • let p2 = p.clone(); 같이 사용 가능하며, 깊은 복사가 이루어집니다.
  • let p2 = p; 이라고 하면 Copy가 아니기 때문에 p는 이동(move)되고, 사용 불가가 되므로 clone을 사용하는 것입니다.
* PartialEq
  • 두 구조체가 같은지 비교할 수 있게 해줍니다 (==, != 사용 가능).
  • 예:
    let a = Point { x: 1, y: 2 };
    let b = Point { x: 1, y: 2 };
    assert_eq!(a, b); // true

나. Attribute 매크로

Attribute 매크로는 함수, 구조체 등 앞에 붙여 동작을 변경합니다.

#[route(GET, "/")]
fn index() {
// 라우트 처리
}
🔸 #[route(GET, “/”)]

Rocket 프레임워크에서 제공하며, 해당 함수가 HTTP 요청을 어떻게 처리할지를 지정합니다.

  • GET → HTTP 메서드 GET, POST, PUT, DELETE 등 중 하나입니다.
  • “/” → 경로. 이 경우 루트 경로(예: http://localhost:8000/)입니다.

🔸 fn index()

이것은 실제 요청을 처리할 함수입니다.
보통 이 함수는 -> &’static str이나 -> Html<String> 같은 반환값을 갖습니다.
index는 함수 이름으로, 자유롭게 바꿀 수 있습니다 (home, root 등).
이 함수는 라우팅된 요청이 들어왔을 때 호출됩니다.

위 코드는 다음 의미를 갖습니다:

  • 클라이언트가 GET / 요청을 보냈을 때,
  • index() 함수를 실행해서 그 요청을 처리한다.

다. Function-like 매크로

마치 함수처럼 사용하는 매크로입니다.

my_macro!(input);

일반적으로 다음처럼 정의합니다.

#[proc_macro]
pub fn my_macro(input: TokenStream) -> TokenStream {
// input을 파싱하고 새로운 코드 생성
}
  • pub fn my_macro(input: TokenStream) -> TokenStream
    my_macro는 매크로 이름입니다. 실제 사용 시에는 my_macro!(…) 형태로 사용됩니다.
    입력: input: TokenStream
    출력: TokenStream
  • 즉, 입력으로 받은 코드를 읽고, 새로운 코드로 변환하는 함수입니다.

4. 매크로의 장단점

가. 장점

  • 코드 중복 제거
  • 성능 저하 없이 추상화 가능
  • 정적 분석 기반으로 안전성 확보

나. 단점

  • 디버깅이 어려움
  • 복잡한 로직일수록 가독성 저하
  • IDE의 지원이 제한적일 수 있음

5. 매크로 관련 도구

  • syn: Rust 코드를 파싱하는 라이브러리
  • quote: 코드 생성을 위한 DSL(Domain Specific Languages)
  • proc-macro2: proc_macro를 확장한 안정적인 인터페이스
[dependencies]
syn = "2"
quote = "1"
proc-macro2 = "1"

6. 마무리

Rust의 매크로 시스템은 다른 언어의 템플릿이나 메타 프로그래밍 기능보다 훨씬 강력하고 안전하게 설계되어 있습니다. macro_rules!로 간단한 코드 생성을 처리하고, 절차적 매크로로 복잡한 로직도 커버할 수 있습니다.

메타 프로그래밍은 러스트의 안전성과 추상화를 동시에 만족시키는 중요한 도구이므로, 초반에는 어렵더라도 반드시 익혀야 할 개념입니다.

Rust에서 테스트(test) 작성하기

Rust는 안정성과 신뢰성을 강조하는 언어답게 테스트를 매우 중요하게 여깁니다. 표준 라이브러리에는 테스트를 쉽게 작성하고 실행할 수 있도록 test 프레임워크가 기본 내장되어 있으며, cargo test 명령어로 손쉽게 실행할 수 있습니다. 오늘은 단위 테스트, 통합 테스트, 그리고 테스트 관련 어노테이션 및 모범 사례에 대해 알아보겠습니다.


1. 기본적인 단위 테스트 구조

Rust의 테스트는 일반적으로 소스 파일 내에 다음과 같은 구조로 작성됩니다.

// 실제 함수
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);
}
}
  • #[cfg(test)]: 이 모듈은 테스트 시에만 컴파일되도록 지정합니다.
  • mod tests { … }: 테스트용 하위 모듈을 의미합니다. 실제 코드와 분리된 테스트 코드입니다.
  • use super::*;: 상위 모듈(super)의 add 함수를 불러오는 것입니다. 이 줄을 주석처리하면 “add함수가 없다”고 합니다.
  • #[test]: 테스트 함수임을 표시합니다.
  • test_add 함수를 만들어서 add(2, 3)의 결과와 5가 같은지 비교해서 맞으면 ok, 다르면 failed가 발생합니다.
    assert_eq!: 기대값과 실제값이 같은지 검사합니다.

가. 실행 방법

cargo test

cargo는 프로젝트 전체를 빌드하고 #[test] 함수들을 자동으로 실행합니다.

나. lib.rs 파일 작성 및 실행

(1) assert_eq!의 결과 두 값이 같은 경우

src 폴더 아래에 lib.rs란 파일로 작성했습니다.

Rust의 test 모듈 작성 및 실행 방법

day21 폴더에서 cargo test를 실행하니(VS Code를 보면 함수 위에 Run Test가 있으므로 이걸 눌러도 됨)
컴파일되고 실행되는데 먼저 unittests src\lib.rs라고 표시되어 lib.rs의 test annotation을 먼저 실행하고, 그 다음은 main.rs를 실행합니다.

lib.rs의 테스트 결과는
test tests::test_add … ok라고 표시되고,
그 아래 test result: ok. 1 passed; 0 failed… 등 내용이 표시되는데,

main.rs의 테스트 결과는 test 어노테이션이 없어서 0으로 표시됩니다.

그리고, assert_eq! 구문만 실행되고, main.rs에 있는 println!(“Hello, world!”);문은 실행되지 않습니다. lib.rs에 println!문이 있어도 실행되지 않습니다.

cargo test –lib라고 하면 lib.rs의 test annotation만 실행하고, main.rs에 test 어노테이션이 있는지 체크하지 않습니다.

(2) assert_eq!의 결과 두 값이 다른 경우

asserteq!의 실패시 panic 및 살패에 대한 설명문

add(2, 3)의 결과는 5인데, 6과 비교하면 두 값이 다르므로 FAILED라고 결과가 표시되고,

그 아래를 보면 thread ‘tests::test_add’ panicked at day21\src\lib.rs:12:9:라고 panic이 발생했고,
왼쪽은 5, 오른쪽은 6으로 “왼쪽과 오른쪽이 같다는 단언(주장)이 실패했다”고 설명합니다.


2. 다양한 assert 매크로

Rust에서는 다음과 같은 다양한 테스트 도구를 제공합니다.

매크로설명
assert!조건이 true인지 확인
assert_eq!두 값이 같은지 확인
assert_ne!두 값이 다른지 확인(not equal)
dbg!디버깅 용도로 값을 출력
panic!강제로 실패시키기

예제:

#[test]
fn test_assertions() {
let a = 10;
let b = 20;
assert!(a < b);
assert_eq!(a + b, 30);
assert_ne!(a, b);
}

위 코드를 cargo test –lib로 실행하면
모든 결과가 True이므로 test result: ok인데, 세 번이 아니라 한 번만 표시되고,

cargo test --lib

assert!(a > b);
assert_eq!(a + b, 40);
으로 수정해서 2개를 False로 만든 후 실행하면
a > b에서 panic이 되기 때문에 a + b와 40을 비교하는 부분은 가지도 못하고 끝납니다.

assert!가 실패하면 panic이 되므로 그 다음 assert_eq!를 실행하지 못함

따라서, a < b로 수정하고 실행하면 a + b = 40에서 실패가 발생합니다.


3. 실패하는 테스트

테스트가 실패했을 경우에는 어떤 테스트가 어떤 이유로 실패했는지 친절히 알려줍니다. 예를 들어:

#[test]
fn test_fail() {
assert_eq!(1 + 1, 3); // 실패
}

실행 시 다음과 같은 메시지가 출력됩니다.
“왼쪽은 2인데, 오른쪽은 3으로 왼쪽과 오른쪽이 같지 않다”고 합니다.

---- test_fail stdout ----
thread 'test_fail' panicked at day21\src\lib.rs:27:5:
assertion `left == right` failed
left: 2
right: 3

4. 테스트 함수에서 panic이 발생하면?

Rust에서는 테스트 함수가 panic을 일으키면 테스트가 실패한 것으로 간주됩니다. 일부러 panic을 유발하는 테스트는 다음처럼 작성할 수 있습니다:

#[test]
#[should_panic]
fn test_panic() {
panic!("강제로 실패시킴");
}
  • #[should_panic]: 이테스트는 panic이 발생해야 성공으로 간주됩니다.
테스트 함수에서 panic이 발생하면 : 
#[should_panic]: panic이 발생해야 ok

5. 결과 반환형을 이용한 테스트

테스트 함수가 Result<(), E>를 반환할 수도 있습니다.

#[test]
fn test_with_result() -> Result<(), String> {
if 2 + 2 == 4 {
Ok(())
} else {
Err(String::from("'2 + 2는 4'가 아닙니다!"))
}
}

가. Ok(성공)인 경우

successes:
test_with_result
라고 test_with_result가 성공임을 표시합니다.

Result를 이용한 결괏값 반환

나. 에러인 경우

failures에 Error: “‘2 + 2는 4’가 아닙니다!”라고 표시됩니다.

Result 결괏값이 Err인 경우

이 방식은 ? 연산자를 활용할 수 있어 더 깔끔하게 작성할 수 있습니다.

다. ? 연산자를 이용한 구문

fn check_math() -> Result<(), String> {
if 2 + 2 == 4 {
Ok(())
} else {
Err(String::from("'2 + 2는 4'가 아닙니다!"))
}
}

#[test]
fn test_with_result() -> Result<(), String> {
check_math()?; // 실패하면 즉시 반환됨
Ok(())
}

6. 테스트 분류

가. 단위 테스트 (Unit Test)

  • 하나의 함수나 모듈의 기능을 검사
  • 일반적으로 동일한 파일 내 mod tests 안에 작성

나. 통합 테스트 (Integration Test)

  • 실제 사용 시나리오처럼 여러 모듈이 함께 작동하는지를 테스트
  • tests/ 디렉터리 하위에 별도의 .rs 파일로 작성

예:

my_project/
├── src/
│ └── lib.rs
└── tests/
└── integration_test.rs
// src/lib.rs
pub fn add(a: i32, b: i32) -> i32 {
a + b
}

// tests/integration_test.rs
use my_project::add;

#[test]
fn test_add_integration() {
assert_eq!(add(10, 20), 30);
}

위 integration_test.rs의 use문에 my_project가 있는데, day21 등으로 프로젝트명에 맞게 수정해야 합니다.

cargo test를 실행하면 src폴더의 lib.rs와 main.rs뿐만 아니라, tests폴더의 integration_test.rs까지 test가 있는지 찾아서 test를 진행하고, day21 폴더의 Doc-tests도 진행합니다.

통합 테스트 - doctest 포함

그러나, tests 폴더의 integration_test.rs에서 Run Test를 누르면
integration_test.rs의 test만 실행합니다.

vs code의 Run Test 명령

7. #[ignore]와 특정 테스트만 실행하기

테스트가 너무 오래 걸리거나 특별한 조건에서만 실행하고 싶을 때는 #[ignore] 어노테이션을 붙일 수 있습니다.

#[test]
#[ignore]
fn long_running_test() {
// 오래 걸리는 테스트
}

cargo test 실행 시 무시되며,
test long_running_test … ignored

#[ignore] - 테스트시 제외

아래와 같이 실행하면 #[ignore]가 붙은 테스트만 실행합니다.

cargo test -- --ignored

위 코드를 실행하면 1 passed; … 1 filtered out;이라고 표시되는데, long_running_test는 통과되고, test_add_integration은 제외되었다는 뜻입니다.

#[ignore]가 붙은 테스트만 실행

특정 테스트만 실행하려면 이름을 지정합니다:

cargo test test_add

8. 테스트 모범 사례

  • 작고 독립적인 테스트를 작성하세요.
  • 각 테스트는 부작용이 없어야 합니다 (예: 파일 시스템 접근, DB 쓰기 등).
  • 테스트 함수 이름은 명확하게 지어야 합니다(test_add, test_divide_by_zero 등).
  • 테스트는 문서화 주석과 함께 유지하면 가독성이 높아집니다.

9. 문서화 테스트 (doctest)

Rust는 문서 주석(///) 안에 포함된 코드도 테스트로 실행합니다.

/// 두 수를 더합니다.
///
/// # 예시
/// ```
/// let result = my_crate::add(2, 3);
/// assert_eq!(result, 5);
/// ```
pub fn add(a: i32, b: i32) -> i32 {
a + b
}

cargo test를 실행하면 이 문서 예제도 자동으로 테스트합니다.

my_crate는 프로젝트명에 맞게 수정해야 하는데, day21 프로젝트이므로 day21로 수정했습니다.

위에서는 Doc-tests시 test 개수가 0이었는데, 여기는 1로 표시되고, ‘add (line 48) … ok’라고 표시됩니다.

doctest

10. 마무리

Rust의 테스트 프레임워크는 매우 강력하면서도 간단하게 사용할 수 있습니다. 실수를 미리 잡아내고 코드를 문서화하며 안정성을 높이기 위해 테스트는 필수적인 도구입니다.

외부 라이브러리 사용법 (Crate 활용)

Rust는 Crate(크레이트) 단위로 코드와 라이브러리를 구성합니다.
크레이트는 Rust의 패키지 시스템에서 가장 작은 단위로, 우리가 Cargo.toml에 추가해서 사용하는 외부 라이브러리들도 모두 크레이트입니다.

크레이트의 기본 개념과 사용법, 외부 라이브러리를 프로젝트에 추가하고 사용하는 법, 그리고 버전 관리와 의존성에 대해 알아보겠습니다.


1. Crate란?

  • 크레이트(Crate)는 하나의 컴파일 단위입니다.
  • 두 가지로 나뉩니다:
    Binary Crate: main() 함수를 포함하고, 실행 가능한 프로그램이 됨
    Library Crate: 다른 크레이트에서 가져와 사용할 수 있는 재사용 가능한 코드 집합

예를 들어 우리가 사용하는 tokio, serde, rand 등은 모두 라이브러리 크레이트입니다.


2. Cargo로 외부 크레이트 사용하기

Rust 프로젝트는 Cargo라는 빌드 도구를 중심으로 관리됩니다.

가. Cargo.toml에 의존성(dependency) 추가

예를 들어 난수를 생성하는 rand 크레이트를 사용하려면, 프로젝트 루트의 Cargo.toml 파일의 dependencies 절에 다음과 같이 작성합니다.

처음에 버전을 0.8.0을 적었더니 버전이 0.9.1까지 있으니 여기서 선택하라고 화면이 표시됩니다.

dependency  더 높은 버전 제시
[dependencies]
rand = "0.9.1"

이제 cargo build 또는 cargo run을 실행하면 자동으로 해당 크레이트가 다운로드되고 프로젝트에 포함됩니다.

나. 코드에서 사용하기

use rand::Rng;

fn main() {
let mut rng = rand::rng();
let n: u8 = rng.random_range(1..=10);
println!("1부터 10 사이의 무작위 수: {}", n);
}
use rand::Rng;
  • Rng는 random_range() 같은 메서드를 제공하는 트레잇(trait)입니다.
  • 이걸 use해야 random_range(…) 같은 메서드를 쓸 수 있습니다.

let mut rng = rand::rng();
  • 최신 rand에서는 rand::rng()로 난수 생성기를 가져옵니다.
  • 이전에는 rand::thread_rng()를 사용했지만, 그건 deprecated 되었고 이제는 rng()로 대체됩니다. rand 버전을 0.8로 하면 아래와 같은 에러가 발생합니다.
crate 버전이 낮아 오류 발생
  • 반환 타입은 여전히 내부적으로 ThreadRng입니다.

📝 의미:

“현재 스레드에서 사용할 난수 생성기를 가져와서 rng에 저장하라”


let n: u8 = rng.random_range(1..=10);
  • random_range(1..=10)은 1부터 10까지의 정수 중 무작위 값을 생성합니다.
  • inclusive range (..=)를 사용했으므로, 10도 포함됩니다.
  • 반환되는 값은 u8 타입으로 명시적으로 지정했습니다.
  • random_range는 gen_range() 대신 사용되는 최신 방식입니다.

println!(“1부터 10 사이의 무작위 수: {}”, n);
  • 생성된 난수 n을 콘솔에 출력합니다.
  • 실행할 때마다 1~10 사이의 숫자가 무작위로 나옵니다.


3. 크레이트 문서 확인하기

모든 주요 크레이트는 문서를 잘 갖추고 있습니다.
https://docs.rs에서 크레이트 이름으로 검색하면 API 문서를 확인할 수 있습니다.

예: https://docs.rs/rand

문서에는 모듈 구조, 사용 예제, Trait 설명 등이 포함되어 있어 매우 유용합니다.


4. 크레이트 버전 지정

Cargo는 Semantic Versioning을 따릅니다.

  • rand = “0.9” → 0.9.x까지 자동 업데이트 (1.0은 제외)
  • rand = “=0.9.1” → 정확한 버전
  • rand = “>=0.9, <10.0” → 범위 지정

보통은 “0.9”와 같이 호환 가능한 최신 버전으로 표시하는 것이 일반적입니다.


5. 여러 크레이트 함께 사용하기

Rust 프로젝트는 여러 외부 크레이트를 함께 사용할 수 있습니다.

예:serde와 serde_json을 함께 사용하여 JSON을 파싱:

[Cargo.toml]

[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

[main.rs]

use serde::{Deserialize, Serialize};
use serde_json::Result;

#[derive(Serialize, Deserialize, Debug)]
struct Person {
name: String,
age: u8,
}

fn main() -> Result<()> {
let data = r#"{"name": "홍길동", "age": 30}"#;
let p: Person = serde_json::from_str(data)?;
println!("{:?}", p);
Ok(())
}

가. use 키워드

use serde::{Deserialize, Serialize};
use serde_json::Result;
  • Rust의 use 키워드는 코드에서 다른 모듈이나 라이브러리의 특정 항목(함수, 구조체, 열거형 등)을 현재 스코프(범위)로 가져와 사용하기 위해 사용됩니다. 이를 통해 코드를 더 간결하고 읽기 쉽게 만들 수 있습니다. 
  • serde::{Deserialize, Serialize}: 구조체를 직렬화(Serialize) 또는 역직렬화(Deserialize) 하려면 이 트레잇이 필요합니다.
  • serde_json::Result: serde_json이 제공하는 Result 타입(Enum)을 사용합니다. 에러 처리를 쉽게 하기 위해 사용됩니다.

나. 구조체 정의

#[derive(Serialize, Deserialize, Debug)]
struct Person {
name: String,
age: u8,
}
  • [derive(…)]: 구조체에 자동으로 트레잇 구현을 추가합니다.
    – Serialize: 구조체를 JSON으로 변환할 수 있게 함.
    – Deserialize: JSON을 구조체로 변환할 수 있게 함.
    – Debug: println!(“{:?}”, …)으로 구조체 내용을 출력할 수 있게 함.
  • Person 구조체 정의
    – name: 문자열 타입
    – age: u8(0~255 정수 타입)

다. main 함수

fn main() -> Result<()> {
  • 반환 타입이Result<()>인 이유는 serde_json::from_str가 실패할 수 있기 때문입니다. 실패하면 에러를 리턴하고, 성공하면 Ok(())를 반환합니다.

라. JSON 문자열 → 구조체

let data = r#"{"name": "홍길동", "age": 30}"#;
  • r#”…#”는 raw string(원시 문자열) 문법입니다. 문자열 내부에 따옴표가 있어도 별도로 이스케이프할 필요가 없어 편리합니다.
  • {“name”: “홍길동”, “age”: 30}는 JSON 포맷 문자열입니다.
let p: Person = serde_json::from_str(data)?;
  • serde_json::from_str는 JSON 문자열을 파싱해서 Person 구조체로 변환합니다.
  • ? 연산자는 실패 시 에러를 리턴하고, 성공 시 결과값을 반환합니다.

마. 구조체 출력

println!("{:?}", p);
  • 구조체 p를 Debug 포맷으로 출력합니다. 결과는 예를 들어 다음과 같이 나옵니다.
    Person { name: “홍길동”, age: 30 }

6. Crate.io에서 크레이트 찾기

외부 라이브러리는 모두 https://crates.io에서 검색할 수 있습니다.

  • 인기 순, 다운로드 수, 최근 업데이트 순으로 정렬 가능
  • 사용자 리뷰, 문서 링크, GitHub 코드도 확인 가능

예:

  • 웹 요청 라이브러리: reqwest
  • 비동기 실행기: tokio
  • CLI 도구 만들기: clap

7. 크레이트 네임스페이스(namespace)와 모듈(module)

크레이트를 임포트할 때는 보통 최상위 네임스페이스부터 시작합니다.

use regex::Regex; // regex 크레이트 내 Regex 타입 사용

내부 모듈에 접근할 땐 점(::)을 따라 구조를 확인합니다.

예:

use tokio::time::{sleep, Duration};

8. 주의사항

  • 크레이트마다 버전이 다르면 충돌이 생길 수 있음 → Cargo가 자동으로 중복 조율
  • 사용하지 않는 크레이트는 지워주는 게 좋음
  • 일부 크레이트는 feature flag를 통해 기능을 선택적으로 활성화함

9. 정리

항목설명
CrateRust 코드의 재사용 단위, 외부 라이브러리
Cargo.toml의존성을 정의하는 설정 파일
crates.io외부 크레이트 검색/다운로드 플랫폼
docs.rs모든 크레이트의 공식 문서 모음 사이트
use크레이트 또는 모듈에서 항목을 가져오는(import) 키워드

10. 마무리

Rust의 크레이트 생태계는 매우 강력하고 체계적입니다.
외부 라이브러리를 잘 활용하면, 코드의 양을 줄이고 품질을 높일 수 있습니다.
이제는 필요한 기능이 있다면 직접 구현하기보다 crates.io에서 검색해보는 것이 먼저입니다.