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

https://overmt.com/yahoo-finance에서-주식-정보-가져오기-1/
의 코드 중 코드 2를 기준으로 fetch_stock_data과 fetch_multiple_stocks 함수에 대해 알아보겠습니다.

Ⅰ. fetch_stock_data 함수에 대해 알아보기

async fn fetch_stock_data(symbol: &str) -> Result<StockData, Box<dyn std::error::Error + Send + Sync>> {
let url = format!("https://query1.finance.yahoo.com/v8/finance/chart/{}", symbol);

let client = reqwest::Client::new();
let response = client
.get(&url)
.header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
.send()
.await?;
let text = response.text().await?;
let yahoo_response: YahooResponse = serde_json::from_str(&text)?;
if let Some(result) = yahoo_response.chart.result.first() {
let meta = &result.meta;

Ok(StockData {
symbol: symbol.to_string(),
long_name: meta.long_name.clone().unwrap_or_else(|| "N/A".to_string()),
regular_market_price: meta.regular_market_price.unwrap_or(0.0),
currency: meta.currency.clone(),
regular_market_time: meta.regular_market_time.unwrap_or(0),
})
} else {
Err(format!("No data found for symbol: {}", symbol).into())
}
}

1. 함수 시그니처

async fn fetch_stock_data(symbol: &str) 
-> Result<StockData, Box<dyn std::error::Error + Send + Sync>>
  • async fn → 비동기 함수, await를 사용할 수 있음
  • 입력값: symbol → “AAPL”, “TSLA” 같은 종목 코드
  • 반환값은 성공 시는 StockData 구조체, 실패 시는 에러(Box<dyn std::error::Error + Send + Sync>를 반환하는데, 멀티스레드 환경이라 Box<dyn std::error::Error >에 Send(스레드간 이동)와 Sync(동시 접근) trait를 추가한 것임

2. 함수 동작 흐름

가. API URL 만들기

let url = format!(
"https://query1.finance.yahoo.com/v8/finance/chart/{}",
symbol
);
  • symbol을 이용해 Yahoo Finance의 차트 데이터 API 주소 구성
    예) https://query1.finance.yahoo.com/v8/finance/chart/AAPL

나. HTTP 클라이언트 준비

let client = reqwest::Client::new();
  • reqwest 라이브러리의 비동기 HTTP 클라이언트를 생성

다. GET 요청 보내기

let response = client
.get(&url)
.header(
"User-Agent",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
)
.send()
.await?;
  • Yahoo Finance API 서버에 GET 요청
  • 브라우저처럼보이게 하려고 User-Agent 헤더를 추가 (봇 차단 방지 목적)
  • .await? → 요청 완료를 기다리고, 실패 시 에러 전파

라. 응답 본문(JSON) 텍스트 추출

let text = response.text().await?;
  • HTTP 응답을 문자열 형태로 읽어옴

마. JSON 파싱

let yahoo_response: YahooResponse = serde_json::from_str(&text)?;
  • serde_json을 사용해 JSON 문자열을 YahooResponse구조체로 변환
  • 여기서 YahooResponse와 내부 구조(chart.result, meta 등)는 미리 serde를 이용해 파싱할 수 있도록 정의되어 있어야 함

바. 데이터 꺼내서 StockData 만들기

if let Some(result) = yahoo_response.chart.result.first() {
let meta = &result.meta;

Ok(StockData {
symbol: symbol.to_string(),
long_name: meta.long_name.clone().unwrap_or_else(|| "N/A".to_string()),
regular_market_price: meta.regular_market_price.unwrap_or(0.0),
currency: meta.currency.clone(),
regular_market_time: meta.regular_market_time.unwrap_or(0),
})
} else {
Err(format!("No data found for symbol: {}", symbol).into())
}
  • chart.result의 첫 번째 요소를 가져옴
  • 거기서 meta 정보를 추출
  • 회사명, 현재 시장 가격, 통화 단위, 마지막 거래 시간 등을 꺼내 StockData에 담음
  • 데이터가 없으면 Err로 반환
위 예에서, unwrap_or는 Option이 None일 때 값을 0.0 또는 0으로 지정하는데,
unwrap_or_else는 Option이 None일 때 클로저가 실행되고, clone()이 추가된 차이점이 있음

Ⅱ. fetch_multiple_stocks 함수에 대해 알아보기

async fn fetch_multiple_stocks(symbols: &[&str]) -> Vec<Result<StockData, Box<dyn std::error::Error + Send + Sync>>> {
let mut handles = Vec::new();

for &symbol in symbols {
let symbol_owned = symbol.to_string();
let handle = tokio::spawn(async move {
fetch_stock_data(&symbol_owned).await
});
handles.push(handle);
}

let mut results = Vec::new();
for handle in handles {
match handle.await {
Ok(result) => results.push(result),
Err(e) => results.push(Err(e.into())),
}
}

results
}

이 함수는 여러 주식 종목(symbol)을 동시에 비동기로 조회(fetch) 하기 위해 tokio::spawn을 사용하는 구조입니다.

1. 함수 시그니처

async fn fetch_multiple_stocks(
symbols: &[&str]
) -> Vec<Result<StockData, Box<dyn std::error::Error + Send + Sync>>>
  • async fn → 비동기 함수이므로 호출 시 .await 필요.
  • 입력값은 &[&str]로 &str 슬라이스 (예: &[“AAPL”, “GOOG”, “TSLA”])
  • 반환값: Result의 벡터
    – 각 종목(symbol)에 대해 Ok(StockData) 또는 Err(에러)가 담긴 리스트.
    – 즉, 한 종목 실패해도 다른 종목은 결과를 받을 수 있음.

2. 주요 동작 흐름

가. handles 벡터 생성

let mut handles = Vec::new();
  • 비동기 작업(태스크) 핸들을 저장해 둘 벡터.

나. 종목별 비동기 작업 생성

for &symbol in symbols {
let symbol_owned = symbol.to_string(); // 소유권 있는 String으로 변환
let handle = tokio::spawn(async move {
fetch_stock_data(&symbol_owned).await
});
handles.push(handle);
}
  • for 루프를 돌면서 각 종목 기호(&str)를 String으로 복사(to_string)
    → 이유: tokio::spawn의 async move 블록은 ‘static 라이프타임을 요구하기 때문.
    원본 &str는 반복문이 끝나면 사라질 수 있으니, 안전하게 소유권 있는 String 사용.
  • tokio::spawn(…) → 배경(백그라운드)에서 새로운 비동기 태스크 생성
  • async move → 클로저에 캡처되는 값(symbol_owned)을 이동(move)시켜 사용.
  • 결과: 각 종목을 조회하는 여러 비동기 태스크가 동시에 실행됨.

다. 모든 태스크 완료 대기

let mut results = Vec::new();
for handle in handles {
match handle.await {
Ok(result) => results.push(result),
Err(e) => results.push(Err(e.into())),
}
}
  • handle.await → 해당 비동기 태스크가 끝날 때까지 대기.
  • handle.await의 반환값:
    – Ok(result) → 작업이 정상 종료 → result(= Result)를 results에 저장.
    – Err(e) → 태스크 자체가 패닉 또는 취소 → 에러를 Box로 변환해 저장.

라. 결과 반환

results
  • 벡터에는 각 종목별 Result<StockData, Error>가 순서대로 저장됨.

마. 주의점

종목 수가 매우 많으면 동시에 많은 태스크가 실행되어 서버나 네트워크에 부하 발생 가능 → tokio::task::JoinSet이나 futures::stream::FuturesUnordered로 동시 실행 수를 제한하는 방법 고려 가능.

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 표시, 계산)에서 바로 쓰기 편하도록 만든 것입니다.

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

모방은 창조의 어머니인가요? claude.ai의 도움을 받아 만든 “Yahoo Finace API를 이용해서 주식 데이터를 가져오는 프로그램”을 살펴보겠습니다. Rust는 먼저 Cargo.toml 파일에서 가져올 크레이트(라이브러리)를 정의하고, main.rs에서 실행 코드를 구현합니다.

1. Cargo.toml

[dependencies]
reqwest = { version = "0.11", features = ["json"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tokio = { version = "1.0", features = ["full"] }

dependencies 섹션에 reqwest, serde, serde_json, tokio 크레이트(crate, library)를 버전과 features를 이용해 지정합니다.

가. reqwest

request가 맞는 단어인데, reqwest로 약간 다른 점 주의해야 합니다.
reqwest 크레이트는 편리하고 높은 수준의 HTTP 클라이언트를 제공합니다.

version은 크레이트의 버전을 지정하는 것은 알겠는데, features는 크레이트의 특정 기능(선택적 기능)을 활성화하거나 비활성화할 때 사용되며, 조건부 컴파일을 가능하게 하여, 필요한 기능만 컴파일하도록 하는 것입니다.

features = [“json”]은 “json” 기능을 켜서 JSON 직렬화/역직렬화 기능을 사용할 수 있게 하는 것입니다.

아래와 같이 버전 목록이 표시되는데, 맨 위에 0.12.23이 있으므로 클릭합니다.

그러면 버전이 자동으로 변경되고, X표시가 없어지고, 녹색 체크 표시로 바뀝니다.

나. serde

데이터 직렬화(Serialize) / 역직렬화(Deserialize) 라이브러리입니다.
JSON, TOML, YAML 등 다양한 포맷과 Rust 구조체를 변환할 때 사용하며,

features = [“derive”]는

[derive(Serialize, Deserialize)] 어트리뷰트를 쓸 수 있게 해주는 것입니다.

다. serde_json

serde의 JSON 전용 확장판으로서, Rust 데이터와 JSON 문자열간에 변환을 가능하게 해줍니다.

let user = User { name: "Kim".into(), age: 30 };
let json_str = serde_json::to_string(&user)?; // 구조체 → JSON 문자열
let parsed: User = serde_json::from_str(&json_str)?; // JSON → 구조체

라. tokio

Rust의 비동기 런타임 (async/await 동작을 실제로 수행하는 엔진)으로서,
features = [“full”]은 모든 기능(네트워킹, 파일 I/O, 타이머 등)을 한 번에 활성화하는 것이며,
#[tokio::main] 매크로로 main 함수를 비동기로 만들 수 있습니다.

2. main.rs

가. 코드 1

아래 코드를 src 폴더의 main.rs를 연 후 Ctrl + A해서 전체를 선택한 후 Ctrl + V를 하면 기존 내용에 덮어씌워집니다.

use reqwest;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use tokio;

#[derive(Debug, Deserialize, Serialize)]
struct YahooResponse {
    chart: Chart,
}

#[derive(Debug, Deserialize, Serialize)]
struct Chart {
    result: Vec<ChartResult>,
    error: Option<serde_json::Value>,
}

#[derive(Debug, Deserialize, Serialize)]
struct ChartResult {
    meta: Meta,
}

#[derive(Debug, Deserialize, Serialize)]
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>,
}

#[derive(Debug, Clone)]
struct StockData {
    symbol: String,
    long_name: String,
    regular_market_price: f64,
    currency: String,
    regular_market_time: i64,
}

async fn fetch_stock_data(symbol: &str) -> Result<StockData, Box<dyn std::error::Error + Send + Sync>> {
    let url = format!("https://query1.finance.yahoo.com/v8/finance/chart/{}", symbol);
    
    let client = reqwest::Client::new();
    let response = client
        .get(&url)
        .header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
        .send()
        .await?;

    let text = response.text().await?;
    let yahoo_response: YahooResponse = serde_json::from_str(&text)?;

    if let Some(result) = yahoo_response.chart.result.first() {
        let meta = &result.meta;
        
        Ok(StockData {
            symbol: symbol.to_string(),
            long_name: meta.long_name.clone().unwrap_or_else(|| "N/A".to_string()),
            regular_market_price: meta.regular_market_price.unwrap_or(0.0),
            currency: meta.currency.clone(),
            regular_market_time: meta.regular_market_time.unwrap_or(0),
        })
    } else {
        Err(format!("No data found for symbol: {}", symbol).into())
    }
}

async fn fetch_multiple_stocks(symbols: &[&str]) -> Vec<Result<StockData, Box<dyn std::error::Error + Send + Sync>>> {
    let mut handles = Vec::new();
    
    for &symbol in symbols {
        let symbol_owned = symbol.to_string();
        let handle = tokio::spawn(async move {
            fetch_stock_data(&symbol_owned).await
        });
        handles.push(handle);
    }
    
    let mut results = Vec::new();
    for handle in handles {
        match handle.await {
            Ok(result) => results.push(result),
            Err(e) => results.push(Err(e.into())),
        }
    }
    
    results
}

fn format_timestamp(timestamp: i64) -> String {
    use std::time::{UNIX_EPOCH, Duration};
    
    let datetime = UNIX_EPOCH + Duration::from_secs(timestamp as u64);
    
    // 간단한 포맷팅 (실제로는 chrono 크레이트 사용 권장)
    format!("Unix timestamp: {}", timestamp)
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    let symbols = [
        "005930.KS", // 삼성전자
        "AAPL",      // Apple
        "TSLA",      // Tesla
        "LIT",       // Global X Lithium & Battery Tech ETF
        "BRK-B",     // Berkshire Hathaway Class B
        "AMZN",      // Amazon
        "O",         // Realty Income Corporation
        "TQQQ",      // ProShares UltraPro QQQ
        "XOM",       // Exxon Mobil
        "WMT",       // Walmart
    ];

    println!("Fetching stock data...\n");
    
    let results = fetch_multiple_stocks(&symbols).await;
    
    for (i, result) in results.iter().enumerate() {
        match result {
            Ok(stock) => {
                println!("Symbol: {}", stock.symbol);
                println!("Long Name: {}", stock.long_name);
                println!("Regular Market Price: {:.2} {}", stock.regular_market_price, stock.currency);
                println!("Currency: {}", stock.currency);
                println!("Regular Market Time: {}", format_timestamp(stock.regular_market_time));
                println!("---");
            }
            Err(e) => {
                println!("Error fetching data for {}: {}", symbols[i], e);
                println!("---");
            }
        }
    }

    Ok(())
}

cargo run을 하면 compile과 build를 한 후

Run을 하는데, 주식 정보 조회 결과를 주식별로 하나씩 보여주고, 시간이 Unix timestampt로 보여줘서 날짜와 시간을 알 수 없습니다.

나 코드 2

그래서 엑셀 처럼 표 형태로 보여주고, Unix time을 년월일시로 바꿔달라고 했더니 아래 코드가 되는데,

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

#[derive(Debug, Deserialize, Serialize)]
struct YahooResponse {
    chart: Chart,
}

#[derive(Debug, Deserialize, Serialize)]
struct Chart {
    result: Vec<ChartResult>,
    error: Option<serde_json::Value>,
}

#[derive(Debug, Deserialize, Serialize)]
struct ChartResult {
    meta: Meta,
}

#[derive(Debug, Deserialize, Serialize)]
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>,
}

#[derive(Debug, Clone)]
struct StockData {
    symbol: String,
    long_name: String,
    regular_market_price: f64,
    currency: String,
    regular_market_time: i64,
}

async fn fetch_stock_data(symbol: &str) -> Result<StockData, Box<dyn std::error::Error + Send + Sync>> {
    let url = format!("https://query1.finance.yahoo.com/v8/finance/chart/{}", symbol);
    
    let client = reqwest::Client::new();
    let response = client
        .get(&url)
        .header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
        .send()
        .await?;

    let text = response.text().await?;
    let yahoo_response: YahooResponse = serde_json::from_str(&text)?;

    if let Some(result) = yahoo_response.chart.result.first() {
        let meta = &result.meta;
        
        Ok(StockData {
            symbol: symbol.to_string(),
            long_name: meta.long_name.clone().unwrap_or_else(|| "N/A".to_string()),
            regular_market_price: meta.regular_market_price.unwrap_or(0.0),
            currency: meta.currency.clone(),
            regular_market_time: meta.regular_market_time.unwrap_or(0),
        })
    } else {
        Err(format!("No data found for symbol: {}", symbol).into())
    }
}

async fn fetch_multiple_stocks(symbols: &[&str]) -> Vec<Result<StockData, Box<dyn std::error::Error + Send + Sync>>> {
    let mut handles = Vec::new();
    
    for &symbol in symbols {
        let symbol_owned = symbol.to_string();
        let handle = tokio::spawn(async move {
            fetch_stock_data(&symbol_owned).await
        });
        handles.push(handle);
    }
    
    let mut results = Vec::new();
    for handle in handles {
        match handle.await {
            Ok(result) => results.push(result),
            Err(e) => results.push(Err(e.into())),
        }
    }
    
    results
}

fn format_timestamp(timestamp: i64) -> String {
    match Utc.timestamp_opt(timestamp, 0) {
        chrono::LocalResult::Single(datetime) => datetime.format("%Y-%m-%d %H:%M:%S").to_string(),
        _ => "Invalid timestamp".to_string(),
    }
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    let symbols = [
        "005930.KS", // 삼성전자
        "AAPL",      // Apple
        "TSLA",      // Tesla
        "LIT",       // Global X Lithium & Battery Tech ETF
        "BRK-B",     // Berkshire Hathaway Class B
        "AMZN",      // Amazon
        "O",         // Realty Income Corporation
        "TQQQ",      // ProShares UltraPro QQQ
        "XOM",       // Exxon Mobil
        "WMT",       // Walmart
    ];

    println!("Fetching stock data...\n");
    
    let results = fetch_multiple_stocks(&symbols).await;
    
    // 테이블 헤더 출력
    println!("{:<3} {:<10} {:<35} {:<15} {:<8} {:<20}", 
             "No.", "Symbol", "Long Name", "Regular Price", "Currency", "Regular Market Time");
    println!("{}", "-".repeat(95));
    
    for (i, result) in results.iter().enumerate() {
        let row_number = i + 1;
        match result {
            Ok(stock) => {
                println!("{:<3} {:<10} {:<35} {:<15.2} {:<8} {:<20}", 
                         row_number,
                         stock.symbol,
                         if stock.long_name.len() > 35 {
                             format!("{}...", &stock.long_name[..32])
                         } else {
                             stock.long_name.clone()
                         },
                         stock.regular_market_price,
                         stock.currency,
                         format_timestamp(stock.regular_market_time)
                );
            }
            Err(e) => {
                println!("{:<3} {:<10} {:<35} {:<15} {:<8} {:<20}", 
                         row_number,
                         symbols[i],
                         "Error fetching data",
                         "N/A",
                         "N/A",
                         "N/A"
                );
            }
        }
    }

    Ok(())
}

앞 부분에 use chrono::{DateTime, Utc, TimeZone};이 있어서

먼저 Cargo.toml에 chrono 크레이트를 추가해야 합니다.

chrono 버전에 커서를 갖다대보니 버전이 0.4.41로 표시되는데, 0.4 버전대이므로 그대로 둬도 문제 없습니다.

Cargo.toml과 main.rs를 저장하고, 실행하면

표 형태로 잘 표시되고, 날짜도 연월일 시분초로 잘 표시됩니다.

다음 편에서는 main.rs 코드를 하나씩 살펴보겠습니다.

비동기 프로그래밍 (async/await)

Rust는 성능과 안전성을 동시에 추구하는 언어입니다. 이런 철학은 비동기 프로그래밍(asynchronous programming)에도 그대로 적용됩니다. Rust의 async/await 문법은 동시성(concurrency)을 효과적으로 다루기 위한 강력한 도구로, 네트워크 프로그래밍이나 고성능 IO 처리에 자주 사용됩니다.


1. 비동기 프로그래밍이란?

비동기 프로그래밍이란, 어떤 작업이 완료될 때까지 기다리는 대신 다른 작업을 먼저 수행하도록 코드를 구성하는 방식입니다. 예를 들어 웹 서버가 여러 클라이언트의 요청을 동시에 처리할 때, 각각의 요청마다 새로운 스레드를 만들기보다, 하나의 스레드에서 여러 요청을 비동기적으로 처리하면 더 적은 리소스로 높은 성능을 얻을 수 있습니다.


2. async/await 개념

Rust의 비동기 프로그래밍은 크게 세 가지로 나뉩니다:

  • async fn: 비동기 함수를 정의하는 키워드
  • await: 비동기 함수의 결과를 기다리는 키워드
  • Future: 아직 완료되지 않은 비동기 작업을 나타내는 타입
async fn say_hello() {
println!("Hello!");
}

이 함수는 호출해도 바로 실행되지 않고, Future를 반환합니다. 실제로 실행되려면 .await를 사용해야 합니다.

say_hello().await;

3. 비동기 실행을 위한 런타임 (tokio)

Rust 표준 라이브러리는 자체적인 비동기 런타임을 제공하지 않습니다. 따라서 일반적으로 tokio 같은 서드파티 런타임을 사용합니다. tokio는 가장 널리 사용되는 비동기 런타임이며, 다양한 네트워크, 타이머, 채널 등 유틸리티를 제공합니다.

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

Cargo.toml의 dependencies에 tokio를 추가하고 cargo run을 하면 관련 라이브러리들이 자동으로 설치됩니다.

비동기 main 함수를 사용하려면 다음과 같이 작성합니다:

#[tokio::main]
async fn main() {
say_hello().await;
}

Cargo.toml과 별도로 main.rs의 코드는 아래와 같습니다.

#[tokio::main]
async fn main() {
    say_hello().await; 
}

async fn say_hello() {
    println!("Hello!");
}

#[tokio::main] 어트리뷰트

이 부분은 Tokio 런타임을 자동으로 시작해주는 매크로 어트리뷰트입니다.

  • Rust에서 async fn main()을 그냥 실행할 수는 없습니다. 왜냐하면 Rust는 기본적으로 비동기 실행 환경(런타임)을 제공하지 않기 때문이에요.
  • [tokio::main]은 main 함수에 Tokio 런타임을 삽입해서 비동기 코드를 실행할 수 있게 만들어줍니다.

async fn main()

이 함수는 비동기 함수입니다.

  • 비동기 함수는 실행될 때 Future를 반환합니다.
  • 이 Future는 .await될 때까지는 실제로 실행되지 않습니다.
  • 하지만 #[tokio::main] 덕분에 main() 함수도 비동기 함수로 정의할 수 있게 되었고, 프로그램은 say_hello().await를 실행하면서 say_hello 함수의 Future를 기다립니다.

say_hello().await

  • .await를 사용하면 이 Future의 실행을 기다립니다.
  • 즉, say_hello() 함수의 본문이 실행될 때까지 main 함수는 멈춰서 기다립니다.

async fn say_hello()

비동기 함수이지만 내부에 특별한 비동기 작업은 없습니다.

async fn say_hello() {
println!("Hello!");
}
  • 이 함수는 단지 “Hello!”를 출력합니다.
  • 비록 내부에 await는 없지만, 비동기 함수로 작성된 이유는 비동기 구조의 연습 또는 나중에 비동기 작업 (예: 네트워크 요청) 을 넣기 위함입니다.

위 코드를 실행하면 Hello!가 출력되는데,

아래와 같이 main과 say_hello함수의 순서를 바꾸고 실행하니 에러가 발생합니다.


4. 비동기 예제: 타이머

다음은 두 개의 비동기 작업을 동시에 실행하는 예제입니다:

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

#[tokio::main]
async fn main() {
let task1 = async {
sleep(Duration::from_secs(2)).await;
println!("2초 작업 완료");
};

let task2 = async {
println!("즉시 실행");
};

tokio::join!(task1, task2);
}

위 코드에서 sleep은 비동기적으로 대기하는 함수입니다. tokio::join! 매크로는 두 작업을 동시에 실행하고, 둘 다 완료될 때까지 기다립니다.

위 코드를 실행하면 “즉시 실행”을 먼저 출력하고, 2초 후에 “2초 작업 완료”를 출력합니다.


5. Future의 본질

Rust의 비동기 함수는 내부적으로 Future 트레잇을 구현하는 구조체를 반환합니다. 이 구조체는 “언제 실행될지 모르는 작업”을 표현하며, .await가 호출되었을 때에만 실행이 시작됩니다.

간단한 예시:

use std::future::Future;

async fn return_num() -> u32 {
10
}

fn main() {
let fut = return_num(); // 실행되지 않음
// fut는 Future 타입, 여기선 실행되지 않음
}

async 블록이나 함수는 Future를 만들기 위한 “공장(factory)” 역할만 하며, 실제 실행은 .await 또는 런타임에 의해 수행됩니다.

  • async fn return_num() -> u32:
    이 함수는 u32를 비동기로 반환하는 함수입니다.
    하지만 실제로는 u32인 10을 바로 리턴하는 단순한 함수입니다. Rust의 async fn은 항상 Future를 반환합니다.
  • let fut = return_num();
    여기서 return_num()을 호출했지만 실제 10을 반환하지 않습니다.
    대신 미래에 10을 반환할 수 있는 준비 상태의 Future 객체만 생성했을 뿐입니다.
  • fut는 실행 가능한 비동기 작업을 담고 있는 Future 타입입니다.
    하지만 실제로 실행(await)하지 않았기 때문에 아무 일도 일어나지 않습니다.
    실제 실행하려면 아래와 같이 .await를 사용해서 실행해야 합니다.
#[tokio::main]
async fn main() {
    let fut = return_num(); // fut: Future
    let result = fut.await; // 이제 실행됨!
    println!("결과: {}", result); // 출력: 결과: 10
}

6. 동시성(concurrency) vs 병렬성(parallelism)

Rust에서 async는 동시성을 위한 기능입니다. 하나의 스레드에서 여러 작업을 번갈아가며 처리하는 구조입니다. 반면, 병렬성은 여러 CPU 코어에서 동시에 작업을 수행하는 것으로, std::thread 등을 통해 구현합니다. 둘은 목적이 다르지만 상호 보완적으로 사용될 수 있습니다.


7. 실전 예제: HTTP 요청

비동기의 진가는 네트워크 작업에서 드러납니다. 예를 들어, reqwest 라이브러리를 이용한 HTTP GET 요청은 다음과 같습니다.

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let resp = reqwest::get("https://httpbin.org/get").await?;
let body = resp.text().await?;
println!("응답 본문:\n{}", body);
Ok(())
}
async fn main() -> Result<(), Box<dyn std::error::Error>>
  • async fn main(): main 함수 자체가 비동기 함수로 정의되어 있습니다.
  • Result<(), Box<dyn std::error::Error>> :
    ? 연산자를 사용하기 위해 오류 처리를 Result 타입으로 합니다.
    다양한 종류의 오류(reqwest::Error, std::io::Error, 등)를 포괄하기 위해 Box<dyn std::error::Error>>를 사용합니다.

let resp = reqwest::get(“https://httpbin.org/get”).await?;
  • reqwest::get(…): reqwest 라이브러리로 GET 요청을 보냅니다.
  • .await: 비동기 요청이 완료될 때까지 기다립니다.
  • ?: 요청 중 오류가 발생하면 main 함수에서 곧바로 리턴됩니다.
  • resp는 reqwest::Response 타입입니다.

let body = resp.text().await?;
  • 응답 객체인 resp에서 본문을 텍스트로 변환합니다.
  • text()는 Future를 반환하므로 .await로 기다립니다.
  • ?로 오류 처리합니다.
  • 결과는 String 타입입니다.

println!(“응답 본문:\n{}”, body);
  • 응답 본문 전체를 콘솔에 출력합니다.

Ok(())

함수가 정상적으로 끝났다는 것을 알리는 반환값입니다.reqwest::get()과 resp.text()는 모두 Future를 반환하므로 .await를 사용해야 합니다.


실행했더니 reqwest에서 에러가 발생해서 Cargo.toml에 reqwest = “0.12.20”을 추가해야 합니다.

[dependencies]
reqwest = "0.12.20"
tokio = { version = "1", features = ["full"] }

그리고, 실행하면 관련 라이브러리들을 설치하고, 컴파일하고 run을 하고, get한 결과를 출력합니다.

8. async와 에러 처리

비동기 함수에서도 Result 타입을 반환하여 에러를 처리할 수 있습니다.

#[tokio::main]
async fn main() {
match fetch_data().await {
Ok(body) => println!("응답 본문:\n{}", body),
Err(e) => eprintln!("요청 중 오류 발생: {}", e),
}
}

async fn fetch_data() -> Result<String, reqwest::Error> {
let resp = reqwest::get("https://example.com").await?;
let text = resp.text().await?;
Ok(text)
}
fetch_data().await
  • fetch_data는 비동기 함수이므로 실행하려면 .await를 붙여야 실제로 실행됩니다.
  • Result를 반환합니다.

이줄에서 fetch_data()의 실행이 끝날 때까지 기다리고, 그 결과에 따라 아래 match가 분기됩니다.

성공한 경우 (Ok)
  • HTTP 요청이 성공하면 응답 본문이 String 타입으로 body에 담깁니다.
  • 그것을 println!으로 콘솔에 출력합니다.
실패한 경우 (Err)
  • 요청 도중 네트워크 오류, 타임아웃 등 문제가 발생하면 reqwest::Error가 e에 들어옵니다.
  • println!는 표준 에러 스트림에 메시지를 출력합니다 (보통 터미널에서 빨간 글씨로 나옴).
async fn fetch_data() -> Result <String, reqwest::Error> {
  • async fn: 비동기 함수입니다. 호출 시 바로 실행되지 않고, Future를 반환합니다.
  • Result<String, reqwest::Error>:
    Ok(String): 요청이 성공하면 응답 본문을 String으로 감싸서 반환합니다.
    Err(reqwest::Error): 네트워크 오류, HTTP 오류 등이 발생하면 오류를 반환합니다.

let resp = reqwest::get(“https://example.com”).await?;
  • reqwest::get(…): GET 방식으로 HTTP 요청을 보냅니다.
  • .await: 이 작업이 완료될 때까지 기다립니다. 이때 다른 작업은 블로킹되지 않으며, tokio 런타임이 비동기적으로 대기합니다.
  • ?: 요청 도중 에러가 발생하면 바로 Err를 반환하며, 함수가 종료됩니다.

이 줄의 결과는 resp 변수에 저장되며, 이는 응답(Response) 객체입니다.


let text = resp.text().await?;
  • resp.text(): 응답 본문을 String으로 변환하는 Future를 반환합니다.
  • .await: 이 본문을 받아올 때까지 대기합니다.
  • ?: 변환 과정에서 에러가 나면 역시 Err를 반환하며 함수가 종료됩니다.

Ok(text)
  • 모든 작업이 성공적으로 끝나면 text 값을 Result의 Ok로 감싸서 반환합니다.

성공할 경우의 실행 결과는 아래와 같고,

사이트 주소에서 e를 제거해서 exampl.com으로 수정하고 실행하니 아래와 같이 “요청 중 오류 발생: error sending request” 에러 메시지가 표시됩니다.


9. 정리

개념설명
async fn비동기 함수 정의
.awaitFuture의 완료를 기다림
Future완료되지 않은 작업
tokio비동기 런타임 라이브러리
join!여러 Future를 동시에 실행
sleep()비동기 대기 함수

10. 마무리

Rust의 비동기 프로그래밍은 고성능 서버나 네트워크 애플리케이션을 만들 때 매우 강력합니다. 하지만 컴파일러가 엄격하게 검사하므로 안전한 동시성 코드를 작성할 수 있습니다.