Yahoo Finance에서 주식 정보 가져오기 (2)

https://overmt.com/yahoo-finance에서-주식-정보-가져오기-1/
의 코드 중 코드 2를 기준으로 use(임포트)와 struct(구조체) 부분의 코드에 대해 알아보겠습니다.

1. use

use reqwest;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use tokio;
use chrono::{DateTime, Utc, TimeZone};

가. use reqwest;

HTTP 클라이언트 라이브러리를 불러오는(import) 기능으로, Yahoo Finance API에서 주식 데이터를 가져오는 HTTP를 요청하기 위해 사용합니다.
예시: reqwest::get(“https://api.example.com”)

Cargo.toml에서는 dependency를 선언한 것이고, 사용하려면 다시 use를 해야 합니다.

나. use serde::{Deserialize, Serialize};

직렬화/역직렬화 라이브러리인 serde를 import 하는 구문으로, ::{Deserialize, Serialize}는 라이브러리의 전체를 가져오는 것이 아니라 Deserialize와 Serialize 트레이트(trait)만 가져오는 것입니다.

JSON 데이터를 Rust 구조체로 변환하거나, 그 반대 역확을 하며, 본 코드에서는 Yahoo API의 JSON 응답을 StockData 구조체로 변환하는데 사용합니다.

dependencies의 serde와 main.rs의 serde의 차이점을 알아보면 아래와 같습니다.

[Cargo.toml의 dependencies]
serde = { version = "1.0", features = ["derive"] }는 serde 크레이트의 추가 기능인 derive를 활성화하는 것으로 #[derive(Serialize, Deserialize)] 매크로를 사용 가능하게 합니다.

[main.rs의 use]
use serde::{Deserialize, Serialize};는 실제로 구조체에 적용할 트레이트인 Deserialize, Serialize 트레이트를 사용하기 위한 것입니다.

[main.rs의 struct 구문]
// derive 기능이 활성화되어서 이 매크로 사용 가능
#[derive(Debug, Deserialize, Serialize)]
struct StockData {
symbol: String,
price: f64,
}

[3단계 구조]
features = ["derive"]로 derive 기능을 활성화하고,
use serde::{...}로 Deserialize, Serialize 트레이트를 가져오고,
#[derive(...)]로 구조체에 실제 매크로를 적용하는 3단계 구조입니다.

다. use std::collections::HashMap;

키-값 쌍을 저장하는 해시맵을 import 하는 것인데, 현재 코드에서는 사용하지 않습니다. 지우고 실행해보니 문제없습니다.

claude.ai가 필요없으니 삭제를 해야 하는데, 삭제를 하지 않았네요.

cargo run을 하니 unused가 HashMap뿐만 아니라 DateTime, e도 있습니다.

use std::collections::HashMap;은 한 줄을 지우고,

use chrono::{DateTime, Utc, TimeZone};에서는 DateTime만 지우고,

error를 의미하는 e는 위 화면의 제안에 따라 _e로 바꾸고 실행하니 문제없이 깔끔하게 실행됩니다.

라. use tokio;

tokio라는 비동기 런타임 라이브러리를 임포트하는 구문으로 async/await를 이용해 여러 주식의 정보를 동시에 병렬로 가져오기가 가능해집니다. 따라서, 10개 주식을 순차적으로 가져오면 10초가 걸리는데, 병렬로 가져오면 1초뿐이 안걸립니다.

마. use chrono::{Utc, TimeZone};

chrono는 날짜/시간 처리 라이브러리로서 Unix 타임스탬프를 사람이 읽기 쉬운 날짜로 변환해줍니다. 예를 들어 1692345600를 “2023-08-18 12:00:00″로 바꿔줍니다.

Unix 타임 스탬프는 UTC 기준으로 1970.1.1부터의 누적된 초이며, UTC는 협정 세계시(Coordinated Universal Time의 약어)로서 그리니치 표준시 (GMT)의 후속 표준이라고 합니다.

2. struct

이 코드는 Yahoo Finance API에서 주식 정보를 받아오기 위한 **데이터 구조(Struct)**를 정의한 것입니다.

serde 라이브러리를 이용해 JSON 데이터 → Rust 구조체 변환(Deserialize)과 반대로 변환(Serialize)을 하기 위해 설계되어 있습니다.

postman 사이트에서 https://query1.finance.yahoo.com/v8/finance/chart/AAPL를 열어보면 아래와 같이 깔끔한 JSON 포맷의 데이터를 보여줍니다.

중괄호 안에 chart(key)가 있고, 그 안에 result와 대괄호(배열)가 있고, 다시 중괄호 다음에 meta가 있으며, 그 안에 우리가 얻고자 하는 currency, symbol, regularMarketPrice 등이 key: Value 쌍으로 담겨져 있습니다. 이에 따라 단계별로 struct를 만듭니다.

가. YahooResponse

#[derive(Debug, Deserialize, Serialize)]
struct YahooResponse {
chart: Chart,
}
  • API 응답 전체를 감싸는 최상위 구조체로서,
  • Yahoo API가 반환하는JSON 최상단에 있는 “chart” 필드를 받기 위해 사용하는데, 데이터형식은 아래 ‘나. Chart’입니다.

나. Chart

struct Chart {
result: Vec<ChartResult>,
error: Option<serde_json::Value>,
}
  • result: 실제 주식 데이터가 담긴 배열이므로 Vec 타입이고, ChartResult 형식의 데이터를 담습니다.
  • error: 에러가 있을 경우 그 내용을 담는 필드로서, 값이 없을 수도 있으니 Option 열거형이며, T값은 serde_json::Value입니다.

다. ChartResult

struct ChartResult {
meta: Meta,
}
  • meta에는 해당 종목의 기본 정보(symbol, regularMartketPrice, currency 등)가 들어 있으며, 데이터 타입은 ‘라. Meta’입니다.

라. Meta

struct Meta {
currency: String,
symbol: String,
#[serde(rename = "longName")]
long_name: Option<String>,
#[serde(rename = "regularMarketPrice")]
regular_market_price: Option<f64>,
#[serde(rename = "regularMarketTime")]
regular_market_time: Option<i64>,
}
  • 개별 종목의 메타데이터입니다.
  • #[serde(rename = “longName”)]  속성(Attribute)은 JSON의 필드 이름인 long_name을 Rust 필드 이름인 longName으로 바꾸는 역할을 하며,
  • Option을 쓰는 이유는 해당 값이 API 응답에서 없을 수 있기 때문입니다.

마. StockData

#[derive(Debug, Clone)]
struct StockData {
symbol: String,
long_name: String,
regular_market_price: f64,
currency: String,
regular_market_time: i64,
}
  • 실제 사용할 가공된 데이터 구조체로서, Debug와 Clone trait을 자동 구현하며,
  • 위의 Meta에서 필요한 값만 골라와서, 모두 Option 없이 바로 사용 가능한 형태로 변환한 것이며,
  • 프로그램 내부 로직(예: UI 표시, 계산)에서 바로 쓰기 편하도록 만든 것입니다.

Rust Formatting Cheat Sheet

formatting trait와 placeholder, formatter 기본 옵션에 대해 알아보고, 천단위 구분 표시(나라별 차이)에 대해서도 추가로 알아보겠습니다.

1. formatting trait과 placeholder

traitplaceholder 예설명
Display
{}사용자 친화적 출력
Debug{:?}/{:#?}개발·디버그 출력 / Pretty print
Binary{:b}2진수
Octal{:o}8진수
LowerHex{:x}16진수(소문자)
UpperHex{:X}16진수(대문자)
Pointer{:p}포인터 주소
LowerExp{:e}지수 표기법(소문자 e)
UpeperExp{:E}{:E}지수 표기법(대문자 E)

2. Formatter 옵션 (기본)

{[arg]:[fill][align][sign][#][0][width][.precision][type]}
옵션의미예시결과
{}자동 인덱스println!(“{} {}”, 1, 2); “1 2”
{0}(1}위치 인덱스println!(“{0} {1} {0}”, “a”, “b”);“a b a”
{name}이름(키) 인자println!(“{name} is {age}”, name=”Alice”, age=30);“Alice is 30”
<왼쪽 정렬{:<10}“left “
>오른쪽 정렬{:>10}” right”
^가운데 정렬{:^10}” center “
fill채움 문자 변경{:*^10}”“***hi****”
width폭 지정{:8}”” 42″
width$변수 폭 지정{:width$}”폭이 변수값
+항상 부호 표시. 양수도 +, 음수는 –
※ 기본은 음수만 – 표시하고, 양수는 미표시
{:+}”+42
‘ ‘(공백)(열 맞추기 위해) 양수일 때 공백 한 칸 추가println!(“{: }”, 42); ” 42″
#진번 접두사 표시#{:#x}”“0x2a”
0제로 패딩(0으로 채움)println!(“{:08.2}”, 3.14159); “00003.14” (폭 8, 소수점 2자리)
.precision소수점 자리수{:.2}“3.14”

3. 천 단위 구분 쉼표

Rust 표준 라이브러리에는 없으므로 외부 크레이트 또는 수동 구현이 필요합니다.

가. num-format (다국어 지원)

[dependencies]
num-format = "0.4"
use num_format::{Locale, ToFormattedString};

fn main() {
let n = 123456789;
println!("{}", n.to_formatted_string(&Locale::en)); // 123,456,789
println!("{}", n.to_formatted_string(&Locale::de)); // 123.456.789
}

나. separators (경량)

[dependencies]
separators = "0.4"
use separators::Separatable;

fn main() {
let n = 123456789;
println!("{}", n.separated_string()); // 123,456,789
}

다. 직접 구현

fn format_with_commas(n: i64) -> String {
let s = n.to_string();
let mut chars: Vec<char> = s.chars().rev().collect();
let mut result = String::new();
for (i, c) in chars.iter().enumerate() {
if i != 0 && i % 3 == 0 {
result.push(',');
}
result.push(*c);
}
result.chars().rev().collect()
}

fn main() {
println!("{}", format_with_commas(123456789)); // 123,456,789
}

4. 예시 모음

fn main() {
let n = 42;
println!("{:<10}", n); // 왼쪽 정렬
println!("{:>10}", n); // 오른쪽 정렬
println!("{:^10}", n); // 가운데 정렬
println!("{:*^10}", n); // 가운데 정렬 + * 채움
println!("{:08}", n); // 0으로 채움
println!("{:+}", n); // +42
println!("{:#x}", n); // 0x2a
println!("{:b}", n); // 101010
println!("{:width$}", n, width = 6); // 변수 폭 지정
}

Rust의 attribute 종류 및 의미

Rust에서의 attribute(속성)는 컴파일러에게 특정 코드에 대한 추가적인 정보를 제공하여, 코드의 컴파일 방식 또는 동작 방식을 제어하거나, 경고/에러 메시지를 제어하는 데 사용됩니다. derive, allow, warn, cfg, test 등 다양한 종류가 있습니다.

1. attribute의 적용 범위별 종류

Rust의 attribute는 크게 다음과 같은 종류로 나눌 수 있습니다:

종류형태 예시설명
Item attribute#[derive(Debug)]함수, 구조체 등 개별 항목에 적용
Crate attribute#![allow(dead_code)]크레이트 전체에 적용, 보통 파일 상단에 위치
Inner attribute#![cfg(test)]모듈 또는 크레이트 내부에 선언, 내부 항목에 영향을 줌
Outer attribute#[test]특정 항목(함수 등)에만 적용

2. 주요 attribute

가. 컴파일러 관련 attribute

Attribute설명예시
#[allow(…)]특정 경고를 무시#[allow(dead_code)]
#[warn(…)]경고를 표시 (기본값)#[warn(unused_variables)]
#[deny(…)]해당 사항이 있으면 컴파일 에러#[deny(missing_docs)]
#[forbid(…)]deny보다 강하게 재정의 불가#[forbid(unsafe_code)]

나. 파생(derive) 관련 attribute

Rust의 많은 기능은 trait을 자동으로 구현해주는 #[derive(…)]를 통해 사용합니다.

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

Debug, Clone, PartialEq 트레이트를 자동 구현합니다.

다. 조건부 컴파일 관련 attribute

Attribute설명예시
#[cfg(…)]조건에 따라 컴파일 여부 결정#[cfg(target_os = “windows”)]
#[cfg_attr(…)]조건이 참일 때에만 다른 attribute 적용#[cfg_attr(test, derive(Debug))]

[cfg_attr(test, derive(Debug))]는 테스트를 실행할 때만 Debug 트레이트를 자동 구현합니다.

라. 테스트/벤치마크 관련 attribute

Attribute설명예시
#[test]단위 테스트 함수임을 표시#[test]
fn test_add() { … }
#[bench]벤치마크 함수 표시 (불안정)#[bench]
fn bench_sort(…)
#[ignore]테스트 실행 시 무시됨#[test] #[ignore]
fn test_heavy() {}

마. 매크로 관련 attribute

Attribute설명예시
#[macro_use]외부 크레이트의 매크로를 현재 스코프로 불러옴 (Rust 2015 방식)#[macro_use]
extern crate log;
#[proc_macro]프로시저 매크로 정의프로시저 매크로 crate에서 사용됨

바. 기타 유용한 attribute

Attribute설명예시
#[inline], #[inline(always)]함수 인라인화 힌트#[inline(always)]
fn fast_fn() {}
#[repr(C)]구조체 메모리 레이아웃 C와 호환#[repr(C)]
struct MyStruct { … }
#[non_exhaustive]미래 확장을 위해 열거형 등의 exhaustive 검사 방지#[non_exhaustive]
enum MyEnum { … }
#[must_use]반환값을 사용하지 않으면 경고#[must_use]
fn compute() -> i32 { … }

※ 함수를 호출할 때 일반적으로는 함수 본문은 어딘가에 있고 호출하면 그 함수로 점프(jump)해서 실행한 뒤 다시 돌아옵니다. 하지만 인라인화는 이런 점프 없이 함수의 코드를 호출한 곳에 ‘복붙’하듯 직접 삽입하는 방식입니다.

사. #![…]과 #[…] 차이

구분형태설명
Inner attribute#![…]모듈, 크레이트 전체에 적용 (파일 상단에 위치)
Outer attribute#[…]특정 항목(함수, struct 등)에 적용

3. attribute 예제

가. #[derive(…)] — 트레이트 자동 구현

(1) 설명: 구조체(struct)나 열거형(enum)에 대해, 특정 트레이트의 기본 구현을 자동 생성합니다.

(2) 자주 사용되는 트레이트:

  • Debug : {:?} 포맷으로 출력 가능
  • Clone, Copy : 값 복사 가능
  • PartialEq, Eq : 값 비교 가능
  • PartialOrd, Ord : 정렬 가능
  • Hash : 해시 사용 가능

(3) 예제:

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

fn main() {
let p1 = Point { x: 1, y: 2 };
let p2 = p1.clone(); // Clone 사용
println!("{:?}", p2); // Debug 사용
println!("p1 == p2? {}", p1 == p2); // PartialEq 사용
}

나. #[allow(…)], #[warn(…)], #[deny(…)], #[forbid(…)]

(1) 설명: 컴파일 경고/에러를 제어합니다. => 위 2.의 가. 컴파일러 관련 attribute 설명 참고

(2) 주요 옵션:

  • dead_code : 사용되지 않는 코드
  • unused_variables : 사용되지 않는 변수
  • non_snake_case : snake_case 규칙 위반

(3) 예제:

#[allow(dead_code)]
fn unused() {
println!("사용되지 않지만 경고 없음");
}

#[deny(unused_variables)]
fn foo() {
let x = 5; // 컴파일 에러 발생: 변수 사용 안 함
}

다. #[test], #[ignore] — 테스트 함수 지정

(1) 설명: 테스트 프레임워크를 위한 attribute입니다.

(2) 예제:

#[cfg(test)]
mod tests {
#[test]
fn it_works() {
assert_eq!(2 + 2, 4);
}

#[test]
#[ignore]
fn heavy_test() {
// 실행하려면 `cargo test -- --ignored`
}
}

라. #[cfg(…)] — 조건부 컴파일

(1) 설명: 플랫폼, 기능 등에 따라 코드 포함 여부 결정

(2) 예제:

#[cfg(target_os = "windows")]
fn run_on_windows() {
println!("Windows 전용 코드");
}

#[cfg(not(target_os = "windows"))]
fn run_on_unix() {
println!("Unix 계열 전용 코드");
}

(3) Cargo.toml에서 feature와 연계:

[features]
my_feature = []
#[cfg(feature = "my_feature")]
fn my_func() {
println!("my_feature가 활성화됨");
}

“my_feature”라는 기능(feature)이 활성화됐을 때만 my_func() 함수의 정의 자체를 컴파일에 포함시키고, 아니면 아예 없는 코드처럼 무시해버립니다.

마. #[inline] — 인라인 최적화 힌트

(1) 설명: 컴파일러에게 해당 함수를 인라인하도록 유도

(2) 예제:

#[inline(always)]
fn fast_add(a: i32, b: i32) -> i32 {
a + b
}

fn main() {
let x = add(3, 4);
}
  • 일반 호출: main에서 add 함수로 점프하고, 결과를 받아와서 x에 저장
  • 인라인: 컴파일할 때 add(3, 4)를 그냥 3 + 4로 바꿔서 main 함수 안에 넣어버림

바. #[repr(…)] — 메모리 레이아웃 제어

(1) 설명: 구조체/열거형의 메모리 정렬 방법을 지정(repr은 representation(표현)의 약어)

(2) 종류:

  • C: C 언어와 동일한 레이아웃
  • packed: 패딩 없이 압축
  • transparent: 단일 필드 감싸기

(3) 예제:

#[repr(C)]
struct MyStruct {
a: u8,
b: u32,
}

사. #[non_exhaustive] — 미래 확장을 위한 열거형

(1) 설명: 나중에 항목이 더 생길 수 있으니, “지금 있는 것만 가지고 match를 완벽하게 쓰지 마세요“라는 의미입니다.

(2) 예제:

#[non_exhaustive]
pub enum Error {
Io,
Parse,
}

이 코드는 다음과 같은 의미를 가집니다.

Error enum은 지금은 Io와 Parse 두 가지 variant만 있지만, 앞으로 새로운 variant가 추가될 수 있으므로 다른 크레이트에서는 이 enum을 match할 때 지금 있는 것만으로 열거하지 못하게 제한하는 것입니다.

그래서 #[non_exhaustive]를 붙이면, 사용자는 반드시 _를 이용한 default arm을 추가해야만 컴파일이 가능하며, 이것은 다른 크레이트가 match를 지금 있는 것만으로 exhaustively(완전하게) 작성하면, 나중에 enum에 새로운 variant를 추가했을 때 컴파일이 깨질 수 있기 때문입니다.

아. #[must_use] — 반환값 사용하지 않으면 경고

(1) 설명: 실수로 무시하면 안 되는 반환값을 경고로 알려줌

(2) 예제:

#[must_use]
fn important_result() -> Result<(), String> {
Err("Error".into())
}

fn main() {
important_result(); // 경고 발생!
}

위 코드는 important_result 함수의 반환 값을 사용하지 않아 warning(경고)이 발생하므로 아래와 같은 식으로 수정해야 합니다.

fn main() {
    let result = important_result(); // OK
    if let Err(e) = result {
        println!("에러 발생: {}", e);
    }
}

자. #[macro_use], #[macro_export] — 매크로 관련

(1) 설명: 외부 crate의 매크로를 가져오거나 내보낼 때 사용

(2) 예제:

// 외부 매크로 사용
#[macro_use]
extern crate log;

// 매크로 정의 및 export
#[macro_export]
macro_rules! hello {
() => {
println!("Hello, macro!");
};
}
  • #[macro_use] : 외부 크레이트 log에서 정의된 매크로(log!, info!, warn! 등)들을 이 파일 안에서 직접 사용할 수 있도록 가져오라는 의미로서, 예전 Rust 스타일 (Rust 2018 이전)의 방식이며, 최신 Rust (2018 edition 이후)에서는 use log::info; 식으로 가져오는 게 일반적입니다.
  • #[macro_export] : 이 매크로를 다른 모듈/크레이트에서 사용할 수 있게 공개(export) 하겠다는 의미입니다. 따라서, 이 매크로를 crate_name::hello!() 식으로 다른 crate에서도 쓸 수 있습니다.

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의 위치가 달라졌습니다.

매크로(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!로 간단한 코드 생성을 처리하고, 절차적 매크로로 복잡한 로직도 커버할 수 있습니다.

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

구조체 (Structs)

Rust에서 복잡한 데이터를 다루기 위해 사용하는 기본 단위가 바로 구조체(struct)입니다. 구조체는 여러 개의 관련된 데이터를 하나의 타입으로 묶어 표현할 수 있도록 해줍니다. Rust는 구조체와 메서드(impl 블록)를 통해 모듈화, 캡슐화, 데이터 모델링이 가능합니다.


1. 기본 구조체 정의 및 사용

가장 기본적인 구조체는 struct 다음에 구조체 이름을 쓰고, 중괄호 안에 필드의 이름과 타입을 :으로 연결해 선언합니다.

Rust의 구조체 이름 규칙은 대문자 카멜 케이스(Camel case)입니다. 예를 들어 User, MySruct와 같이 단어 시작을 대문자로 하고, _를 사용할 수 있으며 숫자로 시작할 수 없고, 공백이 포함되면 안됩니다.

struct User {
    username: String,
    email: String,
    active: bool,
}

사용할땐 struct의 인스턴스를 생성합니다.

일반 변수 선언할 때와 마찬가지로 let 키워드를 사용하고, 그 다음에 인스턴스 이름을 적고, = 구조체 이름을 적은 다음 중괄호안에 필드의 이름과 값을, key: value 쌍으로 아래와 같이 입력합니다. 구조체는 모든 필드의 타입이 명확해야 합니다.

fn main() {
    let user1 = User {
        username: String::from("alice"),
        email: String::from("alice@example.com"),
        active: true,
    };

    println!("username: {}", user1.username);
}
  • 문자열(String)은 “alice”와 같이 큰따옴표 안에 입력한다고 되는 것이 아니며, String::from(“alice”)라고 하거나, “alice”.to_string()으로 입력해야 합니다.
  • bool(논리값)도 true, false와 같이 모두 소문자로 표기합니다.
  • 필드 값을 끝까지 입력하고, 쉼표가 있어도 문제가 없습니다.
  • 구조체 인스턴스는 tuple과 마찬가지로 . 연산자(notation)로 접근할 수 있습니다.
  • Rust는 사용하는 기호도 여러가지가 사용돼서 복잡합니다.

지금까지 나온 것이
변수의 형식은 : 다음에 표시하고,
println다음에 !를 붙여야 하며,
match 패턴의 경우 => 을 사용해서 실행 코드를 지정하고, else를 _로 표시하며,
숫자 입력시 천단위 구분 기호로 _를 사용하고,
char를 입력할 때는 작은 따옴표, String을 입력할 때는 큰따옴표,
반환 값의 타입을 지정할 때는 ->,
loop label은 ‘로 시작하며,
참조를 표시할 때는 &를 사용하고,
튜플과 구조체의 값을 지정할 때는 .을 사용합니다.


2. 구조체는 소유권을 가진다

Rust에서 구조체는 일반 변수처럼 소유권을 가집니다. 즉, 구조체를 다른 변수로 이동시키면 원래 변수는 더 이상 사용할 수 없습니다.

let user2 = user1; // user1의 소유권이 user2로 이동
// println!("{}", user1.email); // 오류!

필드 하나만 이동하는 경우도 마찬가지입니다.

    let username = user1.username; // 소유권 이동 (user1.username에 대한 소유권은 종료됨)

    // user1.username은 더 이상 유효하지 않음, username 변수가 소유권을 갖게 됨

    println!("username: {}", username);

일부 필드를 참조로 처리하거나 클론(clone)을 사용해야 합니다.

let username = &user1.username;
또는
let username = user1.username.clone();

3. 기존 구조체 인스턴스로 새 구조체 인스턴스 생성하기

구조체 인스턴스를 만들 때 기존 구조체를 기반으로 일부 필드만 바꾸고 싶은 경우, 다음과 같이 .. 문법을 사용하여 나머지는 (user2와) 동일하다고 할 수 있습니다:

let user3 = User {
    email: String::from("bob@example.com"),
    ..user2
};

단, user2는 이후 더 이상 사용할 수 없습니다. 그 이유는 username, email과 active 필드의 소유권이 user3에게로 넘어갔기 때문입니다.

또한 ..user2라고 나머지 필드는 똑같다고 할 때 맨 뒤에 ,를 붙이면 안됩니다. 구조체 정의할 때는 ,로 끝나도 되는 것과 구분됩니다.


4. 튜플 구조체 (Tuple Struct)

필드의 이름이 없고 형식만 있는 구조체도 정의할 수 있습니다. 이를 튜플 구조체라고 하며, 단순한 데이터 묶음에 유용합니다. 구조체 이름 다음이 중괄호가 아니라 소괄호인 것도 다릅니다.

struct Color(i32, i32, i32);

fn main() {
    let red = Color(255, 0, 0);
    println!("Red: {}, {}, {}", red.0, red.1, red.2);
}


5. 유사 유닛 구조체 (Unit-like Struct)

필드가 없는 구조체도 정의할 수 있습니다. 이를 유닛 구조체라고 하며, 마치 빈 enum처럼 동작하거나 타입 태깅 등에 사용됩니다.

struct Marker;

fn main() {
    let _m = Marker;
}

이런 구조체는 메모리를 차지하지 않으며, 값 자체보다 타입에 의미를 둘 때 사용됩니다.


6. 구조체에 메서드 구현

Rust는 구조체에 메서드(method)를 추가할 수 있습니다. impl 블록을 통해 구조체에 동작을 부여할 수 있습니다.

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

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

&self는 해당 메서드가 구조체 인스턴스를 참조로 빌려서 사용한다는 뜻입니다.

fn main() {
    let rect = Rectangle { width: 30, height: 50 };
    println!("면적: {}", rect.area());
}

impl 블록 안에는 여러 메서드(함수)를 정의할 수 있으며, 정적 메서드(fn new, 생성자 역할)는 다음처럼 작성합니다:

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

위와 같이 생성자를 선언한 경우 아래와 같이 Rectangle::new 다음의 괄호 안에 필드 이름을 입력할 필요 없이 너비와 높이만을 입력해서 인스턴스를 만들 수 있으며 , 면적을 계산하는 것은 같습니다.

    let rect1 = Rectangle::new(10, 20);
    println!("rect1 면적: {}", rect1.area());

7. 디버깅을위한 #[derive(Debug)]

구조체를 println!으로 출력하려면 Debug 트레이트를 구현해야 합니다.

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

fn main() {
    let p = Point { x: 3, y: 7 };
    println!("{:?}", p);
}

위에서 {:?} 포맷은 Debug 형식 출력을 의미하며, 결과는 Point { x: 3, y: 7 }처럼 구조체의 필드 이름과 값을 포함한 형태로 출력됩니다.

그러나, 아래와 같이 #[derive(Debug)]를 주석 처리하고 실행하면 “Point가 {:?}를 사용해서 포맷될 수 없다”는 에러 메시지가 표시됩니다.


마무리

Rust의 구조체는 단순한 데이터 묶음을 넘어서, 로직과 상태를 함께 표현할 수 있는 강력한 도구입니다. 구조체를 메서드와 함께 사용하면 객체지향적 모델도 자연스럽게 구현할 수 있으며, 안전하고 구조화된 데이터 설계가 가능합니다.