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

해쉬맵 (HashMap)

HashMap은 컬렉션의 일종으로, HashMap<K, V> 타입은 K 타입의 키에 V 타입의 값을 매핑한 것을 저장합니다. 해쉬맵은 벡터를 이용하듯 인덱스를 이용하는 것이 아니라 임의의 타입으로 된 키를 이용하여 데이터를 찾기를 원할때 유용합니다.


1. Struct와 HashMap

항목StructHashMap
정의 방식컴파일 타임에 고정된 필드와 타입 정의런타임에 키-값 쌍으로 유동적으로 데이터 저장
고정된 필드 이름임의의 키 (보통 String 등 Eq + Hash 트레잇 필요)
타입 안정성필드 타입이 컴파일 시점에 결정됨모든 값이 동일 타입이거나 제네릭으로 지정됨
사용 목적고정된 구조의 데이터 표현동적으로 키/값 데이터를 저장하고 조회

※ 제네릭(Generic) : 어떤 자료형이든 사용할 수 있도록 유연하고 재사용 가능한 코드를 만드는 기능

2. HashMap 생성과 삽입

HashMap을 사용하려면 먼저 std::collections에서 가져와야 합니다.

use std::collections::HashMap;

fn main() {
    let mut scores = HashMap::new();
    scores.insert("Blue", 10);
    scores.insert("Red", 50);
}

HashMap::new()로 HashMap을 생성합니다.

let mut scores = HashMap::new();
는 가변형 scores란 변수에 빈 HashMap을 대입하는 것입니다. 다시 말하면 빈 HashMap을 가변형 scroes란 이름으로 만드는 것입니다.

insert(key, value)로 값을 추가할 수 있습니다.

scores.insert(“Blue”, 10);
는 scores는 HashMap의 키로 Blue(&str type), 값으로 10을 대입하는 것이고,

scores.insert(“Red”, 50);
는 scores는 HashMap의 키로 Red(&str type), 값으로 50을 대입하는 것입니다.

이때 HashMap은 키와 값의 타입을 모두 제네릭으로 받으며, 보통 자동 추론됩니다.
Blue와 Red는 &str 형식이고, 10과 50은 정수이므로 i32로 해쉬맵의 형식이 추론된 것입니다.

“Blue”와 “Red”를 String::from(“Blue”)와 String::from(“Red”)로 수정하면, Key의 type이 String으로 자동 변경됩니다.

첫번째는 String type이고, 두번째는 &str 타입이면 두번째 key 아래에 빨간색 물결 줄이 표시되는데, 커서를 갖다대면 mismatched types 에러가 표시됩니다.


3. 값 접근 및 소유권

값을 읽을 때는 get() 메서드를 사용합니다.

let team_name = "Blue";
let score = scores.get(team_name);
println!("{:?}", score); // Some(10)

get()은 Option<&V>를 반환하므로, Some(v) 또는 None으로 패턴 매칭하거나 unwrap_or 등을 활용해야 합니다.

    match score {
        Some(score) => println!("{team_name} 팀의 점수: {score}"),
        None => println!("{team_name} 팀의 점수를 찾을 수 없습니다."),
    }

println!(“{:?}”, score); 의 결과는 Some(10)인데, 위와 같이 match 흐름 제어 연산자를 사용하면 점수가 있으면 점수, 여기서는 10이 반환되고,

팀 이름을 이름이 없는 Green으로 변경하면 Option 값은 None이 출력되고, match 연산자의 결과는 “Green 팀의 점수를 찾을 수 없습니다.”란 메시지가 나오게 됩니다.

소유권 이슈

Rust의 HashMap은 키와 값의 소유권을 가져갑니다.

let field = String::from("Favorite color");
let value = String::from("Blue");

let mut map = HashMap::new();
map.insert(field, value);
// println!("{}", field); // 오류! 소유권이 이동됨

따라서, 위 코드에서 println! 앞의 주석을 제거하고 실행하면
value borrowed here after move 에러가 발생합니다.

위 오류를 없애려면 map.insert안 의 field를 &field로 수정하면 됩니다.


4. for 루프로 순회하기

HashMap의 모든 값을 순회하려면 for 루프를 사용합니다.

for (key, value) in &scores {
    println!("{}: {}", key, value);
}

참조로 순회하므로 키나 값을 수정하진 않지만, 읽을 수 있습니다.


5. 조건부 삽입: entry()와 or_insert()

entry()는 키의 존재여부에 따라 다른 동작을 하게 해주는 API입니다. entry 함수의 리턴값은 열거형 Entry인데, 해당 키가 있는지 혹은 없는지를 나타냅니다.

scores.entry("Yellow").or_insert(30);
scores.entry("Blue").or_insert(50);
  • “Yellow”라는 key가 없기 때문에 value 30이 추가되고,
  • “Blue”는 이미 존재하므로 기존 값(10)이 유지됩니다.

or_insert()는 존재하지 않을 때만 값을 삽입하는 매우 유용한 메소드입니다.


6. 값을 업데이트하기

HashMap의 값을 직접 변경하려면 get_mut() 또는 entry()를 활용할 수 있습니다.

기존 값 수정 (1) :

if let Some(score) = scores.get_mut("Red") {
    *score += 10;
}

if let Some(score) = scores.get_mut(“Red”) {
: score가 Red의 score라면 score를 가변 참조로 반환하는데,

*score += 10;
: 참조를 반환하므로 값을 수정하기 위해서는 dereference(역참조, 포인터가 가리키는 메모리 주소에 저장된 값을 읽거나 쓰는 것)하는 연산자 *를 앞에 붙여야 하며, 이 구문은 score에 10을 더하는 것입니다.

기존 값 수정 (2) :

또는 entry() 기반으로도 가능:

scores.entry("Red").and_modify(|v| *v += 1);

Red라는 키가 있으면, value를 v로 받아서, 1을 더하는 것입니다.


7. 예제: 단어 개수 세기

HashMap은 문자열 데이터를 처리할 때 특히 유용합니다. 예를 들어, 문장에서 각 단어의 개수를 세어보겠습니다.

이 코드는 split_whitespace()로 공백 기준으로 단어를 나누고, 각 단어가 등장한 횟수를 계산합니다.

use std::collections::HashMap;

fn main() {
    let text = "hello world hello rust";

    let mut counts = HashMap::new();

    for word in text.split_whitespace() {
        *counts.entry(word).or_insert(0) += 1;
    }

    println!("{:?}", counts);
}

*counts.entry(word).or_insert(0) += 1;
: word란 키가 있으면 value에 1을 더하고, 없으면 value에 0을 추가하는 것입니다.

출력 결과는 아래와 같은데
{“hello”: 2, “world”: 1, “rust”: 1}

key 순으로 정렬하지 않아 표시되는 순서가 매번 달라집니다.

9. HashMap 요약

  • HashMap는 키-값 쌍을 저장하는 자료구조
  • insert()로 값 추가, get()으로 값 조회
  • 키는 고유해야 하며, 같은 키로 다시 삽입하면 덮어씀
  • entry()와 or_insert()로 조건부 삽입 가능
  • 순회는 for (k, v) in &map
  • 소유권은 HashMap으로 이동됨
  • 문자열 처리에 매우 유용함

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