DaleSchool

파일 읽고 쓰기, JSON 직렬화

중급15분

학습 목표

  • std::fs로 파일을 읽고 쓸 수 있다
  • serde/serde_json으로 구조체를 직렬화·역직렬화할 수 있다
  • 파일 I/O에서 Result와 ?를 활용해 에러를 처리할 수 있다

동작하는 코드

Phase 4 프로젝트에서는 설정, 데이터, 로그 등을 파일로 남길 일이 많아요. std::fsserde를 조합하면 JSON 기반 설정 파일을 쉽게 다룰 수 있어요.

use serde::{Deserialize, Serialize};
use std::fs;
use std::path::Path;

't static CONFIG_PATH: &str = "config.json";

#[derive(Debug, Serialize, Deserialize)]
struct AppConfig {
    host: String,
    port: u16,
    debug: bool,
}

impl Default for AppConfig {
    fn default() -> Self {
        Self {
            host: "127.0.0.1".into(),
            port: 8080,
            debug: false,
        }
    }
}

fn load_config<P: AsRef<Path>>(path: P) -> Result<AppConfig, Box<dyn std::error::Error>> {
    if !path.as_ref().exists() {
        let default = AppConfig::default();
        save_config(&default, &path)?;
        return Ok(default);
    }

    let raw = fs::read_to_string(&path)?;
    let config = serde_json::from_str(&raw)?;
    Ok(config)
}

fn save_config<P: AsRef<Path>>(config: &AppConfig, path: P) -> Result<(), Box<dyn std::error::Error>> {
    let json = serde_json::to_string_pretty(config)?;
    fs::write(path, json)?;
    Ok(())
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut config = load_config(CONFIG_PATH)?;
    config.debug = true;
    save_config(&config, CONFIG_PATH)?;
    println!("현재 설정: {:?}", config);
    Ok(())
}
  • serde_json::to_string_pretty를 사용하면 사람이 읽기 좋은 형태로 저장돼요.
  • Box<dyn Error>를 사용하면 다양한 에러를 한 번에 반환할 수 있어요.
  • 파일이 없다면 기본 설정을 만들어 바로 저장하는 패턴은 CLI 도구에서 자주 쓰입니다.

read_to_string / write 초간단 예시

use std::fs;

fn main() -> std::io::Result<()> {
    fs::write("hello.txt", "안녕하세요?\nRust입니다.")?;
    let contents = fs::read_to_string("hello.txt")?;
    println!("파일 내용:\n{contents}");
    Ok(())
}

? 덕분에 에러 처리가 깔끔하죠? 실패하면 호출자에게 Err가 전달되고, 성공하면 계속 진행합니다.

직접 해보기

  1. AppConfiglog_path: Option<String> 필드를 추가하고, serde(default) 속성을 붙여 필드가 빠져도 기본값을 사용하도록 만들어 보세요.
  2. load_config에서 JSON 파싱이 실패했을 때 친절한 메시지를 로그로 남기고 기본값을 반환하도록 바꿔 보세요.
  3. serde_yaml 크레이트를 사용해 YAML 형식으로도 저장/불러오기가 가능하도록 함수를 확장해 보세요.
  1. 히스토리 저장소struct History(Vec<String>)를 정의하고, 앱을 종료할 때 JSON으로 저장한 뒤 다시 실행했을 때 복원되도록 함수를 작성해 보세요.
  2. pretty vs compact — 설정 파일을 compact JSON(to_string)으로 저장했을 때와 pretty JSON으로 저장했을 때의 파일 크기를 비교해 보고, 언제 어떤 방식을 선택할지 정리해 보세요.
  3. 에러 타입 만들기enum ConfigError를 만들고 From<std::io::Error>/From<serde_json::Error>를 구현해서 Result<T, ConfigError>를 반환하도록 load/save 함수를 리팩토링해 보세요.

Q1. AppConfig를 JSON으로 저장하려면 어떤 트레이트를 파생(derive)해야 할까요?

  • A) Copy
  • B) Serialize / Deserialize
  • C) Debug / Display
  • D) Default

Q2. fs::read_to_string이 반환하는 타입은?

  • A) String
  • B) Result<String, std::io::Error>
  • C) Option<String>
  • D) &str

Q3. serde_json::to_string_pretty의 특징은?

  • A) 바이너리 형식으로 직렬화한다
  • B) 들여쓰기가 포함된 사람이 읽기 쉬운 JSON을 생성한다
  • C) 반드시 UTF-16을 사용한다
  • D) 에러를 반환하지 않는다