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

https://overmt.com/yahoo-finance에서-주식-정보-가져오기-1/
의 코드 중 코드 2를 기준으로 chrono crate을 이용한 format_timestamp 함수와 tokio main 매크로에 대해 알아보겠습니다.

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

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(),
}
}

1. 입력값

timestamp: i64 : 이 값은 유닉스 타임스탬프(1970-01-01 00:00:00 UTC 기준의 초 단위 정수) 입니다.

2. Utc.timestamp_opt(timestamp, 0)

  • chrono::Utc는 UTC(협정 세계시) 타임존을 나타냅니다.
  • timestamp_opt(secs, nsecs)는 주어진 초(secs)와 나노초(nsecs)를 UTC 시각으로 변환하려고 시도합니다.
  • 반환값은 -chrono::LocalResult> enum인데, 이에는 세 가지 경우가 있습니다.
    – Single(datetime) → 정상적으로 변환됨
    – None → 변환 불가
    – Ambiguous(, ) → 모호한 시간 (주로 로컬 타임존에서 섬머타임 전환 시 발생, UTC에서는 거의 없음)

3. match 표현식

  • chrono::LocalResult::Single(datetime)일 경우에는 변환된 datetime을 format(“%Y-%m-%d %H:%M:%S”)로 지정한 문자열 포맷(연-월-일 시:분:초)으로 변환하고,
  • 그 밖의 경우(None, Ambiguous)는 “Invalid timestamp”라는 문자열을 반환합니다.

Ⅱ. tokio main 매크로에 대해 알아보기

#[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(())
}

1. Tokio 런타임

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
  • #[tokio::main]은 Tokio 비동기 런타임을 사용하는 매크로.
  • async fn main() → 메인 함수 자체가 비동기 함수로 실행됨.
  • 반환 타입이 Result<(), Box<dyn std::error::Error + Send + Sync>>으로서 성공했을 때는 (), 다시 말해 빈 튜플을 반환하므로 아무 값도 안돌려주고, 오류가 발생했을 때 다양한 에러 타입을 동적으로 담아 반환합니다.

2. 주식 심볼 배열

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
];
  • fetch_multiple_stocks함수의 인수로 전달하기 위해 여러 주식의 티커(symbol)를 배열로 정의합니다.

3. 주식 데이터 가져오기

println!("Fetching stock data...\n");

let results = fetch_multiple_stocks(&symbols).await;
  • “Fetching stock data…” → 데이터 가져오기 시작 알림 출력.
  • fetch_multiple_stocks 함수는 비동기 함수로, 각 심볼에 대해 API 호출을 시도하여 Vec<Result<StockData, Error>> 를 반환함

4. 표 헤더 출력

println!("{:<3} {:<10} {:<35} {:<15} {:<8} {:<20}",
"No.", "Symbol", "Long Name", "Regular Price", "Currency", "Regular Market Time");
println!("{}", "-".repeat(95));
  • {:<n} 포맷은 왼쪽 정렬(Left Align) 하여 n 자리 확보.
  • 따라서 표 형태로 깔끔하게 정렬됨.
  • 이어서 “-“.repeat(95)로 구분선 출력.

5. 각 결과 출력

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"
);
}
}
}
  • enumerate()를 쓰면 (index, result) 튜플이 넘어옴. row_number = i + 1로 1씩 증가하는 번호를 출력.
    • match result:
      ① 성공(Ok(stock))하면
      – stock(StockData 구조체)를 이용해 주식 데이터를 출력하는데, stock은 result가 성공했을 때 값입니다.
      – stock.symbol 값을 symbol로 출력하고,
      – stock.long_name은 너무 길면 앞의 32자만 출력하고, + “…” 붙여서 가독성을 유지하고,
      – stock.regular_market_price는 소수점 둘째 자리까지 표시하고,({:.2}),
      – 화폐 단위(currency)를 출력하고,
      – stock.regular_market_time은 format_timestamp 함수로 변환하여 표시함

      ② 실패(Err)하면
      – 주어진 symbols[i] 심볼과 함께 “Error fetching data”, “N/A” 값들을 표시.

6. 종료

Ok(())
  • main 함수가 정상 종료됨을 의미.

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로 동시 실행 수를 제한하는 방법 고려 가능.

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에서 검색해보는 것이 먼저입니다.

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

에러 처리 (panic, Result, Option, unwrap, expect, ? 연산자)

Rust는 안전한 시스템 프로그래밍 언어답게, 명시적이고 예측 가능한 에러 처리 방식을 제공합니다. 오늘은 Rust의 주요 에러 처리 도구인 panic, Result, Option, unwrap, expect, ? 연산자에 대해 다루겠습니다.


1. 패닉(panic!)

fn main() {
    panic!("예기치 못한 오류 발생!");
}
  • panic!를 사용하면 프로그램이 즉시 종료되고, panic! 안에 있는 메시지를 보여줍니다.
  • 디버깅 중 주로 사용되며, 복구 불가능한 에러에 적합합니다.

let v = vec![1, 2, 3];
v[99]; // 존재하지 않는 인덱스 → 자동 panic!

Vector v의 요소가 3개이고, index의 최대값이 2인데, 인덱스를 99로 지정하면 “index가 경계를 넘어섰다”는 메시지를 보여주면서 프로그램이 멈춥니다.


2. Result<T, E> 타입

Resut<T, E>는 복구 가능한 에러 처리를 위한 열거형으로, 성공했을 때는 Ok(T), 에러가 발생했을 때는 Err(E)를 반환합니다.

enum Result<T, E> {
    Ok(T),
    Err(E),
}

[예제]

use std::fs::File;

fn main() {
    let result = File::open("hello.txt");

    match result {
        Ok(file) => println!("파일 열기 성공"),
        Err(e) => println!("에러 발생: {}", e),
    }
}
  • use std::fs::File;
    : 표준 라이브러리의 fs 모듈에 정의된 File 구조체를 현재 스코프에서 사용할 수 있도록 가져오는 구문입니다. 
  • let result = File::open(“hello.txt”);
    : hello.txt 파일을 여는데, 그 결과를 result에 저장합니다. result는 Result 열거형으로 File 구조체와 Error라는 2개의 variant(변형)를 가지고 있습니다.
  • match result {
    : match 연산자를 이용해 result의 결과값에 따라 처리합니다.
  • Ok(file) => println!(“파일 열기 성공”),
    : 파일 열기가 성공(Ok)이면 File 구조체 형식의 file을 variant로 갖습니다.
  • Err(e) => println!(“에러 발생: {}”, e),
    : 파일 열기에 실패하면 Err가 발생하는데, e라는 에러 종류를 갖습니다.

프로그램과 같은 폴더에 hello.txt가 없으면
“에러 발생: 지정된 파일을 찾을 수 없습니다. (os error 2)”가 화면에 출력되고,
파일이 있으면 “파일 열기 성공”이 출력됩니다.

Ok(file) => println!(“{:?} 파일 열기 성공”, file), 라고 코드를 수정하고 실행하면, hello.txt 파일이 있을 경우 file 구조체가 출력문(println!)에 전달되어
“File { handle: 0xb4, path: “\\?\D:\rust-practice\day12\hello.txt” } 파일 열기 성공”이 출력됩니다.


3. Option<T> 타입

값이 있을 수도 있고[Some(T)] 없을 수도 있음(None)을 나타냅니다. Result의 경우는 Ok(T), Err(E)인 것과 대비됩니다.

fn main() {
    let some_number = Some(10);
    // let some_number: Option<i32> = None;

    match some_number {
        Some(n) => println!("Some: {n}"),
        None => println!("None"),
    }
}
  • let some_number = Some(10);인 상태에서 위 코드를 실행하면
    Match 제어 흐름 연산자에서 Some(n)에 해당되므로 “Some: 10″이 화면에 출력되고,
  • 첫번째 줄 let some_number = Some(10);을 주석 처리하고, 두번째 줄의 주석을 제거하면 None과 매칭되어 “None”이 하면에 출력됩니다.

4. unwrap, expect

let f = File::open("hello.txt").unwrap(); // 실패 시 panic!
let f = File::open("hello.txt").expect("파일 열기 실패"); // 사용자 메시지 포함
  • unwrap과 expect는 Some(T)이거나, Ok(T)인 경우 내부의 T를 꺼내는(unwrap) 기능을 하고, None이거나, Err(E)인 경우는 빠르게 실패(fail fast)하고 싶을 때 사용하는데, expect는 unwrap와 달리 에러 메시지를 제공합니다.

  • hello.txt가 있을 경우 println!(“{:?} 파일 열기 성공”, f}를 이용해 f를 출력해보면 둘 다 아래와 같이 f를 출력합니다.
    File { handle: 0xb4, path: “\\?\D:\rust-practice\day12\hello.txt” } 파일 열기 성공
    File { handle: 0xb8, path: “\\?\D:\rust-practice\day12\hello.txt” } 파일 열기 성공

  • 그러나, 파일이 없으면 둘다 패닉이 발생하고, 에러 메시지를 표출하는데,
    let f = File::open(“hello.txt”).unwrap();은 사용자 메시지가 없고,

  • let f = File::open(“hello.txt”).expect(“파일 열기 실패”);은 에러 메시지 전에 “파일 읽기 실패”라는 사용자 메시지를 표시하는 것만 다릅니다.

5. ? 연산자(에러 전파)

use std::fs::File;
use std::io::{self, Read};

fn read_file() -> Result<String, io::Error> {
    let mut f = File::open("hello.txt");
    let mut content = String::from("This is a test file.\n");
    f.read_to_string(&mut content)?;
    Ok(content)
}

fn main() {
    match read_file() {
        Ok(content) => println!("파일 내용:\n{}", content),
        Err(e) => eprintln!("파일 읽기 오류: {}", e),
    }
}
  • Err이면, 다시 말해 hello.txt가 없을 경우, ?는 panic을 발생시키지 않고 즉시 리턴을 해서 main문의 Err 분기를 처리합니다.
  • 그러나, ?가 없으면 다음 줄의 f.read_to_string(&mut content)?;으로 넘어가는데, read_to_string 메소드가 없다고 하면서 컴파일 에러가 발생합니다.
  • Result 타입에서만 사용 가능합니다.


6. Option<T>와 Result<T, E> 비교

타입의미실패 시
Option<T>값이 있을 수도[Some(T)], 없을 수도 있음(None)None
Result<T, E>성공[Ok(T)], 또는 실패[Err(E)] 결과 포함Err(E)

둘 다 match, if let, unwrap 등을 통해 사용가능합니다.


7. 요약

도구용도설명
panic!치명적 에러프로그램 즉시 종료
Result<T, E>복구 가능한 에러성공과 실패 구분
unwrap/expect빠르게 실패간결하지만 안전하지 않음
? 연산자에러 전파Result를 간단히 처리
Option<T>존재 여부 표현값이 있을 수도 없을 수도 있음